diff --git a/app/controllers/api/coding_test_submissions_controller.rb b/app/controllers/api/coding_test_submissions_controller.rb new file mode 100644 index 00000000000..ce2014962a8 --- /dev/null +++ b/app/controllers/api/coding_test_submissions_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class API::CodingTestSubmissionsController < API::BaseController + def create + cts = CodingTestSubmission.new(coding_test_submission_params) + cts.user = current_user + + if cts.save + head :ok + else + render json: { errors: cts.errors }, status: :unprocessable_entity + end + end + + private + + def coding_test_submission_params + params.require(:coding_test_submission).permit( + :coding_test_id, + :code + ) + end +end diff --git a/app/controllers/coding_tests/coding_test_submissions_controller.rb b/app/controllers/coding_tests/coding_test_submissions_controller.rb new file mode 100644 index 00000000000..7f2aa57e401 --- /dev/null +++ b/app/controllers/coding_tests/coding_test_submissions_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class CodingTests::CodingTestSubmissionsController < ApplicationController + before_action :set_coding_test + + def index + @coding_test_submissions = @coding_test.coding_test_submissions.page(params[:page]) + end + + def show + @coding_test_submission = @coding_test.coding_test_submissions.find(params[:id]) + end + + private + + def set_coding_test + @coding_test = CodingTest.find(params[:coding_test_id]) + end +end diff --git a/app/controllers/coding_tests_controller.rb b/app/controllers/coding_tests_controller.rb new file mode 100644 index 00000000000..d899295faf0 --- /dev/null +++ b/app/controllers/coding_tests_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class CodingTestsController < ApplicationController + def show + @coding_test = CodingTest.find(params[:id]) + end +end diff --git a/app/controllers/mentor/coding_tests_controller.rb b/app/controllers/mentor/coding_tests_controller.rb new file mode 100644 index 00000000000..177a5d84d54 --- /dev/null +++ b/app/controllers/mentor/coding_tests_controller.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +class Mentor::CodingTestsController < ApplicationController + before_action :require_admin_or_mentor_login, only: %i[index new create edit update] + before_action :set_coding_test, only: %i[show edit update destroy] + + def index + @coding_tests = CodingTest.joins(:practice).order('practices.id, coding_tests.position') + end + + def show; end + + def new + @coding_test = CodingTest.new(user: current_user) + end + + def edit; end + + def create + @coding_test = CodingTest.new(coding_test_params) + if @coding_test.save + redirect_to @coding_test, notice: 'コーディング問題を作成しました。' + else + render :new + end + end + + def update + if @coding_test.update(coding_test_params) + redirect_to @coding_test, notice: 'コーディング問題を更新しました。' + else + render :edit + end + end + + def destroy + @coding_test.destroy! + redirect_to mentor_coding_tests_path, notice: 'コーディング問題を削除しました。' + end + + private + + def coding_test_params + params.require(:coding_test).permit( + :title, + :description, + :practice_id, + :user_id, + :language, + coding_test_cases_attributes: %i[id input output _destroy] + ) + end + + def set_coding_test + @coding_test = CodingTest.find(params[:id]) + end +end diff --git a/app/javascript/bootcamp.js b/app/javascript/bootcamp.js index 6429d710bbb..2a357ff2621 100644 --- a/app/javascript/bootcamp.js +++ b/app/javascript/bootcamp.js @@ -22,12 +22,13 @@ export default { }) }, - post(path) { + post(path, params = {}) { return fetch(path, { method: 'POST', headers: headers(), credentials: 'same-origin', - redirect: 'manual' + redirect: 'manual', + body: JSON.stringify(params) }) }, diff --git a/app/javascript/coding-test.js b/app/javascript/coding-test.js new file mode 100644 index 00000000000..e296a9f5c1e --- /dev/null +++ b/app/javascript/coding-test.js @@ -0,0 +1,64 @@ +import { OnBrowserJudge } from './onbrowserjudge.js' +import ace from 'ace-builds' +import 'ace-builds/webpack-resolver' +import 'ace-builds/src-noconflict/mode-javascript' +import 'ace-builds/src-noconflict/mode-ruby' +import 'ace-builds/src-noconflict/theme-github' +import Bootcamp from 'bootcamp' + +document.addEventListener('DOMContentLoaded', () => { + const id = 'code_editor' + const element = document.getElementById(id) + if (!element) { + return null + } + + const codingTestId = element.dataset.codingTestId + const practiceId = element.dataset.practiceId + const language = element.dataset.language + const editor = ace.edit(id) + + editor.session.setMode(`ace/mode/${language}`) + editor.setTheme('ace/theme/github') + + OnBrowserJudge.workerFile = `../${language}.js` + OnBrowserJudge.getProgram = () => editor.getValue() + OnBrowserJudge.dict.ready = "提出" + OnBrowserJudge.dict.running = "停止" + OnBrowserJudge.dict.preparation = "準備中" + OnBrowserJudge.dict.case_name = "テストケース名" + OnBrowserJudge.dict.status = "結果" + OnBrowserJudge.dict.AC = "正解" + OnBrowserJudge.dict.WA = "不正解" + OnBrowserJudge.dict.RE = "エラー" + OnBrowserJudge.dict.TLE = "時間超過" + OnBrowserJudge.dict.WJ = "ジャッジ待ち" + OnBrowserJudge.timeLimit = 2001 + OnBrowserJudge.process = (program, _casename, _input) => program + OnBrowserJudge.assertEqual = (expected, actual) => { + console.log(`expected: ${expected}, actual: ${actual}`) + console.log(expected === actual.trimEnd()) + return expected === actual.trimEnd() + } + OnBrowserJudge.congratulations = async () => { + alert("正解!") + + const params = { + coding_test_submission: { + coding_test_id: codingTestId, + code: editor.getValue() + } + } + + try { + const response = await Bootcamp.post('/api/coding_test_submissions', params) + if (response.ok) { + location.href = `/practices/${practiceId}` + } else { + console.warn('提出に失敗しました。') + } + } catch (error) { + console.error(error) + } + } + }) diff --git a/app/javascript/onbrowserjudge.js b/app/javascript/onbrowserjudge.js new file mode 100644 index 00000000000..84c3afd75f1 --- /dev/null +++ b/app/javascript/onbrowserjudge.js @@ -0,0 +1,205 @@ +const OnBrowserJudge = { + dict: { + ready: "▶Run", + running: "■Stop", + preparation: "In preparation", + case_name: "Case Name", + status: "Status", + AC: "AC", + WA: "WA", + RE: "RE", + CE: "CE", + IE: "IE", + TLE: "TLE", + MLE: "MLE", + OLE: "OLE", + WJ: "WJ" + }, + + timeLimit: 2000, + + initialData: null, + + congratulations: () => {}, + + process: (program, _casename, _input) => program, + + assertEqual: (expected, actual) => expected === actual.trimEnd(), + + + status: "preparation", + + updateStatus: function(status) { + this.status = status + const button = document.getElementById("run") + button.disabled = status === "preparation" + button.innerHTML = this.dict[status] + }, + + runButtonPressed: function() { + switch (this.status) { + case "ready": + this.run() + break + case "running": + this.stop() + break + } + }, + + worker: null, + + timer: null, + + workerEvent: function(e) { + let d = null + switch (e.data[0]) { + case "init": + this.worker.postMessage(["init", this.initialData]) + break + case "ready": + this.updateStatus("ready") + break + case "executed": + d = e.data[1] + this.executed(d.testCase, d.output, d.error, d.errorMessage, d.execTime) + break + } + }, + + loadWorker: function(path) { + const baseURL = window.location.href.replaceAll("\\", "/").replace(/\/[^/]*$/, "/") + const array = [`importScripts("${baseURL}${path}");`] + const blob = new Blob(array, { type: "text/javascript" }) + const url = window.URL.createObjectURL(blob) + return new Worker(url) + }, + + resetWorker: function() { + if (this.worker) this.worker.terminate() + this.worker = this.loadWorker(this.workerFile) + this.worker.addEventListener("message", event => { this.workerEvent(event) }, false) + }, + + run: async function() { + if (this.status !== "ready") return + this.updateStatus("running") + const autocopy = document.getElementById("autocopy") + if (!autocopy || autocopy.checked) this.copyProgram() + this.initializeResult() + this.restTests = Array.from(this.tests) + this.allPassed = true + this.program = this.getProgram() + this.nextTest() + }, + + nextTest: function() { + const testCase = this.restTests.shift() + const input = document.getElementById(`${testCase}_input`).innerText.trim() + const program = this.process(this.getProgram(), testCase, input) + this.timer = setTimeout(() => this.tle(testCase), this.timeLimit * 2) + this.worker.postMessage(["execute", { testCase, program, input }]) + }, + + tle: function(testCase) { + this.updateResult(testCase, "TLE", '', '', '', this.timeLimit * 2) + this.stop() + }, + + executed: function(testCase, output, error, errorMessage, execTime) { + clearTimeout(this.timer) + let result = "AC" + if (error !== 0) { + result = error === 1 ? "CE" : "RE" + } else { + if (execTime > this.timeLimit) result = "TLE" + const expected = document.getElementById(`${testCase}_output`).innerText.trim() + if (! this.assertEqual(expected, output)) result = "WA" + } + + console.log('output:', output); + console.log('error:', error); + console.log('errorMessage:', errorMessage); + + this.updateResult(testCase, result, output, error, errorMessage, execTime) + if (result !== "AC") this.allPassed = false + if (this.restTests.length === 0) { + if (this.allPassed) setTimeout(this.congratulations, 20) + this.updateStatus("ready") + } else { + this.nextTest() + } + }, + + initializeResult: function() { + document.getElementById("result").innerHTML = ` + + ${this.dict.case_name} + 出力 + エラー + ${this.dict.status} +` + + for(const testCase of OnBrowserJudge.tests) { + const tr = document.createElement("tr") + tr.innerHTML = ` +${testCase} +
+
+${this.dict.WJ}` + document.getElementById("result").appendChild(tr) + } + document.getElementById("result").scrollIntoView({ behavior: "smooth" }) + }, + + updateResult: function(testCase, result, output, _error, errorMessage, _execTime) { + document.getElementById(`${testCase}_std_output`).innerHTML = `
${output}
` + document.getElementById(`${testCase}_std_error`).innerHTML = `
${errorMessage}
` + const span = `${this.dict[result]}` + document.getElementById(`${testCase}_status`).innerHTML = span + }, + + stop: function() { + window.clearTimeout(this.timer) + Array.from(document.getElementsByClassName("wj")).forEach((elm) => { + elm.innerText = "" + }) + + this.updateStatus("preparation") + this.resetWorker() + }, + + copyProgram: function() { + navigator.clipboard.writeText(this.getProgram()) + } +} + + +window.addEventListener("DOMContentLoaded", () => { + const editor = document.getElementById('code_editor') + if (!editor) return null + + function getTestNames() { + return Array.from(document.getElementsByTagName("pre")).map(elm => + elm.id + ).filter(id => + id.match(/_input$/) && document.getElementById(id.replace(/_input$/, "_output")) + ).map(id => + id.replace(/_input$/, "") + ) + } + OnBrowserJudge.tests = getTestNames() + + function trimAllSampleCases() { + const samples = document.getElementsByClassName("sample") + for (const elm of samples) elm.innerText = elm.innerText.trim() + } + trimAllSampleCases() + + OnBrowserJudge.updateStatus("preparation") + OnBrowserJudge.resetWorker() + document.getElementById("run").onclick = () => OnBrowserJudge.runButtonPressed() +}) + +export { OnBrowserJudge } diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index c37e1298744..9242c603772 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -56,6 +56,7 @@ import '../header-dropdown.js' import '../editor-selection-form.js' import '../user_mentor_memo.js' import '../invitation-url-updater.js' +import '../coding-test.js' import VueMounter from '../VueMounter.js' import Books from '../components/books.vue' diff --git a/app/models/coding_test.rb b/app/models/coding_test.rb new file mode 100644 index 00000000000..4620a7bd443 --- /dev/null +++ b/app/models/coding_test.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class CodingTest < ApplicationRecord + enum language: { + ruby: 1, + javascript: 2 + }, _prefix: true + + belongs_to :practice + belongs_to :user + has_many :coding_test_cases, dependent: :destroy + has_many :coding_test_submissions, dependent: :destroy + + accepts_nested_attributes_for :coding_test_cases + + acts_as_list scope: :practice + + validates :language, presence: true + validates :title, presence: true + validates :description, presence: true + + def passed_by?(user) + coding_test_submissions.exists?(user:) + end +end diff --git a/app/models/coding_test_case.rb b/app/models/coding_test_case.rb new file mode 100644 index 00000000000..6f526d151aa --- /dev/null +++ b/app/models/coding_test_case.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class CodingTestCase < ApplicationRecord + belongs_to :coding_test +end diff --git a/app/models/coding_test_submission.rb b/app/models/coding_test_submission.rb new file mode 100644 index 00000000000..34e72643a79 --- /dev/null +++ b/app/models/coding_test_submission.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class CodingTestSubmission < ApplicationRecord + belongs_to :coding_test + belongs_to :user + + validates :coding_test_id, uniqueness: { scope: :user_id } +end diff --git a/app/models/practice.rb b/app/models/practice.rb index 3103daa6be0..624470e3a28 100644 --- a/app/models/practice.rb +++ b/app/models/practice.rb @@ -42,6 +42,7 @@ class Practice < ApplicationRecord accepts_nested_attributes_for :practices_books, reject_if: :all_blank, allow_destroy: true has_one :submission_answer, dependent: :destroy + has_many :coding_tests, dependent: :nullify validates :title, presence: true validates :description, presence: true diff --git a/app/models/user.rb b/app/models/user.rb index 6e00d7e151e..cbdc7c41f84 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -100,6 +100,8 @@ class User < ApplicationRecord has_many :surveys, dependent: :destroy has_many :survey_questions, dependent: :destroy has_many :external_entries, dependent: :destroy + has_many :coding_tests, dependent: :destroy + has_many :coding_test_submissions, dependent: :destroy has_one :report_template, dependent: :destroy has_one :talk, dependent: :destroy has_one :discord_profile, dependent: :destroy @@ -167,6 +169,8 @@ class User < ApplicationRecord through: :regular_event_participations, source: :regular_event + has_many :coding_test_submissions, dependent: :destroy + has_one_attached :avatar has_one_attached :profile_image @@ -488,6 +492,10 @@ def create_followup_comment(student) end end + def submitted?(coding_test) + coding_test_submissions.exists?(coding_test_id: coding_test.id) + end + def away? last_activity_at && (last_activity_at <= 10.minutes.ago) end diff --git a/app/views/application/_mentor_menu.html.slim b/app/views/application/_mentor_menu.html.slim index d0a2a7957f5..838dade47f4 100644 --- a/app/views/application/_mentor_menu.html.slim +++ b/app/views/application/_mentor_menu.html.slim @@ -6,6 +6,10 @@ = link_to mentor_root_path, class: 'header-dropdown__item-link' do | メンターページ + li.header-dropdown__item + = link_to [:mentor, :coding_tests], + class: 'header-dropdown__item-link' do + | コーディングテスト li.header-dropdown__item = link_to mentor_practices_path, class: 'header-dropdown__item-link' do diff --git a/app/views/coding_tests/coding_test_submissions/_coding_test_submission.html.slim b/app/views/coding_tests/coding_test_submissions/_coding_test_submission.html.slim new file mode 100644 index 00000000000..e9e310a5d11 --- /dev/null +++ b/app/views/coding_tests/coding_test_submissions/_coding_test_submission.html.slim @@ -0,0 +1,28 @@ +- cts = coding_test_submission +- coding_test = cts.coding_test + +.card-list-item + .card-list-item__inner + .card-list-item__user + = render 'users/icon', + user: cts.user, + link_class: 'card-list-item__user-link', + image_class: 'card-list-item__user-icon' + .card-list-item__rows + .card-list-item__row + .card-list-item-title + .card-list-item-title__start + h2.card-list-item-title__title(itemprop='name') + = link_to [coding_test, cts], + itemprop: 'url', + class: 'card-list-item-title__link a-text-link js-unconfirmed-link' do + = coding_test.title + .card-list-item__row + .card-list-item-meta + .card-list-item-meta__items + .card-list-item-meta__item + = link_to cts.user, class: 'a-user-name' do + = cts.user.long_name + .card-list-item-meta__item + time.a-meta(datetime="#{cts.updated_at}") + = l cts.updated_at diff --git a/app/views/coding_tests/coding_test_submissions/index.html.slim b/app/views/coding_tests/coding_test_submissions/index.html.slim new file mode 100644 index 00000000000..4a95b05dc16 --- /dev/null +++ b/app/views/coding_tests/coding_test_submissions/index.html.slim @@ -0,0 +1,32 @@ +- title '提出コード' +- description '提出コードの一覧ページです。' + +header.page-header + .container + .page-header__inner + .page-header__start + .page-header__title = title + .page-header__end + +hr.a-border + +.page-main + - if @coding_test_submissions.present? + .page-body + .container.is-md + .page-content.reports + = paginate @coding_test_submissions + .card-list.a-card + .card-list__items + = render @coding_test_submissions + = paginate @coding_test_submissions + - else + .o-empty-message + .o-empty-message__icon + .card-list.a-card + .card__description + .o-empty-message + .o-empty-message__icon + i.fa-regular.fa-sad-tear + .o-empty-message__text + | 提出コードはまだありません。 diff --git a/app/views/coding_tests/coding_test_submissions/show.html.slim b/app/views/coding_tests/coding_test_submissions/show.html.slim new file mode 100644 index 00000000000..ad7bf09e1fa --- /dev/null +++ b/app/views/coding_tests/coding_test_submissions/show.html.slim @@ -0,0 +1,45 @@ +- ctd = @coding_test_submission +- title "#{@coding_test.title}の提出コード" +- description "#{ctd.user.login_name}さんの提出コードです。" + +header.page-header + .container + .page-header__inner + .page-header__start + h2.page-header__title 提出コード + +hr.a-border + +.page-body + .container.is-md + .page-content.is-work + header.page-content-header + .page-content-header__start + .page-content-header__user + = link_to ctd.user, + itemprop: 'url', + class: 'page-content-header__user-link' do + span class="a-user-role is-#{ctd.user.primary_role}" + = image_tag ctd.user.avatar_url, + title: ctd.user.icon_title, + class: 'page-content-header__user-icon a-user-icon' + .page-content-header__end + .page-content-header__row + .page-content-header__before-title + = link_to ctd.user, class: 'a-user-name' do + = ctd.user.login_name + h1.page-content-header__title = title + .page-content-header__row + .page-content-header-metas + .page-content-header-metas__start + .page-content-header-metas__meta + .a-meta + .a-meta__label 提出日 + time.a-meta__value(datetime="#{ctd.created_at}" pubdate='pubdate') + = l ctd.created_at + + .a-card + .card-body + .card__description + pre(class="language-#{@coding_test.language}") + code(style="display:block;padding: 1em") = ctd.code diff --git a/app/views/coding_tests/show.html.slim b/app/views/coding_tests/show.html.slim new file mode 100644 index 00000000000..59481f381ec --- /dev/null +++ b/app/views/coding_tests/show.html.slim @@ -0,0 +1,117 @@ +- title @coding_test.title +- description "#{@coding_test.title}のコーディングテストです。" +- practice = @coding_test.practice + +header.page-header + .container + .page-header__inner + .page-header__start + h2.page-header__title = title + +hr.a-border + +- if current_user.submitted?(@coding_test) + .a-completion-message() + .container + .a-completion-message__inner + .a-completion-message__inner-start + h2.a-completion-message__title + | このコーディングテストは修了しました🎉 + +.page-body + .container.is-xl + .row.is-gutter-width-32 + .col-lg-8.col-xs-12 + + .practice.page-content + header.page-content-header + .page-content-header__end + .page-content-header__row + h1.page-content-header__title + = "#{@coding_test.title}(#{@coding_test.language})" + + .a-card + header.card-header + h2.card-header__title + = CodingTest.human_attribute_name :description + hr.a-border + .card-body + .card__description + .a-long-text.is-md.js-markdown-view + = @coding_test.description + hr.a-border + + - @coding_test.coding_test_cases.each_with_index do |coding_test_case, i| + .coding_test_case + header.card-header + h2.card-header__title = "入力例#{i + 1}" + + .card-body + .card__description + pre.sample.coding_test_case-input(id="coding_test_case_#{coding_test_case.id}_input") + = coding_test_case.input + + header.card-header + h2.card-header__title = "出力例#{i + 1}" + + .card-body + .card__description + pre.sample.coding_test_case-output(id="coding_test_case_#{coding_test_case.id}_output") + = coding_test_case.output + + hr.a-border-tint + footer.card-footer + .card-main-actions + + - unless current_user.submitted?(@coding_test) + .a-card + #code_editor( + data-language="#{@coding_test.language}" style="width:90%;height:400px" + data-coding-test-id="#{@coding_test.id}" + data-practice-id="#{@coding_test.practice_id}" + ) + + hr.a-border-tint + footer.card-footer + .card-main-actions + ul.card-main-actions__items + li.card-main-actions__item + button#run.a-button.is-lg.is-primary.is-block 提出 + + .a-card + header.card-header + h2.card-header__title 結果 + + table.result-table#result + + .col-lg-4.col-xs-12 + nav.page-nav.a-card + header.page-nav__header + h2.page-nav__title + = link_to practice, + class: 'page-nav__title-inner' do + = practice.title + hr.a-border-tint + + ul.page-nav__items + - practice.coding_tests.each do |coding_test| + li.page-nav__item(class="#{@coding_test == coding_test ? 'is-current' : ''}") + = link_to coding_test, class: 'page-nav__item-link' do + span.page-nav__item-link-inner + = coding_test.title + +css: + .result-table { + border-top: solid 1px hsl(242, 7%, 89%); + margin-top: 0.5em; + color: rgb(79, 79, 100); + } + + .result-table th { + background-color: rgb(241, 241, 243); + } + + .result-table th, td { + padding: 0.5em; + border: solid 1px solid 1px hsl(242, 7%, 89%); + } diff --git a/app/views/mentor/_mentor_page_tabs.html.slim b/app/views/mentor/_mentor_page_tabs.html.slim index e4f3cadb4fe..66d6f857aad 100644 --- a/app/views/mentor/_mentor_page_tabs.html.slim +++ b/app/views/mentor/_mentor_page_tabs.html.slim @@ -5,6 +5,10 @@ = link_to 'メンターページ', mentor_root_path, class: "page-tabs__item-link #{current_link(/^mentor-home/)}" + li.page-tabs__item + = link_to 'コーディングテスト', + mentor_coding_tests_path, + class: "page-tabs__item-link #{current_link(/^mentor-coding_tests/)}" li.page-tabs__item = link_to 'プラクティス', mentor_practices_path, diff --git a/app/views/mentor/coding_tests/_coding_test_case_fields.html.slim b/app/views/mentor/coding_tests/_coding_test_case_fields.html.slim new file mode 100644 index 00000000000..7da66e44062 --- /dev/null +++ b/app/views/mentor/coding_tests/_coding_test_case_fields.html.slim @@ -0,0 +1,13 @@ +.nested-fields + .field + = f.label :input + br + = f.text_area :input, + style: 'border:solid 1px black;background:white;margin:12px 0' + .field + = f.label :output + br + = f.text_area :output, + style: 'border:solid 1px black;background:white;margin:12px 0' + = link_to_remove_association '削除', + f, style: 'border:solid 1px black;background:white;padding:3px;margin:6px 0' diff --git a/app/views/mentor/coding_tests/_form.html.slim b/app/views/mentor/coding_tests/_form.html.slim new file mode 100644 index 00000000000..f96ad5c14a2 --- /dev/null +++ b/app/views/mentor/coding_tests/_form.html.slim @@ -0,0 +1,60 @@ += form_with model: [:mentor, coding_test], local: true, html: { name: 'coding_test', class: 'form' } do |f| + = render 'errors', object: coding_test + = f.hidden_field :course_id, value: params[:course_id] + .form__items + .form-item + .row + .col-lg-6.col-xs-12 + = f.label :practice, class: 'a-form-label' + .select-practices + = f.collection_select :practice_id, + Practice.all, + :id, + :title, + { include_blank: 'プラクティスを選択してください' }, + { id: 'js-choices-single-select' } + .col-md-3.col-xs-6 + = f.label :language, class: 'a-form-label' + .select-users + = f.select :language, + CodingTest.languages.keys, + { include_blank: '言語を選択してください' }, + { class: 'js-select2' } + .form-item + .row + .col-md-6.col-xs-12 + = f.label :title, class: 'a-form-label' + = f.text_field :title, class: 'a-text-input js-warning-form', placeholder: '文字列操作' + .col-md-3.col-xs-6 + = f.label :user, class: 'a-form-label' + .select-users + = f.select :user_id, + User.where(retired_on: nil).pluck(:login_name, :id).sort, + { include_blank: '作成者を選択してください' }, + { class: 'js-select2' } + .form-item + .row.js-markdown-parent + .col-md-6.col-xs-12 + = f.label :description, class: 'a-form-label' + = f.text_area :description, class: 'a-text-input js-warning-form js-markdown markdown-form__text-area practices-edit__input', data: { 'preview': '.js-preview' } + .col-md-6.col-xs-12 + .a-form-label プレビュー + .js-preview.a-long-text.is-md.practices-edit__input.markdown-form__preview + + h2(style='font-weight:bold;margin:12px 0;') テストケース + + .form-item + = f.fields_for :coding_test_cases do |coding_test_case| + = render 'coding_test_case_fields', f: coding_test_case + .links + = link_to_add_association '追加', f, :coding_test_cases, style: 'border:solid 1px black;background:white;padding:3px;margin:6px 0' + + .form-actions + ul.form-actions__items + li.form-actions__item.is-main + = f.submit nil, class: 'a-button is-lg is-primary is-block' + li.form-actions__item.is-sub + = link_to 'キャンセル', mentor_categories_path, class: 'a-button is-sm is-text' + - if coding_test.id.present? + li.form-actions__item.is-muted + = link_to '削除', mentor_coding_test_path(coding_test), method: :delete, data: { confirm: '本当によろしいですか?' }, class: 'a-button is-sm is-muted-text' diff --git a/app/views/mentor/coding_tests/edit.html.slim b/app/views/mentor/coding_tests/edit.html.slim new file mode 100644 index 00000000000..5ceeb3f0ebc --- /dev/null +++ b/app/views/mentor/coding_tests/edit.html.slim @@ -0,0 +1,27 @@ +- title 'コーディング問題編集' + +header.page-header + .container + .page-header__inner + .page-header__start + .page-header__title メンターページ + += render 'mentor/mentor_page_tabs' + +.page-main + header.page-main-header + .container + .page-main-header__inner + .page-main-header__start + h1.page-main-header__title + = title + .page-main-header__end + .page-main-header-actions + .page-main-header-actions__items + .page-main-header-actions__item + = link_to mentor_coding_tests_path, class: 'a-button is-md is-secondary is-block is-back' do + | コーディング問題 + hr.a-border + .page-body + .container.is-xxl + = render 'form', coding_test: @coding_test diff --git a/app/views/mentor/coding_tests/index.html.slim b/app/views/mentor/coding_tests/index.html.slim new file mode 100644 index 00000000000..88695ac4e99 --- /dev/null +++ b/app/views/mentor/coding_tests/index.html.slim @@ -0,0 +1,61 @@ +- title 'コーディング問題' + +header.page-header + .container + .page-header__inner + .page-header__start + h2.page-header__title メンターページ + += render 'mentor/mentor_page_tabs' + +main.page-main + header.page-main-header + .container + .page-main-header__inner + .page-main-header__start + h1.page-main-header__title = title + .page-main-header__end + .page-header-actions + .page-header-actions__items + .page-header-actions__item + = link_to new_mentor_coding_test_path, class: 'a-button is-md is-secondary is-block' do + i.fa-regular.fa-plus + | コーディング問題作成 + + hr.a-border + .page-body + .container.is-lg + .admin-table.is-grab id='mentor-practices' + table.admin-table__table + thead.admin-table__header + tr.admin-table__labels + th.admin-table__label = CodingTest.human_attribute_name :title + th.admin-table__label = CodingTest.human_attribute_name :language + th.admin-table__label 入力例・出力例 + th.admin-table__label = CodingTest.human_attribute_name :practice + th.admin-table__label = CodingTest.human_attribute_name :coding_test_submissions + th.admin-table__label.actions 編集 + tbody.admin-table__items + - @coding_tests.each do |coding_test| + tr.admin-table__item + td.admin-table__item-value + = link_to coding_test.title, coding_test + td.admin-table__item-value + = coding_test.language + td.admin-table__item-value + ul + - coding_test.coding_test_cases.each do |coding_test_case| + li + = coding_test_case.input + = ' / ' + = coding_test_case.output + td.admin-table__item-value + = link_to coding_test.practice.title, coding_test.practice + td.admin-table__item-value.is-text-align-center + = link_to '一覧', [coding_test, :coding_test_submissions] + td.admin-table__item-value.is-text-align-center + ul.is-inline-buttons + li + = link_to edit_mentor_coding_test_path(coding_test), + class: 'a-button is-sm is-secondary is-icon' + i.fa-solid.fa-pen diff --git a/app/views/mentor/coding_tests/new.html.slim b/app/views/mentor/coding_tests/new.html.slim new file mode 100644 index 00000000000..1113a418602 --- /dev/null +++ b/app/views/mentor/coding_tests/new.html.slim @@ -0,0 +1,27 @@ +- title 'メンターページ' + +header.page-header + .container + .page-header__inner + .page-header__start + h2.page-header__title + = title + += render 'mentor/mentor_page_tabs' + +.page-main + header.page-main-header + .container + .page-main-header__inner + .page-main-header__start + h1.page-main-header__title コーディング問題作成 + .page-main-header__end + .page-main-header-actions + .page-main-header-actions__items + .page-main-header-actions__item + = link_to mentor_coding_tests_path, class: 'a-button is-md is-secondary is-block is-back' do + | コーディング問題 + hr.a-border + .page-body + .container.is-xxl + = render 'form', coding_test: @coding_test diff --git a/app/views/practices/_coding_tests.html.slim b/app/views/practices/_coding_tests.html.slim new file mode 100644 index 00000000000..2e35c950014 --- /dev/null +++ b/app/views/practices/_coding_tests.html.slim @@ -0,0 +1,31 @@ +.a-card.coding_tests + header.card-header + h2.card-header__title コーディング問題 + + table + - coding_tests.order(:position).each do |coding_test| + tr + td = link_to coding_test.title, coding_test + td = coding_test.passed_by?(current_user) ? 'O' : 'X' + hr.a-border-tint + footer.card-footer + +css: + .coding_tests { + margin-bottom: 1.5em; + } + + .coding_tests table { + border-top: solid 1px hsl(242, 7%, 89%); + margin-top: 0.5em; + color: rgb(79, 79, 100); + } + + .coding_tests th { + background-color: rgb(241, 241, 243); + } + + .coding_tests th, td { + padding: 0.5em; + border: solid 1px solid 1px hsl(242, 7%, 89%); + } diff --git a/app/views/practices/show.html.slim b/app/views/practices/show.html.slim index 15072b8e526..71e8d9c34bf 100644 --- a/app/views/practices/show.html.slim +++ b/app/views/practices/show.html.slim @@ -111,6 +111,9 @@ br | 終了条件をクリアしたら修了にしてください。 + - if @practice.coding_tests.present? + = render partial: 'coding_tests', locals: { coding_tests: @practice.coding_tests } + - if current_user.admin_or_mentor? = render 'memo', practice: @practice diff --git a/config/locales/ja.yml b/config/locales/ja.yml index b359f5f89a9..6412fc8b2b3 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -47,6 +47,7 @@ ja: book: 参考書籍 regular_event: 定期イベント request_retirement: 退会申請 + coding_test: コーディング問題 attributes: user: login_name: アカウント @@ -301,6 +302,16 @@ ja: request_retirement/keep_data/option: keep: 残す delete: 削除する + coding_test: + title: タイトル + description: 問題文 + user: 作成者 + practice: 所属プラクティス + language: 言語 + coding_test_submissions: 提出コード + coding_test_case: + input: 入力 + output: 出力 enums: user: job: diff --git a/config/routes.rb b/config/routes.rb index 39da1941562..0fd47290f51 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -50,6 +50,11 @@ resource :completion, only: %i(show), controller: "practices/completion" resource :submission_answer, only: %i(show), controller: "practices/submission_answer" end + resources :coding_tests, only: %i(show) do + resources :coding_test_submissions, + only: %i(index show new create show), + controller: "coding_tests/coding_test_submissions" + end resources :pages, param: :slug_or_id namespace :notification do resource :redirector, only: %i(show), controller: "redirector" diff --git a/config/routes/api.rb b/config/routes/api.rb index 8a9d29ff48e..c9c754692f5 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -45,6 +45,7 @@ resource :completion_message, only: %i(update), controller: "practices/learning/completion_message" end end + resources :coding_test_submissions, only: %i(create) resources :reports, only: %i(index) namespace "reports" do resources :unchecked, only: %i(index) do diff --git a/config/routes/mentor.rb b/config/routes/mentor.rb index fba2e993ac6..f3059fc1a1d 100644 --- a/config/routes/mentor.rb +++ b/config/routes/mentor.rb @@ -9,6 +9,7 @@ resources :practices, only: %i(index new edit create update) do resource :submission_answer, only: %i(new edit create update), controller: "practices/submission_answer" end + resources :coding_tests, only: %i(index new edit create update destroy) resources :courses, only: %i(index new edit create update) do resources :categories, only: %i(index), controller: "courses/categories" end diff --git a/db/fixtures/coding_test_cases.yml b/db/fixtures/coding_test_cases.yml new file mode 100644 index 00000000000..a06e780e73e --- /dev/null +++ b/db/fixtures/coding_test_cases.yml @@ -0,0 +1,74 @@ +coding_test_case1: + coding_test: coding_test1 + input: "" + output: hello + +coding_test_case2: + coding_test: coding_test2 + input: world + output: hello world + +coding_test_case3: + coding_test: coding_test2 + input: programming + output: hello programming + +coding_test_case4: + coding_test: coding_test3 + input: 1 + output: 1 + +coding_test_case5: + coding_test: coding_test3 + input: 3 + output: Fizz + +coding_test_case6: + coding_test: coding_test3 + input: 5 + output: Buzz + +coding_test_case7: + coding_test: coding_test3 + input: 15 + output: FizzBuzz + +coding_test_case8: + coding_test: coding_test4 + input: a b c d + output: d c b a + +coding_test_case9: + coding_test: coding_test4 + input: hello + output: olleh + +coding_test_case10: + coding_test: coding_test5 + input: hello + output: h e l l o + +coding_test_case11: + coding_test: coding_test5 + input: world + output: w o r l d + +coding_test_case12: + coding_test: coding_test6 + input: "" + output: hello + +coding_test_case13: + coding_test: coding_test7 + input: 3 + output: Fizz + +coding_test_case14: + coding_test: coding_test7 + input: 5 + output: Buzz + +coding_test_case15: + coding_test: coding_test7 + input: 15 + output: FizzBuzz diff --git a/db/fixtures/coding_test_submissions.yml b/db/fixtures/coding_test_submissions.yml new file mode 100644 index 00000000000..a0248f084e0 --- /dev/null +++ b/db/fixtures/coding_test_submissions.yml @@ -0,0 +1,30 @@ +coding_test_submission1: + coding_test: coding_test1 + user: hajime + code: |- + console.log(\"hello\") + +coding_test_submission6: + coding_test: coding_test6 + user: hajime + code: |- + puts 'hello' + +coding_test_submission7: + coding_test: coding_test7 + user: hajime + code: |- + input = gets.to_i + + output = + if input % 15 == 0 + 'FizzBuzz' + elsif input % 3 == 0 + 'Fizz' + elsif input % 5 == 0 + 'Buzz' + else + input.to_s + end + + puts output diff --git a/db/fixtures/coding_tests.yml b/db/fixtures/coding_tests.yml new file mode 100644 index 00000000000..8a173fe1744 --- /dev/null +++ b/db/fixtures/coding_tests.yml @@ -0,0 +1,80 @@ +coding_test1: + practice: practice62 + user: komagata + language: javascript + title: 最初の出力 + description: |- + `hello`という文字列を出力しなさい。 + hint: "" + position: 1 + +coding_test2: + practice: practice62 + user: komagata + language: javascript + title: 最初の入力 + description: |- + 文字列sが与えられます。 + `hello `とsをつなげて出力しなさい。 + hint: "" + position: 2 + +coding_test3: + practice: practice62 + user: komagata + language: javascript + title: 条件分岐 + description: |- + 正の整数aが与えられます。 + aが3の倍数なら`Fizz`と出力してください。 + aが5の倍数なら`Buzz`と出力してください。 + aが3の倍数かつ5の倍数なら`FizzBuzz`と出力してください。 + どれでもないならaを出力してください。 + hint: 条件分岐にはifが使えます。 + position: 3 + +coding_test4: + practice: practice62 + user: komagata + language: javascript + title: 配列を並び替え + description: |- + 文字a, b, c, dが与えられます。 + 逆に並び替えて`d c b a`と出力してください。 + hint: 逆に並び替えるにはreverseが使えます。 + position: 4 + +coding_test5: + practice: practice62 + user: komagata + language: javascript + title: スペース区切り + description: |- + 文字列aが与えられます。 + 1文字ずつスペースを入れて出力してください。 + hint: 文字列を分割するにはsplitが使えます。 + position: 5 + +coding_test6: + practice: practice26 + user: komagata + language: ruby + title: 最初の出力 + description: |- + `hello`という文字列を出力しなさい。 + hint: "" + position: 1 + +coding_test7: + practice: practice26 + user: komagata + language: ruby + title: 条件分岐 + description: |- + 正の整数aが与えられます。 + aが3の倍数なら`Fizz`と出力してください。 + aが5の倍数なら`Buzz`と出力してください。 + aが3の倍数かつ5の倍数なら`FizzBuzz`と出力してください。 + どれでもないならaを出力してください。 + hint: 条件分岐にはifが使えます。 + position: 2 diff --git a/db/migrate/20240601203433_create_coding_tests.rb b/db/migrate/20240601203433_create_coding_tests.rb new file mode 100644 index 00000000000..81474dec4c8 --- /dev/null +++ b/db/migrate/20240601203433_create_coding_tests.rb @@ -0,0 +1,15 @@ +class CreateCodingTests < ActiveRecord::Migration[6.1] + def change + create_table :coding_tests do |t| + t.integer :language, null: false + t.string :title, null: false + t.text :description + t.text :hint + t.integer :position + t.references :practice, null: false, foreign_key: true + t.references :user, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20240601212831_create_coding_test_cases.rb b/db/migrate/20240601212831_create_coding_test_cases.rb new file mode 100644 index 00000000000..554511703ee --- /dev/null +++ b/db/migrate/20240601212831_create_coding_test_cases.rb @@ -0,0 +1,11 @@ +class CreateCodingTestCases < ActiveRecord::Migration[6.1] + def change + create_table :coding_test_cases do |t| + t.text :input + t.text :output + t.references :coding_test, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20240601212854_create_coding_test_submissions.rb b/db/migrate/20240601212854_create_coding_test_submissions.rb new file mode 100644 index 00000000000..06063ea0cdb --- /dev/null +++ b/db/migrate/20240601212854_create_coding_test_submissions.rb @@ -0,0 +1,12 @@ +class CreateCodingTestSubmissions < ActiveRecord::Migration[6.1] + def change + create_table :coding_test_submissions do |t| + t.text :code, null: false + t.references :coding_test, null: false, foreign_key: true + t.references :user, null: false, foreign_key: true + + t.timestamps + end + add_index :coding_test_submissions, [:coding_test_id, :user_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 23e1e1716c3..0b20059fc92 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -172,6 +172,40 @@ t.index ["user_id"], name: "index_checks_on_user_id" end + create_table "coding_test_cases", force: :cascade do |t| + t.text "input" + t.text "output" + t.bigint "coding_test_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["coding_test_id"], name: "index_coding_test_cases_on_coding_test_id" + end + + create_table "coding_test_submissions", force: :cascade do |t| + t.text "code", null: false + t.bigint "coding_test_id", null: false + t.bigint "user_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["coding_test_id", "user_id"], name: "index_coding_test_submissions_on_coding_test_id_and_user_id", unique: true + t.index ["coding_test_id"], name: "index_coding_test_submissions_on_coding_test_id" + t.index ["user_id"], name: "index_coding_test_submissions_on_user_id" + end + + create_table "coding_tests", force: :cascade do |t| + t.integer "language", null: false + t.string "title", null: false + t.text "description" + t.text "hint" + t.integer "position" + t.bigint "practice_id", null: false + t.bigint "user_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["practice_id"], name: "index_coding_tests_on_practice_id" + t.index ["user_id"], name: "index_coding_tests_on_user_id" + end + create_table "comments", id: :serial, force: :cascade do |t| t.text "description" t.integer "user_id" @@ -806,6 +840,11 @@ add_foreign_key "categories_practices", "practices" add_foreign_key "check_box_choices", "check_boxes" add_foreign_key "check_boxes", "survey_questions" + add_foreign_key "coding_test_cases", "coding_tests" + add_foreign_key "coding_test_submissions", "coding_tests" + add_foreign_key "coding_test_submissions", "users" + add_foreign_key "coding_tests", "practices" + add_foreign_key "coding_tests", "users" add_foreign_key "discord_profiles", "users" add_foreign_key "external_entries", "users" add_foreign_key "hibernations", "users" diff --git a/db/seeds.rb b/db/seeds.rb index acc291f14b4..515cbb34d41 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -60,6 +60,9 @@ survey_question_listings buzzes inquiries + coding_tests + coding_test_cases + coding_test_submissions ] ActiveRecord::FixtureSet.create_fixtures 'db/fixtures', tables diff --git a/package.json b/package.json index 29d589e3ffd..d535e139e3d 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,11 @@ }, "dependencies": { "@babel/preset-react": "^7.18.6", + "@codemirror/basic-setup": "^0.20.0", "@johmun/vue-tags-input": "^2.0.1", "@rails/webpacker": "5.4.3", "@yaireo/tagify": "^4.17.6", + "ace-builds": "^1.35.0", "autosize": "^4.0.2", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "choices.js": "^10.1.0", diff --git a/public/javascript.js b/public/javascript.js new file mode 100644 index 00000000000..40892a3ec70 --- /dev/null +++ b/public/javascript.js @@ -0,0 +1,48 @@ +/** + * @file OnBrowserJudge - for JavaScript + * @author nodai2hITC + * @license MIT License + * @version v1.0.0 + */ + +"use strict" + +self.postMessage(["init"]) + +self.addEventListener("message", async function(e) { + switch (e.data[0]) { + case "init": + this.self.postMessage(["ready"]) + break + case "execute": + const data = e.data[1] + const testCase = data.testCase + const program = data.program + const input = data.input + + let output = "" + const _readFileSync = { + readFileSync: function(stdin, encoding){ + if (stdin != "/dev/stdin" || encoding != "utf8") throw "readFileSync error" + return input + } + } + const _console = { log: function(text) { output += text + "\n" } } + const _require = function(fs) { + if (fs != "fs") throw "require error" + return _readFileSync + } + + let error = 0 + let errorMessage = "" + const startTime = performance.now() + try { + (new Function("console", "require", program))(_console, _require) + } catch(err) { + errorMessage = err.toString() + error = 2 + } + const execTime = performance.now() - startTime + self.postMessage(["executed", { testCase, output, error, errorMessage, execTime }]) + } +}) diff --git a/public/ruby.js b/public/ruby.js new file mode 100644 index 00000000000..bc82c1f1a01 --- /dev/null +++ b/public/ruby.js @@ -0,0 +1,54 @@ +/** + * @file OnBrowserJudge - for Ruby + * @author nodai2hITC + * @license MIT License + * @version v1.0.0 + */ + +"use strict" + +const script = "https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.5.0-2022-12-19-a/dist/browser.umd.js" +const wasm = "https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.5.0-2022-12-19-a/dist/ruby+stdlib.wasm" + +importScripts(script) +let RubyModule +const { DefaultRubyVM } = this["ruby-wasm-wasi"] +const main = async () => { + const response = await fetch(wasm) + const buffer = await response.arrayBuffer() + RubyModule = await WebAssembly.compile(buffer) + self.postMessage(["init"]) +} +main() + +self.addEventListener("message", async function(e) { + switch (e.data[0]) { + case "init": + this.self.postMessage(["ready"]) + break + case "execute": + const data = e.data[1] + const testCase = data.testCase + const program = data.program + const input = data.input + const { vm } = await DefaultRubyVM(RubyModule) + vm.eval(` +require 'stringio' +$stdin = StringIO.new('${input.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}', 'r') +$stdout = $stderr = StringIO.new(+'', 'w') + `) + let output = "" + let error = 0 + let errorMessage = "" + const startTime = performance.now() + try { + vm.eval(program) + output = vm.eval("$stdout.string").toString() + } catch(err) { + errorMessage = err.toString() + error = 2 + } + const execTime = performance.now() - startTime + self.postMessage(["executed", { testCase, output, error, errorMessage, execTime }]) + } +}) diff --git a/test/models/coding_test_case_test.rb b/test/models/coding_test_case_test.rb new file mode 100644 index 00000000000..3bc9066f115 --- /dev/null +++ b/test/models/coding_test_case_test.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require 'test_helper' + +class CodingTestCaseTest < ActiveSupport::TestCase +end diff --git a/test/models/coding_test_language_test.rb b/test/models/coding_test_language_test.rb new file mode 100644 index 00000000000..0d10dd69f13 --- /dev/null +++ b/test/models/coding_test_language_test.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require 'test_helper' + +class CodingTestLanguageTest < ActiveSupport::TestCase +end diff --git a/test/models/coding_test_submission_test.rb b/test/models/coding_test_submission_test.rb new file mode 100644 index 00000000000..5c722c9316c --- /dev/null +++ b/test/models/coding_test_submission_test.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require 'test_helper' + +class CodingTestSubmissionTest < ActiveSupport::TestCase +end diff --git a/test/models/coding_test_test.rb b/test/models/coding_test_test.rb new file mode 100644 index 00000000000..c6038643fcd --- /dev/null +++ b/test/models/coding_test_test.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require 'test_helper' + +class CodingTestTest < ActiveSupport::TestCase +end diff --git a/yarn.lock b/yarn.lock index 60be03a5b5d..9278b0b2904 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1034,6 +1034,83 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@codemirror/autocomplete@^0.20.0": + version "0.20.3" + resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-0.20.3.tgz#affe2d7e2b2e0be42ee1ac5fb74a1c84a6f1bfd7" + integrity sha512-lYB+NPGP+LEzAudkWhLfMxhTrxtLILGl938w+RcFrGdrIc54A+UgmCoz+McE3IYRFp4xyQcL4uFJwo+93YdgHw== + dependencies: + "@codemirror/language" "^0.20.0" + "@codemirror/state" "^0.20.0" + "@codemirror/view" "^0.20.0" + "@lezer/common" "^0.16.0" + +"@codemirror/basic-setup@^0.20.0": + version "0.20.0" + resolved "https://registry.yarnpkg.com/@codemirror/basic-setup/-/basic-setup-0.20.0.tgz#ed331e0b2d29efc0a09317de9e10467b992b0c7b" + integrity sha512-W/ERKMLErWkrVLyP5I8Yh8PXl4r+WFNkdYVSzkXYPQv2RMPSkWpr2BgggiSJ8AHF/q3GuApncDD8I4BZz65fyg== + dependencies: + "@codemirror/autocomplete" "^0.20.0" + "@codemirror/commands" "^0.20.0" + "@codemirror/language" "^0.20.0" + "@codemirror/lint" "^0.20.0" + "@codemirror/search" "^0.20.0" + "@codemirror/state" "^0.20.0" + "@codemirror/view" "^0.20.0" + +"@codemirror/commands@^0.20.0": + version "0.20.0" + resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-0.20.0.tgz#51405d442e6b8687b63e8fa27effc28179917c88" + integrity sha512-v9L5NNVA+A9R6zaFvaTbxs30kc69F6BkOoiEbeFw4m4I0exmDEKBILN6mK+GksJtvTzGBxvhAPlVFTdQW8GB7Q== + dependencies: + "@codemirror/language" "^0.20.0" + "@codemirror/state" "^0.20.0" + "@codemirror/view" "^0.20.0" + "@lezer/common" "^0.16.0" + +"@codemirror/language@^0.20.0": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-0.20.2.tgz#31c3712eac2251810986272dcd6a50510e0c1529" + integrity sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw== + dependencies: + "@codemirror/state" "^0.20.0" + "@codemirror/view" "^0.20.0" + "@lezer/common" "^0.16.0" + "@lezer/highlight" "^0.16.0" + "@lezer/lr" "^0.16.0" + style-mod "^4.0.0" + +"@codemirror/lint@^0.20.0": + version "0.20.3" + resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-0.20.3.tgz#34c0fd45c5acd522637f68602e3a416162e03a15" + integrity sha512-06xUScbbspZ8mKoODQCEx6hz1bjaq9m8W8DxdycWARMiiX1wMtfCh/MoHpaL7ws/KUMwlsFFfp2qhm32oaCvVA== + dependencies: + "@codemirror/state" "^0.20.0" + "@codemirror/view" "^0.20.2" + crelt "^1.0.5" + +"@codemirror/search@^0.20.0": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-0.20.1.tgz#9eba0514218a673e29501a889a4fcb7da7ce24ad" + integrity sha512-ROe6gRboQU5E4z6GAkNa2kxhXqsGNbeLEisbvzbOeB7nuDYXUZ70vGIgmqPu0tB+1M3F9yWk6W8k2vrFpJaD4Q== + dependencies: + "@codemirror/state" "^0.20.0" + "@codemirror/view" "^0.20.0" + crelt "^1.0.5" + +"@codemirror/state@^0.20.0": + version "0.20.1" + resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-0.20.1.tgz#de5c6dc0de3e216eaa3a9ee9391c926b766f6b46" + integrity sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ== + +"@codemirror/view@^0.20.0", "@codemirror/view@^0.20.2": + version "0.20.7" + resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-0.20.7.tgz#1d0acc740f71f92abef4b437c030d4e6c39ab6dc" + integrity sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ== + dependencies: + "@codemirror/state" "^0.20.0" + style-mod "^4.0.0" + w3c-keyname "^2.2.4" + "@csstools/convert-colors@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" @@ -1226,6 +1303,25 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@lezer/common@^0.16.0": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@lezer/common/-/common-0.16.1.tgz#3b98b42fdb11454b89e8a340da10bee1b0f94071" + integrity sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA== + +"@lezer/highlight@^0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-0.16.0.tgz#95f7b7ee3c32c8a0f6ce499c085e8b1f927ffbdc" + integrity sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ== + dependencies: + "@lezer/common" "^0.16.0" + +"@lezer/lr@^0.16.0": + version "0.16.3" + resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-0.16.3.tgz#1e4cc581d2725c498e6a731fc83c379114ba3a70" + integrity sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw== + dependencies: + "@lezer/common" "^0.16.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1562,6 +1658,11 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" +ace-builds@^1.35.0: + version "1.35.0" + resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.35.0.tgz#141fd103016d1a1a7febab57a223af28c75a89bc" + integrity sha512-bwDKqjqNccC/MSujqnYTeAS5dIR8UmGLP0R90mvsJY0FRC8NUWBSTfj34+EIzo2NWc/gV8IZTqv4fXaiZJpCtA== + acorn-jsx@^5.2.0, acorn-jsx@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -2768,6 +2869,11 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +crelt@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" + integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== + cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -8774,6 +8880,11 @@ style-loader@^1.3.0: loader-utils "^2.0.0" schema-utils "^2.7.0" +style-mod@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67" + integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw== + stylehacks@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" @@ -9460,6 +9571,11 @@ vuex@^3.1.1: resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71" integrity sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw== +w3c-keyname@^2.2.4: + version "2.2.8" + resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" + integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ== + watchpack-chokidar2@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"