├── .prettierignore ├── docs ├── dist └── index.md ├── .gitignore ├── README.md ├── tsconfig.json ├── mkdocs.yml ├── package.json └── src ├── run_code.css ├── pyodide.ts ├── run_python.ts └── main.ts /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | -------------------------------------------------------------------------------- /docs/dist: -------------------------------------------------------------------------------- 1 | /Users/samuel/code/mkdocs-run-code/dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /dist/ 3 | /docs/dist/ 4 | /env*/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mkdocs-run-code 2 | 3 | Run code blocks in mkdocs, currently just for use on [docs.pydantic.dev](https://docs.pydantic.dev). 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "module": "commonjs", 5 | "target": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "alwaysStrict": true, 8 | "strict": true, 9 | "preserveConstEnums": true, 10 | "sourceMap": true, 11 | "esModuleInterop": true 12 | }, 13 | "include": ["src"], 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Mkdocs Run Code Example 2 | site_description: Example of using mkdocs-run-code 3 | strict: true 4 | 5 | theme: 6 | name: 'material' 7 | palette: 8 | - media: "(prefers-color-scheme: light)" 9 | scheme: default 10 | primary: pink 11 | accent: pink 12 | toggle: 13 | icon: material/lightbulb-outline 14 | name: "Switch to dark mode" 15 | - media: "(prefers-color-scheme: dark)" 16 | scheme: slate 17 | primary: pink 18 | accent: pink 19 | toggle: 20 | icon: material/lightbulb 21 | name: "Switch to light mode" 22 | features: 23 | - content.tabs.link 24 | - content.code.annotate 25 | - content.code.copy 26 | - announce.dismiss 27 | - navigation.tabs 28 | 29 | extra_javascript: 30 | - 'dist/run_code_main.js?v1' 31 | #- 'https://samuelcolvin.github.io/mkdocs-run-code/run_code_main.js' 32 | 33 | markdown_extensions: 34 | - tables 35 | - toc: 36 | permalink: true 37 | title: Page contents 38 | - admonition 39 | - pymdownx.details 40 | - pymdownx.superfences 41 | - pymdownx.highlight: 42 | pygments_lang_class: true 43 | - pymdownx.extra 44 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Example docs 2 | 3 | 6 | 7 | ```py title="Pydantic Example" 8 | from datetime import datetime 9 | from typing import Tuple 10 | 11 | from pydantic import BaseModel 12 | 13 | 14 | class Delivery(BaseModel): 15 | timestamp: datetime 16 | dimensions: Tuple[int, int] 17 | 18 | 19 | m = Delivery(timestamp='2020-01-02T03:04:05Z', dimensions=['10', '20']) 20 | print(repr(m.timestamp)) 21 | #> datetime.datetime(2020, 1, 2, 3, 4, 5, tzinfo=TzInfo(UTC)) 22 | print(m.dimensions) 23 | #> (10, 20) 24 | ``` 25 | 26 | Some more text here. 27 | 28 | 29 | # Another example 30 | 31 | Here we go: 32 | 33 | ```py title="Validation Successful" 34 | from datetime import datetime 35 | 36 | from pydantic import BaseModel, PositiveInt 37 | 38 | 39 | class User(BaseModel): 40 | id: int 41 | name: str = 'John Doe' 42 | signup_ts: datetime | None 43 | tastes: dict[str, PositiveInt] 44 | 45 | 46 | external_data = { 47 | 'id': 123, 48 | 'signup_ts': '2019-06-01 12:22', 49 | 'tastes': { 50 | 'wine': 9, 51 | b'cheese': 7, 52 | 'cabbage': '1', 53 | }, 54 | } 55 | 56 | user = User(**external_data) 57 | 58 | print(user.id) 59 | #> 123 60 | print(user.model_dump()) 61 | """ 62 | { 63 | 'id': 123, 64 | 'name': 'John Doe', 65 | 'signup_ts': datetime.datetime(2019, 6, 1, 12, 22), 66 | 'tastes': {'wine': 9, 'cheese': 7, 'cabbage': 1}, 67 | } 68 | """ 69 | ``` 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "run-code", 3 | "version": "0.0.1", 4 | "description": "Run code in mkdocs", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "format": "prettier --write '**/*.{ts,js,json}'", 9 | "lint": "tsc && eslint --max-warnings=0 src && prettier --check '**/*.{ts,js,json}'", 10 | "build": "esbuild src/main.ts --minify --bundle --sourcemap --outfile=dist/run_code_main.js", 11 | "update-pages": "uvx ghp-import --push dist" 12 | }, 13 | "author": "Samuel Colvin", 14 | "license": "MIT", 15 | "prettier": { 16 | "singleQuote": true, 17 | "semi": false, 18 | "trailingComma": "all", 19 | "tabWidth": 2, 20 | "printWidth": 80 21 | }, 22 | "eslintConfig": { 23 | "root": true, 24 | "extends": [ 25 | "typescript", 26 | "prettier" 27 | ], 28 | "rules": { 29 | "@typescript-eslint/no-explicit-any": "off" 30 | } 31 | }, 32 | "dependencies": { 33 | "@babel/runtime": "^7.22.15", 34 | "@codemirror/lang-python": "^6.1.3", 35 | "@codemirror/view": "^6.18.0", 36 | "@typescript-eslint/eslint-plugin": "^6.6.0", 37 | "@typescript-eslint/parser": "^6.6.0", 38 | "@uiw/codemirror-theme-dracula": "^4.21.13", 39 | "ansi-to-html": "^0.7.2", 40 | "codemirror": "^6.0.1", 41 | "esbuild": "^0.19.2", 42 | "eslint": "^8.48.0", 43 | "eslint-config-prettier": "^9.0.0", 44 | "eslint-config-typescript": "^3.0.0", 45 | "prettier": "^3.0.3", 46 | "typescript": "^5.2.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/run_code.css: -------------------------------------------------------------------------------- 1 | .run-code-btn { 2 | position: absolute; 3 | top: 0.5rem; 4 | width: 1.5em; 5 | height: 1.5em; 6 | cursor: pointer; 7 | color: var(--md-default-fg-color--lightest); 8 | transition: color .25s; 9 | z-index: 1; 10 | /*outline: 1px dashed red;*/ 11 | } 12 | 13 | .run-code-btn:focus, .run-code-btn:hover, :hover>.run-code-btn { 14 | color: var(--md-accent-fg-color); 15 | } 16 | 17 | .run-code-btn::after { 18 | display: block; 19 | background-color: currentcolor; 20 | height: 1rem; 21 | margin: 0 auto; 22 | content: ""; 23 | } 24 | 25 | .run-code-btn.play-btn { 26 | padding-left: 2px; 27 | right: 3em; 28 | } 29 | 30 | .run-code-btn.play-btn::after { 31 | width: 0.9em; 32 | -webkit-mask-image: url('data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzODQgNTEyIj48cGF0aCBkPSJNNzMgMzljLTE0LjgtOS4xLTMzLjQtOS40LTQ4LjUtLjlTMCA2Mi42IDAgODBWNDMyYzAgMTcuNCA5LjQgMzMuNCAyNC41IDQxLjlzMzMuNyA4LjEgNDguNS0uOUwzNjEgMjk3YzE0LjMtOC43IDIzLTI0LjIgMjMtNDFzLTguNy0zMi4yLTIzLTQxTDczIDM5eiIvPjwvc3ZnPg=='); 33 | } 34 | 35 | 36 | .run-code-btn.reset-btn { 37 | right: 5em; 38 | } 39 | 40 | .run-code-btn.reset-btn::after { 41 | width: 1.2em; 42 | -webkit-mask-image: url('data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBkPSJNMTI1LjcgMTYwSDE3NmMxNy43IDAgMzIgMTQuMyAzMiAzMnMtMTQuMyAzMi0zMiAzMkg0OGMtMTcuNyAwLTMyLTE0LjMtMzItMzJWNjRjMC0xNy43IDE0LjMtMzIgMzItMzJzMzIgMTQuMyAzMiAzMnY1MS4yTDk3LjYgOTcuNmM4Ny41LTg3LjUgMjI5LjMtODcuNSAzMTYuOCAwczg3LjUgMjI5LjMgMCAzMTYuOHMtMjI5LjMgODcuNS0zMTYuOCAwYy0xMi41LTEyLjUtMTIuNS0zMi44IDAtNDUuM3MzMi44LTEyLjUgNDUuMyAwYzYyLjUgNjIuNSAxNjMuOCA2Mi41IDIyNi4zIDBzNjIuNS0xNjMuOCAwLTIyNi4zcy0xNjMuOC02Mi41LTIyNi4zIDBMMTI1LjcgMTYweiIvPjwvc3ZnPg=='); 43 | } 44 | 45 | .run-code-title { 46 | margin-top: 0 !important; 47 | border-top: 0.05rem solid var(--md-default-fg-color--lightest) 48 | } 49 | 50 | .hide-code { 51 | margin: 0 !important; 52 | height: 0 !important; 53 | padding: 0 !important; 54 | } 55 | 56 | .cm-editor { 57 | background-color: var(--md-code-bg-color); 58 | } 59 | 60 | .run-code-hidden { 61 | display: none; 62 | } 63 | -------------------------------------------------------------------------------- /src/pyodide.ts: -------------------------------------------------------------------------------- 1 | interface TTYOps { 2 | put_char: (tty: TTY, val: number | null) => void 3 | fsync: (tty: TTY) => void 4 | } 5 | 6 | export interface TTY { 7 | output: number[] 8 | register: (dev: number, ops: TTYOps) => void 9 | } 10 | 11 | interface PyodideFileSystem { 12 | makedev: (major: number, minor: number) => number 13 | createDevice: { major: number } 14 | mkdev: (path: string, mode: number) => void 15 | unlink: (path: string) => void 16 | symlink: (oldpath: string, newpath: string) => void 17 | closeStream: (fd: number) => void 18 | open: (path: string, flags: number) => number 19 | } 20 | 21 | interface PyodideModule { 22 | TTY: TTY 23 | } 24 | 25 | interface MicroPip { 26 | install: (wheels: string[]) => Promise 27 | } 28 | 29 | export interface Pyodide { 30 | _module: PyodideModule 31 | FS: PyodideFileSystem 32 | loadPackage: (packages: string[]) => Promise 33 | pyimport: (name: string) => MicroPip 34 | runPythonAsync: (code: string) => Promise 35 | globals: Map 36 | } 37 | 38 | declare const loadPyodide: () => Promise 39 | 40 | function importScripts(url: string): Promise { 41 | return new Promise((resolve, reject) => { 42 | const script = document.createElement('script') 43 | script.src = url 44 | script.onload = () => resolve() 45 | script.onerror = () => reject() 46 | document.head.appendChild(script) 47 | }) 48 | } 49 | 50 | export async function downloadPyodide(): Promise { 51 | await importScripts( 52 | 'https://cdn.jsdelivr.net/pyodide/v0.27.7/full/pyodide.js', 53 | ) 54 | return await loadPyodide() 55 | } 56 | 57 | type OnPrint = (data: TTY) => void 58 | 59 | function make_tty_ops(onPrint: OnPrint): TTYOps { 60 | return { 61 | put_char(tty: TTY, val: number | null) { 62 | if (val !== null) { 63 | tty.output.push(val) 64 | } 65 | if (val === null || val === 10) { 66 | onPrint(tty) 67 | } 68 | }, 69 | fsync(tty: TTY) { 70 | onPrint(tty) 71 | }, 72 | } 73 | } 74 | 75 | function setupStreams(FS: PyodideFileSystem, tty: TTY, onPrint: OnPrint) { 76 | const mytty = FS.makedev(FS.createDevice.major++, 0) 77 | const myttyerr = FS.makedev(FS.createDevice.major++, 0) 78 | tty.register(mytty, make_tty_ops(onPrint)) 79 | tty.register(myttyerr, make_tty_ops(onPrint)) 80 | FS.mkdev('/dev/mytty', mytty) 81 | FS.mkdev('/dev/myttyerr', myttyerr) 82 | FS.unlink('/dev/stdin') 83 | FS.unlink('/dev/stdout') 84 | FS.unlink('/dev/stderr') 85 | FS.symlink('/dev/mytty', '/dev/stdin') 86 | FS.symlink('/dev/mytty', '/dev/stdout') 87 | FS.symlink('/dev/myttyerr', '/dev/stderr') 88 | FS.closeStream(0) 89 | FS.closeStream(1) 90 | FS.closeStream(2) 91 | FS.open('/dev/stdin', 0) 92 | FS.open('/dev/stdout', 1) 93 | FS.open('/dev/stderr', 1) 94 | } 95 | 96 | export function preparePyodide(pyodide: Pyodide, onPrint: OnPrint): void { 97 | const { FS } = pyodide 98 | setupStreams(FS, pyodide._module.TTY, onPrint) 99 | } 100 | -------------------------------------------------------------------------------- /src/run_python.ts: -------------------------------------------------------------------------------- 1 | import { downloadPyodide, preparePyodide, TTY, Pyodide } from './pyodide' 2 | 3 | const chunks: string[] = [] 4 | let lastPost = 0 5 | let updateOut: ((data: string[]) => void) | null = null 6 | const decoder = new TextDecoder() 7 | 8 | function print(tty: TTY) { 9 | if (tty.output && tty.output.length > 0) { 10 | const arr = new Uint8Array(tty.output) 11 | chunks.push(decoder.decode(arr)) 12 | tty.output.length = 0 13 | const now = performance.now() 14 | if (now - lastPost > 100) { 15 | update() 16 | lastPost = now 17 | } 18 | } 19 | } 20 | 21 | function update() { 22 | if (updateOut) { 23 | updateOut(chunks) 24 | } 25 | chunks.length = 0 26 | } 27 | 28 | function log(msg: string) { 29 | console.debug('log:', msg) 30 | if (updateOut) { 31 | updateOut([msg + '\n']) 32 | } 33 | } 34 | 35 | interface PyodideWrapper { 36 | pyodide: Pyodide 37 | reformatException: () => string 38 | } 39 | 40 | let _pyodideWrapper: PyodideWrapper | null = null 41 | 42 | async function load(dependencies: string[]) { 43 | if (_pyodideWrapper === null) { 44 | console.debug('Downloading pyodide...') 45 | 46 | const pyodide = await downloadPyodide() 47 | preparePyodide(pyodide, print) 48 | 49 | console.debug('Loading micropip...') 50 | await pyodide.loadPackage(['micropip']) 51 | const micropip = pyodide.pyimport('micropip') 52 | 53 | // this is required to avoid the pydantic-core install installign the wrong version of typing-extensions 54 | await micropip.install(['typing-extensions>=4.14.1']) 55 | 56 | // pydantic-core requires special handling as it's installed from the file on the github release 57 | const pydantic_core_dep = dependencies.find(d => d.startsWith('pydantic-core')) 58 | if (pydantic_core_dep) { 59 | const pyd_c = pydantic_core_dep.split('==')[1] 60 | const { platform } = (pyodide as any)._api.lockfile_info 61 | const version_info = (pyodide.pyimport('sys') as any).version_info 62 | const pv = `cp${version_info.major}${version_info.minor}` 63 | 64 | const pydantic_core_wheel = `https://githubproxy.samuelcolvin.workers.dev/pydantic/pydantic-core/releases/download/v${pyd_c}/pydantic_core-${pyd_c}-${pv}-${pv}-${platform}_wasm32.whl` 65 | console.debug(`Installing pydantic-core from "${pydantic_core_wheel}"...`) 66 | await micropip.install([pydantic_core_wheel]) 67 | } 68 | 69 | const other_deps = dependencies.filter(d => !d.startsWith('pydantic-core')) 70 | 71 | console.debug(`Installing ${other_deps}...`) 72 | await micropip.install(other_deps) 73 | 74 | await pyodide.runPythonAsync( 75 | // language=python 76 | ` 77 | def reformat_exception(): 78 | import sys 79 | from traceback import format_exception 80 | # Format a modified exception here 81 | # this just prints it normally but you could for instance filter some frames 82 | lines = format_exception(sys.last_type, sys.last_value, sys.last_traceback) 83 | # remove the traceback line about running pyodide 84 | lines.pop(1) 85 | lines.pop(1) 86 | return ''.join(lines) 87 | `, 88 | ) 89 | _pyodideWrapper = { 90 | pyodide, 91 | reformatException: pyodide.globals.get('reformat_exception'), 92 | } 93 | } 94 | return _pyodideWrapper 95 | } 96 | 97 | export async function runCode( 98 | code: string, 99 | onMessage: (data: string[]) => void, 100 | dependencies: string[], 101 | ): Promise { 102 | updateOut = onMessage 103 | let py: PyodideWrapper 104 | try { 105 | py = await load(dependencies) 106 | } catch (e) { 107 | update() 108 | log(`Error starting Python: ${e}`) 109 | updateOut = null 110 | throw e 111 | } 112 | await py.pyodide.runPythonAsync(` 113 | import pydantic, pydantic_core 114 | print(f'pydantic version: v{pydantic.__version__}, pydantic-core version: v{pydantic_core.__version__}') 115 | `) 116 | try { 117 | await py.pyodide.runPythonAsync(code) 118 | update() 119 | } catch (e) { 120 | update() 121 | log(py.reformatException()) 122 | } 123 | updateOut = null 124 | } 125 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { EditorView, minimalSetup } from 'codemirror' 2 | import { indentUnit } from '@codemirror/language' 3 | import { lineNumbers } from '@codemirror/view' 4 | import { dracula } from '@uiw/codemirror-theme-dracula' 5 | import { python } from '@codemirror/lang-python' 6 | import Convert from 'ansi-to-html' 7 | import { runCode } from './run_python' 8 | import './run_code.css' 9 | 10 | function getUrl(filename: string, query?: URLSearchParams): string { 11 | const srcEl: HTMLScriptElement | null = document.querySelector( 12 | 'script[src*="run_code_main.js"]', 13 | ) 14 | if (srcEl) { 15 | const url = new URL(srcEl.src) 16 | url.search = '' 17 | // remove the filename from the pathname 18 | url.pathname = url.pathname.replace('run_code_main.js', '') 19 | url.pathname += filename 20 | if (query) { 21 | url.search = '?' + query.toString() 22 | } 23 | return url.toString() 24 | } else { 25 | throw new Error('could not find script tag for `run_code_main.js`.') 26 | } 27 | } 28 | 29 | function load_css(): Promise { 30 | return new Promise((resolve) => { 31 | const head = document.head 32 | const link = document.createElement('link') 33 | link.type = 'text/css' 34 | link.rel = 'stylesheet' 35 | link.href = getUrl('run_code_main.css') 36 | head.appendChild(link) 37 | link.addEventListener('load', () => resolve()) 38 | }) 39 | } 40 | 41 | const ansi_converter = new Convert() 42 | 43 | declare global { 44 | interface Window { 45 | code_blocks: CodeBlock[] 46 | mkdocs_run_deps?: string[] 47 | } 48 | } 49 | 50 | async function main() { 51 | await load_css() 52 | window.code_blocks = [] 53 | document.querySelectorAll('.language-py, .language-python').forEach((block) => { 54 | window.code_blocks.push(new CodeBlock(block)) 55 | }) 56 | } 57 | main() 58 | 59 | class CodeBlock { 60 | readonly block: Element 61 | terminal_output = '' 62 | code_html = '' 63 | output_el: HTMLElement | null = null 64 | readonly resetBtn: HTMLElement 65 | readonly preEl: HTMLElement 66 | readonly codeEl: HTMLElement 67 | readonly onMessage: (data: string[]) => void 68 | active = false 69 | 70 | constructor(block: Element) { 71 | this.block = block 72 | 73 | const pre = block.querySelector('pre') as HTMLElement 74 | 75 | const playBtn = document.createElement('button') 76 | playBtn.className = 'run-code-btn play-btn' 77 | playBtn.title = 'Run code' 78 | playBtn.addEventListener('click', this.run.bind(this)) 79 | pre.appendChild(playBtn) 80 | 81 | this.resetBtn = document.createElement('button') 82 | this.resetBtn.className = 'run-code-btn reset-btn run-code-hidden' 83 | this.resetBtn.title = 'Reset code' 84 | this.resetBtn.addEventListener('click', this.reset.bind(this)) 85 | pre.appendChild(this.resetBtn) 86 | 87 | const preEl = block.querySelector('pre') 88 | if (!preEl) { 89 | throw new Error('could not find `pre` element in code block') 90 | } 91 | this.preEl = preEl 92 | const codeEl = preEl.querySelector('code') 93 | if (!codeEl) { 94 | throw new Error('could not find `code` element in code block `pre`') 95 | } 96 | this.codeEl = codeEl 97 | 98 | this.onMessage = this.onMessageMethod.bind(this) 99 | } 100 | 101 | run() { 102 | const cmElement = this.block.querySelector('.cm-content') 103 | let python_code 104 | if (cmElement) { 105 | const view = (cmElement as any).cmView.view as EditorView 106 | python_code = view.state.doc.toString() 107 | } else { 108 | this.preEl.classList.add('hide-code') 109 | python_code = this.codeEl.innerText 110 | this.code_html = this.codeEl.innerHTML 111 | this.codeEl.classList.add('hide-code') 112 | this.codeEl.innerText = '' 113 | 114 | const extensions = [ 115 | minimalSetup, 116 | lineNumbers(), 117 | python(), 118 | indentUnit.of(' '), 119 | ] 120 | 121 | const back = parseInt( 122 | window.getComputedStyle(this.codeEl).backgroundColor.match(/\d+/g)![0], 123 | ) 124 | if (back < 128) { 125 | extensions.push(dracula) 126 | } 127 | 128 | new EditorView({ 129 | extensions, 130 | parent: this.block, 131 | doc: python_code, 132 | }) 133 | } 134 | 135 | this.resetBtn.classList.remove('run-code-hidden') 136 | 137 | this.terminal_output = '' 138 | this.output_el = this.block.querySelector('.run-code-output') 139 | if (!this.output_el) { 140 | const output_div = document.createElement('div') 141 | output_div.className = 'highlight output-parent' 142 | output_div.innerHTML = ` 143 | Output 144 |
145 | ` 146 | this.block.appendChild(output_div) 147 | this.output_el = this.block.querySelector( 148 | '.run-code-output', 149 | ) as HTMLElement 150 | } 151 | this.output_el.innerText = 'Starting Python and installing dependencies...' 152 | python_code = python_code.replace(new RegExp(`^ {8}`, 'gm'), '') 153 | 154 | this.active = true 155 | 156 | // reset other code blocks 157 | for (const block of window.code_blocks) { 158 | if (block != this) { 159 | if (block.active) { 160 | block.reset() 161 | } 162 | } 163 | } 164 | 165 | // for backwards compatibility 166 | const default_deps = ['pydantic_core_version==2.6.3', 'pydantic_version==2.3.0'] 167 | const dependencies = window.mkdocs_run_deps || default_deps 168 | runCode(python_code, this.onMessage, dependencies) 169 | } 170 | 171 | reset() { 172 | const cmElement = this.block.querySelector('.cm-editor') 173 | if (cmElement) { 174 | cmElement.remove() 175 | } 176 | const output_parent = this.block.querySelector('.output-parent') 177 | if (output_parent) { 178 | output_parent.remove() 179 | } 180 | 181 | this.preEl.classList.remove('hide-code') 182 | this.codeEl.innerHTML = this.code_html 183 | this.codeEl.classList.remove('hide-code') 184 | 185 | this.resetBtn.classList.add('run-code-hidden') 186 | 187 | this.active = false 188 | } 189 | 190 | onMessageMethod(data: string[]) { 191 | this.terminal_output += data.join('') 192 | const output_el = this.output_el 193 | if (output_el) { 194 | output_el.innerHTML = ansi_converter.toHtml(this.terminal_output) 195 | // scrolls to the bottom of the div 196 | output_el.scrollIntoView(false) 197 | } 198 | } 199 | } 200 | --------------------------------------------------------------------------------