CF Linemaster

Adds supports for line highlighting and copying on Codeforces

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)

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)

// ==UserScript==
// @name        CF Linemaster
// @namespace   https://github.com/meooow25
// @match       *://*.codeforces.com/*
// @grant       GM.setClipboard
// @version     0.9
// @author      meooow
// @description Adds supports for line highlighting and copying on Codeforces
// ==/UserScript==

/**
 * This script enables line highlighting for Codeforces submissions.
 *
 * On a submission page or on a submission popup:
 * Click on a line number to highlight the line, Shift+click on another line
 * number to select all lines in between.
 * Press Ctrl+Shift+C to copy all highlighted lines.
 *
 * On a submission page:
 * You can set #L<start> or #L<start>-L<end> as the URL fragment to specify
 * the lines to highlight.
 */

/**
 * Changelog:
 * v0.9  Added support for EDU submission, status and standings pages.
 *       Disabled inspect element opening on copy.
 * v0.8  Updated highlight color to work with dark themes.
 * v0.7  Added support for standings pages.
 * v0.6  Added support for submission popups.
 *       Added support for problemset and acmsguru URLs.
 *       Added an option to remove extra indent from copied code.
 *       Removed unnecessary GM_setClipboard permission.
 */

(async function() {
  'use strict';

  // Set to false to leave extra indent in code when copying with Ctrl+Shift+C.
  const REMOVE_EXTRA_INDENT_ON_COPY = true;

  const SUBMISSION_PAGE_REGEXES = [
    String.raw`^https?://codeforces.com/(?:contest|gym)/\d+/submission/\d+`,
    String.raw`^https?://codeforces.com/(?:problemset|problemsets/acmsguru)/submission/\d+/\d+`,
    String.raw`^https?://codeforces.com/edu/course/\d+/lesson/\d+/\d+/practice/contest/\d+/submission/\d+`,
  ].map(s => new RegExp(s));

  const STATUS_AND_STANDINGS_PAGE_REGEXES = [
    String.raw`^https?://codeforces.com/(?:contest|gym)/\d+/(?:status|my|standings)`,
    String.raw`^https?://codeforces.com/(?:problemset|problemsets/acmsguru)/status`,
    String.raw`^https?://codeforces.com/submissions/.+`, // User status pages
    String.raw`^https?://codeforces.com/edu/course/\d+/lesson/\d+/(?:standings|\d+/practice/(?:status|standings))`,
  ].map(s => new RegExp(s));

  const ON_SUBMISSION_PAGE = SUBMISSION_PAGE_REGEXES.some(re => re.test(location.href));
  const ON_STATUS_OR_STANDINGS_PAGE = STATUS_AND_STANDINGS_PAGE_REGEXES.some(re => re.test(location.href));
  if (!ON_SUBMISSION_PAGE && !ON_STATUS_OR_STANDINGS_PAGE) {
    return;
  }

  const HIGHLIGHTED_LI_CLS     = 'cf-linemaster-highlighted-li';
  const LINE_NUMS_CONTAINER_ID = 'cf-linemaster-line-nums';
  const LINE_NUM_LI_CLS        = 'cf-linemaster-line-num';
  const CONTAINER_DIV_ID       = 'cf-linemaster-line-nums-and-source-container';

  // On submission page
  const SOURCE_PRE_ID = 'program-source-text';
  const SCROLL_TO_HIGHLIGHTED_TOP_OFFSET = 40;

  // On status or standings page
  const FACEBOX_DIV_ID = 'facebox';
  const SOURCE_CODE_CLS = 'source-popup-source';
  const SOURCE_PRE_ADDED_ID = 'cf-linemaster-source-pre';

  const CSS = `
    #${CONTAINER_DIV_ID} {
      display: flex;
      line-height: 1.25;
      ${ON_SUBMISSION_PAGE ? 'margin-top: -0.3em;' : ''}
    }

    #${SOURCE_PRE_ADDED_ID} {
      border: 1px solid rgb(185, 185, 185);
      border-radius: 3px;
    }

    #${LINE_NUMS_CONTAINER_ID} {
      text-align: right;
      padding: 0.5em;
      user-select: none;
      opacity: 0.5;
    }

    .${LINE_NUM_LI_CLS} {
      cursor: pointer;
    }

    #${SOURCE_PRE_ID},
    .${SOURCE_CODE_CLS} {
      padding: 0.5em 0;
      border: none;
      border-left: 1px solid rgb(185, 185, 185);
      flex-grow: 1;
    }

    #${SOURCE_PRE_ID} li,
    .${SOURCE_CODE_CLS} li {
      padding-left: 0.5em;
    }

    .${HIGHLIGHTED_LI_CLS} {
      background-color: rgba(215, 180, 35, 0.25);
    }
  `;

  function createLineNumsContainer() {
    let elem;
    if (ON_SUBMISSION_PAGE) {
      elem = document.createElement('pre');
    } else { // ON_STATUS_OR_STANDINGS_PAGE
      elem = document.createElement('code');
    }
    elem.id = LINE_NUMS_CONTAINER_ID;
    return elem;
  }

  function getSourceCodeContainer() {
    if (ON_SUBMISSION_PAGE) {
      return document.getElementById(SOURCE_PRE_ID);
    } else { // ON_STATUS_OR_STANDINGS_PAGE
      const facebox = document.getElementById(FACEBOX_DIV_ID);
      return facebox && facebox.querySelector(`.${SOURCE_CODE_CLS}`);
    }
  }

  function tweakSourceContainer() {
    if (ON_SUBMISSION_PAGE) {
      getSourceCodeContainer().style.padding = null;
    } else { // ON_STATUS_OR_STANDINGS_PAGE
      getSourceCodeContainer().closest('pre').id = SOURCE_PRE_ADDED_ID;
    }
  }

  let currentRange = { start: null, end: null };
  let disableScrollOnce = false;

  function setCurrentRange(start, end) {
    if (start > end) {
      [start, end] = [end, start];
    }
    if (start === currentRange.start && end === currentRange.end) {
      return false;
    }
    currentRange = { start: start, end: end };
    return true;
  }

  function highlightCurrentRange() {
    Array.from(document.getElementsByClassName(HIGHLIGHTED_LI_CLS))
        .forEach(li => li.classList.remove(HIGHLIGHTED_LI_CLS));
    Array.from(getSourceCodeContainer().querySelectorAll('li'))
        .slice(currentRange.start - 1, currentRange.end)
        .forEach(li => li.classList.add(HIGHLIGHTED_LI_CLS));
  }

  function updateCurrentRange(start, end) {
    const changed = setCurrentRange(start, end);
    if (!changed) {
      return;
    }
    if (ON_SUBMISSION_PAGE) {
      // The hash change listener calls highlightCurrentRange.
      disableScrollOnce = true;
      if (currentRange.start === currentRange.end) {
        location.hash = `L${currentRange.start}`;
      } else {
        location.hash = `L${currentRange.start}-L${currentRange.end}`;
      }
    } else { // ON_STATUS_OR_STANDINGS_PAGE
      highlightCurrentRange();
    }
  }

  function hashUpdated() {
    const matches = /L(\d+)(?:-L(\d+))?/.exec(location.hash);
    if (!matches) {
      return;
    }
    const start = parseInt(matches[1]);
    const end = matches[2] ? parseInt(matches[2]) : start;
    setCurrentRange(start, end);
    highlightCurrentRange();
    if (disableScrollOnce) {
      disableScrollOnce = false;
      return;
    }
    const first = document.querySelector(`.${HIGHLIGHTED_LI_CLS}`);
    if (first) {
      window.scrollTo(
          0,
          first.getBoundingClientRect().top
              + window.scrollY - SCROLL_TO_HIGHLIGHTED_TOP_OFFSET);
    }
  }

  function updatePage() {
    tweakSourceContainer();

    const sourceCodeContainer = getSourceCodeContainer();
    const numLines = sourceCodeContainer.querySelectorAll('li').length;
    const lineNumsContainer = createLineNumsContainer();

    for (let i = 1; i <= numLines; i++) {
      const div = document.createElement('div');
      div.classList.add(LINE_NUM_LI_CLS);
      div.textContent = i;
      lineNumsContainer.appendChild(div);
    }
    const containerDiv = document.createElement('div');
    containerDiv.id = CONTAINER_DIV_ID;
    sourceCodeContainer.replaceWith(containerDiv);
    containerDiv.appendChild(lineNumsContainer);
    containerDiv.appendChild(sourceCodeContainer);

    lineNumsContainer.addEventListener('click', e => {
      if (!e.target.classList.contains(LINE_NUM_LI_CLS)) {
        return;
      }
      const lineNum = parseInt(e.target.textContent);
      if (e.shiftKey && currentRange.start) {
        updateCurrentRange(currentRange.start, lineNum);
      } else {
        updateCurrentRange(lineNum, lineNum);
      }
    });
  }

  function getHighlightedLines() {
    const lines =
        Array.from(document.getElementsByClassName(HIGHLIGHTED_LI_CLS))
        .map(li => li.textContent);
    if (!REMOVE_EXTRA_INDENT_ON_COPY) {
      return lines.join('\n');
    }
    // This logic assumes indent consists of only tabs or only spaces, if that
    // is not the case the text is already ugly and the copy will also be ugly.
    let minSpaceCount = Infinity;
    for (const line of lines) {
      const result = /\S+/.exec(line);
      if (result) {
        minSpaceCount = Math.min(minSpaceCount, result.index);
      }
    }
    return lines.map(line => line.slice(minSpaceCount)).join('\n').trim();
  }

  function showMessage(text) {
    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.text = `Codeforces.showMessage('${text}');`;
    document.head.appendChild(script);
    script.remove();
  }

  function setupCopyShortcut() {
    document.addEventListener('keydown', e => {
      if (e.key.toLowerCase() === 'c' && e.shiftKey && e.ctrlKey) {
        e.preventDefault(); // Disable inspect element opening
      }
    });
    document.addEventListener('keyup', async e => {
      if (e.key.toLowerCase() === 'c' && e.shiftKey && e.ctrlKey) {
        const highlighted = getHighlightedLines();
        if (highlighted) {
          await GM.setClipboard(highlighted);
          showMessage('The highlighted text has been copied into the clipboard');
        }
      }
    });
  }

  function addStyle() {
    const style = document.createElement('style');
    style.type = 'text/css';
    style.textContent = CSS;
    document.head.appendChild(style);
  }

  function isPrettyPrintDone() {
    const sourceCodeContainer = getSourceCodeContainer();
    return sourceCodeContainer && sourceCodeContainer.querySelector('ol');
  }

  function prettyPrintDone() {
    updatePage();
    if (ON_SUBMISSION_PAGE) {
      hashUpdated();
      window.addEventListener('hashchange', hashUpdated);
    }
  }

  if (ON_SUBMISSION_PAGE && isPrettyPrintDone()) {
    prettyPrintDone();
  } else {
    const observer = new MutationObserver(mutationsList => {
      for (const mutation of mutationsList) {
        for (const node of mutation.addedNodes) {
          let sourceCodeContainer;
          const isPrettyPrintContainer =
              node.nodeName === 'OL' &&
              node.classList.contains('linenums') &&
              (sourceCodeContainer = getSourceCodeContainer()) &&
              sourceCodeContainer.contains(node);
          if (isPrettyPrintContainer) {
            prettyPrintDone();
            if (ON_SUBMISSION_PAGE) {
              observer.disconnect();
            }
            return;
          }
        }
      }
    });
    // For status or standings page, the facebox gets added later so can't just
    // put the observer on it on load.
    observer.observe(document.body, { childList: true, subtree: true });
  }

  addStyle();
  setupCopyShortcut();

})();