├── .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 |
--------------------------------------------------------------------------------