Prettier Codewars

Polish Codewars with cleaner training pages, better editor typography, responsive fixes, subtle typing effects, and promotion cleanup.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Prettier Codewars
// @namespace    https://codewars.com/
// @version      1.2.0
// @description  Polish Codewars with cleaner training pages, better editor typography, responsive fixes, subtle typing effects, and promotion cleanup.
// @author       NihilDigit
// @match        https://www.codewars.com/*
// @match        https://codewars.com/*
// @icon         https://www.codewars.com/favicon.ico
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  const STYLE_ID = "cw-polish-style";
  const HIDDEN_MARK = "data-cw-polish-hidden";
  const SPARKS_ID = "cw-polish-sparks";
  const STORAGE_PREFIX = "prettier-codewars:";

  const defaultConfig = {
    hidePromotions: true,
    useMapleMono: true,
    tuneCodeMirror: true,
    lineWrapping: true,
    autoFormat: true,
    lightweightAutocomplete: true,
    typingSparks: true,
    deleteAnnihilation: true,
    editorFontSize: "15px",
    editorLineHeight: 1.55
  };

  const menuOptions = [
    ["hidePromotions", "Hide promotions"],
    ["useMapleMono", "Maple Mono font"],
    ["tuneCodeMirror", "CodeMirror polish"],
    ["lineWrapping", "Line wrapping"],
    ["autoFormat", "AutoFormat"],
    ["lightweightAutocomplete", "Lightweight autocomplete"],
    ["typingSparks", "Typing sparks"],
    ["deleteAnnihilation", "Delete annihilation"]
  ];

  const C_KEYWORDS = [
    "auto",
    "break",
    "case",
    "char",
    "const",
    "continue",
    "default",
    "do",
    "double",
    "else",
    "enum",
    "extern",
    "float",
    "for",
    "goto",
    "if",
    "inline",
    "int",
    "long",
    "register",
    "restrict",
    "return",
    "short",
    "signed",
    "sizeof",
    "static",
    "struct",
    "switch",
    "typedef",
    "union",
    "unsigned",
    "void",
    "volatile",
    "while"
  ];

  const C_STANDARD_WORDS = [
    "EOF",
    "FILE",
    "NULL",
    "SEEK_CUR",
    "SEEK_END",
    "SEEK_SET",
    "bool",
    "calloc",
    "char",
    "double",
    "errno",
    "exit",
    "fclose",
    "feof",
    "ferror",
    "fflush",
    "fgets",
    "fopen",
    "fprintf",
    "fputc",
    "fputs",
    "fread",
    "free",
    "fscanf",
    "fseek",
    "ftell",
    "fwrite",
    "getchar",
    "int16_t",
    "int32_t",
    "int64_t",
    "int8_t",
    "intptr_t",
    "malloc",
    "max_align_t",
    "memcmp",
    "memcpy",
    "memmove",
    "memset",
    "offsetof",
    "printf",
    "ptrdiff_t",
    "putchar",
    "puts",
    "qsort",
    "realloc",
    "scanf",
    "size_t",
    "snprintf",
    "sprintf",
    "sscanf",
    "stderr",
    "stdin",
    "stdout",
    "strcat",
    "strchr",
    "strcmp",
    "strcpy",
    "strlen",
    "strncmp",
    "strncpy",
    "strrchr",
    "strstr",
    "strtol",
    "strtoul",
    "uint16_t",
    "uint32_t",
    "uint64_t",
    "uint8_t",
    "uintptr_t",
    "va_arg",
    "va_end",
    "va_list",
    "va_start",
    "void"
  ];

  function readSetting(key) {
    const fallback = defaultConfig[key];
    const storageKey = STORAGE_PREFIX + key;

    try {
      if (typeof GM_getValue === "function") {
        return GM_getValue(storageKey, fallback);
      }

      const value = window.localStorage.getItem(storageKey);
      return value === null ? fallback : JSON.parse(value);
    } catch (_error) {
      return fallback;
    }
  }

  function writeSetting(key, value, reload = true) {
    const storageKey = STORAGE_PREFIX + key;

    try {
      if (typeof GM_setValue === "function") {
        GM_setValue(storageKey, value);
      } else {
        window.localStorage.setItem(storageKey, JSON.stringify(value));
      }
    } catch (_error) {
      return;
    }

    if (reload) {
      window.location.reload();
    }
  }

  function readConfig() {
    return Object.fromEntries(Object.keys(defaultConfig).map((key) => [key, readSetting(key)]));
  }

  const config = readConfig();

  const effectColors = {
    sparks: ["#ffd166", "#ff9f1c", "#ff6b35", "#e5383b", "#fff3b0"]
  };

  function buildCss() {
    return `
    ${
      config.hidePromotions
        ? `
    [data-cw-polish-hidden="true"] {
      display: none !important;
    }

    .partner-display,
    .promoted {
      display: none !important;
    }
    `
        : ""
    }

    ${
      config.useMapleMono
        ? `
    @font-face {
      font-family: "Maple Mono Web";
      font-style: normal;
      font-weight: 400;
      font-display: swap;
      src:
        local("Maple Mono NF"),
        local("MapleMono NF"),
        local("Maple Mono Normal NF"),
        url("https://cdn.jsdelivr.net/fontsource/fonts/maple-mono@latest/latin-400-normal.woff2") format("woff2");
    }

    @font-face {
      font-family: "Maple Mono Web";
      font-style: italic;
      font-weight: 400;
      font-display: swap;
      src:
        local("Maple Mono NF Italic"),
        local("MapleMono NF Italic"),
        local("Maple Mono Normal NF Italic"),
        url("https://cdn.jsdelivr.net/fontsource/fonts/maple-mono@latest/latin-400-italic.woff2") format("woff2");
    }

    .CodeMirror,
    .CodeMirror pre,
    .CodeMirror code,
    .CodeMirror-line,
    .CodeMirror-line *,
    pre,
    code,
    kbd,
    samp {
      font-family: "Maple Mono Web", "Maple Mono NF", "Maple Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important;
      font-variant-ligatures: contextual common-ligatures !important;
    }
    `
        : ""
    }

    ${
      config.tuneCodeMirror
        ? `
    .CodeMirror {
      font-size: ${config.editorFontSize} !important;
      line-height: ${config.editorLineHeight} !important;
    }

    .CodeMirror-lines,
    .CodeMirror pre.CodeMirror-line,
    .CodeMirror pre.CodeMirror-line-like {
      line-height: ${config.editorLineHeight} !important;
    }

    .CodeMirror-cursor {
      transition:
        left 80ms ease-out,
        top 80ms ease-out,
        height 80ms ease-out !important;
    }

    .CodeMirror-activeline-background {
      background: rgb(255 255 255 / 5.5%) !important;
    }

    .CodeMirror-hscrollbar {
      display: none !important;
    }

    .CodeMirror-scroll {
      overflow-x: hidden !important;
    }
    `
        : ""
    }

    #description_area .description-content[data-cw-polish-short-overflow="true"] {
      overflow: visible !important;
    }

    #description_area .description.h-full > :not(.description-content) {
      display: none !important;
    }

    #description_area .description.h-full {
      overflow: hidden !important;
    }

    #description_area .description-content.p-4 {
      height: 100% !important;
      overflow: auto !important;
    }

    .CodeMirror-hints {
      z-index: 2147483646 !important;
      border: 1px solid rgb(255 255 255 / 14%) !important;
      border-radius: 6px !important;
      background: rgb(18 20 26 / 96%) !important;
      box-shadow: 0 12px 30px rgb(0 0 0 / 35%) !important;
      color: rgb(238 241 246 / 94%) !important;
      font-family: "Maple Mono Web", "Maple Mono NF", "Maple Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important;
      font-size: 13px !important;
    }

    .CodeMirror-hint {
      border-radius: 4px !important;
      color: inherit !important;
    }

    li.CodeMirror-hint-active {
      background: rgb(232 98 36 / 92%) !important;
      color: white !important;
    }

    #cw-polish-sparks {
      position: fixed;
      inset: 0;
      z-index: 2147483647;
      pointer-events: none;
      overflow: hidden;
    }

    @media (max-width: 1100px) {
      body.play_view #cc_play_view .game-title .panel > .flex.flex-col.md\\:flex-row {
        flex-direction: column !important;
      }

      body.play_view #cc_play_view .game-title .w-full.md\\:w-5\\/12,
      body.play_view #cc_play_view .game-title .w-full.md\\:w-7\\/12 {
        width: 100% !important;
      }

      body.play_view #cc_play_view .game-title .w-full.md\\:w-7\\/12.pt-4.md\\:pl-4 {
        display: flex !important;
        flex-wrap: wrap !important;
        align-items: stretch !important;
        gap: 8px !important;
        padding-left: 0 !important;
        padding-top: 12px !important;
      }

      body.play_view #cc_play_view .game-title .language-selector,
      body.play_view #cc_play_view .game-title #language_dd,
      body.play_view #cc_play_view .game-title #language_version {
        flex: 1 1 180px !important;
        min-width: 160px !important;
        max-width: none !important;
      }

      body.play_view #cc_play_view .game-title .w-full.md\\:w-7\\/12.pt-4.md\\:pl-4 > a {
        display: flex !important;
        flex: 0 0 auto !important;
      }
    }

    @media (max-width: 1000px) {
      body#users.show_view main .bg-ui-section .flex.flex-col.md\\:flex-row {
        flex-direction: column !important;
        align-items: stretch !important;
      }

      body#users.show_view main .bg-ui-section .flex.flex-col.md\\:flex-row > .w-full.md\\:w-6\\/12 {
        width: 100% !important;
        padding-left: 0 !important;
      }

      body#users.show_view #report .honor-chart-container {
        display: grid !important;
        grid-template-columns: 220px minmax(220px, 1fr) !important;
        align-items: center !important;
        column-gap: 32px !important;
        width: max-content !important;
        max-width: 100% !important;
        margin: 16px auto 0 !important;
      }

      body#users.show_view #report #honor_chart {
        grid-column: 1 !important;
      }

      body#users.show_view #report .honor-chart-center {
        left: 55px !important;
        top: 55px !important;
      }

      body#users.show_view #report .honor-chart-container > .md\\:w-64 {
        position: static !important;
        grid-column: 2 !important;
        width: auto !important;
        height: auto !important;
        overflow: visible !important;
        padding-left: 0 !important;
        margin-top: 0 !important;
      }
    }

    @media (max-width: 720px) {
      body#users.show_view main .bg-ui-section .flex.flex-col.md\\:flex-row {
        flex-direction: column !important;
      }

      body#users.show_view main .bg-ui-section .flex.flex-col.md\\:flex-row > .w-full.md\\:w-6\\/12 {
        width: 100% !important;
      }

      body#users.show_view #report .honor-chart-container {
        grid-template-columns: 1fr !important;
        justify-items: center !important;
        row-gap: 18px !important;
        width: 100% !important;
      }

      body#users.show_view #report .honor-chart-container > .md\\:w-64 {
        grid-column: 1 !important;
      }
    }
  `;
  }

  const adSelectors = [
    "#house_ad_display",
    ".cw-ad",
    ".ads-container",
    "[id*='ad_display' i]",
    "[id*='ad-container' i]",
    "[class*='ad-container' i]",
    "a[href*='/ads/']",
    "a[href*='house_srv']",
    "iframe[src*='ad' i]",
    "ins.adsbygoogle",
    ".partner-display",
    ".promoted",
    ".my-4.flex.flex-col.md\\:flex-row.space-y-4.md\\:space-y-0.md\\:space-x-4",
    ".mt-4.flex.flex-col.md\\:flex-row.space-y-4.md\\:space-y-0.md\\:space-x-4"
  ];

  const classSetBlocklist = [
    ["my-4", "flex", "flex-col", "md:flex-row", "space-y-4", "md:space-y-0", "md:space-x-4"],
    ["mt-4", "flex", "flex-col", "md:flex-row", "space-y-4", "md:space-y-0", "md:space-x-4"],
    ["description-footer", "flex", "flex-row"],
    ["w-256", "max-w-full", "mx-auto", "my-4"],
    ["partner-display"],
    ["promoted"]
  ];

  function injectStyle() {
    let style = document.getElementById(STYLE_ID);
    if (!style) {
      style = document.createElement("style");
      style.id = STYLE_ID;
      (document.head || document.documentElement).append(style);
    }

    style.id = STYLE_ID;
    style.textContent = buildCss();
  }

  function registerSettingsMenu() {
    if (typeof GM_registerMenuCommand !== "function") return;

    menuOptions.forEach(([key, label]) => {
      const state = config[key] ? "On" : "Off";
      GM_registerMenuCommand(`${state} - ${label}`, () => writeSetting(key, !config[key]));
    });

    GM_registerMenuCommand(`Set editor font size (${config.editorFontSize})`, () => {
      const value = window.prompt("Editor font size, for example 15px:", config.editorFontSize);
      if (value && /^\d+(?:\.\d+)?(?:px|rem|em)$/.test(value.trim())) {
        writeSetting("editorFontSize", value.trim());
      }
    });

    GM_registerMenuCommand(`Set editor line height (${config.editorLineHeight})`, () => {
      const value = window.prompt("Editor line height, for example 1.55:", String(config.editorLineHeight));
      const numberValue = Number(value);
      if (Number.isFinite(numberValue) && numberValue >= 1 && numberValue <= 3) {
        writeSetting("editorLineHeight", numberValue);
      }
    });

    GM_registerMenuCommand("Reset Prettier Codewars settings", () => {
      Object.entries(defaultConfig).forEach(([key, value]) => writeSetting(key, value, false));
      window.location.reload();
    });
  }

  function hide(node) {
    if (node && node.nodeType === Node.ELEMENT_NODE) {
      node.setAttribute(HIDDEN_MARK, "true");
    }
  }

  function isProtected(element) {
    return Boolean(
      element.closest("html, body") === element ||
        element.matches("main, #main_header, #trainer, #trainer *") ||
        element.querySelector?.("#trainer")
    );
  }

  function hideSafely(element) {
    if (!element || isProtected(element)) return;
    hide(element);
  }

  function nearestAdContainer(element) {
    return (
      element.closest(
        "#house_ad_display, .ads-container, .cw-ad, aside, article, section:not(#trainer), .panel, div[class*='md:w-'], div[class*='w-full']"
      ) ||
      element
    );
  }

  function removeAds(root = document) {
    if (!config.hidePromotions) return;

    for (const selector of adSelectors) {
      root.querySelectorAll(selector).forEach((element) => hideSafely(nearestAdContainer(element)));
    }

    root.querySelectorAll("div").forEach((element) => {
      if (classSetBlocklist.some((classSet) => classSet.every((name) => element.classList.contains(name)))) {
        hideSafely(element);
      }
    });

  }

  function tuneEditors(root = document) {
    if (!config.tuneCodeMirror) return;

    const mirrors = [];

    if (root.matches?.(".CodeMirror")) {
      mirrors.push(root);
    }

    if (root.querySelectorAll) {
      mirrors.push(...root.querySelectorAll(".CodeMirror"));
    }

    mirrors.forEach((element) => {
      if (element.CodeMirror) {
        tuneEditor(element.CodeMirror);
        scheduleInitialAutoFormat(element.CodeMirror, element);
        if (config.typingSparks || config.deleteAnnihilation) {
          attachEffects(element.CodeMirror);
        }
      }
    });
  }

  function tuneEditor(cm) {
    const optionsKey = [config.lineWrapping, 4, 4, false].join(":");

    if (cm.__cwPolishOptionsKey !== optionsKey) {
      cm.__cwPolishOptionsKey = optionsKey;
      cm.setOption("lineWrapping", config.lineWrapping);
      cm.setOption("indentUnit", 4);
      cm.setOption("tabSize", 4);
      cm.setOption("indentWithTabs", false);
      cm.refresh();
    }

    attachEditorFeatures(cm);
  }

  function scheduleInitialAutoFormat(cm, element) {
    if (!config.autoFormat || cm.__cwPolishInitialFormatted || !isCMode(cm) || !isSolutionEditor(element)) return;
    cm.__cwPolishInitialFormatted = true;

    window.setTimeout(() => {
      if (!cm.getWrapperElement?.().isConnected) return;
      autoFormat(cm);
    }, 200);
  }

  function isSolutionEditor(element) {
    return document.querySelector(".CodeMirror") === element;
  }

  function attachEditorFeatures(cm) {
    if (cm.__cwPolishFeatures) return;
    cm.__cwPolishFeatures = true;

    const keyMap = {
      Tab: (instance) => {
        insertSpaces(instance, 4);
        return true;
      }
    };

    if (config.autoFormat) {
      keyMap["Alt-Shift-F"] = (instance) => {
        autoFormat(instance);
        return true;
      };
    }

    if (config.lightweightAutocomplete && isCMode(cm)) {
      keyMap["Ctrl-Space"] = (instance) => {
        showIdentifierHints(instance, true);
        return true;
      };

      cm.on("inputRead", (instance, change) => {
        if (instance.state.completionActive || !change.text || change.text.length !== 1) return;
        if (!/^[A-Za-z_$]$/.test(change.text[0])) return;

        const prefix = currentPrefix(instance);
        if (prefix.length >= 2) {
          showIdentifierHints(instance, false);
        }
      });
    }

    cm.addKeyMap(keyMap);
  }

  function insertSpaces(cm, count) {
    const spaces = " ".repeat(count);

    if (typeof cm.replaceSelections === "function" && typeof cm.listSelections === "function") {
      cm.replaceSelections(cm.listSelections().map(() => spaces), "end", "+input");
      return;
    }

    cm.replaceSelection(spaces, "end", "+input");
  }

  function autoFormat(cm) {
    if (!cm || typeof cm.indentLine !== "function") return;

    const cursor = cm.getCursor();
    const scroll = cm.getScrollInfo();

    if (isCMode(cm)) {
      const formatted = formatKAndRC(cm.getValue());
      if (formatted !== cm.getValue()) {
        cm.setValue(formatted);
      }
    }

    cm.operation(() => {
      for (let line = cm.firstLine(); line <= cm.lastLine(); line += 1) {
        cm.indentLine(line, "smart");
      }
    });

    cm.setCursor(cursor);
    cm.scrollTo(scroll.left, scroll.top);
  }

  function formatKAndRC(text) {
    const normalized = text.replace(/\t/g, "    ");
    const lines = normalized.split("\n");
    const output = [];

    lines.forEach((line) => {
      const leading = line.match(/^\s*/)[0];
      const trimmed = line.trim();

      if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("//") || trimmed.startsWith("*")) {
        output.push(line);
        return;
      }

      if (trimmed === "{" && output.length > 0) {
        const previous = output[output.length - 1];
        const previousTrimmed = previous.trim();

        if (isKAndRBraceLine(previousTrimmed)) {
          output[output.length - 1] = `${previous.replace(/\s*$/, "")} {`;
          return;
        }
      }

      if (/^else\b/.test(trimmed) && output.length > 0 && output[output.length - 1].trim() === "}") {
        output[output.length - 1] = `${output[output.length - 1].replace(/\s*$/, "")} ${trimmed}`;
        return;
      }

      output.push(line.replace(/^(\s*)}\s*else\b/, "$1} else"));
    });

    return output.join("\n");
  }

  function isKAndRBraceLine(beforeBrace) {
    return (
      /^(?:if|for|while|switch)\s*\(.*\)$/.test(beforeBrace) ||
      /^}?\s*else(?:\s+if\s*\(.*\))?$/.test(beforeBrace) ||
      /^do$/.test(beforeBrace) ||
      /^(?:[A-Za-z_]\w*[\w\s*]*\s+)?[A-Za-z_]\w*\s*\([^;]*\)$/.test(beforeBrace) ||
      /^(?:struct|union|enum)\b.*$/.test(beforeBrace)
    );
  }

  function showIdentifierHints(cm, explicit) {
    if (!cm || !isCMode(cm) || typeof cm.showHint !== "function") return;

    const hints = buildIdentifierHints(cm);
    if (!explicit && hints.list.length < 2) return;

    cm.showHint({
      hint: () => hints,
      completeSingle: false,
      closeCharacters: /[\s()[\]{};:>,]/,
      customKeys: {
        Tab: (_editor, handle) => handle.pick(),
        Enter: (_editor, handle) => handle.pick(),
        Esc: (_editor, handle) => handle.close(),
        Up: (_editor, handle) => handle.moveFocus(-1),
        Down: (_editor, handle) => handle.moveFocus(1)
      }
    });
  }

  function buildIdentifierHints(cm) {
    const cursor = cm.getCursor();
    const prefix = currentPrefix(cm);
    const from = { line: cursor.line, ch: cursor.ch - prefix.length };
    const to = { line: cursor.line, ch: cursor.ch };
    const candidates = buildCompletionCandidates(cm, cursor);
    const seen = new Set();

    const list = candidates
      .filter((word) => {
        const normalized = word.toLowerCase();
        if (seen.has(normalized)) return false;
        seen.add(normalized);
        return word.length > 1 && word !== prefix && (!prefix || normalized.startsWith(prefix.toLowerCase()));
      })
      .slice(0, 80);

    return { list, from, to };
  }

  function buildCompletionCandidates(cm, cursor) {
    const text = cm.getValue();
    const cache = cm.__cwPolishCompletionCache;

    if (cache && cache.text === text && cache.line === cursor.line) {
      return cache.candidates;
    }

    const candidates = [
      ...collectLocalIdentifiers(cm, cursor),
      ...collectPreprocessorIdentifiers(text),
      ...C_STANDARD_WORDS,
      ...C_KEYWORDS
    ];

    cm.__cwPolishCompletionCache = {
      candidates,
      text,
      line: cursor.line
    };

    return candidates;
  }

  function collectLocalIdentifiers(cm, cursor) {
    const startLine = findLocalBlockStart(cm, cursor.line);
    const endLine = findLocalBlockEnd(cm, cursor.line);
    const text = cm.getRange({ line: startLine, ch: 0 }, { line: endLine, ch: cm.getLine(endLine).length });
    return collectIdentifiers(text);
  }

  function collectPreprocessorIdentifiers(text) {
    const identifiers = [];
    const definePattern = /^\s*#\s*define\s+([A-Za-z_]\w*)/gm;
    let match;

    while ((match = definePattern.exec(text))) {
      identifiers.push(match[1]);
    }

    return identifiers;
  }

  function findLocalBlockStart(cm, line) {
    let depth = 0;

    for (let index = line; index >= cm.firstLine(); index -= 1) {
      const text = stripCCommentsAndStrings(cm.getLine(index));

      for (let ch = text.length - 1; ch >= 0; ch -= 1) {
        if (text[ch] === "}") depth += 1;
        if (text[ch] === "{") {
          if (depth === 0) return Math.max(cm.firstLine(), index - 1);
          depth -= 1;
        }
      }
    }

    return cm.firstLine();
  }

  function findLocalBlockEnd(cm, line) {
    let depth = 0;

    for (let index = line; index <= cm.lastLine(); index += 1) {
      const text = stripCCommentsAndStrings(cm.getLine(index));

      for (let ch = 0; ch < text.length; ch += 1) {
        if (text[ch] === "{") depth += 1;
        if (text[ch] === "}") {
          if (depth === 0) return index;
          depth -= 1;
          if (depth === 0) return index;
        }
      }
    }

    return cm.lastLine();
  }

  function collectIdentifiers(text) {
    const identifiers = [];
    const wordPattern = /\b[A-Za-z_]\w*\b/g;
    const cleaned = stripCCommentsAndStrings(text);
    let match;

    while ((match = wordPattern.exec(cleaned))) {
      identifiers.push(match[0]);
    }

    return identifiers;
  }

  function stripCCommentsAndStrings(text) {
    return text
      .replace(/\/\*[\s\S]*?\*\//g, " ")
      .replace(/\/\/.*/g, " ")
      .replace(/"(?:\\.|[^"\\])*"/g, " ")
      .replace(/'(?:\\.|[^'\\])*'/g, " ");
  }

  function currentPrefix(cm) {
    const cursor = cm.getCursor();
    const line = cm.getLine(cursor.line).slice(0, cursor.ch);
    const match = line.match(/[A-Za-z_$][\w$]*$/);
    return match ? match[0] : "";
  }

  function isCMode(cm) {
    return languageKey(cm.getOption("mode")) === "c";
  }

  function languageKey(mode) {
    const value = String(mode || "").toLowerCase();

    if (value === "text/x-c" || value === "text/x-csrc" || value === "text/x-chdr") return "c";
    return "";
  }

  function attachRunAutoFormat() {
    if (!config.autoFormat || document.__cwPolishRunAutoFormat) return;
    document.__cwPolishRunAutoFormat = true;

    document.addEventListener(
      "click",
      (event) => {
        if (!event.target.closest?.("#validate_btn, #attempt_btn, #submit_btn")) return;
        const solutionEditor = document.querySelector(".CodeMirror")?.CodeMirror;
        autoFormat(solutionEditor);
      },
      true
    );
  }

  function ensureSparkLayer() {
    let layer = document.getElementById(SPARKS_ID);
    if (!layer) {
      layer = document.createElement("div");
      layer.id = SPARKS_ID;
      document.documentElement.append(layer);
    }

    return layer;
  }

  function sparkAt(x, y, intensity = 1) {
    const layer = ensureSparkLayer();
    const count = Math.min(7, Math.max(3, Math.round(4 * intensity)));

    for (let index = 0; index < count; index += 1) {
      const spark = document.createElement("i");
      const angle = -Math.PI + Math.random() * Math.PI;
      const distance = 18 + Math.random() * 34 * intensity;
      const dx = Math.cos(angle) * distance;
      const dy = Math.sin(angle) * distance - Math.random() * 8;
      const size = 4 + Math.random() * 4;
      const color = effectColors.sparks[Math.floor(Math.random() * effectColors.sparks.length)];
      const rotation = (Math.random() - 0.5) * 160;

      spark.style.cssText = [
        "position:absolute",
        `left:${x}px`,
        `top:${y}px`,
        `width:${size}px`,
        `height:${size}px`,
        `background:${color}`,
        "border-radius:1px",
        `box-shadow:0 0 ${7 + size * 2}px ${color}`,
        `transform:translate(-50%,-50%) rotate(${rotation}deg) scale(1)`,
        "opacity:.9",
        "will-change:transform,opacity"
      ].join(";");

      layer.append(spark);

      spark
        .animate(
          [
            { transform: `translate(-50%, -50%) rotate(${rotation}deg) scale(1)`, opacity: 0.95 },
            {
              transform: `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px)) rotate(${rotation + 120}deg) scale(.25)`,
              opacity: 0
            }
          ],
          {
            duration: 520 + Math.random() * 260,
            easing: "cubic-bezier(.16, 1, .3, 1)"
          }
        )
        .finished.finally(() => spark.remove());
    }
  }

  function annihilateAt(x, y, intensity = 1) {
    const layer = ensureSparkLayer();
    const driftX = -5 - Math.random() * 8;
    const driftY = -2 + (Math.random() - 0.5) * 6;
    const count = Math.min(5, Math.max(3, Math.round(4 * intensity)));

    for (let index = 0; index < count; index += 1) {
      const voidBit = document.createElement("i");
      const angle = Math.random() * Math.PI * 2;
      const distance = 18 + Math.random() * 32 * intensity;
      const sx = Math.cos(angle) * distance;
      const sy = Math.sin(angle) * distance;
      const endX = driftX * 0.35 + (Math.random() - 0.5) * 1.5;
      const endY = driftY * 0.35 + (Math.random() - 0.5) * 1.5;
      const size = 4 + Math.random() * 5;
      const rotation = (Math.random() - 0.5) * 180;

      voidBit.style.cssText = [
        "position:absolute",
        `left:${x}px`,
        `top:${y}px`,
        `width:${size}px`,
        `height:${size}px`,
        "background:oklch(5% 0.01 265 / .92)",
        "border-radius:2px",
        "box-shadow:0 0 8px oklch(0% 0 0 / .9)",
        `transform:translate(calc(-50% + ${sx}px), calc(-50% + ${sy}px)) rotate(${rotation}deg) scale(1)`,
        "opacity:.86",
        "will-change:transform,opacity,filter"
      ].join(";");

      layer.append(voidBit);

      voidBit
        .animate(
          [
            {
              transform: `translate(calc(-50% + ${sx}px), calc(-50% + ${sy}px)) rotate(${rotation}deg) scale(1)`,
              opacity: 0.86
            },
            {
              transform: `translate(calc(-50% + ${endX}px), calc(-50% + ${endY}px)) rotate(${rotation + 210}deg) scale(.05)`,
              opacity: 0
            }
          ],
          {
            duration: 500 + Math.random() * 140,
            easing: "cubic-bezier(.55, 0, .1, 1)"
          }
        )
        .finished.finally(() => voidBit.remove());
    }
  }

  function attachEffects(cm) {
    if (cm.__cwPolishEffects) return;
    cm.__cwPolishEffects = true;

    let lastSpark = 0;

    cm.on("change", (_instance, change) => {
      if (!change.origin || change.origin === "setValue") return;

      const now = performance.now();
      if (now - lastSpark < 30) return;
      lastSpark = now;

      const cursor = cm.getCursor();
      const pos = cm.cursorCoords(cursor, "window");
      const typed = change.text.join("").length;
      const removed = change.removed ? change.removed.join("").length : 0;
      const x = pos.left + 2;
      const y = pos.top + (pos.bottom - pos.top) / 2;

      if (typed > 0 && config.typingSparks) {
        sparkAt(x, y, Math.min(1.8, 1 + typed / 8));
      } else if (removed > 0 && config.deleteAnnihilation) {
        annihilateAt(x, y, Math.min(1.6, 1 + removed / 8));
      }
    });
  }

  function tuneDescriptionScroll(root = document) {
    const contents = [];

    if (root.matches?.("#description_area .description-content")) {
      contents.push(root);
    }

    if (root.querySelectorAll) {
      contents.push(...root.querySelectorAll("#description_area .description-content"));
    }

    contents.forEach((element) => {
      if (element.scrollHeight - element.clientHeight <= 20) {
        element.setAttribute("data-cw-polish-short-overflow", "true");
      } else {
        element.removeAttribute("data-cw-polish-short-overflow");
      }
    });
  }

  function boot() {
    registerSettingsMenu();
    injectStyle();
    removeAds();
    tuneEditors();
    tuneDescriptionScroll();
    attachRunAutoFormat();

    [100, 300, 800, 1500, 3000].forEach((delay) => {
      window.setTimeout(() => {
        injectStyle();
        removeAds();
        tuneEditors();
        tuneDescriptionScroll();
      }, delay);
    });

    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node.nodeType === Node.ELEMENT_NODE) {
            removeAds(node);
            tuneEditors(node);
            tuneDescriptionScroll(node);
          }
        }
      }
    });

    observer.observe(document.documentElement, {
      childList: true,
      subtree: true
    });
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", boot, { once: true });
    injectStyle();
  } else {
    boot();
  }
})();