Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

サンプルコードの右肩にRUNボタンを追加、コードをブラウザ上で実行出来るようにした #177

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions data/bitclust/template.offline/layout
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<title><%=h @title %> (Ruby <%=h ruby_version %> リファレンスマニュアル)</title>
<meta name="description" content="<%=h @description %>">
<script src="<%=h custom_js_url('script.js') %>"></script>
<script type="module" src="<%=h custom_js_url('run.mjs') %>"></script>
</head>
<body>
<%= yield %>
Expand Down
2 changes: 2 additions & 0 deletions lib/bitclust/subcommands/statichtml_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ def exec(argv, options)
@outputdir.to_s, :verbose => @verbose, :preserve => true)
FileUtils.cp(@manager_config[:themedir] + "script.js",
@outputdir.to_s, :verbose => @verbose, :preserve => true)
FileUtils.cp(@manager_config[:themedir] + "run.mjs",
@outputdir.to_s, :verbose => @verbose, :preserve => true)
FileUtils.cp(@manager_config[:themedir] + @manager_config[:favicon_url],
@outputdir.to_s, :verbose => @verbose, :preserve => true)
Dir.mktmpdir do |tmpdir|
Expand Down
131 changes: 131 additions & 0 deletions theme/default/run.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
const rubyWasmUrl = 'https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/ruby.wasm'
const rubyVmUrl = 'https://cdn.jsdelivr.net/npm/ruby-wasm-wasi@latest/dist/browser.esm.js'

let moduleCache
const loadRubyModule = async () => {
if (moduleCache) {
return moduleCache
}
moduleCache = await WebAssembly.compileStreaming(fetch(rubyWasmUrl))
return moduleCache
}

let defaultRubyVMCache
const loadRubyVM = async () => {
if (!defaultRubyVMCache) {
const { DefaultRubyVM } = await import(rubyVmUrl)
defaultRubyVMCache = DefaultRubyVM
}
return await defaultRubyVMCache(await loadRubyModule(), { consolePrint: false })
}

const isHighlightElement = (preElement) => {
if (!preElement || preElement.tagName !== 'PRE') return false

const [highlight, lang] = [...preElement.classList]
return highlight === 'highlight' && lang === 'ruby'
}

const setupWriteSync = (fs, output) => {
const originalWriteSync = fs.writeSync.bind(fs)
const writeSync = function () {
const fd = arguments[0]
if (fd === 1 || fd === 2) {
const textOrBuffer = arguments[1]
const text = arguments.length === 4 ? textOrBuffer : new TextDecoder('utf-8').decode(textOrBuffer)
output(text)
}
return originalWriteSync(...arguments)
}
fs.writeSync = writeSync
}

const createOutputTextArea = () => {
const textarea = document.createElement('textarea')
textarea.classList.add('highlight__run-output')
return textarea
}

const runRuby = async (event) => {
const runButton = event.target
const preElement = runButton.parentElement
if (!isHighlightElement(preElement)) return
if (runButton.dataset.loading) return

let rubyVM
runButton.dataset.loaderror = false
try {
runButton.dataset.loading = true
runButton.innerText = 'LOADING...'
rubyVM = await loadRubyVM()
} catch (error) {
runButton.dataset.loaderror = true
return
} finally {
runButton.dataset.loading = false
runButton.innerText = 'RUN'
}

const outputTextarea = createOutputTextArea()
preElement.insertAdjacentElement('afterend', outputTextarea)
const { vm, fs: { fs } } = rubyVM
setupWriteSync(fs, (text) => { outputTextarea.value += text })

const codeElement = preElement.querySelector('code')

const evalSource = () => {
outputTextarea.value = ''
try {
runButton.dataset.running = true
runButton.innerText = 'RUNNING...'
vm.eval(codeElement.textContent)
} catch (error) {
outputTextarea.value = error
} finally {
setTimeout(() => { runButton.dataset.running = false }, 600)
runButton.innerText = 'RUN'
}
}
runButton.onclick = evalSource
runButton.onkeydown = (event) => {
if (event.code === 'Enter' || event.code === 'Space') {
event.stopPropagation()
evalSource()
return false
}
}

preElement.dataset.editing = true
codeElement.setAttribute('spellcheck', 'off')
codeElement.setAttribute('contenteditable', 'true')
preElement.addEventListener('keydown', (event) => {
if (event.code === 'Enter' && event.ctrlKey) {
event.stopPropagation()
evalSource()
}
})

evalSource()
}

const createRunButton = () => {
const button = document.createElement('span')
button.innerText = 'RUN'
button.setAttribute('role', 'button')
button.setAttribute('class', 'highlight__run-button')
button.setAttribute('tabindex', '0')
button.onclick = runRuby
button.onkeydown = (event) => {
if (event.code === 'Enter' || event.code === 'Space') {
event.stopPropagation()
runRuby(event)
return false
}
}
return button
}

document.querySelectorAll('.highlight.ruby').forEach((elem) => {
const button = createRunButton()
elem.insertAdjacentElement('afterbegin', button)
})
2 changes: 2 additions & 0 deletions theme/default/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
tempDiv.innerHTML = elem.innerHTML
const caption = tempDiv.getElementsByClassName("caption")[0]
if (caption) tempDiv.removeChild(caption)
const runButton = tempDiv.getElementsByClassName("highlight__run-button")[0]
if (runButton) tempDiv.removeChild(runButton)

// textarea for preserving the copy text
const copyText = document.createElement('textarea')
Expand Down
39 changes: 39 additions & 0 deletions theme/default/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,45 @@ pre.highlight {
position: relative;
}

main {
position: relative;
}

/* for RUN */
.highlight__run-button {
padding: 0.25em 0.5em;
background-color: #DDD;
opacity: 0.75;
cursor: pointer;
float: right;
}
.highlight__run-button:hover {
background-color: #EE8;
opacity: 1;
}
.highlight__run-button[data-running="true"] {
background-color: #070;
color: white;
opacity: 1;
}

.highlight__run-output {
width: 100%;
box-sizing: border-box;
background-color: #f2f2f2;
border: 0;
}
pre.highlight.ruby[data-editing="true"]:focus-within {
outline: auto;
}
pre.highlight.ruby[data-editing="true"] > code {
display: inline-block;
width: 100%;
}
pre.highlight.ruby[data-editing="true"] > code:focus {
outline: none;
}

/* for COPY */
.highlight__copy-button {
float: right;
Expand Down