Skip to content

Add tabbed Julia/Python language switcher to documentation code blocks#89

Closed
Copilot wants to merge 9 commits intomainfrom
copilot/add-tabbed-code-blocks
Closed

Add tabbed Julia/Python language switcher to documentation code blocks#89
Copilot wants to merge 9 commits intomainfrom
copilot/add-tabbed-code-blocks

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 28, 2026

Documentation only had Julia code examples; the Python lmpc package provides equivalent functionality but was undiscoverable from the docs.

Changes

Infrastructure

  • docs/src/assets/lang-switcher.css — Tab button + panel styles using CSS custom properties to inherit Documenter.jl's theme; uses inline-flex with language icons
  • docs/src/assets/lang-switcher.js — Tab switching with localStorage preference persistence and page-level sync across all switchers; triggers hljs.highlightElement() on code in raw HTML blocks
  • docs/make.jl — Wires assets in via Documenter.HTML(assets=[...]); contains a markdown preprocessor that expands @tab/@tabexample/@tabsetup blocks before makedocs runs and restores source files in a try/finally

Maintainable custom block types

Three custom fenced-code block types are introduced and processed by the docs/make.jl preprocessor:

  • @tab — Display-only tab switcher (no code execution). Julia and Python code written once per block; expands to the full HTML lang-switcher.
  • @tabexample name — Julia code written once: shown in the Julia tab AND executed via @example (all lines auto-hidden so only output such as plots is shown). An optional # exec-only section handles lines that execute but are not shown to users (e.g. ylims! for docs plot sizing).
  • @tabsetup name — Julia code written once: shown in the Julia tab AND executed via @setup (silently, no output). Shares state with subsequent @tabexample name blocks of the same name.

All three block types follow the same syntax:

```@tab
# julia
<julia code shown in the Julia tab>
# python
<python code shown in the Python tab>

The preprocessor expands each block into a `@raw html` lang-switcher (and for `@tabexample`/`@tabsetup`, also a `@example`/`@setup` block). Source files are unmodified after the build.

### Documentation pages updated (14 files)
All code blocks that have a `lmpc` Python equivalent are replaced with tab switchers using the new custom block types. All tab buttons display the Julia (three-dot) and Python (snake) SVG icons from `docs/src/assets/`:

| Page | Blocks converted |
|---|---|
| `simple.md` | MPC setup, compute_control, Simulation, plotting, codegen |
| `model.md` | Discrete/continuous MPC construction, disturbances |
| `objective.md` | set_objective, reference preview, linear cost |
| `constraints.md` | set_bounds, constraint horizon, soft constraints |
| `observer.md` | set_state_observer |
| `prestab.md` | set_prestabilizing_feedback |
| `moveblock.md` | move_block |
| `explicit.md` | ExplicitMPC, parameter range, build_tree, compute_control, codegen |
| `game.md` | set_objective with uids + `@tabsetup`/`@tabexample` blocks |
| `solver.md` | DAQP settings, codegen with settings |
| `reference_preview.md` | Enable preview, matrix/vector reference + `@tabsetup`/`@tabexample` blocks |
| `robust.md` | set_x0_uncertainty + `@tabexample` blocks |
| `hybrid.md` | Binary controls setup + `@tabsetup`/`@tabexample` blocks |
| `linear_cost.md` | Price generation, MPC setup, simulation, results + `@tabsetup`/`@tabexample` blocks |

All 37 display-only lang-switcher blocks now use `@tab` instead of hand-written `@raw html`. The only remaining `@raw html` blocks in the documentation are image tags.

### What was intentionally left Julia-only
- **`@example` blocks in `moveblock.md` and `explicit.md`** — use `mpc_examples(...)` and Julia-specific visualizations (`plot(empc, parameters=...)`) with no `lmpc` equivalent
- **ControlSystems.jl integration** — no `lmpc` equivalent
- **`Model` struct / nonlinear linearization** — no Python API surface

## Example tab block (rendered)

Julia tab active:
![Julia tab](https://github.com/user-attachments/assets/6373bde0-5156-4709-b073-43c09dfde58b)

Python tab active:
![Python tab](https://github.com/user-attachments/assets/ba4a1252-a35f-40de-abc5-737d7896270a)

Note: `lmpc` passes `uids` for `set_objective` directly to Julia without index offset, so 1-based indices are used in both language tabs.

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Copilot AI changed the title [WIP] Add tabbed code blocks for Python examples Add tabbed Julia/Python language switcher to documentation code blocks Mar 28, 2026
Copilot AI requested a review from darnstrom March 28, 2026 11:29
Copy link
Copy Markdown
Owner

@darnstrom darnstrom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to use the added icons for julia and python in docs/src/assets for the different code sections? Also, would it be possible to somehow also run have the Python alternative also for @example? The code that is actually run by Documenter only have to be the Julia version, but it would be nice to have the Julia alternative.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

Would it be possible to use the added icons for julia and python in docs/src/assets for the different code sections? Also, would it be possible to somehow also run have the Python alternative also for...

Done in commit 321f00c. Two changes were made:

1. Language icons — The Julia (three-dot) and Python (snake) SVGs already in docs/src/assets/ are now shown in all tab buttons across all 14 documentation pages:

Julia tab Python tab
Julia tab Python tab

2. Python tabs for @example blocks — Added to robust.md, game.md, hybrid.md, reference_preview.md, and linear_cost.md. The approach: a tab switcher (with both Julia and Python code) appears above each example, and the @example block below runs the Julia code and shows only the output (plots) by adding # hide to all code lines. For blocks that produce no output (nothing # hide), they were converted to @setup blocks which run silently but share state with subsequent @example blocks.

Pages left Julia-only for @example blocks: moveblock.md and explicit.md — these use mpc_examples(...) and Julia-specific visualizations (plot(empc, parameters=...)) with no Python equivalent.

Copilot AI requested a review from darnstrom March 28, 2026 12:29
Copy link
Copy Markdown
Owner

@darnstrom darnstrom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way of doing html raw does not feel very maintainable. Is there a way to not have to write the julia code twice for @example and in a more maintainable way?

@darnstrom darnstrom self-requested a review March 28, 2026 13:18
Copy link
Copy Markdown
Owner

@darnstrom darnstrom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way of doing html raw does not feel very maintainable. Is there a way to not have to write the julia code twice for @example and in a more maintainable way?

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

The way of doing html raw does not feel very maintainable. Is there a way to not have to write the julia code twice for @example and in a more maintainable way?

Addressed in commit 810f447. The approach is a markdown preprocessor added to docs/make.jl that eliminates the duplication entirely.

Two new fenced-code block types are introduced:

```@tabexample name
# julia
<julia code — shown in the Julia tab AND executed (all lines auto-hidden)>
# exec-only        ← optional: executed but not shown (e.g., ylims! for plot sizing)
<extra execution lines>
# python
<python code — shown in the Python tab>

# julia
<julia code — shown in Julia tab AND run via @setup (silently)>
# python
<python code — shown in Python tab>

The preprocessor expands each block into a `@raw html` lang-switcher + an `@example`/`@setup` block before `makedocs` runs, then restores the source files in a `try/finally`. Julia code is now written **once** per example.

For example, the entire `robust.md` nominal-MPC-plus-plot block (previously ~30 lines of duplicated Julia) becomes:

```markdown
```@tabexample robust_mpc
# julia
using LinearMPC, Plots
F, G = [1 0.1; 0 1], [0.005; 0.1;;]
true_dynamics = (x, u, d) -> F*x + G*u + 0.01*(rand(2) .- 0.5)
mpc_nominal = LinearMPC.MPC(F, G; Ts=0.1, Np=25, C=[1 0;])
...
plot!(sim_nominal.ys[1,:], xlabel="Time step", ylabel="Position [m]", label="Nominal MPC")
# exec-only
ylims!(0.4, 0.6)
# python
import numpy as np
...


> The way of doing html raw does not feel very maintainable. Is there a way to not have to write the julia code twice for @example and in a more maintainable way?

Same fix as above — see commit `810f447`. The preprocessor approach means the `@raw html` tab switcher is auto-generated from the single source block, so there's no manual HTML to maintain.

Copilot AI requested a review from darnstrom March 28, 2026 13:44
Copy link
Copy Markdown
Owner

@darnstrom darnstrom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now the make.jl does not build:
Make sure that the new functionality builds!
julia> include("make.jl")
ERROR: LoadError: FieldError: type SubString has no field captures, available fields: string, offset, ncodeunits
Stacktrace:
[1] getproperty
@ ./Base_compiler.jl:54 [inlined]
[2] (::var"#_expand_tab_blocks##12#_expand_tab_blocks##13")(m::SubString{String})
@ Main ~/code/LinearMPC.jl/docs/make.jl:87
[3] _replace(io::IOBuffer, repl::var"#_expand_tab_blocks##12#_expand_tab_blocks##13", str::String, r::UnitRange{…}, pattern::Base.RegexAndMatchData)
@ Base ./strings/util.jl:947
[4] _replace_finish(io::IOBuffer, str::String, count::Int64, e1::Int64, patterns::Tuple{…}, replaces::Tuple{…}, rs::Tuple{…})
@ Base ./strings/util.jl:990
[5] replace(str::String, pat_repl::Tuple{Pair{Regex, var"#_expand_tab_blocks##12#_expand_tab_blocks##13"}}, count::Int64)
@ Base ./strings/util.jl:1043
[6] replace
@ ./strings/util.jl:1096 [inlined]
[7] _expand_tab_blocks(content::String)
@ Main ~/code/LinearMPC.jl/docs/make.jl:85
[8] top-level scope
@ ~/code/LinearMPC.jl/docs/make.jl:114
[9] include(mapexpr::Function, mod::Module, _path::String)
@ Base ./Base.jl:307
[10] top-level scope
@ REPL[1]:1
in expression starting at /home/darnstrom/code/LinearMPC.jl/docs/make.jl:108
Some type information was truncated. Use show(err) to see complete types.

…cess regex captures

Agent-Logs-Url: https://github.com/darnstrom/LinearMPC.jl/sessions/db10d7ba-5a52-4564-84b5-28b762d421d3

Co-authored-by: darnstrom <55484604+darnstrom@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

Now the make.jl does not build:
Make sure that the new functionality builds!
julia> include("make.jl")...

Fixed in commit cd2c0c4. The root cause: replace(str, regex => fn) in Julia passes the matched SubString to fn, not a RegexMatch, so .captures doesn't exist. Rewrote _expand_tab_blocks to use eachmatch (which yields proper RegexMatch objects with .captures) and assembles the result string manually with an IOBuffer.

Copilot AI requested a review from darnstrom March 28, 2026 14:51
Copy link
Copy Markdown
Owner

@darnstrom darnstrom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are still some code parts that uses the raw html tag to get the switching between Julia and Python (mainly the parts which do not include the examples now.) Could you also make sure that they are made more "maintainable" similar to what you just did for the @tabexample and @tabsetup

Agent-Logs-Url: https://github.com/darnstrom/LinearMPC.jl/sessions/f93d3479-a4b1-4c56-85be-3217fac5a86a

Co-authored-by: darnstrom <55484604+darnstrom@users.noreply.github.com>
@darnstrom
Copy link
Copy Markdown
Owner

@copilot Try again. To reiterate: I want the raw html parts currently used in some parts of the the documentation to be replaced by the new @tab functions. This will make the documentation more maintainable

@darnstrom
Copy link
Copy Markdown
Owner

@copilot Try again. To reiterate: I want the raw html parts currently used in some parts of the the documentation to be replaced by the new @tab functions. This will make the documentation more maintainable

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

@copilot Try again. To reiterate: I want the raw html parts currently used in some parts of the the documentation to be replaced by the new @tab functions. This will make the documen...

Done in commit ee94537. All 37 @raw html lang-switcher blocks across 12 documentation files have been converted to the maintainable @tab format:

```@tab
# julia
<julia code>
# python
<python code>

The only remaining `@raw html` blocks are image tags (e.g. `<p><img src="..."/></p>`), which correctly stay as-is since they have nothing to do with the language switcher.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants