AtCoder Formatter

Add formatting buttons to source codes on AtCoder.

As of 2023-11-28. See the latest version.

  1. // ==UserScript==
  2. // @name AtCoder Formatter
  3. // @name:en AtCoder Formatter
  4. // @namespace
  5. // @version 1.5.0
  6. // @description AtCoder の解説コードなどをフォーマットできるようにします.
  7. // @description:en Add formatting buttons to source codes on AtCoder.
  8. // @author kichi2004
  9. // @match https://atcoder.jp/contests/*
  10. // @grant none
  11. // @namespace kichi2004.jp
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. 'use strict';
  16.  
  17. async function request(code, lang) {
  18. const res = await fetch(`https://formatter.api.kichi2004.jp/format?lang=${lang}`, {
  19. body: code,
  20. method: 'POST'
  21. })
  22. if (!res.ok) {
  23. alert('Formatting Request Failed!')
  24. return
  25. }
  26. const json = await res.json()
  27. if (json['status'] === 'error') {
  28. alert('Formatting Error!\n' + json['error'])
  29. return
  30. }
  31. return json['result']
  32. }
  33.  
  34. function createButtons(className, id) {
  35. return `
  36. <div class="btn-group" role="group">
  37. <button type="button" class="btn ${className} btn-sm" title="C/C++ のソースコードをフォーマット" id="${id}-fmt-cpp">
  38. C/C++
  39. </button>
  40. <button type="button" class="btn ${className} btn-sm" title="Python のソースコードをフォーマット" id="${id}-fmt-py">
  41. Python
  42. </button>
  43. <button type="button" class="btn ${className} btn-sm" title="C# のソースコードをフォーマット" id="${id}-fmt-cs">
  44. C#
  45. </button>
  46. </div>`;
  47. }
  48. const SOURCE_ID = 'source'
  49.  
  50. ;(async function () {
  51. const showModal = (id, formatInner) => {
  52. if (!document.getElementById(`modal-${id}-format-warning`)) {
  53. document.body.insertAdjacentHTML('afterbegin', `
  54. <div id="modal-${id}-format-warning" class="modal fade" tabindex="-1" role="dialog">
  55. <div class="modal-dialog" role="document">
  56. <div class="modal-content">
  57. <div class="modal-header">
  58. <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
  59. <h4 class="modal-title">フォーマットの注意</h4>
  60. </div>
  61. <div class="modal-body">
  62. <p>このコンテストはまだ終了していません。</p>
  63. <p>フォーマットを行うとソースコードが外部に送信され、スクリプトの作成者が閲覧可能な状態になります。</p>
  64. <p>まだ公開されていないコンテストではフォーマットを行わないでください。</p>
  65. </div>
  66. <div class="modal-footer">
  67. <button class="btn btn-warning" id="${id}-force-format">フォーマットする</button>
  68. <button class="btn btn-success" id="${id}-cancel">キャンセル</button>
  69. </div>
  70. </div>
  71. </div>
  72. </div>
  73. `)
  74.  
  75. document.getElementById(`${id}-force-format`).addEventListener('click', async () => await formatInner(true))
  76. document.getElementById(`${id}-cancel`).addEventListener('click', () => removeModal(id))
  77. }
  78.  
  79. $(`#modal-${id}-format-warning`).modal('show')
  80. }
  81. const removeModal = (id) => {
  82. const modal = $(`#modal-${id}-format-warning`)
  83. modal.modal('hide')
  84. }
  85. const formatCode = async (event, pre, id, lang) => {
  86. const formatInner = async (modal = false) => {
  87. if (modal) removeModal(id)
  88.  
  89. event.target.disabled = true
  90. const data = document.getElementById(id)
  91. const result = await request(data.innerText, lang, event)
  92. event.target.disabled = false
  93. if (!result) return
  94.  
  95. if (pre.classList.contains('ace_editor')) {
  96. /** @type {AceAjax.Editor} */
  97. const editor = pre.env.editor
  98. editor.setValue(result)
  99. editor.clearSelection()
  100. return
  101. }
  102.  
  103. const nextPre = document.createElement('pre')
  104. nextPre.textContent = result
  105. nextPre.classList.add('prettyprint', `lang-${lang}`, 'linenums')
  106. pre.before(nextPre)
  107. const preId = pre.id
  108. pre.remove()
  109. if (preId) {
  110. nextPre.id = preId
  111. }
  112.  
  113. data.textContent = result
  114. PR.prettyPrint()
  115. }
  116.  
  117. const finished = endTime.toDate() < new Date()
  118. if (finished) {
  119. await formatInner()
  120. return
  121. }
  122.  
  123. showModal(id, formatInner)
  124. }
  125.  
  126. const formatTextArea = async (event, lang) => {
  127. const formatInner = async (modal = false) => {
  128. if (modal) removeModal(SOURCE_ID)
  129.  
  130. event.target.disabled = true
  131. const sw = $(".editor-buttons > button:nth-child(3)")
  132. const active = sw.attr('aria-pressed') === 'true'
  133.  
  134. const textarea = sourceCodeDiv.children('textarea#plain-textarea')
  135.  
  136.  
  137. if (!active) sw.trigger('click')
  138. const code = textarea.val()
  139. if (!active) sw.trigger('click')
  140.  
  141. const result = await request(code, lang, event)
  142. event.target.disabled = false
  143. if (!result) return
  144.  
  145. if (!active) sw.trigger('click')
  146. textarea.val(result)
  147. if (!active) sw.trigger('click')
  148. }
  149.  
  150. const finished = endTime.toDate() < new Date()
  151. if (finished) {
  152. await formatInner()
  153. return
  154. }
  155.  
  156. showModal(SOURCE_ID, formatInner)
  157. }
  158.  
  159. const buttonClass = endTime.toDate() < new Date() ? 'btn-info' : 'btn-danger'
  160.  
  161. const prePrettyPrint = Array.from(
  162. document.getElementsByClassName('prettyprint')
  163. )
  164. const preAceEditor = Array.from(
  165. document.getElementsByClassName('ace_editor')
  166. )
  167. for (const pre of prePrettyPrint.concat(preAceEditor)) {
  168. const next = pre.nextElementSibling
  169. if (next.className !== 'source-code-for-copy') continue
  170. const id = next.id
  171.  
  172. let adding = pre.previousElementSibling
  173. while (adding.className === 'div-btn-copy')
  174. adding = adding.previousElementSibling
  175.  
  176. adding.insertAdjacentHTML('afterend', createButtons(buttonClass, id))
  177. for (const lang of ['cpp', 'py', 'cs']) {
  178. document.getElementById(`${id}-fmt-${lang}`)
  179. .addEventListener('click', async (e) => await formatCode(e, pre, id, lang))
  180. }
  181. }
  182.  
  183. const sourceCodeDiv = $('#sourceCode')
  184. if (!sourceCodeDiv) return
  185.  
  186. $('.editor-buttons')[0].insertAdjacentHTML('beforeend', createButtons(buttonClass, SOURCE_ID))
  187. for (const lang of ['cpp', 'py', 'cs']) {
  188. document.getElementById(`source-fmt-${lang}`)
  189. .addEventListener('click', async (e) => await formatTextArea(e, lang))
  190. }
  191. })()