@hyperbook/markdown 0.21.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,79 +11,228 @@ hyperbook.python = (function () {
11
11
  `${HYPERBOOK_ASSETS}directive-pyide/webworker.js`
12
12
  );
13
13
 
14
- const callbacks = {};
15
- let isRunning = false;
14
+ let callback = null;
15
+ /**
16
+ * @type Uint8Array
17
+ */
18
+ let interruptBuffer;
19
+ /**
20
+ * @type Int32Array
21
+ */
22
+ let stdinBuffer;
23
+ if (window.crossOriginIsolated) {
24
+ interruptBuffer = new Uint8Array(new SharedArrayBuffer(1));
25
+ stdinBuffer = new Int32Array(new SharedArrayBuffer(1024));
26
+ pyodideWorker.postMessage({
27
+ type: "setStdinBuffer",
28
+ payload: { stdinBuffer },
29
+ });
30
+ pyodideWorker.postMessage({
31
+ type: "setInterruptBuffer",
32
+ payload: { interruptBuffer },
33
+ });
34
+ } else {
35
+ interruptBuffer = new ArrayBuffer(1);
36
+ pyodideWorker.postMessage({
37
+ type: "setInterruptBuffer",
38
+ payload: { interruptBuffer },
39
+ });
40
+ }
16
41
 
17
- const asyncRun = (id) => {
18
- if (isRunning) return;
42
+ const asyncRun = (id, type) => {
43
+ if (callback) return;
19
44
 
20
- isRunning = true;
21
- updateRunning();
45
+ interruptBuffer[0] = 0;
22
46
  return (script, context) => {
23
- // the id could be generated more carefully
24
47
  return new Promise((onSuccess) => {
25
- callbacks[id] = onSuccess;
48
+ callback = onSuccess;
49
+ updateRunning(id, type);
26
50
  pyodideWorker.postMessage({
27
- ...context,
28
- python: script,
51
+ type: "run",
52
+ payload: {
53
+ ...context,
54
+ python: script,
55
+ },
29
56
  id,
30
57
  });
31
58
  });
32
59
  };
33
60
  };
34
61
 
35
- const updateRunning = () => {
62
+ function interruptExecution() {
63
+ // 2 stands for SIGINT.
64
+ interruptBuffer[0] = 2;
65
+ }
66
+
67
+ const updateRunning = (id, type) => {
68
+ const elems = document.getElementsByClassName("directive-pyide");
36
69
  for (let elem of elems) {
37
70
  const run = elem.getElementsByClassName("run")[0];
38
- if (isRunning) {
39
- run.classList.add("running");
40
- run.textContent = "Running ...";
71
+ const test = elem.getElementsByClassName("test")[0];
72
+ if (callback) {
73
+ if (elem.id === id && type === "run") {
74
+ run.textContent = "Running (Click to stop) ...";
75
+ run.addEventListener("click", interruptExecution);
76
+ } else if (test && elem.id === id && type === "test") {
77
+ test.textContent = "Testing (Click to stop) ...";
78
+ test.addEventListener("click", interruptExecution);
79
+ } else {
80
+ run.classList.add("running");
81
+ run.disabled = true;
82
+ if (test) {
83
+ test.classList.add("running");
84
+ test.disabled = true;
85
+ }
86
+ }
41
87
  } else {
42
88
  run.classList.remove("running");
43
89
  run.textContent = "Run";
90
+ run.disabled = false;
91
+ run.removeEventListener("click", interruptExecution);
92
+ if (test) {
93
+ test.classList.remove("running");
94
+ test.textContent = "Test";
95
+ test.disabled = false;
96
+ test.removeEventListener("click", interruptExecution);
97
+ }
44
98
  }
45
- run.disabled = isRunning;
46
99
  }
47
100
  };
48
101
 
49
102
  pyodideWorker.onmessage = (event) => {
50
- const { id, ...data } = event.data;
51
- if (data.type === "stdout") {
52
- const output = document
53
- .getElementById(id)
54
- .getElementsByClassName("output")[0];
55
- output.appendChild(document.createTextNode(data.message + "\n"));
56
- return;
103
+ const { id, type, payload } = event.data;
104
+ switch (type) {
105
+ case "stdout": {
106
+ const output = document
107
+ .getElementById(id)
108
+ .getElementsByClassName("output")[0];
109
+ output.appendChild(document.createTextNode(payload + "\n"));
110
+ break;
111
+ }
112
+ case "error": {
113
+ const onSuccess = callback;
114
+ onSuccess({ error: payload });
115
+ break;
116
+ }
117
+ case "success": {
118
+ const onSuccess = callback;
119
+ onSuccess({ results: payload });
120
+ break;
121
+ }
57
122
  }
58
- const onSuccess = callbacks[id];
59
- delete callbacks[id];
60
- isRunning = false;
61
- updateRunning();
62
- onSuccess(data);
63
123
  };
64
124
 
65
- const elems = document.getElementsByClassName("directive-pyide");
66
-
67
- for (let elem of elems) {
68
- const editor = elem.getElementsByClassName("editor")[0];
69
- const run = elem.getElementsByClassName("run")[0];
70
- const output = elem.getElementsByClassName("output")[0];
71
- const id = elem.id;
72
-
73
- run?.addEventListener("click", () => {
74
- const script = editor.value;
75
- output.innerHTML = "";
76
- asyncRun(id)(script, {})
77
- .then(({ results, error }) => {
78
- if (results) {
79
- output.textContent = results;
80
- } else if (error) {
81
- output.textContent = error;
82
- }
125
+ const init = (root) => {
126
+ const elems = root.getElementsByClassName("directive-pyide");
127
+
128
+ for (let elem of elems) {
129
+ const editor = elem.getElementsByClassName("editor")[0];
130
+ const run = elem.getElementsByClassName("run")[0];
131
+ const test = elem.getElementsByClassName("test")[0];
132
+ const output = elem.getElementsByClassName("output")[0];
133
+ const input = elem.getElementsByClassName("input")[0];
134
+ const outputBtn = elem.getElementsByClassName("output-btn")[0];
135
+ const inputBtn = elem.getElementsByClassName("input-btn")[0];
136
+ const id = elem.id;
137
+ let tests = [];
138
+ try {
139
+ tests = JSON.parse(atob(elem.getAttribute("data-tests")));
140
+ } catch (e) {}
141
+
142
+ function showInput() {
143
+ outputBtn.classList.remove("active");
144
+ inputBtn.classList.add("active");
145
+ output.classList.add("hidden");
146
+ input.classList.remove("hidden");
147
+ }
148
+ function showOutput() {
149
+ outputBtn.classList.add("active");
150
+ inputBtn.classList.remove("active");
151
+ output.classList.remove("hidden");
152
+ input.classList.add("hidden");
153
+ }
154
+
155
+ outputBtn?.addEventListener("click", showOutput);
156
+ inputBtn?.addEventListener("click", showInput);
157
+
158
+ test?.addEventListener("click", async () => {
159
+ showOutput();
160
+ if (callback) return;
161
+
162
+ output.innerHTML = "";
163
+
164
+ const script = editor.value;
165
+ for (let test of tests) {
166
+ const testCode = test.code.replace("#SCRIPT#", script);
167
+
168
+ const heading = document.createElement("div");
169
+ console.log(test);
170
+ heading.innerHTML = `== Test ${test.name} ==`;
171
+ heading.classList.add("test-heading");
172
+ output.appendChild(heading);
173
+
174
+ await asyncRun(id, "test")(testCode, {})
175
+ .then(({ results, error }) => {
176
+ if (results) {
177
+ output.textContent += results;
178
+ } else if (error) {
179
+ output.textContent += error;
180
+ }
181
+ callback = null;
182
+ updateRunning(id, "test");
183
+ })
184
+ .catch((e) => {
185
+ output.textContent = `Error: ${e}`;
186
+ console.log(e);
187
+ callback = null;
188
+ updateRunning(id, "test");
189
+ });
190
+ }
191
+ });
192
+
193
+ run?.addEventListener("click", async () => {
194
+ showOutput();
195
+ if (callback) return;
196
+
197
+ const script = editor.value;
198
+ output.innerHTML = "";
199
+ asyncRun(id, "run")(script, {
200
+ inputs: input.value.split("\n"),
83
201
  })
84
- .catch((e) => {
85
- output.textContent = `Error: ${e}`;
202
+ .then(({ results, error }) => {
203
+ if (results) {
204
+ output.textContent += results;
205
+ } else if (error) {
206
+ output.textContent += error;
207
+ }
208
+ callback = null;
209
+ updateRunning(id, "run");
210
+ })
211
+ .catch((e) => {
212
+ output.textContent = `Error: ${e}`;
213
+ console.log(e);
214
+ callback = null;
215
+ updateRunning(id, "run");
216
+ });
217
+ });
218
+ }
219
+ };
220
+
221
+ const observer = new MutationObserver((mutations) => {
222
+ mutations.forEach((mutation) => {
223
+ if (mutation.addedNodes.length) {
224
+ mutation.addedNodes.forEach((node) => {
225
+ if (node.type === 1 && node.classList?.contains("directive-pyide")) {
226
+ init(node);
227
+ }
86
228
  });
229
+ }
87
230
  });
88
- }
231
+ });
232
+
233
+ observer.observe(document.body, { childList: true, subtree: true });
234
+
235
+ document.addEventListener("DOMContentLoaded", () => {
236
+ init(document);
237
+ });
89
238
  })();
@@ -14,15 +14,30 @@
14
14
 
15
15
  .directive-pyide .container {
16
16
  width: 100%;
17
+ overflow: hidden;
18
+ height: 200px;
19
+ display: flex;
20
+ flex-direction: column;
21
+ }
22
+
23
+ .directive-pyide .output,
24
+ .directive-pyide .input {
25
+ white-space: pre;
26
+ height: 100%;
17
27
  border: 1px solid var(--color-spacer);
18
28
  border-radius: 8px;
19
- overflow: hidden;
20
- padding: 10px;
29
+ border-top-left-radius: 0;
30
+ border-top-right-radius: 0;
21
31
  }
22
32
 
23
33
  .directive-pyide .output {
24
- height: 200px;
25
- white-space: pre;
34
+ padding: 16px;
35
+ margin-bottom: 0px;
36
+ font-family: hyperbook-monospace, monospace;
37
+ }
38
+
39
+ .directive-pyide .hidden {
40
+ display: none;
26
41
  }
27
42
 
28
43
  .directive-pyide .editor-container {
@@ -41,26 +56,52 @@
41
56
  flex: 1;
42
57
  }
43
58
 
44
- .directive-pyide button {
45
- padding: 8px 16px;
59
+ .directive-pyide .buttons {
60
+ display: flex;
46
61
  border: 1px solid var(--color-spacer);
47
62
  border-radius: 8px;
48
63
  border-bottom: none;
49
64
  border-bottom-left-radius: 0;
50
65
  border-bottom-right-radius: 0;
66
+ }
67
+
68
+ .directive-pyide .test-heading {
69
+ font-size: 1.1em;
70
+ font-weight: bold;
71
+ text-decoration: underline;
72
+ margin-top: 8px;
73
+ }
74
+
75
+ .directive-pyide .test-heading:first-of-type {
76
+ margin-top: 0;
77
+ }
78
+
79
+ .directive-pyide button {
80
+ flex: 1;
81
+ padding: 8px 16px;
82
+ border: none;
83
+ border-right: 1px solid var(--color-spacer);
51
84
  background-color: var(--color--background);
52
85
  color: var(--color-text);
53
86
  cursor: pointer;
54
87
  }
55
88
 
89
+ .directive-pyide .buttons:last-child {
90
+ border-right: none;
91
+ }
92
+
93
+ .directive-pyide button.active {
94
+ background-color: var(--color-spacer);
95
+ }
96
+
56
97
  .directive-pyide button:hover {
57
98
  background-color: var(--color-spacer);
58
99
  }
59
100
 
60
101
  .directive-pyide button.running {
61
- pointer-events: none;
62
- cursor: not-allowed;
63
- opacity: 0.5;
102
+ pointer-events: none;
103
+ cursor: not-allowed;
104
+ opacity: 0.5;
64
105
  }
65
106
 
66
107
  @media screen and (min-width: 1024px) {
@@ -69,7 +110,7 @@
69
110
  height: calc(100dvh - 128px);
70
111
 
71
112
  .output {
72
- height: 100%;
113
+ height: 100%;
73
114
  }
74
115
 
75
116
  .container {
@@ -3,40 +3,84 @@
3
3
  // and `.wasm` files as well:
4
4
  importScripts("https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js");
5
5
 
6
+ class StdinHandler {
7
+ constructor(results, options) {
8
+ this.results = results;
9
+ this.idx = 0;
10
+ Object.assign(this, options);
11
+ }
12
+
13
+ stdin() {
14
+ return this.results[this.idx++];
15
+ }
16
+ }
17
+
6
18
  async function loadPyodideAndPackages() {
7
- self.pyodide = await loadPyodide();
19
+ var pyodide = await loadPyodide();
20
+ self.pyodide = pyodide;
8
21
  await self.pyodide.loadPackage([]);
9
22
  }
10
23
  let pyodideReadyPromise = loadPyodideAndPackages();
11
24
 
12
- self.onmessage = async (event) => {
13
- // make sure loading is done
14
- await pyodideReadyPromise;
15
- // Don't bother yet with this line, suppose our API is built in such a way:
16
- const { id, python, ...context } = event.data;
17
- // The worker copies the context in its own "memory" (an object mapping name to values)
18
- for (const key of Object.keys(context)) {
19
- self[key] = context[key];
20
- }
25
+ self.onmessage = async ({ data: { id, type, payload } }) => {
26
+ switch (type) {
27
+ case "run": {
28
+ // make sure loading is done
29
+ await pyodideReadyPromise;
30
+ // Don't bother yet with this line, suppose our API is built in such a way:
31
+ const { python, inputs, ...context } = payload;
32
+ // The worker copies the context in its own "memory" (an object mapping name to values)
33
+ for (const key of Object.keys(context)) {
34
+ self[key] = context[key];
35
+ }
36
+
37
+ self.pyodide.setStdin(new StdinHandler(inputs));
38
+
39
+ self.pyodide.setStdout({
40
+ batched: (msg) => {
41
+ self.postMessage({ id, type: "stdout", payload: msg });
42
+ },
43
+ });
44
+
45
+ self.pyodide.setStderr({
46
+ batched: (msg) => {
47
+ self.postMessage({ id, type: "stderr", payload: msg });
48
+ },
49
+ });
21
50
 
22
- self.pyodide.setStdout({
23
- batched: (msg) => {
24
- self.postMessage({ id, type: "stdout", message: msg });
25
- },
26
- });
27
-
28
- self.pyodide.setStderr({
29
- batched: (msg) => {
30
- self.postMessage({ id, type: "stderr", message: msg });
31
- },
32
- });
33
-
34
- // Now is the easy part, the one that is similar to working in the main thread:
35
- try {
36
- await self.pyodide.loadPackagesFromImports(python);
37
- let results = await self.pyodide.runPythonAsync(python);
38
- self.postMessage({ results, id });
39
- } catch (error) {
40
- self.postMessage({ error: error.message, id });
51
+ // Now is the easy part, the one that is similar to working in the main thread:
52
+ const filename = "<exec>";
53
+ try {
54
+ await self.pyodide.loadPackagesFromImports(python);
55
+ const dict = self.pyodide.globals.get("dict");
56
+ const globals = dict();
57
+ let results = await self.pyodide.runPythonAsync(python, {
58
+ globals,
59
+ locals: globals,
60
+ filename,
61
+ });
62
+ globals.destroy();
63
+ dict.destroy();
64
+ self.postMessage({ type: "success", id, payload: results });
65
+ } catch (error) {
66
+ // clean up trackback
67
+ let message = error.message;
68
+ if (message.startsWith("Traceback")) {
69
+ const lines = message?.split("\n") || [];
70
+ const i = lines.findIndex((line) => line.includes(filename));
71
+ message = lines[0] + "\n" + lines.slice(i).join("\n");
72
+ self.postMessage({ type: "error", payload: message, id });
73
+ }
74
+ self.postMessage({ type: "error", payload: message, id });
75
+ }
76
+ break;
77
+ }
78
+ case "setInterruptBuffer": {
79
+ const { interruptBuffer } = payload;
80
+ // make sure loading is done
81
+ await pyodideReadyPromise;
82
+ self.pyodide.setInterruptBuffer(interruptBuffer);
83
+ break;
84
+ }
41
85
  }
42
86
  };
@@ -1,5 +1,5 @@
1
1
  hyperbook.scratchblock = (function () {
2
- function init() {
2
+ const init = () => {
3
3
  scratchblocks.renderMatching("pre.directive-scratchblock", {
4
4
  style: "scratch3",
5
5
  languages: [
@@ -24,7 +24,22 @@ hyperbook.scratchblock = (function () {
24
24
  ],
25
25
  scale: 1,
26
26
  });
27
- }
27
+ };
28
28
 
29
- init();
29
+ document.addEventListener("DOMContentLoaded", () => {
30
+ init();
31
+ });
32
+
33
+ // Observe for new elements
34
+ const observer = new MutationObserver((mutations) => {
35
+ mutations.forEach((mutation) => {
36
+ mutation.addedNodes.forEach((node) => {
37
+ if (node.nodeType === 1 && node.matches("pre.directive-scratchblock")) {
38
+ init();
39
+ }
40
+ });
41
+ });
42
+ });
43
+
44
+ observer.observe(document.body, { childList: true, subtree: true });
30
45
  })();
@@ -50,15 +50,36 @@ hyperbook.slideshow = (function () {
50
50
  window.hyperbook.slideshow.update(id);
51
51
  };
52
52
 
53
- const slideshows = document.getElementsByClassName("slideshow");
54
- for (let slideshow of slideshows) {
55
- const id = slideshow.getAttribute("data-id");
56
- update(id);
57
- }
53
+ const init = (root) => {
54
+ const slideshows = root.getElementsByClassName("slideshow");
55
+ for (let slideshow of slideshows) {
56
+ const id = slideshow.getAttribute("data-id");
57
+ update(id);
58
+ }
59
+ };
60
+
61
+ // Initialize existing slideshows on document load
62
+ document.addEventListener("DOMContentLoaded", () => {
63
+ init(document);
64
+ });
65
+
66
+ // Observe for new slideshows added to the DOM
67
+ const observer = new MutationObserver((mutations) => {
68
+ mutations.forEach((mutation) => {
69
+ mutation.addedNodes.forEach((node) => {
70
+ if (node.nodeType === 1) { // Element node
71
+ init(node);
72
+ }
73
+ });
74
+ });
75
+ });
76
+
77
+ observer.observe(document.body, { childList: true, subtree: true });
58
78
 
59
79
  return {
60
80
  update,
61
81
  moveBy,
62
82
  setActive,
83
+ init,
63
84
  };
64
85
  })();
@@ -1,14 +1,15 @@
1
1
  hyperbook.tabs = (function () {
2
- let allTabs = document.querySelectorAll(".directive-tabs .tab[data-tabs-id]");
3
-
4
- allTabs.forEach((tab) =>
5
- tab.addEventListener("click", () => {
6
- selectTab(
7
- tab.getAttribute("data-tabs-id"),
8
- tab.getAttribute("data-tab-id"),
9
- );
10
- }),
11
- );
2
+ const init = (root) => {
3
+ let allTabs = root.querySelectorAll(".directive-tabs .tab[data-tabs-id]");
4
+ allTabs.forEach((tab) =>
5
+ tab.addEventListener("click", () => {
6
+ selectTab(
7
+ tab.getAttribute("data-tabs-id"),
8
+ tab.getAttribute("data-tab-id"),
9
+ );
10
+ }),
11
+ );
12
+ };
12
13
 
13
14
  function selectTab(tabsId, tabId) {
14
15
  let relevantTabButtons = document.querySelectorAll(
@@ -31,7 +32,23 @@ hyperbook.tabs = (function () {
31
32
  });
32
33
  }
33
34
 
35
+ init(document.body);
36
+
37
+ // Observe for new tabs added to the DOM
38
+ const observer = new MutationObserver((mutations) => {
39
+ mutations.forEach((mutation) => {
40
+ mutation.addedNodes.forEach((node) => {
41
+ if (node.nodeType === 1) { // Element node
42
+ init(node);
43
+ }
44
+ });
45
+ });
46
+ });
47
+
48
+ observer.observe(document.body, { childList: true, subtree: true });
49
+
34
50
  return {
35
51
  selectTab,
52
+ init,
36
53
  };
37
54
  })();
@@ -1,3 +1,5 @@
1
1
  /* PrismJS 1.29.0
2
- https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
2
+ https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+java+plsql+python+sql&plugins=line-numbers+toolbar+copy-to-clipboard+download-button */
3
3
  code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
4
+ pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}
5
+ div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none}