Pastebin Shiki Highlighter Codeblocks for Markdown

Replace Pastebin code blocks in markdown mode with Shiki syntax highlighting and copy button

// ==UserScript==
// @name         Pastebin Shiki Highlighter Codeblocks for Markdown
// @namespace    https://pastebin.com/
// @version      1.3.6
// @description  Replace Pastebin code blocks in markdown mode with Shiki syntax highlighting and copy button
// @match        https://pastebin.com/*
// @author       BourbonCrow
// @icon         https://raw.githubusercontent.com/shikijs/shiki/main/docs/public/logo.svg
// @grant        GM_addStyle
// @run-at       document-start
// @license MIT
// ==/UserScript==

(function () {
  "use strict";

  // Hide raw markdown code instantly
  if (typeof GM_addStyle === "function") {
    GM_addStyle(`
      .source.markdown code[class^="language-"],
      .source.markdown pre > code[class^="language-"] {
        display: none !important;
      }
    `);
  }

  function getHeaderHeight() {
    const header = document.querySelector(".header");
    return header ? header.offsetHeight || 0 : 0;
  }

  function init() {
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", init);
      return;
    }

    // Inject ESM script
    const script = document.createElement("script");
    script.type = "module";
    script.innerHTML = `
      import { codeToHtml } from 'https://cdn.jsdelivr.net/npm/shiki/+esm';

      // Canonical language definitions
      const languages = {
        abap:{display:"ABAP",aliases:[]},
        actionscript:{display:"ActionScript",aliases:["actionscript-3"]},
        ada:{display:"Ada",aliases:[]},
        "angular-html":{display:"Angular HTML",aliases:[]},
        "angular-ts":{display:"Angular TypeScript",aliases:[]},
        apache:{display:"Apache Conf",aliases:[]},
        ansi:{display:"ANSI",aliases:[]},
        apex:{display:"Apex",aliases:[]},
        apl:{display:"APL",aliases:[]},
        applescript:{display:"AppleScript",aliases:[]},
        ara:{display:"Ara",aliases:[]},
        asciidoc:{display:"AsciiDoc",aliases:["adoc"]},
        asm:{display:"Assembly",aliases:[]},
        astro:{display:"Astro",aliases:[]},
        awk:{display:"AWK",aliases:[]},
        ballerina:{display:"Ballerina",aliases:[]},
        bat:{display:"Batch File",aliases:["batch"]},
        beancount:{display:"Beancount",aliases:[]},
        berry:{display:"Berry",aliases:["be"]},
        bibtex:{display:"BibTeX",aliases:[]},
        bicep:{display:"Bicep",aliases:[]},
        blade:{display:"Blade",aliases:[]},
        bsl:{display:"1C (Enterprise)",aliases:["1c"]},
        c:{display:"C",aliases:[]},
        cadence:{display:"Cadence",aliases:["cdc"]},
        cairo:{display:"Cairo",aliases:[]},
        clarity:{display:"Clarity",aliases:[]},
        clojure:{display:"Clojure",aliases:["clj"]},
        cmake:{display:"CMake",aliases:[]},
        cobol:{display:"COBOL",aliases:[]},
        codeowners:{display:"CODEOWNERS",aliases:[]},
        codeql:{display:"CodeQL",aliases:["ql"]},
        coffee:{display:"CoffeeScript",aliases:["coffeescript"]},
        "common-lisp":{display:"Common Lisp",aliases:["lisp"]},
        coq:{display:"Coq",aliases:[]},
        cpp:{display:"C++",aliases:["c++"]},
        crystal:{display:"Crystal",aliases:[]},
        csharp:{display:"C#",aliases:["cs","c#"]},
        css:{display:"CSS",aliases:[]},
        csv:{display:"CSV",aliases:[]},
        cue:{display:"CUE",aliases:[]},
        cypher:{display:"Cypher",aliases:["cql"]},
        d:{display:"D",aliases:[]},
        dart:{display:"Dart",aliases:[]},
        dax:{display:"DAX",aliases:[]},
        desktop:{display:"Desktop",aliases:[]},
        diff:{display:"Diff",aliases:[]},
        docker:{display:"Dockerfile",aliases:["dockerfile"]},
        dotenv:{display:"dotEnv",aliases:[]},
        "dream-maker":{display:"Dream Maker",aliases:[]},
        edge:{display:"Edge",aliases:[]},
        elixir:{display:"Elixir",aliases:[]},
        elm:{display:"Elm",aliases:[]},
        "emacs-lisp":{display:"Emacs Lisp",aliases:["elisp"]},
        erb:{display:"ERB",aliases:[]},
        erlang:{display:"Erlang",aliases:["erl"]},
        fennel:{display:"Fennel",aliases:[]},
        fish:{display:"Fish",aliases:[]},
        fluent:{display:"Fluent",aliases:["ftl"]},
        "fortran-fixed-form":{display:"Fortran (Fixed Form)",aliases:["f77"]},
        "fortran-free-form":{display:"Fortran (Free Form)",aliases:["f90","f95","f03","f08","f18"]},
        fsharp:{display:"F#",aliases:["fs","f#"]},
        gdresource:{display:"GDResource",aliases:[]},
        gdscript:{display:"GDScript",aliases:[]},
        gdshader:{display:"GDShader",aliases:[]},
        genie:{display:"Genie",aliases:[]},
        gherkin:{display:"Gherkin",aliases:[]},
        "git-commit":{display:"Git Commit Message",aliases:[]},
        "git-rebase":{display:"Git Rebase Message",aliases:[]},
        gleam:{display:"Gleam",aliases:[]},
        "glimmer-js":{display:"Glimmer JS",aliases:["gjs"]},
        "glimmer-ts":{display:"Glimmer TS",aliases:["gts"]},
        glsl:{display:"GLSL",aliases:[]},
        gnuplot:{display:"Gnuplot",aliases:[]},
        go:{display:"Go",aliases:[]},
        graphql:{display:"GraphQL",aliases:["gql"]},
        groovy:{display:"Groovy",aliases:[]},
        hack:{display:"Hack",aliases:[]},
        haml:{display:"Ruby Haml",aliases:[]},
        handlebars:{display:"Handlebars",aliases:["hbs"]},
        haskell:{display:"Haskell",aliases:["hs"]},
        haxe:{display:"Haxe",aliases:[]},
        hcl:{display:"HashiCorp HCL",aliases:[]},
        hjson:{display:"Hjson",aliases:[]},
        hlsl:{display:"HLSL",aliases:[]},
        html:{display:"HTML",aliases:[]},
        "html-derivative":{display:"HTML (Derivative)",aliases:[]},
        http:{display:"HTTP",aliases:[]},
        hxml:{display:"HXML",aliases:[]},
        hy:{display:"Hy",aliases:[]},
        imba:{display:"Imba",aliases:[]},
        ini:{display:"INI",aliases:["properties"]},
        java:{display:"Java",aliases:[]},
        javascript:{display:"JavaScript",aliases:["js"]},
        jinja:{display:"Jinja",aliases:[]},
        jison:{display:"Jison",aliases:[]},
        json:{display:"JSON",aliases:[]},
        json5:{display:"JSON5",aliases:[]},
        jsonc:{display:"JSON with Comments",aliases:[]},
        jsonl:{display:"JSON Lines",aliases:[]},
        jsonnet:{display:"Jsonnet",aliases:[]},
        jssm:{display:"JSSM",aliases:["fsl"]},
        jsx:{display:"JSX",aliases:[]},
        julia:{display:"Julia",aliases:["jl"]},
        kotlin:{display:"Kotlin",aliases:["kt","kts"]},
        kusto:{display:"Kusto",aliases:["kql"]},
        latex:{display:"LaTeX",aliases:[]},
        lean:{display:"Lean 4",aliases:["lean4"]},
        less:{display:"Less",aliases:[]},
        liquid:{display:"Liquid",aliases:[]},
        llvm:{display:"LLVM IR",aliases:[]},
        log:{display:"Log file",aliases:[]},
        logo:{display:"Logo",aliases:[]},
        lua:{display:"Lua",aliases:[]},
        luau:{display:"Luau",aliases:[]},
        make:{display:"Makefile",aliases:["makefile"]},
        markdown:{display:"Markdown",aliases:["md"]},
        marko:{display:"Marko",aliases:[]},
        matlab:{display:"MATLAB",aliases:[]},
        mdc:{display:"MDC",aliases:[]},
        mdx:{display:"MDX",aliases:[]},
        mermaid:{display:"Mermaid",aliases:["mmd"]},
        mipsasm:{display:"MIPS Assembly",aliases:["mips"]},
        mojo:{display:"Mojo",aliases:[]},
        move:{display:"Move",aliases:[]},
        narrat:{display:"Narrat Language",aliases:["nar"]},
        nextflow:{display:"Nextflow",aliases:["nf"]},
        nginx:{display:"Nginx",aliases:[]},
        nim:{display:"Nim",aliases:[]},
        nix:{display:"Nix",aliases:[]},
        nushell:{display:"nushell",aliases:["nu"]},
        "objective-c":{display:"Objective-C",aliases:["objc"]},
        "objective-cpp":{display:"Objective-C++",aliases:[]},
        ocaml:{display:"OCaml",aliases:[]},
        pascal:{display:"Pascal",aliases:[]},
        perl:{display:"Perl",aliases:[]},
        php:{display:"PHP",aliases:[]},
        plsql:{display:"PL/SQL",aliases:[]},
        po:{display:"Gettext PO",aliases:["pot","potx"]},
        polar:{display:"Polar",aliases:[]},
        postcss:{display:"PostCSS",aliases:[]},
        powerquery:{display:"PowerQuery",aliases:[]},
        powershell:{display:"PowerShell",aliases:["ps","ps1"]},
        prisma:{display:"Prisma",aliases:[]},
        prolog:{display:"Prolog",aliases:[]},
        proto:{display:"Protocol Buffer 3",aliases:["protobuf"]},
        pug:{display:"Pug",aliases:["jade"]},
        puppet:{display:"Puppet",aliases:[]},
        purescript:{display:"PureScript",aliases:[]},
        python:{display:"Python",aliases:["py"]},
        qml:{display:"QML",aliases:[]},
        qmldir:{display:"QML Directory",aliases:[]},
        qss:{display:"Qt Style Sheets",aliases:[]},
        r:{display:"R",aliases:[]},
        racket:{display:"Racket",aliases:[]},
        raku:{display:"Raku",aliases:["perl6"]},
        razor:{display:"ASP.NET Razor",aliases:[]},
        reg:{display:"Windows Registry Script",aliases:[]},
        regexp:{display:"RegExp",aliases:["regex"]},
        rel:{display:"Rel",aliases:[]},
        riscv:{display:"RISC-V",aliases:[]},
        rst:{display:"reStructuredText",aliases:[]},
        ruby:{display:"Ruby",aliases:["rb"]},
        rust:{display:"Rust",aliases:["rs"]},
        sas:{display:"SAS",aliases:[]},
        sass:{display:"Sass",aliases:[]},
        scala:{display:"Scala",aliases:[]},
        scheme:{display:"Scheme",aliases:[]},
        scss:{display:"SCSS",aliases:[]},
        sdbl:{display:"1C (Query)",aliases:["1c-query"]},
        shaderlab:{display:"ShaderLab",aliases:["shader"]},
        shellscript:{display:"Shell",aliases:["bash","sh","zsh"]},
        shellsession:{display:"Shell Session",aliases:["console"]},
        smalltalk:{display:"Smalltalk",aliases:[]},
        solidity:{display:"Solidity",aliases:[]},
        soy:{display:"Closure Templates",aliases:["closure-templates"]},
        sparql:{display:"SPARQL",aliases:[]},
        splunk:{display:"Splunk Query Language",aliases:["spl"]},
        sql:{display:"SQL",aliases:[]},
        "ssh-config":{display:"SSH Config",aliases:[]},
        stata:{display:"Stata",aliases:[]},
        stylus:{display:"Stylus",aliases:["styl"]},
        svelte:{display:"Svelte",aliases:[]},
        swift:{display:"Swift",aliases:[]},
        "system-verilog":{display:"SystemVerilog",aliases:[]},
        systemd:{display:"Systemd Units",aliases:[]},
        talonscript:{display:"TalonScript",aliases:["talon"]},
        tasl:{display:"Tasl",aliases:[]},
        tcl:{display:"Tcl",aliases:[]},
        templ:{display:"Templ",aliases:[]},
        terraform:{display:"Terraform",aliases:["tf","tfvars"]},
        tex:{display:"TeX",aliases:[]},
        toml:{display:"TOML",aliases:[]},
        "ts-tags":{display:"TypeScript with Tags",aliases:["lit"]},
        tsv:{display:"TSV",aliases:[]},
        tsx:{display:"TSX",aliases:[]},
        turtle:{display:"Turtle",aliases:[]},
        twig:{display:"Twig",aliases:[]},
        typescript:{display:"TypeScript",aliases:["ts"]},
        typespec:{display:"TypeSpec",aliases:["tsp"]},
        typst:{display:"Typst",aliases:["typ"]},
        v:{display:"V",aliases:[]},
        vala:{display:"Vala",aliases:[]},
        vb:{display:"Visual Basic",aliases:["cmd"]},
        verilog:{display:"Verilog",aliases:[]},
        vhdl:{display:"VHDL",aliases:[]},
        viml:{display:"Vim Script",aliases:["vim","vimscript"]},
        vue:{display:"Vue",aliases:[]},
        "vue-html":{display:"Vue HTML",aliases:[]},
        "vue-vine":{display:"Vue Vine",aliases:[]},
        vyper:{display:"Vyper",aliases:["vy"]},
        wasm:{display:"WebAssembly",aliases:[]},
        wenyan:{display:"Wenyan",aliases:["文言"]},
        wgsl:{display:"WGSL",aliases:[]},
        wikitext:{display:"Wikitext",aliases:["mediawiki","wiki"]},
        wit:{display:"WebAssembly Interface Types",aliases:[]},
        wolfram:{display:"Wolfram",aliases:["wl"]},
        xml:{display:"XML",aliases:[]},
        xsl:{display:"XSL",aliases:[]},
        yaml:{display:"YAML",aliases:["yml"]},
        zenscript:{display:"ZenScript",aliases:[]},
        zig:{display:"Zig",aliases:[]},
        plaintext:{display:"Plain Text",aliases:["text","txt"]}
      };

      // Build alias map
      const aliasMap = {};
      for (const [key, { aliases }] of Object.entries(languages)) {
        aliasMap[key] = key;
        for (const alias of aliases) {
          aliasMap[alias.toLowerCase()] = key;
        }
      }

      function resolveLanguage(lang) {
        if (!lang) return "Plain Text";
        const key = lang.toLowerCase();
        const canonical = aliasMap[key] || "plaintext";
        return languages[canonical].display;
      }

      async function highlight() {
        const markdownSource = document.querySelector('.source.markdown');
        if (!markdownSource) return;

        const codeBlocks = markdownSource.querySelectorAll("code[class^='language-']");
        for (const block of codeBlocks) {
          try {
            const rawCode = block.textContent;
            let lang = "plaintext";
            const match = block.className.match(/language-(\\w+)/);
            if (match) lang = match[1];
            const displayLang = resolveLanguage(lang);

            const html = await codeToHtml(rawCode, {
              lang,
              theme: 'github-dark'
            });

            const wrapper = document.createElement("div");
            wrapper.className = "shiki-wrapper";
            wrapper.innerHTML = \`
              <div class="shiki-header">
                <span class="shiki-lang">\${displayLang}</span>
                <button class="shiki-copy">Copy code</button>
              </div>
              \${html}
            \`;

            const pre = block.closest('pre') || block.parentElement;
            pre.replaceWith(wrapper);

            const copyBtn = wrapper.querySelector(".shiki-copy");
            const header = wrapper.querySelector(".shiki-header");

            function getButtonRelativePosition() {
              const btnRect = copyBtn.getBoundingClientRect();
              const wrapperRect = wrapper.getBoundingClientRect();
              return {
                top: btnRect.top - wrapperRect.top,
                right: wrapperRect.right - btnRect.right
              };
            }

            function updatePosition() {
              const headerHeight = (${getHeaderHeight.toString()})();
              const wrapperRect = wrapper.getBoundingClientRect();
              const btnHeight = copyBtn.offsetHeight;
              const margin = 7;

              const shouldFloat = wrapperRect.top < headerHeight + margin &&
                                  wrapperRect.bottom > headerHeight + btnHeight + margin;

              if (shouldFloat) {
                if (!copyBtn.classList.contains('floating')) {
                  copyBtn.classList.add('floating');
                  copyBtn.style.position = 'fixed';
                  copyBtn.style.zIndex = '999';
                }
                const targetTop = Math.min(
                  Math.max(headerHeight + margin, wrapperRect.top + margin),
                  wrapperRect.bottom - btnHeight - margin
                );
                const targetRight = window.innerWidth - wrapperRect.right + 8;
                copyBtn.style.top = targetTop + 'px';
                copyBtn.style.right = targetRight + 'px';
                copyBtn.style.left = 'auto';
              } else {
                if (copyBtn.classList.contains('floating')) {
                  copyBtn.classList.remove('floating');
                  copyBtn.style.position = 'absolute';
                  copyBtn.style.zIndex = '1';
                }
                copyBtn.style.top = margin + 'px';
                copyBtn.style.right = '8px';
                copyBtn.style.left = 'auto';
              }
            }

            let ticking = false;
            const requestTick = () => {
              if (!ticking) {
                requestAnimationFrame(() => {
                  updatePosition();
                  ticking = false;
                });
                ticking = true;
              }
            };

            window.addEventListener("scroll", requestTick, { passive: true });
            window.addEventListener("resize", requestTick, { passive: true });
            setTimeout(updatePosition, 100);
          } catch (error) {
            console.warn('Highlight failed:', error);
          }
        }
      }

      highlight();

      document.addEventListener("click", (e) => {
        if (e.target.classList.contains("shiki-copy")) {
          const code = e.target.closest(".shiki-wrapper").querySelector("code").innerText;
          navigator.clipboard.writeText(code);
          e.target.textContent = "Copied!";
          setTimeout(() => (e.target.textContent = "Copy code"), 1500);
        }
      });
    `;
    document.head.appendChild(script);

    // Styles
    GM_addStyle(`
      .shiki-wrapper {
        margin: 1em 0;
        border: 1px solid #171717;
        border-radius: 8px;
        overflow: hidden;
        background: #171717;
        position: relative;
      }
      .shiki-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        background: #171717;
        color: #ccc;
        font-size: 12px;
        font-family: sans-serif;
        padding: 0.3em 0.6em;
        position: relative;
      }
      .shiki-header .shiki-lang {
        user-select: none;
        -webkit-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        font-weight: bold;
        color: #bbb;
        flex: 1;
      }
      .shiki-header .shiki-copy {
        background: #171717;
        border-radius: 3px;
        border: none;
        color: #bbb;
        cursor: pointer;
        font-size: 12px;
        padding: 2px 6px;
        position: absolute;
        top: 7px;
        right: 8px;
        z-index: 1;
        margin-left: auto;
      }
      .shiki-header .shiki-copy:hover {
        color: white;
      }
      pre.shiki {
        margin: 0;
        padding: 1em;
        overflow-x: auto;
        font-size: 14px;
        line-height: 1.5;
        border: 1px solid #171717;
        background: none !important;
      }
      pre.shiki code {
        background: none !important;
      }
      .source.markdown .highlighted-code .source code {
        background: none !important;
        color: inherit !important;
      }
    `);
  }

  init();
})();