Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/Documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ on:
push:
branches:
- main
tags: '*'
tags:
- 'v[0-9]+\.[0-9]+\.[0-9]+'
pull_request:
types: [labeled, opened, synchronize, reopened]

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*.json
!.zenodo.json
!.markdownlint.json
!docs/package.json

# System-specific files and directories generated by the BinaryProvider and BinDeps packages
# They contain absolute paths specific to the host computer, and so should not be committed
Expand Down
4 changes: 4 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
build/
node_modules/
package-lock.json
Manifest.toml
6 changes: 4 additions & 2 deletions docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656"
DocumenterMermaid = "a078cd44-4d9c-4618-b545-3ab9d77f9177"
DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365"
LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589"
ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2"
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
Expand Down Expand Up @@ -42,7 +43,8 @@ CommonSolve = "0.2"
DataFrames = "1"
Documenter = "1"
DocumenterInterLinks = "1"
DocumenterMermaid = "0.2"
DocumenterVitepress = "0.3"
LiveServer = "1"
ExaModels = "0.9"
JLD2 = "0.6"
JSON3 = "1"
Expand Down
61 changes: 16 additions & 45 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ using OrdinaryDiffEq
# documentation
using DocumenterInterLinks
using Documenter
using DocumenterMermaid
using DocumenterVitepress
using Markdown
using MarkdownAST: MarkdownAST

Expand Down Expand Up @@ -174,33 +174,6 @@ ext_dir = abspath(joinpath(@__DIR__, "..", "ext"))
# Include the API reference manager
include("api_reference.jl")

# ═══════════════════════════════════════════════════════════════════════════════
# Logger: silence the informational warning about @example blocks whose HTML
# representation exceeds `example_size_threshold`. The SVG fallback is used
# automatically (which is what we want for plots), so this warning is harmless.
# ═══════════════════════════════════════════════════════════════════════════════
using Logging
struct ExampleSizeThresholdFilter <: AbstractLogger
inner::AbstractLogger
end
function Logging.min_enabled_level(l::ExampleSizeThresholdFilter)
return Logging.min_enabled_level(l.inner)
end
function Logging.shouldlog(l::ExampleSizeThresholdFilter, level, _module, group, id)
return Logging.shouldlog(l.inner, level, _module, group, id)
end
Logging.catch_exceptions(l::ExampleSizeThresholdFilter) = Logging.catch_exceptions(l.inner)
function Logging.handle_message(
l::ExampleSizeThresholdFilter, level, message, args...; kwargs...
)
msg = string(message)
if level == Logging.Warn && occursin("example_size_threshold", msg)
return nothing
end
return Logging.handle_message(l.inner, level, message, args...; kwargs...)
end
global_logger(ExampleSizeThresholdFilter(global_logger()))

# ═══════════════════════════════════════════════════════════════════════════════
# Literate: generate tutorial.md, tutorial.ipynb, tutorial.jl
# ═══════════════════════════════════════════════════════════════════════════════
Expand All @@ -217,7 +190,8 @@ for file in ["tutorial.jl"]
INPUT = joinpath(LITERATE_DIR, file)
# Inject @meta Draft=false so the tutorial executes even with global draft=true
function tutorial_postprocess(content)
return "```@meta\nDraft = false\n```\n\n" * content
# return "```@meta\nDraft = false\n```\n\n" * content
return content
end
Literate.markdown(INPUT, MD_OUTPUT; documenter=true, postprocess=tutorial_postprocess)
Literate.notebook(INPUT, NB_OUTPUT; execute=false)
Expand All @@ -240,23 +214,11 @@ with_api_reference(src_dir, ext_dir) do api_pages
remotes=nothing, # Disable remote links. Needed for DocumenterReference
warnonly=true,
sitename="OptimalControl.jl",
format=Documenter.HTML(;
repolink="https://" * repo_url,
prettyurls=false,
assets=[
asset("https://control-toolbox.org/assets/css/documentation.css"),
asset("https://control-toolbox.org/assets/js/documentation.js"),
"assets/custom.css",
],
size_threshold_ignore=[
joinpath("api", "private.md"),
joinpath("api", "public.md"),
"manual-macro-free.md",
"tutorial.md",
],
format=DocumenterVitepress.MarkdownVitepress(;
repo=repo_url, devbranch="main", devurl="dev", sidebar_drawer=true
),
pages=[
"Introduction" => "index.md",
# index.md is the VitePress root — not listed here
"Guided tour" => "tutorial.md",
"Examples" => [
"Energy minimisation" => "example-double-integrator-energy.md",
Expand Down Expand Up @@ -295,4 +257,13 @@ with_api_reference(src_dir, ext_dir) do api_pages
end

# ═══════════════════════════════════════════════════════════════════════════════
deploydocs(; repo=repo_url * ".git", devbranch="main", push_preview=true)
# Deploy documentation to GitHub Pages
# ═══════════════════════════════════════════════════════════════════════════════
bases_file = joinpath(@__DIR__, "build", "bases.txt")
if isfile(bases_file)
DocumenterVitepress.deploydocs(;
repo=repo_url * ".git", devbranch="main", push_preview=true
)
else
@info "Skipping deployment: no bases were built (prerelease with existing higher stable release)."
end
21 changes: 21 additions & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"devDependencies": {
"@types/node": "^25.3.5",
"@types/markdown-it-footnote": "^3.0.4"
},
"scripts": {
"docs:dev": "vitepress dev build/.documenter",
"docs:build": "vitepress build build/.documenter",
"docs:preview": "vitepress preview build/.documenter"
},
"dependencies": {
"@nolebase/vitepress-plugin-enhanced-readabilities": "^2.18.2",
"@mathjax/src": "^4.1.2",
"@mdit/plugin-mathjax": "^0.26.1",
"@mdit/plugin-tex": "^0.24.1",
"markdown-it-footnote": "^4.0.0",
"markdown-it": "^14.1.0",
"vitepress": "^1.6.4",
"vitepress-plugin-tabs": "^0.8.0"
}
}
117 changes: 117 additions & 0 deletions docs/src/.vitepress/config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { defineConfig } from 'vitepress'
import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'
import { mathjaxPlugin } from './mathjax-plugin'
import { juliaReplTransformer } from './julia-repl-transformer'
import footnote from "markdown-it-footnote";
import path from 'path'

const mathjax = mathjaxPlugin()

function getBaseRepository(base: string): string {
if (!base || base === '/') return '/';
const parts = base.split('/').filter(Boolean);
return parts.length > 0 ? `/${parts[0]}/` : '/';
}

const baseTemp = {
base: 'REPLACE_ME_DOCUMENTER_VITEPRESS',// TODO: replace this in makedocs!
}

const nav = [
{ text: 'Home', link: '/index' },
{ component: 'VersionPicker' }
]

// https://vitepress.dev/reference/site-config
export default defineConfig({
base: 'REPLACE_ME_DOCUMENTER_VITEPRESS',// TODO: replace this in makedocs!
title: 'REPLACE_ME_DOCUMENTER_VITEPRESS',
description: 'REPLACE_ME_DOCUMENTER_VITEPRESS',
lastUpdated: true,
cleanUrls: true,
ignoreDeadLinks: true,
outDir: 'REPLACE_ME_DOCUMENTER_VITEPRESS', // This is required for MarkdownVitepress to work correctly...
head: [
['link', { rel: 'icon', href: 'REPLACE_ME_DOCUMENTER_VITEPRESS_FAVICON' }],
['link', { rel: 'stylesheet', href: 'https://control-toolbox.org/assets/css/vitepress-documentation.css' }],
['script', {src: `${getBaseRepository(baseTemp.base)}versions.js`}],
['script', {src: 'https://control-toolbox.org/assets/js/vitepress-documentation.js'}],
['script', {src: `${baseTemp.base}siteinfo.js`}],
// REPLACE_ME_DOCUMENTER_VITEPRESS_NOINDEX
],

markdown: {
codeTransformers: [juliaReplTransformer()],
config(md) {
md.use(tabsMarkdownPlugin);
md.use(footnote);
mathjax.markdownConfig(md);
},
theme: {
light: "github-light",
dark: "github-dark"
},
},
vite: {
plugins: [
mathjax.vitePlugin,
// Escape {{ }} inside backtick inline code in .md files before VitePress compiles
// them as Vue SFCs. Without this, Julia syntax like `@Lie {{H, K}, L}` (Poisson
// bracket notation) would be parsed as a Vue template expression and break the build.
{
name: 'escape-double-braces-in-inline-code',
enforce: 'pre' as const,
transform(code: string, id: string) {
if (!id.endsWith('.md')) return
return code.replace(/`([^`\n]*)`/g, (match: string, content: string) => {
if (!content.includes('{{') && !content.includes('}}')) return match
return '`' + content.replace(/\{\{/g, '&#123;&#123;').replace(/\}\}/g, '&#125;&#125;') + '`'
})
},
},
],
define: {
__DEPLOY_ABSPATH__: JSON.stringify('REPLACE_ME_DOCUMENTER_VITEPRESS_DEPLOY_ABSPATH'),
},
resolve: {
alias: {
'@': path.resolve(__dirname, '../components')
}
},
optimizeDeps: {
exclude: [
'@nolebase/vitepress-plugin-enhanced-readabilities/client',
'vitepress',
'@nolebase/ui',
],
},
ssr: {
noExternal: [
// If there are other packages that need to be processed by Vite, you can add them here.
'@nolebase/vitepress-plugin-enhanced-readabilities',
'@nolebase/ui',
],
},
},
themeConfig: {
outline: 'deep',
logo: 'REPLACE_ME_DOCUMENTER_VITEPRESS',
search: {
provider: 'local',
options: {
detailedView: true
}
},
nav,
sidebar: 'REPLACE_ME_DOCUMENTER_VITEPRESS',
sidebarDrawer: 'REPLACE_ME_DOCUMENTER_VITEPRESS_SIDEBAR_DRAWER',
editLink: 'REPLACE_ME_DOCUMENTER_VITEPRESS',
socialLinks: [
{ icon: 'github', link: 'REPLACE_ME_DOCUMENTER_VITEPRESS' }
],
footer: {
message: 'Made with <a href="https://luxdl.github.io/DocumenterVitepress.jl/dev/" target="_blank"><strong>DocumenterVitepress.jl</strong></a><br>',
copyright: `© Copyright ${new Date().getUTCFullYear()}.`
}
}
})
98 changes: 98 additions & 0 deletions docs/src/.vitepress/julia-repl-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { ShikiTransformer } from "shiki"
import type { Element, ElementContent } from "hast"

type PromptKind = "julia" | "pkg" | null

export function juliaReplTransformer(): ShikiTransformer {
let promptInfoByLine: Array<{ len: number; kind: PromptKind }> = []
let isJuliaBlock = false
const rules: Array<{ kind: PromptKind; re: RegExp }> = [
{ kind: "julia", re: /^julia>/ },
{ kind: "pkg", re: /^(\([^)]*\)\s*)?pkg>/ }, // handles (@v1.9) pkg>
]

function classify(line: string): { len: number; kind: PromptKind } {
for (const r of rules) {
const m = line.match(r.re)
if (m) return { len: m[0].length, kind: r.kind }
}

return { len: 0, kind: null }
}

// `self` lets the `code` hook pass this transformer to its recursive calls.
const self: ShikiTransformer = {
name: "julia-repl-prompts",

preprocess(code, options) {
isJuliaBlock = options.lang === "julia"
return code
},

tokens(tokens) {
if (!isJuliaBlock) {
promptInfoByLine = []
return
}

promptInfoByLine = tokens.map((lineTokens) => {
const line = lineTokens.map((t) => t.content).join("")
return classify(line)
})
},

span(node, line, col) {
if (!isJuliaBlock) return

const info = promptInfoByLine[line - 1]
if (!info || !info.kind || info.len <= 0) return

if (col < info.len) {
this.addClassToHast(node, "repl-prompt")
this.addClassToHast(node, `repl-prompt-${info.kind}`)
}
},

// `julia-repl-runs=...` fences (writer.jl): re-highlight each run with its
// own grammar, stitch into one <pre>. Julia runs reuse this transformer
// for prompt styling; their empty meta no-ops this hook.
code(node) {
const raw = (this.options.meta as { __raw?: string } | undefined)?.__raw ?? ""
const match = raw.match(/julia-repl-runs=(\S+)/)
if (!match) return

const srcLines = this.source.replace(/\n$/, "").split("\n")
const children: ElementContent[] = []
let cursor = 0
for (const spec of match[1].split(",")) {
const [lang, countStr] = spec.split(":")
const count = Number(countStr)
const text = srcLines.slice(cursor, cursor + count).join("\n")
cursor += count

const hast = this.codeToHast(text, {
...this.options,
lang,
meta: {},
// julia input reuses prompt styling; ansi needs none.
transformers: lang === "julia" ? [self] : [],
})
const pre = hast.children[0] as Element
const codeEl = pre.children.find(
(c): c is Element => c.type === "element" && c.tagName === "code",
)
if (!codeEl) continue
for (const lineEl of codeEl.children) {
if (lineEl.type === "element" && lineEl.tagName === "span") {
children.push(lineEl)
children.push({ type: "text", value: "\n" })
}
}
}
if (children.length > 0) children.pop() // drop the trailing newline
node.children = children
},
}

return self
}
Loading
Loading