Skip to content

Commit f06cf50

Browse files
authored
Merge pull request #229 from znz/copy-button-consistency
COPY ボタンまわりの pre の扱いを統一する
2 parents 8bfc640 + 0369c17 commit f06cf50

7 files changed

Lines changed: 86 additions & 38 deletions

File tree

lib/bitclust/mdcompiler.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,8 +347,9 @@ def code_fence
347347
end
348348
terminator = /\A`{#{fence.size}}\s*$/
349349
if lang
350-
line "<pre class=\"highlight #{lang}\">"
350+
# caption はタブとして pre の前に置く(rd 側と同期)
351351
line "<span class=\"caption\">#{escape_html(caption)}</span>" if caption
352+
line "<pre class=\"highlight #{lang}\">"
352353
line "<code>"
353354
src = +""
354355
@f.until_terminator(terminator) do |code_line|

lib/bitclust/rdcompiler.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,8 +310,10 @@ def emlist
310310
if %r!\A//emlist\[(?<caption>[^\[\]]+?)?\]\[(?<lang>\w+?)\]! =~ command
311311
# @type var caption: String?
312312
# @type var lang: String
313-
line "<pre class=\"highlight #{lang}\">"
313+
# caption は pre 内の absolute 配置ではなく、pre の上に密着した
314+
# タブとして描画する(pre 内だと編集・貼り付けで先頭行と重なる)
314315
line "<span class=\"caption\">#{escape_html(caption)}</span>" if caption
316+
line "<pre class=\"highlight #{lang}\">"
315317
line "<code>"
316318
src = +""
317319
@f.until_terminator(%r<\A//\}>) do |line|

test/js/test_script.mjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,29 @@ async function settle() {
213213
'copied text is trimmed (leading newlines dropped, trailing squeezed)')
214214
}
215215

216+
// RUN 出力など、後から生成される pre にもボタンを付けられる公開フック。
217+
// getText はクリック時に評価されるので、内容が変わる要素にも使える
218+
{
219+
const spy = clipboardSpy()
220+
const pre = new FakeElement('pre', '')
221+
runOnload([pre], spy.navigator)
222+
assert(typeof globalThis.window.ruremaAddCopyButton === 'function',
223+
'window.ruremaAddCopyButton is exposed for dynamically created pre')
224+
const output = new FakeElement('pre', 'highlight__run-output')
225+
let current = 'first output\n'
226+
const btn = globalThis.window.ruremaAddCopyButton(output, () => current)
227+
assert(output.firstChild === btn,
228+
'the hook prepends a COPY button to the given element')
229+
btn.onclick()
230+
await settle()
231+
current = 'second output\n'
232+
btn.onclick()
233+
await settle()
234+
assert(spy.written.length === 2 &&
235+
spy.written[0] === 'first output\n' && spy.written[1] === 'second output\n',
236+
'getText is evaluated at click time (dynamic content is copied as-is)')
237+
}
238+
216239
// Clipboard API が無い環境では textarea + execCommand にフォールバックする
217240
{
218241
const pre = new FakeElement('pre', '', 'fallback code\n')

test/test_rdcompiler.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,8 @@ def test_method_with_param_and_emlist_with_caption_and_lang
359359
<dt class='method-param'>[PARAM] arg:</dt>
360360
<dd>
361361
dsc1
362-
<pre class="highlight ruby">
363362
<span class="caption">This is caption</span>
363+
<pre class="highlight ruby">
364364
<code>
365365
dsc2
366366
dsc3
@@ -387,8 +387,8 @@ def test_method_with_samplecode
387387
<p>
388388
abs
389389
</p>
390-
<pre class="highlight ruby">
391390
<span class="caption">description</span>
391+
<pre class="highlight ruby">
392392
<code>
393393
<span class="nb">puts</span> <span class="s2">"</span><span class="s2">text</span><span class="s2">"</span>
394394
</code></pre>

theme/default/js/run.js

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,22 +106,38 @@ function setupBlock(pre, run) {
106106
}
107107

108108
let output
109+
let outputTextNode
109110
const ensureOutput = () => {
110111
if (!output) {
111112
output = document.createElement('pre')
112113
output.className = 'highlight__run-output'
113114
output.setAttribute('aria-live', 'polite')
115+
// COPY ボタン(先頭)を残したまま本文を書き換えられるように、
116+
// 出力テキストは専用のテキストノードに持つ
117+
outputTextNode = document.createTextNode('')
118+
output.appendChild(outputTextNode)
114119
pre.after(output)
120+
// 実行結果もコピーできるようにする(script.js の公開フック)。
121+
// 出力は実行のたびに変わるので、クリック時のテキストを渡す
122+
if (window.ruremaAddCopyButton) {
123+
window.ruremaAddCopyButton(output, () => outputTextNode.data)
124+
}
115125
}
116126
return output
117127
}
128+
const setOutputText = (text) => {
129+
outputTextNode.data = text
130+
}
118131

119132
// After the first run the sample becomes editable (like a scratchpad);
120-
// Ctrl+Enter (or Cmd+Enter) re-runs the edited code. Editing gradually
121-
// loses the syntax-highlight spans; the COPY button keeps copying the
122-
// original text.
133+
// Ctrl+Enter (or Cmd+Enter) re-runs the edited code. The COPY button
134+
// keeps copying the original text.
123135
const enableEditing = () => {
124136
if (code.isContentEditable) return
137+
// 編集でハイライトの span に文字が食い込むと、貼り付けた文字が
138+
// その場の色を引き継いで中途半端に崩れるため、編集可能にする
139+
// 時点でプレーンテキスト化して色を消す
140+
code.textContent = code.textContent
125141
code.contentEditable = 'true'
126142
code.spellcheck = false
127143
pre.dataset.editing = 'true'
@@ -140,22 +156,22 @@ function setupBlock(pre, run) {
140156
button.textContent = 'LOADING...'
141157
const out = ensureOutput()
142158
out.classList.remove('highlight__run-output--error')
143-
out.textContent = ''
159+
setOutputText('')
144160
let label = 'RUN'
145161
try {
146162
const result = await run(code.textContent, () => {
147163
button.textContent = 'RUNNING...'
148164
})
149165
if (result) {
150-
out.textContent = result.error
166+
setOutputText(result.error
151167
? (result.output === '' ? result.error : result.output + '\n' + result.error)
152-
: result.output
168+
: result.output)
153169
if (result.error) out.classList.add('highlight__run-output--error')
154170
enableEditing()
155171
}
156172
} catch (e) {
157173
out.classList.add('highlight__run-output--error')
158-
out.textContent = formatRunError(e)
174+
setOutputText(formatRunError(e))
159175
label = 'RETRY'
160176
} finally {
161177
button.textContent = label

theme/default/script.js

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,26 @@
2222
})
2323
}
2424

25+
// elem の先頭に COPY ボタンを付ける。getText はクリック時に評価される
26+
// ので、RUN の実行結果のように内容が変わる要素にも使える
27+
function addCopyButton(elem, getText) {
28+
const btn = document.createElement('span')
29+
btn.setAttribute('class', 'highlight__copy-button')
30+
btn.onclick = function(){
31+
writeClipboard(getText()).then(function() {
32+
btn.classList.add("copied")
33+
window.setTimeout(function() { btn.classList.remove("copied") }, 1000)
34+
}).catch(function() {
35+
// コピー失敗時は従来どおり何も表示しない
36+
})
37+
}
38+
elem.insertBefore(btn, elem.firstChild)
39+
return btn
40+
}
41+
42+
// RUN 出力(js/run.js)など、後から動的に生成される pre 用の公開フック
43+
window.ruremaAddCopyButton = addCopyButton
44+
2545
window.onload = function() {
2646
// 言語指定なしのコードブロックは class を持たない素の <pre> になるため、
2747
// highlight クラスではなく pre 要素全体に COPY ボタンを付ける
@@ -39,19 +59,7 @@
3959
// RUN ボタンでサンプルを編集しても COPY は元のテキストを保持する
4060
const text = tempDiv.textContent.replace(/^\n+/, "").replace(/\n{2,}$/, "\n")
4161

42-
// COPY button
43-
const btn = document.createElement('span')
44-
btn.setAttribute('class', 'highlight__copy-button')
45-
elem.insertBefore(btn, elem.firstChild)
46-
47-
btn.onclick = function(){
48-
writeClipboard(text).then(function() {
49-
btn.classList.add("copied")
50-
window.setTimeout(function() { btn.classList.remove("copied") }, 1000)
51-
}).catch(function() {
52-
// コピー失敗時は従来どおり何も表示しない
53-
})
54-
}
62+
addCopyButton(elem, function() { return text })
5563
}
5664
)
5765
}

theme/default/style.css

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -126,21 +126,16 @@ code, pre, tt {
126126
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
127127
}
128128

129+
/* COPY ボタン(script.js)が全 pre の先頭行に入るため、上 padding は
130+
highlight の有無によらず 0 にしてボタンを上端に密着させる */
129131
pre {
130132
line-height: 1.1;
131133
background-color: #f2f2f2;
132-
padding: 1.1em;
134+
padding: 0px 1.1em 1.1em 1.1em;
133135
font-weight: normal;
134136
overflow: auto;
135137
}
136138

137-
pre.highlight {
138-
line-height: 1.1;
139-
background-color: #f2f2f2;
140-
padding: 0px 1.1em 1.1em 1.1em;
141-
font-weight: normal;
142-
position: relative;
143-
}
144139

145140
/* for RUN (js/run.js, statichtml --run-ruby-wasm) */
146141
.highlight__run-button {
@@ -212,13 +207,16 @@ pre.highlight.ruby[data-editing="true"] > code:focus {
212207
left: -1000px;
213208
}
214209

215-
pre .caption {
216-
position: absolute;
217-
top: 0;
218-
left: 0;
219-
padding: 0.2em;
210+
/* サンプルコードのキャプション。pre の上に密着したタブとして表示する
211+
(pre 内の absolute 配置だと、編集・貼り付けで先頭行と重なるため) */
212+
.caption {
213+
display: inline-block;
214+
padding: 0.2em 0.5em;
220215
background: #ddd;
221-
border-width: 0 1px 1px 0;
216+
margin: 1em 0 0;
217+
}
218+
.caption + pre {
219+
margin-top: 0;
222220
}
223221

224222
blockquote {

0 commit comments

Comments
 (0)