cf-fast-submit

append the form to submit to codeforces contest problem page.

// ==UserScript==
// @name         cf-fast-submit
// @name:ja      cf-fast-submit
// @namespace    https://github.com/LumaKernel/cf-fast-submit
// @version      2.8
// @description  append the form to submit to codeforces contest problem page.
// @description:ja codeforcesのコンテストの問題ページに提出フォームを置くツール.
// @author       Luma
// @match        http://codeforces.com/contest/*/problem/*
// @match        http://codeforces.com/gym/*/problem/*
// @match        http://codeforces.com/problemset/problem/*
// @match        http://codeforces.com/group/*/contest/*/problem/*
// @match        http://*.contest.codeforces.com/group/*/contest/*/problem/*
// @match        https://codeforces.com/contest/*/problem/*
// @match        https://codeforces.com/gym/*/problem/*
// @match        https://codeforces.com/problemset/problem/*
// @match        https://codeforces.com/group/*/contest/*/problem/*
// @match        https://*.contest.codeforces.com/group/*/contest/*/problem/*
// @grant        none
// ==/UserScript==

/* global $ ace Codeforces */

;(function () {
  'use strict'

  const openNewWindow = false

  const SCRIPT_NAME = 'cf fast submit'
  const origin = location.origin
  const pathname = location.pathname
  const modelist = ace.require('ace/ext/modelist')
  const logged = !!$('a').filter((_, el) => $(el).text() === 'Logout').length
  let $form
  let $programType
  let $toggleEditor
  let $tabSize
  let $selectProblem
  let editor
  // ~/0 というURLは A 問題として扱われる
  const startId = '0'
  const defaultProblemIds = ['A', 'A1']
  const pattern = /(contest|gym)\/(.*)\/problem\/([^/]*)\/?$/
  const problemsetPattern = /problemset\/problem\/([^/]*)\/([^/]*)\/?$/
  const groupPattern = /group\/([^/]+)\/contest\/([^/]*)\/problem\/([^/]*)\/?$/
  let type // 'contest' | 'gym' | 'problemset' | 'group'
  let submitURL
  let problemId
  let contestId
  let participantId


  // got from submit page
  /* eslint-disable-next-line object-property-newline */
  const extensionMap = {2: "program.cpp", 3: "program.dpr", 4: "program.pas", 6: "program.php", 7: "program.py", 8: "program.rb", 9: "program.cs", 12: "program.hs", 13: "program.pl", 19: "program.ml", 20: "[^{}]*object\s+(\w+).*|$1.scala", 28: "program.d", 31: "a.py", 32: "program.go", 34: "program.js", 36: "[^{}]*public\s+(final)?\s*class\s+(\w+).*|$2.java", 40: "a.py", 41: "a.py", 42: "program.cpp", 43: "program.c", 48: "program.kt", 49: "program.rs", 50: "program.cpp", 51: "program.pas", 52: "program.cpp", 54: "program.cpp", 55: "program.js", 59: "program.cpp", 60: "[^{}]*public\s+(final)?\s*class\s+(\w+).*|$2.java", 61: "program.cpp"}

  const regenerateInterval = 30 // minutes
  const retryInterval = 1000 // msec
  const retryTimes = 20

  let doRegenerateOnSubmit = false

  if (!checkRequirements()) return
  if (!initInfo()) return
  tryToInit(true)
  function checkRequirements () {
    if (!logged) {
      console.error(`[${SCRIPT_NAME}] not logged in.`)
      return false
    }
    if (!$) {
      console.error(`[${SCRIPT_NAME}] not found jQuery.`)
      return false
    }
    if (!ace) {
      console.error(`[${SCRIPT_NAME}] not found ace.`)
      return false
    }
    return true
  }
  function initInfo () {
    if (pathname.match(/^\/problemset\//)) {
      type = 'problemset'
      submitURL = origin + '/problemset/submit'
      const match = pathname.match(problemsetPattern)
      contestId = match[1]
      problemId = match[2]
    } else if (pathname.match(/^\/group\//)) {
      type = 'group'
      const match = pathname.match(groupPattern)
      const groupId = match[1]
      contestId = match[2]
      problemId = match[3]
      submitURL = `${origin}/group/${groupId}/contest/${contestId}/submit`
    } else {
      pathname.match(pattern)
      const match = pathname.match(pattern)
      if (!match) return false
      type = match[1]
      submitURL = origin + '/' + type + '/' + match[2] + '/submit'
      problemId = match[3]
    }
    return true
  }
  async function tryToInit (first) {
    for (let i = 0; i < retryTimes; i++) {
      try {
        if (await initAppendForm(first, false)) return
      } catch (e) {
        removeForm()
        console.error(`[${SCRIPT_NAME}] unexpected error has been occured.`)
        throw e
      }
      removeForm()
      await delay(retryInterval)
    }
    console.error(`[${SCRIPT_NAME}] tried some times but failed.`)
  }
  function delay (ms) {
    return new Promise((resolve, reject) => {
      setTimeout(resolve, ms)
    })
  }
  async function initAppendForm (first = true, doNotRegenerateOnSubmit = false) {
    let code = ''
    let srcFile
    const ajaxData = {}
    const raw = await $.ajax(submitURL, {
      method: 'get',
      ...ajaxData
    })
    const $newForm = $(raw).find('form.submit-form')
    if (!$newForm.length) return false
    if (!first) {
      code = getCode() || ''
      srcFile = $form.find('[name=sourceFile]')
      removeForm()
    }
    $form = $newForm
    $('.problem-statement').append($form)
    editor = ace.edit('editor')
    $form.attr('action', submitURL + $form.attr('action'))
    $programType = $form.find('select[name=programTypeId]')
    $toggleEditor = $form.find('#toggleEditorCheckbox')
    $tabSize = $form.find('#tabSizeInput')
    $selectProblem = $form.find('[name=submittedProblemIndex]')
    // codeforces default settings
    editor.setTheme('ace/theme/chrome')
    editor.setShowPrintMargin(false)
    editor.setOptions({
      enableBasicAutocompletion: true
    })
    if (type === 'contest' || type === 'gym' || type === 'group') {
      const existsProblemID = id => $selectProblem.find('option').filter((_, el) => $(el).val() === id).length
      let exists = existsProblemID(problemId)
      if (!exists && problemId === startId) {
        for (const id of defaultProblemIds) {
          if (existsProblemID(id)) {
            problemId = id
            exists = true
            break
          }
        }
      }
      if (!exists) return false
      $selectProblem.val(problemId)
      // ダミーを作る
      // そのままdisabledにするとformに含まれなくなるので
      const $cloneSelectProblem = $($selectProblem.prop('outerHTML'))
      $cloneSelectProblem.prop('disabled', true)
      $cloneSelectProblem.removeAttr('name')
      $cloneSelectProblem.val(problemId)
      $cloneSelectProblem.attr('id', 'submitted_problem_index_fake_display')
      $selectProblem.after($cloneSelectProblem)
      $selectProblem.prop('hidden', true)
    }
    if (type === 'problemset') {
      if (problemId === startId) {
        $form.find('[name=submittedProblemCode]').val(contestId + 'A')
      }
    }
    if (type === 'contest' || type === 'problemset') {
      contestId = (raw.match(/contestId\s*=\s*(\d+)/) || {1: 0})[1]
      participantId = (raw.match(/participantId\s*:\s*(\d+)/) || {1: 0})[1]
    }
    if (raw.match('updateProblemLockInfo')) updateProblemLockInfo()
    if (raw.match('updateSubmitButtonState')) updateSubmitButtonState()
    applyEditorVisibility()
    setAceMode()
    updateFilesAndLimits()
    $toggleEditor.on('change', () => {
      applyEditorVisibility()
      const editorEnabled = !$toggleEditor.is(':checked')
      $.post(
        '/data/customtest',
        {
          communityCode: '',
          action: 'setEditorEnabled',
          editorEnabled: editorEnabled
        },
        function (response) {}
      )
      return false
    })
    $tabSize.on('change', () => {
      const tabSize = $tabSize.val()
      editor.setOptions({ tabSize })
      $.post(
        '/data/customtest',
        { communityCode: '', action: 'setTabSize', tabSize: tabSize },
        function (response) {}
      )
    })
    $programType.on('change', () => {
      setAceMode()
    })
    editor.getSession().on('change', function () {
      $('#sourceCodeTextarea').val(editor.getValue())
    })
    $('#sourceCodeTextarea').on('change', function () {
      editor.setValue($(this).val(), 1)
    })
    $form.on('submit', preSubmit)
    if (!first) {
      if (code) setCode(code)
      if (srcFile) $form.find('[name=sourceFile]').replaceWith(srcFile)
    }
    doRegenerateOnSubmit = false
    if (!doNotRegenerateOnSubmit) {
      delay(1000 * 60 * regenerateInterval).then(() => { doRegenerateOnSubmit = true })
    }
    return true
  }
  function setAceMode () {
    var filePath = extensionMap[$programType.val()]
    const mode = modelist.getModeForPath(filePath).mode
    if (editor) editor.session.setMode(mode)
  }
  function applyEditorVisibility () {
    if ($('#toggleEditorCheckbox').is(':checked')) {
      $('#editor').hide()
      $('#sourceCodeTextarea').show()
      $('.tabSizeDiv').hide()
    } else {
      $('#editor').show()
      editor.setValue(editor.getValue())
      $('#sourceCodeTextarea').hide()
      $('.tabSizeDiv').show()
    }
  }
  function updateFilesAndLimits () {
    var problemFiles = $('#submittedProblemFiles')
    var problemLimits = $('#submittedProblemLimits')
    var problemIndex = $('select[name=submittedProblemIndex]').val()
    var option = $('select[name=submittedProblemIndex] option:selected')
    var timeLimit = option.attr('data-time-limit')
    var memoryLimit = option.attr('data-memory-limit')
    var inputFile = option.attr('data-input-file')
    var outputFile = option.attr('data-output-file')
    if (problemIndex === '') {
      problemFiles.text('')
      problemLimits.text('')
    } else {
      var filesStyle = 'float: left; font-weight: bold'
      if (inputFile === '') {
        if (outputFile === '') {
          filesStyle = 'float: left;'
          problemFiles.text('standard input/output')
        } else {
          problemFiles.text('standard input / ' + outputFile)
        }
      } else {
        if (outputFile === '') {
          problemFiles.text(inputFile + ' / standard output')
        } else {
          problemFiles.text(inputFile + ' / ' + outputFile)
        }
      }
      problemFiles.attr('style', filesStyle)
      problemLimits.text(timeLimit + ' s, ' + memoryLimit + ' MB')
    }
  }
  function removeForm () {
    $('.submit-form').remove()
  }
  function succeedSubmit() {
    if(openNewWindow) {
      window.open(location.href)
    }
  }
  function preSubmit () {
    if (doRegenerateOnSubmit) {
      initAppendForm(false, true).then(() => {
        $form.trigger('submit')
      })
      return false
    }
    const button = $form.find('input.submit')
    const img = $form.find('img.ajax-loading-gif')
    if ($(this).hasAttr('data-submitting')) {
      succeedSubmit()
      return true
    }
    if (button.prop('disabled')) {
      return false
    }
    var result = callback.call(this)
    let alwaysDisable = false
    if (result || alwaysDisable) {
      img.show()
      button.prop('disabled', true)
      setTimeout(function () {
        img.hide()
        button.prop('disabled', false)
      }, alwaysDisable ? 1000 : 10000)
    }
    if(result) succeedSubmit()
    return result
  }
  function callback () {
    var form = $(this)
    var $ftaa = form.find("input[name='ftaa']")
    var $bfaa = form.find("input[name='bfaa']")
    if (window._ftaa && window._bfaa) {
      $ftaa.val(window._ftaa)
      $bfaa.val(window._bfaa)
    }
    if (form.attr('enctype') === 'multipart/form-data') {
      var sourceFiles = form.find('.table-form input[name=sourceFile]')
      if (
        sourceFiles.length === 1 &&
        sourceFiles[0].files &&
        sourceFiles[0].files.length === 0
      ) {
        form.removeAttr('enctype')
      }
    }
    return true
  }
  function getCode () {
    const $el = $('#sourceCodeTextarea')
    return $el.val()
  }
  function setCode (code) {
    const $el = $('#sourceCodeTextarea')
    $el.val(code)
    $el.trigger('change')
  }
  /* eslint-disable */
  // from contest submit page (/contest/****/submit) {{{
  function updateProblemLockInfo () {
    var problemIndex = $('select[name=submittedProblemIndex]').val()
    updateFilesAndLimits()
    if (problemIndex != '') {
      $.post('/data/problemLock',
        {action: 'checkProblemLock', contestId, participantId, problemIndex: problemIndex},
        function (response) {
          if (response['problemLocked'] == 'true') {
            Codeforces.setAjaxFormErrors('form table',
              {error__submittedProblemIndex: 'Problem was locked for submission, it is impossible to resubmit it'})
            $('.submit-form :submit').attr('disabled', 'disabled')
            $('#submittedProblemFiles').text('')
            $('#submittedProblemLimits').text('')
          } else {
            Codeforces.clearAjaxFormErrors('form table')
            $('.submit-form :submit').removeAttr('disabled')
          }
        },
        'json'
      )
    } else {
      Codeforces.clearAjaxFormErrors('form table')
      $('.submit-form :submit').attr('disabled', 'disabled')
    }
  }
  function updateSubmitButtonState () {
    var problemIndex = $('select[name=submittedProblemIndex]').val()
    updateFilesAndLimits()
    if (problemIndex == '') {
      $('.submit-form :submit').attr('disabled', 'disabled')
    } else {
      $('.submit-form :submit').removeAttr('disabled')
    }
  }
  // }}}
  /* eslint-enable */
})()