Code execution#

The exec directive allows executing code from the browser.

Directive#

{exec} runner#

This directive is similar to code-block, but it allows executing the code in the browser, and optionally editing it. runner is one of the supported code runners (html, micropython, python, sql).

exec directives generate <tdoc-exec> elements.

Options

:after: name [name ...]#

Execute one or more exec blocks before this block, in the same environment.

:caption: text#

A caption for the exec block.

:class: [name ...] (IDs)#

A space-separated list of CSS classes to add to the <tdoc-exec> element.

:console-style: property: value; [property: value; ...]#

CSS styles to apply to console output generated by the code block, e.g. max-height: 10rem;.

:editor: [ID]#

Make the code editable. If ID is provided, the content of the editor is saved, and restored on reload. ID must be unique across all documents, e.g. a UUID. The value none disables the editor.

:editor-config: config#

Configuration for the editor, as a JSON5 object (without enclosing {}). The following keys are supported:

  • store: Where to store editor content.

    • 'local' (default): Store in browser localStorage.

    • 'cloud': Store in the cloud.

:env: [name]#

The environment in which the code must be executed. Code executed in distinct environments is isolated from each other. The default environment name is the empty string.

:include: path [path ...] (relative paths)#

Prepend the content of one or more files to the block's content.

:linenos: [value] (true | false)#

When true or empty, display line numbers next to the code. The default is true if the exec block is editable, and false otherwise.

:name: name#

A reference target for the directive, and a name to be referenced in :after: and :then: options.

:output-style: property: value; [property: value; ...]#

CSS styles to apply to the output generated by the code block, e.g. height: 30rem;. To which element the styles are applied is runner-specific.

:reset: value (show | hide | auto)#

Specify if the "Reset" button () should be visible (show), hidden (hide, the default), or if it should be shown only when the initial editor content isn't empty (auto).

:style: property: value; [property: value; ...]#

CSS styles to apply to the code block, e.g. max-height: 20rem.

:then: name [name ...]#

Execute one or more exec blocks after this block, in the same environment.

:when: [trigger ...]#

Define the triggers that cause the block's code is executed. The arguments can include zero or more of the following values:

  • load: Execute the code when the page loads.

  • click: Execute the code on user request.

The default is click. When no triggers are specified, the code isn't executed, except as part of a sequence.

Trigger#

By default, exec blocks are executed on click (:when: click), with controls displayed next to the block. The controls displayed depend on the type of block.

select * from countries where country_code = 'LI';

They can also be executed immediately on load (:when: load).

select * from countries where country_code = 'LI';

Both can be combined (:when: load click), to execute immediately on load and on click.

1select * from countries where country_code = 'LI';

If no triggers are specified (an empty :when: value), the block isn't executed on its own. It can still be executed as part of a sequence.

Editor#

Blocks can be made editable with the :editor: option.

1select * from countries
2  where population > 10000000
3  order by country_code;

The option takes an optional editor ID. If provided, the content of the editor is saved, and restored on page reload. Using the value none as the editor ID disables the editor.

By default, the editor content is saved in browser local storage. This works for anonymous and logged-in users.

1select * from countries where country_code = 'LI';

Editor content can also be saved in remote storage, by adding store: 'cloud' to :editor-config:. This makes editors fully collaborative, i.e. the same text can be edited simultaneously from multiple clients. This only works for logged-in users, and requires a permanent internet connection.

1select * from countries where country_code = 'LI';

Warning

Remote storage is currently experimental.

Sequencing#

The :after: option allows referencing one or more exec blocks on the same page to be executed before the block, in the same environment. The referenced blocks can themselves have :after: options, forming a dependency graph.

Similarly, the :then: option allows referencing one or more exec blocks to be executed after the block. Unlike :after:, only the blocks referenced by the :then: option of the block itself are executed; the :then: options of referenced blocks are ignored.

If a block appears more than once in the graph, only the first occurrence is executed.

-- :name: sql-people
create table people (first_name text not null, last_name text not null);
-- :name: sql-people-select
select * from people;
1-- :after: sql-people
2-- :then: sql-people-select
3insert into people values ('Joe', 'Bar'), ('Jack', 'Sparrow');

Include file content#

The :include: option allows including the content of one or more external files. The content of the files is prepended to the block's content.

create table people (name text not null, height real, favorite_food text);
insert into people values
  ('Joe', 1.83, null),
  ('Jack', 1.55, 'burgers'),
  ('Jim', null, 'pizza'),
  ('Anthony', 1.78, null);
select * from people;

Output#

The output of running a code block (if any) is displayed below the block. Depending on the runner, blocks can display text, images, or arbitrary HTML. The output can change dynamically.

Image coordinates#

When a block outputs an <svg> image or uses a <canvas>, the coordinates within the element can be viewed by holding the Ctrl key and moving the mouse pointer over the element. The displayed coordinates can be pasted into an editor with Shift+Ctrl+X.

This functionality can be disabled by adding the no-coords class to the exec directive.

Libraries#

tdoc/exec.js#

This module (source) provides functionality to support exec directives.

Classes

class exec.ExecElement()#

The class implementing <tdoc-exec> custom elements. It extends TdocElement.

Properties

ExecElement.runner#

An instance of a subclass of Runner that controls the execution of code.

class exec.Runner()#

The base class for runners that control code execution in specific languages.

Properties

Runner.[static] name#

The name of the runner, as specified in the first argument of the exec directive.

Runner.config#

The configuration for the runner, as provided via exec.* metadata.

Runner.text#

The text content of the code block.

Methods

Runner.[static] register(cls)#

Register a subclass of Runner.

Runner.[static] init(cls)#

Perform per-runner type initialization. This method is called once during runner registration.

Runner.init()#

Perform per-runner initialization. This method is called on each Runner instance, just after object construction.

Runner.addControls(controls)#

Add execution controls to the blocks. This method is called during rendering of the directive, and allows subclasses to add language-specific controls.

Arguments:
  • controls (HTMLDivElement) -- The <div> element containing the controls.

Runner.onReady()#

This method is called after init() completes.

Runner.*codeBlocks()#

Return a generator that yields the code from the directives in the :after: and :then: chain of the directive. Yields {node, code} objects, where node is a <tdoc-exec> element in the chain, and code is the code contained in that element.

Runner.run()#

This method runs the code of the directive.

Runner.stop()#

This method stops code execution.

Runner.preRun()#

This method is called before calling run().

Runner.postRun()#

This method is called after run() returns.

Runners#

HTML#

The {exec} html runner displays a complete HTML document as an <iframe>, with limited browsing functionality. Console output produced by console methods is displayed below the rendered HTML.

Note

Adblockers can interfere with {exec} html blocks served by the local server if they prevent access to localhost, for example uBlock Origin with the "Block outsider intrusion into LAN" filter list. As a workaround, disable the corresponding filter list.

This issue does not happen on the deployed site.

MicroPython#

The {exec} micropython runner connects to an embedded system running MicroPython. The code can either be run from RAM (transient, programs disappear on reset) or be written to the file main.py in flash memory (permanent, runs at boot-time).

Note

The micropython runner only works on devices supporting the WebSerial API. Currently, this limits the use to Chromium-based browsers (e.g. Chrome, Edge).

The target device must already be programmed with a MicroPython firmware. The procedure depends on the target device type.

  • BBC micro:bit V2: Download the .hex file for the latest release. Connect the target and mount its filesystem, then copy the file to it.

  • Raspberry Pi Pico: Follow the documentation to download the appropriate firmware file and program the device.

To enable connecting to a target device, it must first be paired with the browser. Connect the target via USB, select " Connect" in the "Tools" menu (), and select the device in the list. This needs to be done only once; once a device is paired, it will be connected automatically when the page loads, unless multiple paired devices are available.

To run a program from RAM, click the "Run" button (). Text input can be sent to the target via the input field, and the program can be interrupted with the "Stop" button (). When the program terminates, the target returns to the REPL and accepts further commands.

To write a program permanently to a target's flash memory, select " Write to main.py" in the "Tools" menu. The program can be removed again with " Remove main.py".

Python#

The {exec} python runner executes code through Pyodide. Each environment uses a distinct, single-threaded interpreter, and all Python blocks specifying the same environment execute on the same interpreter.

The interpreter for the main environment runs on the main browser thread. Interpreter initialization, as well as blocking Python code, will therefore block the main thread and affect rendering, user input, etc. It should be used only when really necessary, e.g. for code using Pygame. All other interpreters run in their own web worker, and don't block user interactions.

Pyodide can be configured via the exec.python metadata. This enables the following functionality:

  • Load packages: Packages outside of the standard library need to be loaded explicitly by listing them in the packages key. Each entry is either a package name or a URL referencing a wheel (a .whl file). The packages in this list can be loaded by name; others must be loaded by URL or with micropip.

  • Copy files to the filesystem: The files key is a mapping of URL to target path. Relative URLs are resolved relative to the _static directory. Relative target paths are resolved relative to $HOME (/home/pyodide). If the target path ends with a /, the filename part of the URL is used as the target filename.

  • Define environment variables: The env key is a mapping of environment variable name to value. This currently only works in the main environment.

exec:
  python:
    packages: [numpy]
    files:
      input.txt:                    # .../_static/input.txt => $HOME/input.txt
      db/init.sql: /tmp/            # .../_static/db/init.sql => /tmp/init.sql
      ../index.html: homepage.html  # .../index.html => $HOME/homepage.html
    env:
      DEBUG: true

The following conf.py options enable site-wide customization of Pyodide.

tdoc_python_modules#
Type:
list
Default:
[]

A list of directories, relative to the directory containing conf.py, containing Python files to be loaded into Pyodide. A _python entry is added automatically if such a directory exists next to conf.py.

Load packages with micropip#

Packages can be installed directly from PyPI or other package registries using micropip (which must itself be added to the exec.python.packages metadata). The installation should be performed only once per interpreter, e.g. by making it conditional on a once() call.

if once('install-snowballstemmer'):  # Install only once per interpreter
  import micropip
  await micropip.install(['snowballstemmer'])
import snowballstemmer
# ...

Note

Installing from PyPI introduces a serving dependency on PyPI servers. This can reduce the availability of the site. It's also not very nice to the operators of PyPI to drive traffic their way (though browser caching should alleviate the issue). For high-traffic pages, the .whl files should be included in the site's _static and installed via the exec.python.packages metadata.

Synchronous calls to async functions#

async functions can be invoked synchronously with pyodide.ffi.run_sync(). This requires the experimental WebAssembly JavaScript promise integration API (JSPI), which isn't widely supported yet.

  • Chromium-based browsers (Google Chrome, Microsoft Edge): JSPI is supported since Chromium 137.

  • Firefox: Firefox is still working on the implementation and doesn't have an origin trial yet. For local testing, the feature can be enabled with a config (javascript.options.wasm_js_promise_integration), but it doesn't work with Pyodide yet.

  • Safari: Safari doesn't currently support JSPI.

Note

Using pyodide.ffi.run_sync() instead of async concurrency prevents interrupting running code via the button.

SQL#

The {exec} sql runner uses a WebAssembly build of SQLite. Each block execution is performed against a new, empty database. Foreign key constraint enforcement is enabled by default.

Custom runners#

It is possible to create custom runners in site repositories. In the following, substitute RUNNER with the name to be used in {exec} directives.

  • Create the file tdoc/exec-RUNNER.js in the _static directory.

    • Import ./exec.js.

    • Subclass Runner, set the static attribute name and implement the desired functionality in the class.

    • Call Runner.register() with the subclass at the end of the module.

    import {Runner} from './exec.js';
    
    class MyRunner extends Runner {
        static name = 'RUNNER';
    
        // Implement runner functionality
    }
    
    Runner.register(MyRunner);
    
  • In conf.py, add a configuration dict for the runner to the exec key of to the metadata option. Optionally, set the highlight key to the language to use for syntax highlighting (available lexers; the default is text, i.e. no highlighting).

    metadata = {
        # ...
        'exec': {'RUNNER': {'highlight': 'lua'}},
    }
    

The existing runners (html, micropython, python, sql) can serve as examples for the creation of new ones.