记录一次对网页自定义样式的尝试

起因

SYZOJ 是一个用于算法竞赛的在线评测系统,目前由 LibreOJ 维护。作者学校使用远古版本的 SYZOJ,只有默认的亮色主题。作者希望将主题修改为类似 LOJ 的暗色主题,于是有了本文所记录的尝试。

1. simple CSS

对于页面上简单的 CSS,我们可以直接参考隔壁 LOJ 的样式表,直接随便打开一个 LOJ 页面,就能看到类似对 https://static.cdn.menci.xyz/libreoj-frontend/assets/index.a6df240d.css 的请求,去把想要的代码拷贝下来即可。

发现 html 结构有些许不同,所以需要略做修改。

自定义页面 CSS 可以通过浏览器插件实现,例如:Stylus

2. simple: custom code highlight

submission 部分的代码还是保留原本的高亮颜色,希望修改成好看的暗色主题。

观察发现,渲染该部分代码的 css 由 /self/tomorrow.css 导入,前几行为:

.hll { background-color: #d6d6d6 }
.pl-c { color: #8e908c } /* Comment */
.pl-err { color: #c82829 } /* Error */
.pl-k { color: #8959a8 } /* Keyword */
.pl-l { color: #f5871f } /* Literal */

和想要的主题一起喂给 AI,就能得到客制化的代码高亮颜色主题。例如本博客采用的 highlight.js 中 Atom One Dark 主题,link

3. advanced: monaco-editor

part one: editor under problems

还剩下题目下方代码提交部分还是白色的。

查看源码发现是 monaco-editor,并且将 editor 赋给了 window.editor,这留给我们很大的操作空间。纯用 css 实现是不可能了,想到可以在 tampermonkey 上写 JS。

本以为直接 monaco.editor.setTheme('vs-dark') 就行了,但是诡异的一幕出现了:

检查 html 结构发现好像是 parser 的问题,我修改主题咋影响代码解析了?

例如,for (int i = 1; i <= n; ++i) { 对应 html 结构变成了(增强了可读性):

<span>
    <span class="mtk8">for </span>
    <span class="mtk1"> (int i </span>
    <span class="mtk8">= </span>
    <span class="mtk1"> </span>
    <span class="mtk8">1 </span>
    <span class="mtk1">; i </span>
    <span class="mtk8">&lt;= </span>
    <span class="mtk1"> n; </span>
    <span class="mtk8">++ </span>
    <span class="mtk1">i) { </span>
</span>

而正常情况下应该为:

<span>
    <span class="mtk21">for </span>
    <span class="mtk1"> ( </span>
    <span class="mtk21">int </span>
    <span class="mtk1"> i </span>
    <span class="mtk13">= </span>
    <span class="mtk1"> </span>
    <span class="mtk11">1 </span>
    <span class="mtk1">; i </span>
    <span class="mtk13">&lt;= </span>
    <span class="mtk1"> n; </span>
    <span class="mtk13">++ </span>
    <span class="mtk1">i) { </span>
</span>

可以从 class 明显发现差异,怎么只剩下 mtk8mtk1 了?

试了一下,发现设置主题为 vs 就没有问题,这就很神奇了。

找到关于 monaco-editor 的部分。

<!-- Load monaco-editor -->
<script src="/cdnjs/monaco-editor/0.18.1/min/vs/loader.js"></script>
<script src="/self/monaco-editor.js"></script>
<link rel="stylesheet" href="/self/monaco-editor.css">

去看这个 monaco-editor.js 是个啥。

require.config({
  paths: {
    vs: window.pathLib + "monaco-editor/0.18.1/min/vs",
    tokenizer: window.pathSelfLib + 'vendor/tokenizer'
  }
});

window.onEditorLoaded = function (fn) {
  if (window.editorLoaded) {
    fn();
  } else {
    if (!window.editorLoadedHandles) window.editorLoadedHandles = [];
    window.editorLoadedHandles.push(fn);
  }
};

require(['vs/editor/editor.main'], function () {
  require(['tokenizer/monaco-tokenizer',
           'tokenizer/definitions/c_cpp',
           'tokenizer/definitions/csharp',
           'tokenizer/definitions/haskell',
           'tokenizer/definitions/java',
           'tokenizer/definitions/javascript',
           'tokenizer/definitions/pascal',
           'tokenizer/definitions/python',
           'tokenizer/definitions/ruby'],
    function(MonacoAceTokenizer,
             c_cppDefinition,
             CSharpDefinition,
             HaskellDefinition,
             JavaDefinition,
             JavaScriptDefinition,
             PascalDefinition,
             PythonDefinition,
             RubyDefinition) {
      var overrideLangauges = [
        'cpp',
        'c',
        'csharp',
        'haskell',
        'java',
        'javascript',
        'pascal',
        'python',
        'ruby',
        'markdown'
      ];

      monaco.languages.getLanguages().forEach(function (lang) {
        if (overrideLangauges.includes(lang.id)) {
          lang.loader = function () {
            return { then: function () {} };
          };
        }
      });

      var cppAliases = ['c', 'cpp', 'c++', 'cxx', 'cc'];
      for (var i in cppAliases) {
        var alias = cppAliases[i];

        monaco.languages.register({ id: alias });
        MonacoAceTokenizer.registerRulesForLanguage(alias, new c_cppDefinition.default);
        monaco.languages.setLanguageConfiguration(alias, {
          comments: {
            lineComment: '//',
            blockComment: ['/*', '*/'],
          },
          brackets: [
            ['{', '}'],
            ['[', ']'],
            ['(', ')']
          ],
          autoClosingPairs: [
            { open: '[', close: ']' },
            { open: '{', close: '}' },
            { open: '(', close: ')' },
            { open: '\'', close: '\'', notIn: ['string', 'comment'] },
            { open: '"', close: '"', notIn: ['string'] },
          ],
          surroundingPairs: [
            { open: '{', close: '}' },
            { open: '[', close: ']' },
            { open: '(', close: ')' },
            { open: '"', close: '"' },
            { open: '\'', close: '\'' },
          ],
          folding: {
            markers: {
              start: new RegExp("^\\s*#pragma\\s+region\\b"),
              end: new RegExp("^\\s*#pragma\\s+endregion\\b")
            }
          }
        });
      }

      var csharpAliases = ['csharp', 'cs', 'c#'];
      for (var i in csharpAliases) {
        var alias = csharpAliases[i];

        monaco.languages.register({ id: alias });
        MonacoAceTokenizer.registerRulesForLanguage(alias, new CSharpDefinition.default);
        monaco.languages.setLanguageConfiguration(alias, {
          wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
          comments: {
              lineComment: '//',
              blockComment: ['/*', '*/'],
          },
          brackets: [
              ['{', '}'],
              ['[', ']'],
              ['(', ')'],
          ],
          autoClosingPairs: [
              { open: '{', close: '}' },
              { open: '[', close: ']' },
              { open: '(', close: ')' },
              { open: '\'', close: '\'', notIn: ['string', 'comment'] },
              { open: '"', close: '"', notIn: ['string', 'comment'] },
          ],
          surroundingPairs: [
              { open: '{', close: '}' },
              { open: '[', close: ']' },
              { open: '(', close: ')' },
              { open: '<', close: '>' },
              { open: '\'', close: '\'' },
              { open: '"', close: '"' },
          ],
          folding: {
              markers: {
                  start: new RegExp("^\\s*#region\\b"),
                  end: new RegExp("^\\s*#endregion\\b")
              }
          }
        });
      }

      monaco.languages.register({ id: 'haskell' });
      MonacoAceTokenizer.registerRulesForLanguage('haskell', new HaskellDefinition.default);
      monaco.languages.setLanguageConfiguration('haskell', {
        comments: {
            lineComment: '--',
            blockComment: ['{-', '-}']
        },
        brackets: [
           ['{', '}'],
           ['[', ']'],
           ['(', ')']
        ],
        autoClosingPairs: [
            { open: '{', close: '}' },
            { open: '[', close: ']' },
            { open: '(', close: ')' },
            { open: '\'', close: '\'', notIn: ['string'] },
            { open: '`', close: '`', notIn: ['string', 'comment'] }
        ],
        surroundingPairs: [
            ['{', '}'],
            ['[', ']'],
            ['(', ')'],
            ['\'', '\''],
            ['"', '"'],
            ['`', '`']
        ],
        indentationRules: {
            decreaseIndentPattern: new RegExp("[\\]})][ \\t]*$/m"),
            increaseIndentPattern: new RegExp("((\\b(if\\b.*|then|else|do|of|let|in|where))|=|->|>>=|>=>|=<<|(^(data)( |\t)+(\\w|')+( |\\t)*))( |\\t)*$/")
        }
      });

      monaco.languages.register({ id: 'java' });
      MonacoAceTokenizer.registerRulesForLanguage('java', new JavaDefinition.default);
      monaco.languages.setLanguageConfiguration('java', {
        // the default separators except `@$`
        wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
        comments: {
            lineComment: '//',
            blockComment: ['/*', '*/'],
        },
        brackets: [
            ['{', '}'],
            ['[', ']'],
            ['(', ')'],
        ],
        autoClosingPairs: [
            { open: '{', close: '}' },
            { open: '[', close: ']' },
            { open: '(', close: ')' },
            { open: '"', close: '"' },
            { open: '\'', close: '\'' },
        ],
        surroundingPairs: [
            { open: '{', close: '}' },
            { open: '[', close: ']' },
            { open: '(', close: ')' },
            { open: '"', close: '"' },
            { open: '\'', close: '\'' },
            { open: '<', close: '>' },
        ],
        folding: {
            markers: {
                start: new RegExp("^\\s*//\\s*(?:(?:#?region\\b)|(?:<editor-fold\\b))"),
                end: new RegExp("^\\s*//\\s*(?:(?:#?endregion\\b)|(?:</editor-fold>))")
            }
        }
      });

      var javascriptAliases = ['javascript', 'js'];
      for (var i in javascriptAliases) {
        var alias = javascriptAliases[i];
        monaco.languages.register({ id: alias });
        MonacoAceTokenizer.registerRulesForLanguage(alias, new JavaScriptDefinition.default);
        monaco.languages.setLanguageConfiguration(alias, {
          wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
          comments: {
              lineComment: '//',
              blockComment: ['/*', '*/']
          },
          brackets: [
              ['{', '}'],
              ['[', ']'],
              ['(', ')']
          ],
          onEnterRules: [
              {
                  // e.g. /** | */
                  beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
                  afterText: /^\s*\*\/$/,
                  action: { indentAction: monaco.languages.IndentAction.IndentOutdent, appendText: ' * ' }
              },
              {
                  // e.g. /** ...|
                  beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
                  action: { indentAction: monaco.languages.IndentAction.None, appendText: ' * ' }
              },
              {
                  // e.g.  * ...|
                  beforeText: /^(\t|(\ \ ))*\ \*(\ ([^\*]|\*(?!\/))*)?$/,
                  action: { indentAction: monaco.languages.IndentAction.None, appendText: '* ' }
              },
              {
                  // e.g.  */|
                  beforeText: /^(\t|(\ \ ))*\ \*\/\s*$/,
                  action: { indentAction: monaco.languages.IndentAction.None, removeText: 1 }
              }
          ],
          autoClosingPairs: [
              { open: '{', close: '}' },
              { open: '[', close: ']' },
              { open: '(', close: ')' },
              { open: '"', close: '"', notIn: ['string'] },
              { open: '\'', close: '\'', notIn: ['string', 'comment'] },
              { open: '`', close: '`', notIn: ['string', 'comment'] },
              { open: "/**", close: " */", notIn: ["string"] }
          ],
          folding: {
              markers: {
                  start: new RegExp("^\\s*//\\s*#?region\\b"),
                  end: new RegExp("^\\s*//\\s*#?endregion\\b")
              }
          }
        });
      }

      var pascalAliases = ['pascal', 'pas'];
      for (var i in pascalAliases) {
        var alias = pascalAliases[i];

        monaco.languages.register({ id: alias });
        MonacoAceTokenizer.registerRulesForLanguage(alias, new PascalDefinition.default);
        monaco.languages.setLanguageConfiguration(alias, {
          // the default separators except `@$`
          wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
          comments: {
            lineComment: '//',
            blockComment: ['{', '}'],
          },
          brackets: [
            ['{', '}'],
            ['[', ']'],
            ['(', ')'],
            ['<', '>'],
          ],
          autoClosingPairs: [
            { open: '{', close: '}' },
            { open: '[', close: ']' },
            { open: '(', close: ')' },
            { open: '<', close: '>' },
            { open: '\'', close: '\'' },
          ],
          surroundingPairs: [
            { open: '{', close: '}' },
            { open: '[', close: ']' },
            { open: '(', close: ')' },
            { open: '<', close: '>' },
            { open: '\'', close: '\'' },
          ],
          folding: {
            markers: {
              start: new RegExp("^\\s*\\{\\$REGION(\\s\\'.*\\')?\\}"),
              end: new RegExp("^\\s*\\{\\$ENDREGION\\}")
            }
          }
        });
      }

      var pythonAliases = ['python', 'python2', 'python3', 'py', 'py2', 'py3'];
      for (var i in pythonAliases) {
        var alias = pythonAliases[i];

        monaco.languages.register({ id: alias });
        MonacoAceTokenizer.registerRulesForLanguage(alias, new PythonDefinition.default);
        monaco.languages.setLanguageConfiguration(alias, {
          comments: {
              lineComment: '#',
              blockComment: ['\'\'\'', '\'\'\''],
          },
          brackets: [
              ['{', '}'],
              ['[', ']'],
              ['(', ')']
          ],
          autoClosingPairs: [
              { open: '{', close: '}' },
              { open: '[', close: ']' },
              { open: '(', close: ')' },
              { open: '"', close: '"', notIn: ['string'] },
              { open: '\'', close: '\'', notIn: ['string', 'comment'] },
          ],
          surroundingPairs: [
              { open: '{', close: '}' },
              { open: '[', close: ']' },
              { open: '(', close: ')' },
              { open: '"', close: '"' },
              { open: '\'', close: '\'' },
          ],
          onEnterRules: [
              {
                  beforeText: new RegExp("^\\s*(?:def|class|for|if|elif|else|while|try|with|finally|except|async).*?:\\s*$"),
                  action: { indentAction: monaco.languages.IndentAction.Indent }
              }
          ],
          folding: {
              offSide: true,
              markers: {
                  start: new RegExp("^\\s*#region\\b"),
                  end: new RegExp("^\\s*#endregion\\b")
              }
          }
        });
      }

      monaco.languages.register({ id: 'ruby' });
      MonacoAceTokenizer.registerRulesForLanguage('ruby', new RubyDefinition.default);
      monaco.languages.setLanguageConfiguration('ruby', {
        comments: {
            lineComment: '#',
            blockComment: ['=begin', '=end'],
        },
        brackets: [
            ['(', ')'],
            ['{', '}'],
            ['[', ']']
        ],
        autoClosingPairs: [
            { open: '{', close: '}' },
            { open: '[', close: ']' },
            { open: '(', close: ')' },
            { open: '"', close: '"' },
            { open: '\'', close: '\'' },
        ],
        surroundingPairs: [
            { open: '{', close: '}' },
            { open: '[', close: ']' },
            { open: '(', close: ')' },
            { open: '"', close: '"' },
            { open: '\'', close: '\'' },
        ],
        indentationRules: {
            increaseIndentPattern: new RegExp('^\\s*((begin|class|(private|protected)\\s+def|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|while|case)|([^#]*\\sdo\\b)|([^#]*=\\s*(case|if|unless)))\\b([^#\\{;]|("|\'|\/).*\\4)*(#.*)?$'),
            decreaseIndentPattern: new RegExp('^\\s*([}\\]]([,)]?\\s*(#|$)|\\.[a-zA-Z_]\\w*\\b)|(end|rescue|ensure|else|elsif|when)\\b)'),
        }
      });

      var markdownAliases = ['markdown', 'md'];
      for (var i in markdownAliases) {
        var alias = markdownAliases[i];

        monaco.languages.register({ id: alias });
        monaco.languages.setLanguageConfiguration(alias, {
          comments: {
            blockComment: ['<!--', '-->',]
          },
          brackets: [
            ['{', '}'],
            ['[', ']'],
            ['(', ')']
          ],
          autoClosingPairs: [
            { open: '{', close: '}' },
            { open: '[', close: ']' },
            { open: '(', close: ')' },
            { open: '<', close: '>', notIn: ['string'] }
          ],
          surroundingPairs: [
            { open: '(', close: ')' },
            { open: '[', close: ']' },
            { open: '`', close: '`' },
          ],
          folding: {
            markers: {
              start: new RegExp("^\\s*<!--\\s*#?region\\b.*-->"),
              end: new RegExp("^\\s*<!--\\s*#?endregion\\b.*-->")
            }
          }
        });
        monaco.languages.setMonarchTokensProvider(alias, {
          defaultToken: '',
          tokenPostfix: '.md',

          // escape codes
          control: /[\\`*_\[\]{}()#+\-\.!\$]/,
          noncontrol: /[^\\`*_\[\]{}()#+\-\.!\$]/,
          escapes: /\\(?:@control)/,

          // escape codes for javascript/CSS strings
          jsescapes: /\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,

          // non matched elements
          empty: [
            'area', 'base', 'basefont', 'br', 'col', 'frame',
            'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param'
          ],

          tokenizer: {
            root: [

              // headers (with #)
              [/^(\s{0,3})(#+)((?:[^\\#]|@escapes)+)((?:#+)?)/, ['white', 'keyword', 'keyword', 'keyword']],

              // headers (with =)
              [/^\s*(=+|\-+)\s*$/, 'keyword'],

              // headers (with ***)
              [/^\s*((\*[ ]?)+)\s*$/, 'meta.separator'],

              // quote
              [/^\s*>+/, 'comment'],

              // list (starting with * or number)
              [/^\s*([\*\-+:]|\d+\.)\s/, 'keyword'],

              // code block (4 spaces indent)
              [/^(\t|[ ]{4})[^ ].*$/, 'string'],

              // code block (3 tilde)
              [/^\s*~~~\s*((?:\w|[\/\-#])+)?\s*$/, { token: 'string', next: '@codeblock' }],

              // display math
              [/\$\$/, { token: 'string', next: '@displaymath' }],

              // github style code blocks (with backticks and language)
              [/^\s*```\s*((?:\w|[\/\-#\+])+)\s*$/, { token: 'string', next: '@codeblockgh', nextEmbedded: '$1' }],

              // github style code blocks (with backticks but no language)
              [/^\s*```\s*$/, { token: 'string', next: '@codeblock' }],

              // markup within lines
              { include: '@linecontent' },
            ],

            displaymath: [
              [/\\\$/, 'variable.source'],
              [/\$\$/, { token: 'string', next: '@pop' }],
              [/./, 'variable.source']
            ],

            codeblock: [
              [/^\s*~~~\s*$/, { token: 'string', next: '@pop' }],
              [/^\s*```\s*$/, { token: 'string', next: '@pop' }],
              [/.*$/, 'variable.source'],
            ],

            // github style code blocks
            codeblockgh: [
              [/```\s*$/, { token: 'variable.source', next: '@pop', nextEmbedded: '@pop' }],
              [/[^`]+/, 'variable.source'],
            ],

            linecontent: [

              // inline math
              [/\$/, { token: 'string', next: '@inlinemath' }],

              // escapes
              [/&\w+;/, 'string.escape'],
              [/@escapes/, 'escape'],

              // various markup
              [/\b__([^\\_]|@escapes|_(?!_))+__\b/, 'strong'],
              [/\*\*([^\\*]|@escapes|\*(?!\*))+\*\*/, 'strong'],
              [/\b_[^_]+_\b/, 'emphasis'],
              [/\*([^\\*]|@escapes)+\*/, 'emphasis'],
              [/`([^\\`]|@escapes)+`/, 'variable'],

              // links
              [/\{+[^}]+\}+/, 'string.target'],
              [/(!?\[)((?:[^\]\\]|@escapes)*)(\]\([^\)]+\))/, ['string.link', '', 'string.link']],
              [/(!?\[)((?:[^\]\\]|@escapes)*)(\])/, 'string.link'],

              // or html
              { include: 'html' },
            ],

            inlinemath: [
              [/\\\$/, 'variable.source'],
              [/\$/, { token: 'string', next: '@pop' }],
              [/./, 'variable.source']
            ],

            // Note: it is tempting to rather switch to the real HTML mode instead of building our own here
            // but currently there is a limitation in Monarch that prevents us from doing it: The opening
            // '<' would start the HTML mode, however there is no way to jump 1 character back to let the
            // HTML mode also tokenize the opening angle bracket. Thus, even though we could jump to HTML,
            // we cannot correctly tokenize it in that mode yet.
            html: [
              // html tags
              [/<(\w+)\/>/, 'tag'],
              [/<(\w+)/, {
                cases: {
                  '@empty': { token: 'tag', next: '@tag.$1' },
                  '@default': { token: 'tag', next: '@tag.$1' }
                }
              }],
              [/<\/(\w+)\s*>/, { token: 'tag' }],

              [/<!--/, 'comment', '@comment']
            ],

            comment: [
              [/[^<\-]+/, 'comment.content'],
              [/-->/, 'comment', '@pop'],
              [/<!--/, 'comment.content.invalid'],
              [/[<\-]/, 'comment.content']
            ],

            // Almost full HTML tag matching, complete with embedded scripts & styles
            tag: [
              [/[ \t\r\n]+/, 'white'],
              [/(type)(\s*=\s*)(")([^"]+)(")/, ['attribute.name.html', 'delimiter.html', 'string.html',
                { token: 'string.html', switchTo: '@tag.$S2.$4' },
                'string.html']],
              [/(type)(\s*=\s*)(')([^']+)(')/, ['attribute.name.html', 'delimiter.html', 'string.html',
                { token: 'string.html', switchTo: '@tag.$S2.$4' },
                'string.html']],
              [/(\w+)(\s*=\s*)("[^"]*"|'[^']*')/, ['attribute.name.html', 'delimiter.html', 'string.html']],
              [/\w+/, 'attribute.name.html'],
              [/\/>/, 'tag', '@pop'],
              [/>/, {
                cases: {
                  '$S2==style': { token: 'tag', switchTo: 'embeddedStyle', nextEmbedded: 'text/css' },
                  '$S2==script': {
                    cases: {
                      '$S3': { token: 'tag', switchTo: 'embeddedScript', nextEmbedded: '$S3' },
                      '@default': { token: 'tag', switchTo: 'embeddedScript', nextEmbedded: 'text/javascript' }
                    }
                  },
                  '@default': { token: 'tag', next: '@pop' }
                }
              }],
            ],

            embeddedStyle: [
              [/[^<]+/, ''],
              [/<\/style\s*>/, { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }],
              [/</, '']
            ],

            embeddedScript: [
              [/[^<]+/, ''],
              [/<\/script\s*>/, { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }],
              [/</, '']
            ],
          }
        });
      }

      function autoLayout(editor) {
        window.addEventListener('resize', function () {
          editor.layout();
        });
      }

      $.getScript(window.pathSelfLib + "monaco-editor-tomorrow.js", function () {
        window.createCodeEditor = function (editorElement, langauge, content) {
          editorElement.innerHTML = '';
          var editor = monaco.editor.create(editorElement, {
            value: content,
            language: langauge,
            multicursorModifier: 'ctrlCmd',
            cursorWidth: 1,
            theme: 'tomorrow',
            lineHeight: 22,
            fontSize: 14,
            fontFamily: "'Fira Mono', 'Bitstream Vera Sans Mono', 'Menlo', 'Consolas', 'Lucida Console', 'Source Han Sans SC', 'Noto Sans CJK SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft Yahei', monospace",
            lineNumbersMinChars: 4,
            glyphMargin: false,
            renderFinalNewline: true,
            scrollbar: {
              useShadows: false,
              verticalScrollbarSize: 0,
              vertical: 'hidden'
            },
            overviewRulerBorder: false,
            hideCursorInOverviewRuler: true,
            contextmenu: false
          });

          autoLayout(editor);
          return editor;
        };

        window.createMarkdownEditor = function (wrapperElement, content, input) {
          wrapperElement.innerHTML = '';
          var editorElement = document.createElement('div');
          editorElement.classList.add('editor-wrapped');
          wrapperElement.appendChild(editorElement);
          var editor = monaco.editor.create(editorElement, {
            value: content,
            language: 'markdown',
            multicursorModifier: 'ctrlCmd',
            cursorWidth: 1,
            theme: 'tomorrow',
            fontSize: 14,
            fontFamily: "'Fira Mono', 'Source Han Sans SC', 'Noto Sans CJK SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft Yahei', monospace",
            lineNumbersMinChars: 4,
            glyphMargin: false,
            lineNumbers: false,
            folding: false,
            minimap: {
              enabled: false
            },
            hover: {
              enabled: false
            },
            wordWrap: "on",
            renderIndentGuides: false,
            renderFinalNewline: false,
            wordBasedSuggestions: false,
            renderLineHighlight: false,
            occurrencesHighlight: false,
            scrollbar: {
              useShadows: false,
              vertical: 'auto',
              verticalScrollbarSize: 10
            },
            overviewRulerBorder: false,
            hideCursorInOverviewRuler: true,
            contextmenu: false
          });

          input.form.addEventListener('submit', function () {
            input.value = editor.getValue();
          });

          autoLayout(editor);

          return editor;
        };

        window.editorLoaded = true;
        for (var i in window.editorLoadedHandles) {
          window.editorLoadedHandles[i]();
        }
      });
    }
  );
});

发现 $.getScript(window.pathSelfLib + "monaco-editor-tomorrow.js" 动态引入了一个 js,内容为:

monaco.editor.defineTheme('tomorrow', {
    "base": "vs",
    "inherit": true,
    "rules": [
        {
            "foreground": "8e908c",
            "token": "comment"
        },
        {
            "foreground": "666969",
            "token": "keyword.operator.class"
        },
        {
            "foreground": "666969",
            "token": "constant.other"
        },
        {
            "foreground": "666969",
            "token": "source.php.embedded.line"
        },
        {
            "foreground": "c82829",
            "token": "variable"
        },
        {
            "foreground": "c82829",
            "token": "support.other.variable"
        },
        {
            "foreground": "c82829",
            "token": "string.other.link"
        },
        {
            "foreground": "c82829",
            "token": "string.regexp"
        },
        {
            "foreground": "c82829",
            "token": "entity.name.tag"
        },
        {
            "foreground": "c82829",
            "token": "entity.other.attribute-name"
        },
        {
            "foreground": "c82829",
            "token": "meta.tag"
        },
        {
            "foreground": "c82829",
            "token": "declaration.tag"
        },
        {
            "foreground": "f5871f",
            "token": "constant.numeric"
        },
        {
            "foreground": "f5871f",
            "token": "constant.language"
        },
        {
            "foreground": "f5871f",
            "token": "support.constant"
        },
        {
            "foreground": "f5871f",
            "token": "constant.character"
        },
        {
            "foreground": "f5871f",
            "token": "variable.parameter"
        },
        {
            "foreground": "f5871f",
            "token": "punctuation.section.embedded"
        },
        {
            "foreground": "f5871f",
            "token": "keyword.other.unit"
        },
        {
            "foreground": "c99e00",
            "token": "entity.name.class"
        },
        {
            "foreground": "c99e00",
            "token": "entity.name.type.class"
        },
        {
            "foreground": "c99e00",
            "token": "support.type"
        },
        {
            "foreground": "c99e00",
            "token": "support.class"
        },
        {
            "foreground": "718c00",
            "token": "string"
        },
        {
            "foreground": "718c00",
            "token": "constant.other.symbol"
        },
        {
            "foreground": "718c00",
            "token": "entity.other.inherited-class"
        },
        {
            "foreground": "718c00",
            "token": "markup.heading"
        },
        {
            "foreground": "3e999f",
            "token": "keyword.operator"
        },
        {
            "foreground": "3e999f",
            "token": "constant.other.color"
        },
        {
            "foreground": "4271ae",
            "token": "entity.name.function"
        },
        {
            "foreground": "4271ae",
            "token": "meta.function-call"
        },
        {
            "foreground": "4271ae",
            "token": "support.function"
        },
        {
            "foreground": "4271ae",
            "token": "keyword.other.special-method"
        },
        {
            "foreground": "4271ae",
            "token": "meta.block-level"
        },
        {
            "foreground": "8959a8",
            "token": "keyword"
        },
        {
            "foreground": "8959a8",
            "token": "storage"
        },
        {
            "foreground": "8959a8",
            "token": "storage.type"
        },
        {
            "foreground": "373b41",
            "background": "e0e0e0",
            "token": "meta.separator"
        }
    ],
    "colors": {
        "editor.foreground": "#4D4D4C",
        "editor.background": "#FFFFFF",
        "editor.selectionBackground": "#D6D6D6",
        "editor.lineHighlightBackground": "#EFEFEF",
        "editorCursor.foreground": "#AEAFAD",
        "editorWhitespace.foreground": "#D1D1D1"
    }
});

这就是它 tomorrow 主题的实现吧。那我类似它重新 define 一个主题不就也许可行了?去 github 上找到了对 vs-dark 的代码,link。发现没有效果,和直接使用 vs-dark 一样有问题。

转念一想,应该思考为什么解析变得奇怪了。回头看 monaco-editor.js。

require.config 没问题,window.onEditorLoaded 的实现也没问题,接下来一段巨长的代码,都在重新 overrideLangauges。直接用不行吗?也许是为了效率?删掉试试呢?果然,打住断点,删掉这些代码,就能正常使用 monaco.editor.setTheme 了,当然,需要删掉引入的 /self/monaco-editor.css。

虽说没有找到问题的本质,好歹找到了一种办法。

一开始,我想的是,拦截这个 script 的加载。发现通过 tampermonkey 的 @run-at document-start 可以在一定概率下,抢在 script 标签加载执行前,执行我的代码。让 AI 给我写了一个拦截该 script 加载的代码:

// ==UserScript==
// @name         Block Specific Script
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  阻止某个指定的 script 加载或执行
// @author       You
// @match        *://*/*
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    const blockedScriptSrcPart = 'example.com/badscript.js'; // 你想阻止的 script 的部分 URL

    const observer = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
            for (const node of mutation.addedNodes) {
                if (node.tagName === 'SCRIPT') {
                    const script = node;
                    // 如果匹配目标 script
                    if (script.src && script.src.includes(blockedScriptSrcPart)) {
                        console.log('Blocking script:', script.src);
                        script.type = 'javascript/blocked'; // 更改 type 避免执行
                        script.parentElement.removeChild(script); // 可选:直接移除
                    }
                }
            }
        }
    });

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

好像就成功拦截了,接下来就要复刻其效果,除了对语言的 override。但是我学艺不精,发现以下代码永远不输出 'A'

(async function() {
    /* load monaco-editor */
    const requireScript = document.createElement('script');
    requireScript.type = 'text/javascript';
    requireScript.src = '/cdnjs/monaco-editor/0.18.1/min/vs/loader.js';

    await new Promise((resolve) => {
        requireScript.onload = resolve;
        document.head.appendChild(requireScript);
    });

    console.log('loaded', require);

    require.config({
        paths: {
            vs: "/cdnjs/monaco-editor/0.18.1/min/vs",
            tokenizer: '/self/vendor/tokenizer'
        }
    });

    require(['vs/editor/editor.main'], function() {
        console.log('A');
    });
})();

问 AI,发现和 AMD 加载器的 require 没有准备好有关。作者又没有系统学习过,不会解决了。

想不到解决方法了,而且本身 run-at 抢先在 script 加载前就是一个概率事件,有时候不成功。太不优雅了。

只好请出了最不是办法的办法:套一层 iframe

代码写出来倒是十分顺利的。

有一个细节就是要把 iframe 放到页面上,才可以得到 iframe.contentWindow。还有对 iframe 里的 editor 操作要用 iframe 里的 monaco。

const createEditorIframe = function (code, lang, $parent, config) {
    const $iframe = $('<iframe>')
        .attr('class', 'custom-editor')
        .appendTo($parent);
    const iframe = $iframe[0];
    const win = iframe.contentWindow;
    const doc = iframe.contentDocument || iframe.contentWindow.document;

    doc.open();
    doc.write(`
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <style>
            html, body, #container {
                margin: 0;
                height: 100%;
                width: 100%;
                overflow: hidden;
            }
        </style>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
    </head>
    <body>
        <div id="container"></div>
    </body>
    </html>
    `);
    doc.close();

    return new Promise((resolve) => {
        const tryInit = () => {
            if (typeof win.require === 'undefined') {
                setTimeout(tryInit, 50); // retry
                return;
            }

            win.require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@0.44.0/min/vs' } });
            win.require(['vs/editor/editor.main'], function () {
                const editor = win.monaco.editor.create(doc.getElementById('container'), {
                    value: code,
                    language: lang,
                    theme: 'vs-dark',
                    contextmenu: false,
                    renderFinalNewline: true,
                    ...config
                });

                resolve({
                    $iframe: $iframe,
                    editor: editor,
                    winMonaco: win.monaco
                });
            });
        };

        tryInit();
    });
};
const problemEditor = async function () {
    const code = unsafeWindow.editor.getValue();
    const lang = unsafeWindow.editor.getModel().getLanguageIdentifier().language;

    const { $iframe, editor, winMonaco } = await createEditorIframe(code, lang, $('#editor'));

    unsafeWindow.editor.dispose();
    unsafeWindow.editor = editor;

    $('#languages-menu .item').click(function () {
        winMonaco.editor.setModelLanguage(editor.getModel(), $(this).data('mode'));
    });
};

if (typeof unsafeWindow.onEditorLoaded === 'function')
    unsafeWindow.onEditorLoaded(problemEditor);

值得一提的是,一开始我并没有分离出 create 的逻辑,而是放在一起处理。这就导致我顺手将原先 editor 的 dispose 放在了获得 code,lang 之后。导致直到执行 const editor = win.monaco.editor.create 之前,原先的那个 editor 已经 dispose 了。console 里出现了 Error: Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!。头脑发热,没有想到把 dispose 往后挪,而是想,能不能等到对原先 editor 处理结束了,再进行替换。观察以下逻辑:

window.editorLoaded = true;
for (var i in window.editorLoadedHandles) {
  window.editorLoadedHandles[i]();
}

猜测也许是替换函数在 editorLoadedHandles 的位置相较于原先处理逻辑靠前了。那我先往里面插入一个临时函数,执行这个临时函数的时候,往里面重新 push 一个真正的函数。但是这显然是不正确的,新加入的元素不会被 for 遍历到。问半天 AI,给出了一种把列表代理掉的方式,在运行到 for-in 的时候,不去执行 for-in 的内容,而是运行一个回调函数,手动先处理完列表里的函数,再进行最终替换。

function createList(callback) {
  const data = [];

  const handler = {
    ownKeys(target) {
      // 当执行 for...in 时,ownKeys 会被调用
      callback();
      // 返回空数组,防止遍历任何属性
      return [];
    },
    get(target, prop, receiver) {
      if (prop === 'push') {
        return function(...args) {
          data.push(...args);
        };
      }
      if (prop === 'length') {
        return data.length;
      }
      if (typeof prop === 'string' && /^[0-9]+$/.test(prop)) {
        return data[prop];
      }
      return Reflect.get(target, prop, receiver);
    },
    has(target, prop) {
      if (prop in data) return true;
      return prop in target;
    }
  };

  return new Proxy({}, handler);
}

// 用法示例:
const list = createList(() => {
  console.log('触发了for...in的回调');
});

list.push(1);
list.push(2);

for (let i in list) {
  // 这里不会真正遍历属性,而是执行回调
  console.log(i); // 不会输出任何东西
}

发现还是不行。受不了了,为了解除控制台的一个不和谐的报错,大费周章,太不优雅了。最后才想到把 dispose 往后移,还是缺少经验啊。

part two: read-only editor in submissions

注意到 \(\frac{2}{3}\) 的 OJ 的 submission 部分有代码高亮,\(\frac{1}{3}\) 的 OJ 没有对 submission 的代码高亮,那为什么不顺便把所有的 submission 都用 monaco-editor 呢?

发现获取到代码和对应的语言存在困难。想要的值分别存在全局的 const 变量 unformattedCode,formattedCode,vueApp 之中。可是我 tampermonkey 拿不到啊。问 AI,给出了一种暴露页面中的变量的方式。就是插入一个 script,其中拿到想要的变量后,存到 window 上,这样就能拿到了。

点击切换是否格式化代码也是简单的。还剩下两个问题。

第一,我希望 editor 能够将代码完整展示出来,滚动的是一整个 editor,而不是滚动 editor 内部。这个可以通过 editor.getContentHeight() 配合设置 css 的 height 即可。

第二,我希望 editor 获取焦点时,不要将页面滚动到 editor 上端。问 AI 发现这不是 monaco-editor 的特性,而是浏览器的默认行为:当一个元素获得焦点,若它当前不完全在可视区域内,就会自动把它滚动进来。那就不去修改了。

const submissionEditor = async function () {
    const script = document.createElement('script');
    script.textContent = `
        // 尝试暴露页面中的变量
        (function() {
            function decodeHtmlEntities(str) {
                const doc = new DOMParser().parseFromString(str, 'text/html');
                return doc.documentElement.textContent;
            }
            window._exposed = {
                formattedCode: formattedCode && decodeHtmlEntities(formattedCode),
                unformattedCode: unformattedCode && decodeHtmlEntities(unformattedCode),
                language: vueApp.roughData.info['language']
            };
        })();
    `;
    document.documentElement.appendChild(script);
    script.remove();

    await new Promise(resolve => setTimeout(resolve, 50));
    const { formattedCode, unformattedCode, language } = unsafeWindow._exposed;

    let lang = undefined;

    if (language.includes('C++')) {
        lang = 'cpp';
    } else if (language.includes('Python')) {
        lang = 'python';
    } else {
        console.warn('unknown language', language);
        return;
    }

    const $div = $('<div>');
    $($('#submission_content pre:has(>code)')[0])
        .replaceWith($div);

    const { $iframe, editor } = await createEditorIframe(formattedCode || unformattedCode, lang, $div,
        {
            minimap: { enabled: false },
            scrollBeyondLastLine: false,
            readOnly: true,
            automaticLayout: false,
            scrollbar: {
                vertical: 'hidden',
                alwaysConsumeMouseWheel: false,
            },
        }
    );

    const updateHeight = () => {
        const contentHeight = editor.getContentHeight() + 5;
        $div.css({ height: `${contentHeight}px` });
        editor.layout();
    };

    updateHeight();

    if (formattedCode) {
        const $btn = $div.parent().find('>a');
        let formatted = true;
        $btn.on('click', () => {
            formatted = !formatted;
            editor.setValue(formatted ? formattedCode : unformattedCode);
            updateHeight();
        });
    }
};

if (window.location.href.includes('submission'))
    $(document).ready(submissionEditor);

part three: a possible solution without iframe

写本文时,发现真正影响的不是对语言的 override,而是以下代码:

monaco.languages.getLanguages().forEach(function (lang) {
  if (overrideLangauges.includes(lang.id)) {
    lang.loader = function () {
      return { then: function () {} };
    };
  }
});

这里把 loader 赋值为一个空的 thenable,去掉这段代码也能得到正确的结果。

那么另一种解决方式就是把这些 loader 重新写回去。不过本着能跑就别动的原则,还是不进行修改了。

代码

只选择 css 的话,题目下方提交代码部分还是原本的 tomorrow 主题,\(\frac{2}{3}\) 的 OJ submission 部分是自定义的 Atom One Dark 主题,\(\frac{1}{3}\) 的 OJ 还是和原先一样没有代码高亮。

或者通过 tampermonkey 添加 JS 代码,注意将文件头的 @match 修改成目标 OJ 域名。选择 JS with CSS 版本,网页加载完成前没有插入 CSS,所以会闪一下亮色主题;选择 JS without CSS 的版本,需要与 CSS 配合使用,体验更佳。

only CSS
/* simple CSS, reference: LOJ */

body {
    background: var(--theme-background,#fff)!important
}

body {
    --theme-background: #222;
    --theme-background-transparent: #222222dd;
    --theme-foreground: rgba(255, 255, 255, .95);
    --theme-foreground-transparent: rgba(255, 255, 255, .95);
    --theme-foreground-disabled: rgba(230, 230, 230, .3);
    --theme-selection-background: rgba(155, 155, 155, .4);
    --theme-border: rgb(70, 70, 70);
    --theme-border-light: rgb(40, 40, 40);
    --theme-border-hover: #5f5f5f;
    --theme-border-active: #6f6f6f;
    --theme-shadow: rgba(24, 26, 28, .95);
    --theme-hyperlink: #61affb;
    --theme-hyperlink-hover: #97cbff;
    --theme-input-placeholder: rgba(191, 191, 191, .87);
    --theme-input-placeholder-focus: rgba(233, 233, 233, .9);
    --theme-input-border-focus: #6f6f6f;
    --theme-button-background: #4f4f4f;
    --theme-button-background-hover: #444;
    --theme-button-background-active: #3f3f3f;
    --theme-button-foreground: var(--theme-foreground);
    --theme-block-header-background: #353637;
    --theme-table-header-background: #28292a;
    --theme-toggle-lane-unchecked: rgba(255, 255, 255, .1);
    --theme-toggle-lane-unchecked-hover: rgba(255, 255, 255, .2);
    --theme-toggle-lane-checked: #0d71bb;
    --theme-toggle-lane-checked-focus: #2185D0;
    --theme-dialog-actions-background: #292a2b;
    --theme-dropdown-item-hover-background: rgb(52, 52, 52);
    --theme-dropdown-item-selected-background: rgb(47, 47, 47);
    --theme-dropdown-message-foreground: rgba(255, 255, 255, .4);
    --theme-menu-item-hover-background: rgb(47, 47, 47);
    --theme-menu-item-active-background: rgb(52, 52, 52);
    --theme-search-result-background-hover: #292a2b;
    --theme-placeholder-segment-background: #292a2b;
    --theme-accordion-non-active: rgba(255, 255, 255, .6);
    --theme-progress-bar-background: rgba(255, 255, 255, .1);
    --theme-table-progress-bar-background: rgba(255, 255, 255, .15);
    --theme-placeholder-background: var(--theme-background);
    --theme-placeholder-image: linear-gradient(to right, rgba(255, 255, 255, .08) 0, rgba(255, 255, 255, .15) 15%, rgba(255, 255, 255, .08) 30%);
    --theme-message-background: #2a2a29;
    --theme-message-foreground: var(--theme-foreground);
    --theme-message-border: var(--theme-border);
    --theme-message-error-background: #6d2727;
    --theme-message-error-foreground: var(--theme-foreground);
    --theme-message-error-border: #c74e4e;
    --theme-message-success-background: #379a3e;
    --theme-message-success-foreground: var(--theme-foreground);
    --theme-message-success-border: #64d22d;
    --theme-message-info-background: #3584a2;
    --theme-message-info-foreground: var(--theme-foreground);
    --theme-message-info-border: #3cc1dc;
    --theme-message-warning-background: #b98c26;
    --theme-message-warning-foreground: var(--theme-foreground);
    --theme-message-warning-border: #c19f57;
    --theme-scrollbar-track: rgba(255, 255, 255, .1);
    --theme-scrollbar-thumb: rgba(255, 255, 255, .25);
    --theme-scrollbar-thumb-inactive: rgba(255, 255, 255, .15);
    --theme-scrollbar-thumb-hover: rgba(255, 255, 255, .35);
    --theme-action-menu-item-background: var(--theme-background);
    --theme-action-menu-item-background-hover: var(--theme-menu-item-hover-background);
    --theme-action-menu-item-foreground: var(--theme-foreground);
    --theme-action-menu-item-foreground-hover: var(--theme-foreground);
    --theme-action-menu-item-side: rgba(210, 210, 210, .15);
    --theme-action-menu-item-side-hover: rgba(255, 255, 255, .95);
    --theme-action-menu-item-important-background: #2185d0;
    --theme-action-menu-item-important-background-hover: #1678c2;
    --theme-action-menu-item-important-foreground: #fff;
    --theme-action-menu-item-important-foreground-hover: #fff;
    --theme-action-menu-item-important-side: rgba(255, 255, 255, .5);
    --theme-action-menu-item-important-side-hover: rgba(255, 255, 255, .7);
    --theme-action-menu-item-dangerous-background: var(--theme-action-menu-item-background);
    --theme-action-menu-item-dangerous-background-hover: rgba(219, 40, 40, .12);
    --theme-action-menu-item-dangerous-foreground: #db2828;
    --theme-action-menu-item-dangerous-foreground-hover: #db2828;
    --theme-action-menu-item-dangerous-side: rgba(219, 40, 40, .5);
    --theme-action-menu-item-dangerous-side-hover: #db2828;
    --theme-status-pending: #6cf;
    --theme-status-configuration-error: #e28989;
    --theme-status-system-error: #a5a5a5;
    --theme-status-compilation-error: #3387da;
    --theme-status-canceled: #6770d0;
    --theme-status-file-error: #bc35ff;
    --theme-status-runtime-error: #bc35ff;
    --theme-status-time-limit-exceeded: #ff9840;
    --theme-status-memory-limit-exceeded: #ff9840;
    --theme-status-output-limit-exceeded: #ff9840;
    --theme-status-partially-correct: #01bab2;
    --theme-status-wrong-answer: #ff4545;
    --theme-status-accepted: #37da58;
    --theme-status-judgement-failed: #FF5722;
    --theme-status-waiting: #a5a5a5;
    --theme-status-preparing: #de4d9e;
    --theme-status-compiling: #00b5ad;
    --theme-status-running: #6cf;
    --theme-status-skipped: #78909C;
    --theme-score-0: #ff4545;
    --theme-score-1: #ff694f;
    --theme-score-2: #f8603a;
    --theme-score-3: #fc8354;
    --theme-score-4: #fa9231;
    --theme-score-5: #f7bb3b;
    --theme-score-6: #ecdb44;
    --theme-score-7: #e2ec52;
    --theme-score-8: #b0d628;
    --theme-score-9: #a9b42a;
    --theme-score-10: #37da58;
    --theme-subway-graph-level-0: #414141;
    --theme-subway-graph-level-1: #355332;
    --theme-subway-graph-level-2: #37742e;
    --theme-subway-graph-level-3: #27943d;
    --theme-subway-graph-level-4: #1dca3a;
    --theme-os-icon-light: rgba(255, 255, 255, .4);
    --theme-os-icon-heavy: rgba(255, 255, 255, .7);
    --theme-discussion-title: #d6d6d6;
    --theme-discussion-reply-count: #9c9c9c;
    --theme-discussion-border: #4e4e4e;
    --theme-discussion-border-current-user: #384d61;
    --theme-discussion-header-foreground: #9c9c9c;
    --theme-discussion-header-background: #313131;
    --theme-discussion-header-background-current-user: hsl(210 22% 17% / 1);
    --theme-discussion-header-label-border: #888;
    --theme-discussion-delete: #db2828;
    --theme-discussion-reaction-background-hover: hsl(0 0% 18% / 1);
    --theme-discussion-reaction-background-selected: hsl(0 0% 22% / 1);
    --theme-discussion-reaction-disabled: #777;
    --theme-discussion-reaction-picker-hover: rgba(255, 255, 255, .08);
    --theme-discussion-reaction-picker-selected: rgba(255, 255, 255, .13);
    --theme-discussion-line-color: #4e4e4e;
    --theme-discussion-load-more-text: #aaa;
    --theme-discussion-load-more: #474747;
    --theme-footer: #bbb;
    --theme-footer-version: #666;
    --theme-footer-icons: #a7a7a7;
    --theme-login-error-message: #c54f4d;
    --theme-secondary-title: #999;
    --theme-description-message-foreground: rgba(255, 255, 255, .4);
    --theme-blockquote-foreground: #999;
    --theme-blockquote-side: #434343
}

body #root,body .ui.popup:not(.inverted),body .ui.popup:not(.inverted):before,body .ui.modal:not(.basic),body .ui.menu:not(.tabular),body .ui.menu .dropdown.item .menu,body .ui.menu .ui.dropdown .menu>.item,body .ui.dropdown .menu,body .results {
    background: var(--theme-background)!important;
    color: var(--theme-foreground)!important
}

body .ui.basic.label {
    background: var(--theme-background)!important
}

body,body .item,body .ui.menu .ui.dropdown .menu>.item,body .ui.menu .ui.dropdown .menu>.selected.item,body .ui.comments .comment .author,body .ui.comments .comment .text {
    color: var(--theme-foreground)!important
}

body .ui.segment,body .ui.dropdown,body .header,body .content,body .ui.icon:not(.button):not(.item):not(.message),body .ui.breadcrumb .icon.divider,body .ui.input,body input,body .ui.form .inline.fields>label,body .ui.input textarea,body .ui.form textarea,body .ui.radio.checkbox input~label,body .ui.toggle.checkbox input~label,body .ui.toggle.checkbox input:checked~label,body .ui.toggle.checkbox input:focus:checked~label,body .ui.checkbox label:hover,body .ui.checkbox+label:hover,body .ui.checkbox label,body .ui.checkbox+label,body .ui.form .field>label,body .ui.form .field>input,body .ui.selection.visible.dropdown>.text:not(.default),body .ui.menu .ui.dropdown .menu>.active.item,body .ui.menu .ui.dropdown .menu>.item:hover,body .ui.accordion,body .ui.accordion>.title,body .ui.accordion>.content,body .ui.accordion>.content>.accordion,body .result,body .result>.title,body .ui.block>*,body .ui.statistic>*,body table,body tr,body td,body th,body .ui.grid,body .ui.grid>.column,body .accordion>.title,body .accordion>.content,body .padding>*, body .ui.text.loader, #editor, #pie_chart_legend li,body .ui.card,body .ui.card a,body .ui.card .meta {
    background: inherit!important;
    color: inherit!important;
}

.ui.selectable.table tbody tr:hover, .ui.table tbody tr td.selectable:hover {
    color: inherit!important;
}

body .ui.segment:not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black),body .ui.dropdown,body .header,body .content,body .ui.popup:not(.inverted),body .ui.popup:not(.inverted):before,body .ui.menu,body .ui.input,body input,body .ui.input textarea,body .ui.form textarea,body .ui.basic.label:not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black),body .results,body .result,body table:not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black),body tr,body td,body th,body .ui.tabular.menu .active.item {
    border-color: var(--theme-border)!important
}

body table,body .ui.segment {
    border-left-color: var(--theme-border)!important;
    border-right-color: var(--theme-border)!important;
    border-bottom-color: var(--theme-border)!important
}

body table:not(.basic)>thead>tr>th,body table:not(.basic)>tfoot>tr>th {
    background-color: var(--theme-table-header-background)!important
}

body .disabled.item {
    color: var(--theme-foreground-disabled)!important
}

body .ui.basic.label:not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black) {
    color: var(--theme-foreground)!important
}

body .ui.button:not(.primary):not(.positive):not(.negative):not(.inverted):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black),body .ui.label:not(.basic):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black) {
    background: var(--theme-button-background)!important
}

body .ui.button:not(.primary):not(.positive):not(.negative):not(.inverted):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):not(.loading),body .ui.label:not(.basic):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):not(.loading) {
    color: var(--theme-button-foreground)!important
}

body .ui.button:not(.primary):not(.positive):not(.negative):not(.inverted):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):hover,body a.ui.label:not(.basic):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):hover,body .ui.button:not(.primary):not(.positive):not(.negative):not(.inverted):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):focus,body a.ui.label:not(.basic):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):focus {
    background: var(--theme-button-background-hover)!important
}

body .ui.button:not(.primary):not(.positive):not(.negative):not(.inverted):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):active,body a.ui.label:not(.basic):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):active {
    background: var(--theme-button-background-active)!important
}

body input::placeholder,body .ui.input textarea::placeholder,body .ui.form textarea::placeholder {
    color: var(--theme-input-placeholder)!important
}

body input:focus::placeholder,body .ui.input textarea:focus::placeholder,body .ui.form textarea:focus::placeholder {
    color: var(--theme-input-placeholder-focus)!important
}

body .ui.input.focus:not(.transparent)>input,body .ui.input:not(.transparent)>input:focus,body .ui.form textarea:focus {
    border-color: var(--theme-input-border-focus)!important
}

body a,body .ui.breadcrumb a {
    color: var(--theme-hyperlink)
}

body a:hover,body .ui.breadcrumb a:hover {
    color: var(--theme-hyperlink-hover)
}

body .ui.block.header {
    background: var(--theme-block-header-background)!important
}

body .ui.toggle.checkbox label:before {
    background-color: var(--theme-toggle-lane-unchecked)!important
}

body .ui.toggle.checkbox label:hover:before,body .ui.toggle.checkbox label:focus:before {
    background-color: var(--theme-toggle-lane-unchecked-hover)!important
}

body .ui.toggle.checkbox input:checked~label:before,body .ui.toggle.checkbox input:checked:hover~label:before {
    background-color: var(--theme-toggle-lane-checked)!important
}

body .ui.toggle.checkbox input:checked:focus~label:before {
    background-color: var(--theme-toggle-lane-checked-focus)!important
}

body .ui.modal:not(.basic)>.actions {
    background-color: var(--theme-dialog-actions-background)!important
}

body .ui.dropdown .menu {
    border-color: var(--theme-border)!important
}

body .ui.selection.dropdown:hover {
    border-color: var(--theme-border-hover)!important
}

body .ui.selection.dropdown:focus,body .ui.selection.active.dropdown,body .ui.selection.active.dropdown .menu {
    border-color: var(--theme-border-active)!important
}

body .ui.dropdown .menu>.item {
    border-color: var(--theme-border-light)!important
}

body .ui.menu .ui.dropdown .menu>.item:hover,body .ui.dropdown .menu>.item:hover {
    background-color: var(--theme-dropdown-item-hover-background)!important
}

body .ui.dropdown .menu .selected.item,body .ui.dropdown.selected,body .ui.menu .ui.dropdown .menu>.selected.item {
    background-color: var(--theme-dropdown-item-selected-background)!important
}

body .ui.menu:not(.tabular).link .item:hover,body .ui.menu:not(.tabular) .dropdown.item:hover,body .ui.menu:not(.tabular) .link.item:hover,body .ui.menu:not(.tabular) a.item:hover {
    background-color: var(--theme-menu-item-hover-background)!important
}

body .ui.menu:not(.tabular) .active.item,body .ui.menu:not(.tabular) .active.item:hover,body .ui.menu:not(.tabular).vertical.active.item:hover,body .ui.menu:not(.tabular).pagination.active.item {
    background-color: var(--theme-menu-item-active-background)!important
}

body .ui.menu.tabular .active.item {
    background-color: var(--theme-background)!important
}

body .ui.secondary.pointing.menu .item:hover,body .ui.secondary.pointing.menu .active.item,body .ui.secondary.pointing.menu .item {
    background-color: transparent!important
}

body .ui.dropdown .menu>.message:not(.ui) {
    color: var(--theme-dropdown-message-foreground)
}

body .menu .item:before,body .menu:not(.pointing) .item:after {
    background-color: var(--theme-border)!important
}

body .menu.pointing .item:after {
    background-color: var(--theme-menu-item-active-background)!important;
    border-color: var(--theme-border)!important
}

body .ui.menu:not(.attached):not(.secondary) {
    box-shadow: 0 1px 2px 0 var(--theme-shadow)
}

body .ui.popup:not(.inverted):before {
    box-shadow: 1px 1px 0 0 var(--theme-border)!important
}

body .ui.popup:not(.inverted).right.center:before {
    box-shadow: -1px 1px 0 0 var(--theme-border)!important
}

body .ui.popup:not(.inverted).left.center:before {
    box-shadow: 1px -1px 0 0 var(--theme-border)!important
}

body .ui.popup:not(.inverted).bottom:before {
    box-shadow: -1px -1px 0 0 var(--theme-border)!important
}

body .ui.category.search>.results .category .result:hover,body .ui.search>.results .result:hover {
    background-color: var(--theme-search-result-background-hover)!important
}

body .ui.segment:not(.vertical):not(.attached) {
    box-shadow: 0 1px 2px 0 var(--theme-shadow)
}

body .ui.styled.accordion,body .ui.styled.accordion .accordion {
    box-shadow: 0 1px 2px 0 var(--theme-border),0 0 0 1px var(--theme-border)
}

body .ui.styled.accordion>.title,body .ui.styled.accordion .accordion>.title {
    background: none!important
}

body .ui.styled.accordion>.title:not(:hover):not(.active),body .ui.styled.accordion .accordion>.title:not(:hover):not(.active) {
    color: var(--theme-accordion-non-active)!important
}

body .ui.placeholder.segment {
    background: var(--theme-placeholder-segment-background)!important
}

body .ui.divider:not(.vertical):not(.horizontal) {
    border-color: var(--theme-border)!important
}

body .ui.progress {
    background: var(--theme-progress-bar-background)!important
}

body .ui.placeholder:not(.segment) {
    background-image: var(--theme-placeholder-image)
}

body .ui.placeholder:not(.segment),body .ui.placeholder:not(.segment)>.line {
    background-color: var(--theme-placeholder-background)
}

body .ui.message {
    background-color: var(--theme-message-background)!important;
    color: var(--theme-message-foreground)!important;
    box-shadow: 0 0 0 1px var(--theme-message-border) inset,0 0 rgba(0,0,0,0)!important
}

body .ui.negative.message {
    background-color: var(--theme-message-error-background)!important;
    color: var(--theme-message-error-foreground)!important;
    box-shadow: 0 0 0 1px var(--theme-message-error-border) inset,0 0 rgba(0,0,0,0)!important
}

body .ui.message.error,body .noty_type__error {
    background-color: var(--theme-message-error-background)!important;
    color: var(--theme-message-error-foreground)!important;
    box-shadow: 0 0 0 1px var(--theme-message-error-border) inset,0 0 rgba(0,0,0,0)!important
}

body .ui.message.success,body .noty_type__success {
    background-color: var(--theme-message-success-background)!important;
    color: var(--theme-message-success-foreground)!important;
    box-shadow: 0 0 0 1px var(--theme-message-success-border) inset,0 0 rgba(0,0,0,0)!important
}

body .ui.message.info,body .noty_type__info {
    background-color: var(--theme-message-info-background)!important;
    color: var(--theme-message-info-foreground)!important;
    box-shadow: 0 0 0 1px var(--theme-message-info-border) inset,0 0 rgba(0,0,0,0)!important
}

body .ui.message.warning,body .noty_type__warning {
    background-color: var(--theme-message-warning-background)!important;
    color: var(--theme-message-warning-foreground)!important;
    box-shadow: 0 0 0 1px var(--theme-message-warning-border) inset,0 0 rgba(0,0,0,0)!important
}

body .ui.checkbox input:checked~label:after {
    color: var(--theme-foreground)!important
}

body .ui.checkbox input~label:before,body .ui.checkbox input:checked~label:before,body .ui.checkbox input:focus~label:before {
    background-color: var(--theme-background)!important
}

body .ui.checkbox input~label:before {
    border-color: var(--theme-border)!important
}

body .ui.checkbox input:checked~label:before,body .ui.checkbox input~label:hover:before {
    border-color: var(--theme-border-hover)!important
}

body .ui.checkbox input:focus~label:before {
    border-color: var(--theme-border-active)!important
}

body .ui.checkbox.radio input:checked~label:after {
    background-color: var(--theme-foreground)!important
}

body ::selection {
    color: unset!important;
    background-color: var(--theme-selection-background)!important
}

body ::-moz-selection {
    color: unset!important;
    background-color: var(--theme-selection-background)!important
}

body ::-webkit-selection {
    color: unset!important;
    background-color: var(--theme-selection-background)!important
}

html ::-webkit-scrollbar,body ::-webkit-scrollbar {
    -webkit-appearance: none;
    width: 10px;
    height: 10px
}

html ::-webkit-scrollbar-track,body ::-webkit-scrollbar-track {
    background: var(--theme-scrollbar-track)!important;
    border-radius: 0
}

html ::-webkit-scrollbar-thumb,body ::-webkit-scrollbar-thumb {
    cursor: pointer;
    border-radius: 5px;
    background: var(--theme-scrollbar-thumb)!important;
    -webkit-transition: color .2s ease;
    transition: color .2s ease
}

html ::-webkit-scrollbar-thumb:window-inactive,body ::-webkit-scrollbar-thumb:window-inactive {
    background: var(--theme-scrollbar-thumb-inactive)!important
}

html ::-webkit-scrollbar-thumb:hover,body ::-webkit-scrollbar-thumb:hover {
    background: var(--theme-scrollbar-thumb-hover)!important
}

.submit_time, a.black-link, div.description, a.result .title {
    color: inherit !important;
}

i.icon.download {
    color: var(--theme-foreground) !important;
}

body .ui.card, body .ui.cards>.card {
    box-shadow: 0 1px 3px 0 var(--theme-border), 0 0 0 1px var(--theme-border) !important;
}

/* I think lighter green is for higher score */

.score_10,span.status.accepted {
    color: #52ff00!important;
}
.score_7 {
    color: forestgreen;
}

/* highlight code edited from /self/tomorrow.css */

.hll { background-color: #3e4451 } /* 高亮行背景 */
.pl-c { color: #5c6370 } /* Comment */
.pl-err { color: #e06c75 } /* Error */
.pl-k { color: #c678dd } /* Keyword */
.pl-l { color: #56b6c2 } /* Literal */
.pl-n { color: #abb2bf } /* Name */
.pl-o { color: #61aeee } /* Operator */
.pl-p { color: #abb2bf } /* Punctuation */
.pl-ch { color: #5c6370 } /* Comment.Hashbang */
.pl-cm { color: #5c6370 } /* Comment.Multiline */
.pl-cp { color: #c678dd } /* Comment.Preproc */
.pl-cpf { color: #38a } /* Comment.PreprocFile */
.pl-c1 { color: #5c6370 } /* Comment.Single */
.pl-cs { color: #5c6370 } /* Comment.Special */
.pl-gd { color: #e06c75 } /* Generic.Deleted */
.pl-ge { font-style: italic } /* Generic.Emph */
.pl-gh { color: #abb2bf; font-weight: bold } /* Generic.Heading */
.pl-gi { color: #98c379 } /* Generic.Inserted */
.pl-gp { color: #5c6370; font-weight: bold } /* Generic.Prompt */
.pl-gs { font-weight: bold } /* Generic.Strong */
.pl-gu { color: #61aeee; font-weight: bold } /* Generic.Subheading */
.pl-kc { color: #c678dd } /* Keyword.Constant */
.pl-kd { color: #c678dd } /* Keyword.Declaration */
.pl-kn { color: #61aeee } /* Keyword.Namespace */
.pl-kp { color: #c678dd } /* Keyword.Pseudo */
.pl-kr { color: #c678dd } /* Keyword.Reserved */
.pl-kt { color: #d19a66 } /* Keyword.Type */
.pl-ld { color: #98c379 } /* Literal.Date */
.pl-m { color: #d19a66 } /* Literal.Number */
.pl-s { color: #98c379 } /* Literal.String */
.pl-na { color: #d19a66 } /* Name.Attribute */
.pl-nb { color: #e6c07b } /* Name.Builtin */
.pl-nc { color: #e6c07b } /* Name.Class */
.pl-no { color: #e06c75 } /* Name.Constant */
.pl-nd { color: #61aeee } /* Name.Decorator */
.pl-ni { color: #abb2bf } /* Name.Entity */
.pl-ne { color: #e06c75 } /* Name.Exception */
.pl-nf { color: #61aeee } /* Name.Function */
.pl-nl { color: #abb2bf } /* Name.Label */
.pl-nn { color: #e6c07b } /* Name.Namespace */
.pl-nx { color: #61aeee } /* Name.Other */
.pl-py { color: #d19a66 } /* Name.Property */
.pl-nt { color: #e06c75 } /* Name.Tag */
.pl-nv { color: #e06c75 } /* Name.Variable */
.pl-ow { color: #61aeee } /* Operator.Word */
.pl-w { color: #abb2bf } /* Text.Whitespace */
.pl-mb { color: #d19a66 } /* Literal.Number.Bin */
.pl-mf { color: #d19a66 } /* Literal.Number.Float */
.pl-mh { color: #d19a66 } /* Literal.Number.Hex */
.pl-mi { color: #d19a66 } /* Literal.Number.Integer */
.pl-mo { color: #d19a66 } /* Literal.Number.Oct */
.pl-sb { color: #98c379 } /* Literal.String.Backtick */
.pl-sc { color: #abb2bf } /* Literal.String.Char */
.pl-sd { color: #5c6370 } /* Literal.String.Doc */
.pl-s2 { color: #98c379 } /* Literal.String.Double */
.pl-se { color: #d19a66 } /* Literal.String.Escape */
.pl-sh { color: #98c379 } /* Literal.String.Heredoc */
.pl-si { color: #d19a66 } /* Literal.String.Interpol */
.pl-sx { color: #98c379 } /* Literal.String.Other */
.pl-sr { color: #98c379 } /* Literal.String.Regex */
.pl-s1 { color: #98c379 } /* Literal.String.Single */
.pl-ss { color: #98c379 } /* Literal.String.Symbol */
.pl-bp { color: #abb2bf } /* Name.Builtin.Pseudo */
.pl-vc { color: #e06c75 } /* Name.Variable.Class */
.pl-vg { color: #e06c75 } /* Name.Variable.Global */
.pl-vi { color: #e06c75 } /* Name.Variable.Instance */
.pl-il { color: #d19a66 } /* Literal.Number.Integer.Long */

/* custom-editor style */

iframe.custom-editor {
    height: 100%;
    width: 100%;
    border: 1px solid var(--theme-border)!important;
}

#editor {
    border: unset !important;
}
JS with CSS
// ==UserScript==
// @name         SYZOJ darker
// @namespace    http://tampermonkey.net/
// @version      2025-06-15
// @description  use custom dark style for SYZOJ
// @author       XuYueming
// @match        <OJ here>
// @grand        unsafeWindow
// ==/UserScript==

(function () {
    'use strict';
    const customCSS = `body{background:var(--theme-background,#fff)!important;--theme-background:#222;--theme-background-transparent:#222d;--theme-foreground:#fffffff2;--theme-foreground-transparent:#fffffff2;--theme-foreground-disabled:#e6e6e64d;--theme-selection-background:#9b9b9b66;--theme-border:#464646;--theme-border-light:#282828;--theme-border-hover:#5f5f5f;--theme-border-active:#6f6f6f;--theme-shadow:#181a1cf2;--theme-hyperlink:#61affb;--theme-hyperlink-hover:#97cbff;--theme-input-placeholder:#bfbfbfde;--theme-input-placeholder-focus:#e9e9e9e6;--theme-input-border-focus:#6f6f6f;--theme-button-background:#4f4f4f;--theme-button-background-hover:#444;--theme-button-background-active:#3f3f3f;--theme-button-foreground:var(--theme-foreground);--theme-block-header-background:#353637;--theme-table-header-background:#28292a;--theme-toggle-lane-unchecked:#ffffff1a;--theme-toggle-lane-unchecked-hover:#fff3;--theme-toggle-lane-checked:#0d71bb;--theme-toggle-lane-checked-focus:#2185D0;--theme-dialog-actions-background:#292a2b;--theme-dropdown-item-hover-background:#343434;--theme-dropdown-item-selected-background:#2f2f2f;--theme-dropdown-message-foreground:#fff6;--theme-menu-item-hover-background:#2f2f2f;--theme-menu-item-active-background:#343434;--theme-search-result-background-hover:#292a2b;--theme-placeholder-segment-background:#292a2b;--theme-accordion-non-active:#fff9;--theme-progress-bar-background:#ffffff1a;--theme-table-progress-bar-background:#ffffff26;--theme-placeholder-background:var(--theme-background);--theme-placeholder-image:linear-gradient(to right,#ffffff14 0,#ffffff26 15%,#ffffff14 30%);--theme-message-background:#2a2a29;--theme-message-foreground:var(--theme-foreground);--theme-message-border:var(--theme-border);--theme-message-error-background:#6d2727;--theme-message-error-foreground:var(--theme-foreground);--theme-message-error-border:#c74e4e;--theme-message-success-background:#379a3e;--theme-message-success-foreground:var(--theme-foreground);--theme-message-success-border:#64d22d;--theme-message-info-background:#3584a2;--theme-message-info-foreground:var(--theme-foreground);--theme-message-info-border:#3cc1dc;--theme-message-warning-background:#b98c26;--theme-message-warning-foreground:var(--theme-foreground);--theme-message-warning-border:#c19f57;--theme-scrollbar-track:#ffffff1a;--theme-scrollbar-thumb:#ffffff40;--theme-scrollbar-thumb-inactive:#ffffff26;--theme-scrollbar-thumb-hover:#ffffff59;--theme-action-menu-item-background:var(--theme-background);--theme-action-menu-item-background-hover:var(--theme-menu-item-hover-background);--theme-action-menu-item-foreground:var(--theme-foreground);--theme-action-menu-item-foreground-hover:var(--theme-foreground);--theme-action-menu-item-side:#d2d2d226;--theme-action-menu-item-side-hover:#fffffff2;--theme-action-menu-item-important-background:#2185d0;--theme-action-menu-item-important-background-hover:#1678c2;--theme-action-menu-item-important-foreground:#fff;--theme-action-menu-item-important-foreground-hover:#fff;--theme-action-menu-item-important-side:#ffffff80;--theme-action-menu-item-important-side-hover:#ffffffb3;--theme-action-menu-item-dangerous-background:var(--theme-action-menu-item-background);--theme-action-menu-item-dangerous-background-hover:#db28281f;--theme-action-menu-item-dangerous-foreground:#db2828;--theme-action-menu-item-dangerous-foreground-hover:#db2828;--theme-action-menu-item-dangerous-side:#db282880;--theme-action-menu-item-dangerous-side-hover:#db2828;--theme-status-pending:#6cf;--theme-status-configuration-error:#e28989;--theme-status-system-error:#a5a5a5;--theme-status-compilation-error:#3387da;--theme-status-canceled:#6770d0;--theme-status-file-error:#bc35ff;--theme-status-runtime-error:#bc35ff;--theme-status-time-limit-exceeded:#ff9840;--theme-status-memory-limit-exceeded:#ff9840;--theme-status-output-limit-exceeded:#ff9840;--theme-status-partially-correct:#01bab2;--theme-status-wrong-answer:#ff4545;--theme-status-accepted:#37da58;--theme-status-judgement-failed:#FF5722;--theme-status-waiting:#a5a5a5;--theme-status-preparing:#de4d9e;--theme-status-compiling:#00b5ad;--theme-status-running:#6cf;--theme-status-skipped:#78909C;--theme-score-0:#ff4545;--theme-score-1:#ff694f;--theme-score-2:#f8603a;--theme-score-3:#fc8354;--theme-score-4:#fa9231;--theme-score-5:#f7bb3b;--theme-score-6:#ecdb44;--theme-score-7:#e2ec52;--theme-score-8:#b0d628;--theme-score-9:#a9b42a;--theme-score-10:#37da58;--theme-subway-graph-level-0:#414141;--theme-subway-graph-level-1:#355332;--theme-subway-graph-level-2:#37742e;--theme-subway-graph-level-3:#27943d;--theme-subway-graph-level-4:#1dca3a;--theme-os-icon-light:#fff6;--theme-os-icon-heavy:#ffffffb3;--theme-discussion-title:#d6d6d6;--theme-discussion-reply-count:#9c9c9c;--theme-discussion-border:#4e4e4e;--theme-discussion-border-current-user:#384d61;--theme-discussion-header-foreground:#9c9c9c;--theme-discussion-header-background:#313131;--theme-discussion-header-background-current-user:hsl(210 22% 17% / 1);--theme-discussion-header-label-border:#888;--theme-discussion-delete:#db2828;--theme-discussion-reaction-background-hover:hsl(0 0% 18% / 1);--theme-discussion-reaction-background-selected:hsl(0 0% 22% / 1);--theme-discussion-reaction-disabled:#777;--theme-discussion-reaction-picker-hover:#ffffff14;--theme-discussion-reaction-picker-selected:#ffffff21;--theme-discussion-line-color:#4e4e4e;--theme-discussion-load-more-text:#aaa;--theme-discussion-load-more:#474747;--theme-footer:#bbb;--theme-footer-version:#666;--theme-footer-icons:#a7a7a7;--theme-login-error-message:#c54f4d;--theme-secondary-title:#999;--theme-description-message-foreground:#fff6;--theme-blockquote-foreground:#999;--theme-blockquote-side:#434343}body #root,body .ui.popup:not(.inverted),body .ui.popup:not(.inverted):before,body .ui.modal:not(.basic),body .ui.menu:not(.tabular),body .ui.menu .dropdown.item .menu,body .ui.menu .ui.dropdown .menu>.item,body .ui.dropdown .menu,body .results{background:var(--theme-background)!important;color:var(--theme-foreground)!important}body .ui.basic.label{background:var(--theme-background)!important}body,body .item,body .ui.menu .ui.dropdown .menu>.item,body .ui.menu .ui.dropdown .menu>.selected.item,body .ui.comments .comment .author,body .ui.comments .comment .text{color:var(--theme-foreground)!important}body .ui.segment,body .ui.dropdown,body .header,body .content,body .ui.icon:not(.button):not(.item):not(.message),body .ui.breadcrumb .icon.divider,body .ui.input,body input,body .ui.form .inline.fields>label,body .ui.input textarea,body .ui.form textarea,body .ui.radio.checkbox input~label,body .ui.toggle.checkbox input~label,body .ui.toggle.checkbox input:checked~label,body .ui.toggle.checkbox input:focus:checked~label,body .ui.checkbox label:hover,body .ui.checkbox+label:hover,body .ui.checkbox label,body .ui.checkbox+label,body .ui.form .field>label,body .ui.form .field>input,body .ui.selection.visible.dropdown>.text:not(.default),body .ui.menu .ui.dropdown .menu>.active.item,body .ui.menu .ui.dropdown .menu>.item:hover,body .ui.accordion,body .ui.accordion>.title,body .ui.accordion>.content,body .ui.accordion>.content>.accordion,body .result,body .result>.title,body .ui.block>*,body .ui.statistic>*,body table,body tr,body td,body th,body .ui.grid,body .ui.grid>.column,body .accordion>.title,body .accordion>.content,body .padding>*,body .ui.text.loader,#editor,#pie_chart_legend li,body .ui.card,body .ui.card a,body .ui.card .meta{background:inherit!important;color:inherit!important}.ui.selectable.table tbody tr:hover,.ui.table tbody tr td.selectable:hover{color:inherit!important}body .ui.segment:not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black),body .ui.dropdown,body .header,body .content,body .ui.popup:not(.inverted),body .ui.popup:not(.inverted):before,body .ui.menu,body .ui.input,body input,body .ui.input textarea,body .ui.form textarea,body .ui.basic.label:not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black),body .results,body .result,body table:not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black),body tr,body td,body th,body .ui.tabular.menu .active.item{border-color:var(--theme-border)!important}body table,body .ui.segment{border-left-color:var(--theme-border)!important;border-right-color:var(--theme-border)!important;border-bottom-color:var(--theme-border)!important}body table:not(.basic)>thead>tr>th,body table:not(.basic)>tfoot>tr>th{background-color:var(--theme-table-header-background)!important}body .disabled.item{color:var(--theme-foreground-disabled)!important}body .ui.basic.label:not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black){color:var(--theme-foreground)!important}body .ui.button:not(.primary):not(.positive):not(.negative):not(.inverted):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black),body .ui.label:not(.basic):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black){background:var(--theme-button-background)!important}body .ui.button:not(.primary):not(.positive):not(.negative):not(.inverted):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):not(.loading),body .ui.label:not(.basic):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):not(.loading){color:var(--theme-button-foreground)!important}body .ui.button:not(.primary):not(.positive):not(.negative):not(.inverted):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):hover,body a.ui.label:not(.basic):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):hover,body .ui.button:not(.primary):not(.positive):not(.negative):not(.inverted):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):focus,body a.ui.label:not(.basic):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):focus{background:var(--theme-button-background-hover)!important}body .ui.button:not(.primary):not(.positive):not(.negative):not(.inverted):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):active,body a.ui.label:not(.basic):not(.red):not(.orange):not(.yellow):not(.olive):not(.green):not(.teal):not(.blue):not(.violet):not(.purple):not(.pink):not(.brown):not(.grey):not(.black):active{background:var(--theme-button-background-active)!important}body input::placeholder,body .ui.input textarea::placeholder,body .ui.form textarea::placeholder{color:var(--theme-input-placeholder)!important}body input:focus::placeholder,body .ui.input textarea:focus::placeholder,body .ui.form textarea:focus::placeholder{color:var(--theme-input-placeholder-focus)!important}body .ui.input.focus:not(.transparent)>input,body .ui.input:not(.transparent)>input:focus,body .ui.form textarea:focus{border-color:var(--theme-input-border-focus)!important}body a,body .ui.breadcrumb a{color:var(--theme-hyperlink)}body a:hover,body .ui.breadcrumb a:hover{color:var(--theme-hyperlink-hover)}body .ui.block.header{background:var(--theme-block-header-background)!important}body .ui.toggle.checkbox label:before{background-color:var(--theme-toggle-lane-unchecked)!important}body .ui.toggle.checkbox label:hover:before,body .ui.toggle.checkbox label:focus:before{background-color:var(--theme-toggle-lane-unchecked-hover)!important}body .ui.toggle.checkbox input:checked~label:before,body .ui.toggle.checkbox input:checked:hover~label:before{background-color:var(--theme-toggle-lane-checked)!important}body .ui.toggle.checkbox input:checked:focus~label:before{background-color:var(--theme-toggle-lane-checked-focus)!important}body .ui.modal:not(.basic)>.actions{background-color:var(--theme-dialog-actions-background)!important}body .ui.dropdown .menu{border-color:var(--theme-border)!important}body .ui.selection.dropdown:hover{border-color:var(--theme-border-hover)!important}body .ui.selection.dropdown:focus,body .ui.selection.active.dropdown,body .ui.selection.active.dropdown .menu{border-color:var(--theme-border-active)!important}body .ui.dropdown .menu>.item{border-color:var(--theme-border-light)!important}body .ui.menu .ui.dropdown .menu>.item:hover,body .ui.dropdown .menu>.item:hover{background-color:var(--theme-dropdown-item-hover-background)!important}body .ui.dropdown .menu .selected.item,body .ui.dropdown.selected,body .ui.menu .ui.dropdown .menu>.selected.item{background-color:var(--theme-dropdown-item-selected-background)!important}body .ui.menu:not(.tabular).link .item:hover,body .ui.menu:not(.tabular) .dropdown.item:hover,body .ui.menu:not(.tabular) .link.item:hover,body .ui.menu:not(.tabular) a.item:hover{background-color:var(--theme-menu-item-hover-background)!important}body .ui.menu:not(.tabular) .active.item,body .ui.menu:not(.tabular) .active.item:hover,body .ui.menu:not(.tabular).vertical.active.item:hover,body .ui.menu:not(.tabular).pagination.active.item{background-color:var(--theme-menu-item-active-background)!important}body .ui.menu.tabular .active.item{background-color:var(--theme-background)!important}body .ui.secondary.pointing.menu .item:hover,body .ui.secondary.pointing.menu .active.item,body .ui.secondary.pointing.menu .item{background-color:transparent!important}body .ui.dropdown .menu>.message:not(.ui){color:var(--theme-dropdown-message-foreground)}body .menu .item:before,body .menu:not(.pointing) .item:after{background-color:var(--theme-border)!important}body .menu.pointing .item:after{background-color:var(--theme-menu-item-active-background)!important;border-color:var(--theme-border)!important}body .ui.menu:not(.attached):not(.secondary){box-shadow:0 1px 2px 0 var(--theme-shadow)}body .ui.popup:not(.inverted):before{box-shadow:1px 1px 0 0 var(--theme-border)!important}body .ui.popup:not(.inverted).right.center:before{box-shadow:-1px 1px 0 0 var(--theme-border)!important}body .ui.popup:not(.inverted).left.center:before{box-shadow:1px -1px 0 0 var(--theme-border)!important}body .ui.popup:not(.inverted).bottom:before{box-shadow:-1px -1px 0 0 var(--theme-border)!important}body .ui.category.search>.results .category .result:hover,body .ui.search>.results .result:hover{background-color:var(--theme-search-result-background-hover)!important}body .ui.segment:not(.vertical):not(.attached){box-shadow:0 1px 2px 0 var(--theme-shadow)}body .ui.styled.accordion,body .ui.styled.accordion .accordion{box-shadow:0 1px 2px 0 var(--theme-border),0 0 0 1px var(--theme-border)}body .ui.styled.accordion>.title,body .ui.styled.accordion .accordion>.title{background:none!important}body .ui.styled.accordion>.title:not(:hover):not(.active),body .ui.styled.accordion .accordion>.title:not(:hover):not(.active){color:var(--theme-accordion-non-active)!important}body .ui.placeholder.segment{background:var(--theme-placeholder-segment-background)!important}body .ui.divider:not(.vertical):not(.horizontal){border-color:var(--theme-border)!important}body .ui.progress{background:var(--theme-progress-bar-background)!important}body .ui.placeholder:not(.segment){background-image:var(--theme-placeholder-image)}body .ui.placeholder:not(.segment),body .ui.placeholder:not(.segment)>.line{background-color:var(--theme-placeholder-background)}body .ui.message{background-color:var(--theme-message-background)!important;color:var(--theme-message-foreground)!important;box-shadow:0 0 0 1px var(--theme-message-border) inset,0 0 #0000!important}body .ui.negative.message{background-color:var(--theme-message-error-background)!important;color:var(--theme-message-error-foreground)!important;box-shadow:0 0 0 1px var(--theme-message-error-border) inset,0 0 #0000!important}body .ui.message.error,body .noty_type__error{background-color:var(--theme-message-error-background)!important;color:var(--theme-message-error-foreground)!important;box-shadow:0 0 0 1px var(--theme-message-error-border) inset,0 0 #0000!important}body .ui.message.success,body .noty_type__success{background-color:var(--theme-message-success-background)!important;color:var(--theme-message-success-foreground)!important;box-shadow:0 0 0 1px var(--theme-message-success-border) inset,0 0 #0000!important}body .ui.message.info,body .noty_type__info{background-color:var(--theme-message-info-background)!important;color:var(--theme-message-info-foreground)!important;box-shadow:0 0 0 1px var(--theme-message-info-border) inset,0 0 #0000!important}body .ui.message.warning,body .noty_type__warning{background-color:var(--theme-message-warning-background)!important;color:var(--theme-message-warning-foreground)!important;box-shadow:0 0 0 1px var(--theme-message-warning-border) inset,0 0 #0000!important}body .ui.checkbox input:checked~label:after{color:var(--theme-foreground)!important}body .ui.checkbox input~label:before,body .ui.checkbox input:checked~label:before,body .ui.checkbox input:focus~label:before{background-color:var(--theme-background)!important}body .ui.checkbox input~label:before{border-color:var(--theme-border)!important}body .ui.checkbox input:checked~label:before,body .ui.checkbox input~label:hover:before{border-color:var(--theme-border-hover)!important}body .ui.checkbox input:focus~label:before{border-color:var(--theme-border-active)!important}body .ui.checkbox.radio input:checked~label:after{background-color:var(--theme-foreground)!important}body ::selection{color:unset!important;background-color:var(--theme-selection-background)!important}body ::-moz-selection{color:unset!important;background-color:var(--theme-selection-background)!important}body ::-webkit-selection{color:unset!important;background-color:var(--theme-selection-background)!important}html ::-webkit-scrollbar,body ::-webkit-scrollbar{-webkit-appearance:none;width:10px;height:10px}html ::-webkit-scrollbar-track,body ::-webkit-scrollbar-track{background:var(--theme-scrollbar-track)!important;border-radius:0}html ::-webkit-scrollbar-thumb,body ::-webkit-scrollbar-thumb{cursor:pointer;border-radius:5px;background:var(--theme-scrollbar-thumb)!important;-webkit-transition:color .2s ease;transition:color .2s ease}html ::-webkit-scrollbar-thumb:window-inactive,body ::-webkit-scrollbar-thumb:window-inactive{background:var(--theme-scrollbar-thumb-inactive)!important}html ::-webkit-scrollbar-thumb:hover,body ::-webkit-scrollbar-thumb:hover{background:var(--theme-scrollbar-thumb-hover)!important}.submit_time,a.black-link,div.description,a.result .title{color:inherit!important}i.icon.download{color:var(--theme-foreground)!important}body .ui.card,body .ui.cards>.card{box-shadow:0 1px 3px 0 var(--theme-border),0 0 0 1px var(--theme-border)!important}.score_10,span.status.accepted{color:#52ff00!important}.score_7{color:#228b22}.hll{background-color:#3e4451}.pl-c{color:#5c6370}.pl-err{color:#e06c75}.pl-k{color:#c678dd}.pl-l{color:#56b6c2}.pl-n{color:#abb2bf}.pl-o{color:#61aeee}.pl-p{color:#abb2bf}.pl-ch{color:#5c6370}.pl-cm{color:#5c6370}.pl-cp{color:#c678dd}.pl-cpf{color:#38a}.pl-c1{color:#5c6370}.pl-cs{color:#5c6370}.pl-gd{color:#e06c75}.pl-ge{font-style:italic}.pl-gh{color:#abb2bf;font-weight:700}.pl-gi{color:#98c379}.pl-gp{color:#5c6370;font-weight:700}.pl-gs{font-weight:700}.pl-gu{color:#61aeee;font-weight:700}.pl-kc{color:#c678dd}.pl-kd{color:#c678dd}.pl-kn{color:#61aeee}.pl-kp{color:#c678dd}.pl-kr{color:#c678dd}.pl-kt{color:#d19a66}.pl-ld{color:#98c379}.pl-m{color:#d19a66}.pl-s{color:#98c379}.pl-na{color:#d19a66}.pl-nb{color:#e6c07b}.pl-nc{color:#e6c07b}.pl-no{color:#e06c75}.pl-nd{color:#61aeee}.pl-ni{color:#abb2bf}.pl-ne{color:#e06c75}.pl-nf{color:#61aeee}.pl-nl{color:#abb2bf}.pl-nn{color:#e6c07b}.pl-nx{color:#61aeee}.pl-py{color:#d19a66}.pl-nt{color:#e06c75}.pl-nv{color:#e06c75}.pl-ow{color:#61aeee}.pl-w{color:#abb2bf}.pl-mb{color:#d19a66}.pl-mf{color:#d19a66}.pl-mh{color:#d19a66}.pl-mi{color:#d19a66}.pl-mo{color:#d19a66}.pl-sb{color:#98c379}.pl-sc{color:#abb2bf}.pl-sd{color:#5c6370}.pl-s2{color:#98c379}.pl-se{color:#d19a66}.pl-sh{color:#98c379}.pl-si{color:#d19a66}.pl-sx{color:#98c379}.pl-sr{color:#98c379}.pl-s1{color:#98c379}.pl-ss{color:#98c379}.pl-bp{color:#abb2bf}.pl-vc{color:#e06c75}.pl-vg{color:#e06c75}.pl-vi{color:#e06c75}.pl-il{color:#d19a66}iframe.custom-editor{height:100%;width:100%;border:1px solid var(--theme-border)!important}#editor{border:unset!important}`;

    const useCustomCSS = function () {
        $('<style>').html(customCSS).appendTo($('head'));
    };

    const createEditorIframe = function (code, lang, $parent, config) {
        const $iframe = $('<iframe>')
            .attr('class', 'custom-editor')
            .appendTo($parent);
        const iframe = $iframe[0];
        const win = iframe.contentWindow;
        const doc = iframe.contentDocument || iframe.contentWindow.document;

        doc.open();
        doc.write(`
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <style>
                html, body, #container {
                    margin: 0;
                    height: 100%;
                    width: 100%;
                    overflow: hidden;
                }
            </style>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
        </head>
        <body>
            <div id="container"></div>
        </body>
        </html>
        `);
        doc.close();

        return new Promise((resolve) => {
            const tryInit = () => {
                if (typeof win.require === 'undefined') {
                    setTimeout(tryInit, 50); // retry
                    return;
                }

                win.require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@0.44.0/min/vs' } });
                win.require(['vs/editor/editor.main'], function () {
                    const editor = win.monaco.editor.create(doc.getElementById('container'), {
                        value: code,
                        language: lang,
                        theme: 'vs-dark',
                        contextmenu: false,
                        renderFinalNewline: true,
                        ...config
                    });

                    resolve({
                        $iframe: $iframe,
                        editor: editor,
                        winMonaco: win.monaco
                    });
                });
            };

            tryInit();
        });
    };

    const problemEditor = async function () {
        const code = unsafeWindow.editor.getValue();
        const lang = unsafeWindow.editor.getModel().getLanguageIdentifier().language;

        const { $iframe, editor, winMonaco } = await createEditorIframe(code, lang, $('#editor'));

        unsafeWindow.editor.dispose();
        unsafeWindow.editor = editor;

        $('#languages-menu .item').click(function () {
            winMonaco.editor.setModelLanguage(editor.getModel(), $(this).data('mode'));
        });
    };

    const submissionEditor = async function () {
        const script = document.createElement('script');
        script.textContent = `
            // 尝试暴露页面中的变量
            (function() {
                function decodeHtmlEntities(str) {
                    const doc = new DOMParser().parseFromString(str, 'text/html');
                    return doc.documentElement.textContent;
                }
                window._exposed = {
                    formattedCode: formattedCode && decodeHtmlEntities(formattedCode),
                    unformattedCode: unformattedCode && decodeHtmlEntities(unformattedCode),
                    language: vueApp.roughData.info['language']
                };
            })();
        `;
        document.documentElement.appendChild(script);
        script.remove();

        await new Promise(resolve => setTimeout(resolve, 50));
        const { formattedCode, unformattedCode, language } = unsafeWindow._exposed;

        let lang = undefined;

        if (language.includes('C++')) {
            lang = 'cpp';
        } else if (language.includes('Python')) {
            lang = 'python';
        } else {
            console.warn('unknown language', language);
            return;
        }

        const $div = $('<div>');
        $($('#submission_content pre:has(>code)')[0])
            .replaceWith($div);

        const { $iframe, editor } = await createEditorIframe(formattedCode || unformattedCode, lang, $div,
            {
                minimap: { enabled: false },
                scrollBeyondLastLine: false,
                readOnly: true,
                automaticLayout: false,
                scrollbar: {
                    vertical: 'hidden',
                    alwaysConsumeMouseWheel: false,
                },
            }
        );

        const updateHeight = () => {
            const contentHeight = editor.getContentHeight() + 5;
            $div.css({ height: `${contentHeight}px` });
            editor.layout();
        };

        updateHeight();

        if (formattedCode) {
            const $btn = $div.parent().find('>a');
            let formatted = true;
            $btn.on('click', () => {
                formatted = !formatted;
                editor.setValue(formatted ? formattedCode : unformattedCode);
                updateHeight();
            });
        }
    };

    if (typeof unsafeWindow.onEditorLoaded === 'function')
        unsafeWindow.onEditorLoaded(problemEditor);
    if (window.location.href.includes('submission'))
        $(document).ready(submissionEditor);
    if (true)
        useCustomCSS();
})();
JS without CSS
// ==UserScript==
// @name         SYZOJ darker
// @namespace    http://tampermonkey.net/
// @version      2025-06-15
// @description  use custom dark style for SYZOJ
// @author       XuYueming
// @match        <OJ here>
// @grand        unsafeWindow
// ==/UserScript==

(function () {
    'use strict';

    const createEditorIframe = function (code, lang, $parent, config) {
        const $iframe = $('<iframe>')
            .attr('class', 'custom-editor')
            .appendTo($parent);
        const iframe = $iframe[0];
        const win = iframe.contentWindow;
        const doc = iframe.contentDocument || iframe.contentWindow.document;

        doc.open();
        doc.write(`
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <style>
                html, body, #container {
                    margin: 0;
                    height: 100%;
                    width: 100%;
                    overflow: hidden;
                }
            </style>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
        </head>
        <body>
            <div id="container"></div>
        </body>
        </html>
        `);
        doc.close();

        return new Promise((resolve) => {
            const tryInit = () => {
                if (typeof win.require === 'undefined') {
                    setTimeout(tryInit, 50); // retry
                    return;
                }

                win.require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@0.44.0/min/vs' } });
                win.require(['vs/editor/editor.main'], function () {
                    const editor = win.monaco.editor.create(doc.getElementById('container'), {
                        value: code,
                        language: lang,
                        theme: 'vs-dark',
                        contextmenu: false,
                        renderFinalNewline: true,
                        ...config
                    });

                    resolve({
                        $iframe: $iframe,
                        editor: editor,
                        winMonaco: win.monaco
                    });
                });
            };

            tryInit();
        });
    };

    const problemEditor = async function () {
        const code = unsafeWindow.editor.getValue();
        const lang = unsafeWindow.editor.getModel().getLanguageIdentifier().language;

        const { $iframe, editor, winMonaco } = await createEditorIframe(code, lang, $('#editor'));

        unsafeWindow.editor.dispose();
        unsafeWindow.editor = editor;

        $('#languages-menu .item').click(function () {
            winMonaco.editor.setModelLanguage(editor.getModel(), $(this).data('mode'));
        });
    };

    const submissionEditor = async function () {
        const script = document.createElement('script');
        script.textContent = `
            // 尝试暴露页面中的变量
            (function() {
                function decodeHtmlEntities(str) {
                    const doc = new DOMParser().parseFromString(str, 'text/html');
                    return doc.documentElement.textContent;
                }
                window._exposed = {
                    formattedCode: formattedCode && decodeHtmlEntities(formattedCode),
                    unformattedCode: unformattedCode && decodeHtmlEntities(unformattedCode),
                    language: vueApp.roughData.info['language']
                };
            })();
        `;
        document.documentElement.appendChild(script);
        script.remove();

        await new Promise(resolve => setTimeout(resolve, 50));
        const { formattedCode, unformattedCode, language } = unsafeWindow._exposed;

        let lang = undefined;

        if (language.includes('C++')) {
            lang = 'cpp';
        } else if (language.includes('Python')) {
            lang = 'python';
        } else {
            console.warn('unknown language', language);
            return;
        }

        const $div = $('<div>');
        $($('#submission_content pre:has(>code)')[0])
            .replaceWith($div);

        const { $iframe, editor } = await createEditorIframe(formattedCode || unformattedCode, lang, $div,
            {
                minimap: { enabled: false },
                scrollBeyondLastLine: false,
                readOnly: true,
                automaticLayout: false,
                scrollbar: {
                    vertical: 'hidden',
                    alwaysConsumeMouseWheel: false,
                },
            }
        );

        const updateHeight = () => {
            const contentHeight = editor.getContentHeight() + 5;
            $div.css({ height: `${contentHeight}px` });
            editor.layout();
        };

        updateHeight();

        if (formattedCode) {
            const $btn = $div.parent().find('>a');
            let formatted = true;
            $btn.on('click', () => {
                formatted = !formatted;
                editor.setValue(formatted ? formattedCode : unformattedCode);
                updateHeight();
            });
        }
    };

    if (typeof unsafeWindow.onEditorLoaded === 'function')
        unsafeWindow.onEditorLoaded(problemEditor);
    if (window.location.href.includes('submission'))
        $(document).ready(submissionEditor);
})();
posted @ 2025-06-18 17:36  XuYueming  阅读(34)  评论(0)    收藏  举报