diff --git a/astro.config.mjs b/astro.config.mjs
index bd24feeb7f..a65aa3366c 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -7,6 +7,7 @@ import { attributeMarkdown, wrapTables } from '/src/themes/octopus/utilities/cus
import llmMdEmitter from './src/integrations/llm-md-emitter.ts';
import pruneDist from './src/integrations/prune-dist.ts';
import rehypeWbr from './src/plugins/rehype-wbr.js';
+import shikiCodeBlock from './src/plugins/shiki-code-block.js';
// https://astro.build/config
export default defineConfig({
@@ -25,11 +26,20 @@ export default defineConfig({
],
markdown: {
shikiConfig: {
- theme: 'light-plus',
+ // Every token carries both sets. main.css picks the dark one up
+ // under html[data-theme='dark']
+ themes: {
+ light: 'light-plus',
+ dark: 'dark-plus'
+ },
+ defaultColor: 'light',
// OCL is HCL-derived, so reuse the HCL grammar for ```ocl fences
langAlias: {
ocl: 'hcl'
- }
+ },
+ // A transformer, because rehype plugins registered through
+ // `processor` below never reach .mdx pages
+ transformers: [shikiCodeBlock()]
},
processor: unified({
remarkPlugins: [
diff --git a/src/assets/icons/copy.svg b/src/assets/icons/copy.svg
new file mode 100644
index 0000000000..b5656c88d9
--- /dev/null
+++ b/src/assets/icons/copy.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/src/pages/components.mdx b/src/pages/components.mdx
index 91b2693b1f..0c52f307eb 100644
--- a/src/pages/components.mdx
+++ b/src/pages/components.mdx
@@ -358,6 +358,118 @@ The Link component is designed to provide a standardized way to display links wi
+### Code block
+
+Every fenced code block is given a header carrying its language and a copy
+button. There is no component to import, so this works in `.md` as well as
+`.mdx`.
+
+#### Label
+
+Text after the language on the opening fence becomes the block's label. Write
+one that says what the code does; the language is already shown on the right.
+
+````text
+```powershell Write a release marker into the repository
+Write-Host "Hello, World!"
+```
+````
+
+```powershell Write a release marker into the repository
+Write-Host "Hello, World!"
+```
+
+Without a label, the header carries the language and the copy button alone.
+
+```powershell
+Write-Host "Hello, World!"
+```
+
+#### Several languages
+
+Wrap one fence per language in `` elements sharing a `data-group`. Each
+`` names its language, and the header offers them in a menu.
+
+````text
+
+PowerShell
+
+```powershell Rename a deployment target
+$machine = $repository.Machines.Get("machines-1");
+```
+
+
+
+C#
+
+```csharp Rename a deployment target
+var machine = repository.Machines.Get("machines-1");
+```
+
+
+````
+
+
+PowerShell
+
+```powershell Rename a deployment target
+$machine = $repository.Machines.Get("machines-1");
+$machine.Name = "Test Server 1";
+$repository.Machines.Modify($machine);
+```
+
+
+
+C#
+
+```csharp Rename a deployment target
+var machine = repository.Machines.Get("machines-1");
+machine.Name = "Test Server 1";
+repository.Machines.Modify(machine);
+```
+
+
+
+A group whose panels hold anything besides a single code block stays a tab list.
+
+#### Long blocks
+
+A block over 500px tall collapses, fading out at the cut. Clicking the code
+expands it, and clicking away collapses it again.
+
+```yaml A deployment process with every step spelled out
+steps:
+ - name: Approve the release
+ action: manual-intervention
+ instructions: Check the release notes before approving.
+ - name: Deploy to the cluster
+ action: kubernetes-deploy-raw-yaml
+ package: octopus/hello-world
+ namespace: production
+ - name: Smoke test
+ action: run-a-script
+ script: |
+ $response = Invoke-WebRequest -Uri "https://example.com/health"
+ if ($response.StatusCode -ne 200) { throw "Unhealthy" }
+ - name: Notify the team
+ action: send-email
+ to: releases@example.com
+ subject: Deployed #{Octopus.Release.Number}
+ - name: Tag the release
+ action: run-a-script
+ script: |
+ git tag "release/#{Octopus.Release.Number}"
+ git push origin --tags
+ - name: Update the changelog
+ action: run-a-script
+ script: |
+ Add-Content CHANGELOG.md "#{Octopus.Release.Number}"
+ - name: Close the change request
+ action: run-a-script
+ script: |
+ Invoke-RestMethod -Method Post -Uri "https://example.com/changes/close"
+```
+
## Layout
### Grid
diff --git a/src/pages/docs/deployments/custom-scripts/index.md b/src/pages/docs/deployments/custom-scripts/index.md
index 974b558761..656818cc2f 100644
--- a/src/pages/docs/deployments/custom-scripts/index.md
+++ b/src/pages/docs/deployments/custom-scripts/index.md
@@ -123,7 +123,7 @@ Sometimes a script launches a service or application that runs continuously. In
PowerShell
-```powershell PowerShell
+```powershell
Start-Process MyService
```
@@ -131,7 +131,7 @@ Start-Process MyService
Bash
-```bash Bash
+```bash
screen -d -m -S "MyService" MyService
```
diff --git a/src/pages/docs/deployments/custom-scripts/logging-messages-in-scripts.md b/src/pages/docs/deployments/custom-scripts/logging-messages-in-scripts.md
index 066a3e3ceb..de68c8f344 100644
--- a/src/pages/docs/deployments/custom-scripts/logging-messages-in-scripts.md
+++ b/src/pages/docs/deployments/custom-scripts/logging-messages-in-scripts.md
@@ -111,7 +111,7 @@ Progress messages will display and update a progress bar on your deployment task
PowerShell
-```ps PowerShell
+```ps
Update-Progress 10
Update-Progress 50 "We're halfway there!"
```
@@ -205,7 +205,7 @@ def updateprogress(progress, message=None):
```bash
function encode_service_message_value
{
- echo -n "$1" | openssl enc -base64 -A
+ echo -n "$1" | openssl enc -base64 -A
}
echo "##octopus[progress percentage='$(encode_service_message_value "$1")' message='$(encode_service_message_value "$2")']"
@@ -216,7 +216,8 @@ echo "##octopus[progress percentage='$(encode_service_message_value "$1")' messa
## Service message
The following service messages can be written directly to standard output which will be parsed by the server and the subsequent log lines written to standard output will be treated with the relevant log level.
-```
+
+```text Set the standard output log level
##octopus[stdout-ignore]
##octopus[stdout-error]
##octopus[stdout-warning]
@@ -226,23 +227,25 @@ The following service messages can be written directly to standard output which
```
To return to the default standard output log level, write the following message:
-```
+
+```text Return to the default standard output log level
##octopus[stdout-default]
```
+The following service messages can be written directly to standard output which will be parsed by the server and the subsequent log lines written to standard error will be treated with the relevant log level.
-The following service messages can be written directly to standard output which will be parsed by the server and the subsequent log lines written to standard error will be treated with the relevant log level.
-```
+```text Set the standard error log level
##octopus[stderr-ignore]
##octopus[stderr-error]
##octopus[stderr-progress]
##octopus[stderr-output]
```
-- `stderr-progress` will cause error log lines to be written as `verbose` log lines.
-- `stderr-output` will cause error log lines to be written as `info` log lines (standard output). Requires version `2025.3`.
+- `stderr-progress` will cause error log lines to be written as `verbose` log lines.
+- `stderr-output` will cause error log lines to be written as `info` log lines (standard output). Requires version `2025.3`.
To return to the default standard error log level, write the following message:
-```
+
+```text Return to the default standard error log level
##octopus[stderr-default]
```
diff --git a/src/pages/docs/deployments/custom-scripts/output-variables.md b/src/pages/docs/deployments/custom-scripts/output-variables.md
index 1a309bc4b0..4d00bbcf84 100644
--- a/src/pages/docs/deployments/custom-scripts/output-variables.md
+++ b/src/pages/docs/deployments/custom-scripts/output-variables.md
@@ -97,7 +97,7 @@ let appInstanceName3 = Octopus.tryFindVariable "Octopus.Action[Determine App Ins
Python3
-```python Python3
+```python
appInstanceName = get_octopusvariable("Octopus.Action[Determine App Instance Name].Output.AppInstanceName")
```
@@ -106,6 +106,7 @@ appInstanceName = get_octopusvariable("Octopus.Action[Determine App Instance Nam
## Service message
The following service message can be written directly (substituting the properties with the relevant values) to standard output which will be parsed by the server and the values processed as an output variable. Note that the properties must be supplied as a base64 encoded UTF-8 string.
-```
+
+```text Write an output variable from standard output
##octopus[setVariable name='' value='']
```
diff --git a/src/pages/docs/deployments/custom-scripts/scripts-in-packages/reference-files-within-a-package.md b/src/pages/docs/deployments/custom-scripts/scripts-in-packages/reference-files-within-a-package.md
index e8aaace61d..e80b5385aa 100644
--- a/src/pages/docs/deployments/custom-scripts/scripts-in-packages/reference-files-within-a-package.md
+++ b/src/pages/docs/deployments/custom-scripts/scripts-in-packages/reference-files-within-a-package.md
@@ -52,7 +52,7 @@ Get-Content ".\subfolder\file.txt"
C#
-```csharp C#
+```csharp
// in pre-deploy, in post-deploy if custom installation directory has not been defined
var extractPath = OctopusParameters["Octopus.Action.Package.InstallationDirectoryPath"];
// if a custom installation directory has been defined
diff --git a/src/pages/docs/deployments/git/commit-to-git.md b/src/pages/docs/deployments/git/commit-to-git.md
index 9f1a44a0ff..85eec6e01c 100644
--- a/src/pages/docs/deployments/git/commit-to-git.md
+++ b/src/pages/docs/deployments/git/commit-to-git.md
@@ -81,7 +81,7 @@ For example, the following scripts write a release marker into the repository be
PowerShell
-```powershell PowerShell
+```powershell
# Get the path to the cloned repository
$repoPath = $OctopusParameters["Octopus.Calamari.Git.RepositoryPath"]
@@ -93,7 +93,7 @@ $repoPath = $OctopusParameters["Octopus.Calamari.Git.RepositoryPath"]
C#
-```csharp C#
+```csharp
// Get the path to the cloned repository
var repoPath = OctopusParameters["Octopus.Calamari.Git.RepositoryPath"];
@@ -105,7 +105,7 @@ System.IO.File.WriteAllText(System.IO.Path.Combine(repoPath, "release-marker.txt
Bash
-```bash Bash
+```bash
# Get the path to the cloned repository
repo_path=$(get_octopusvariable "Octopus.Calamari.Git.RepositoryPath")
@@ -117,7 +117,7 @@ echo "Released #{Octopus.Release.Number} to #{Octopus.Environment.Name}" > "$rep
Python
-```python Python
+```python
# Get the path to the cloned repository
repo_path = get_octopusvariable("Octopus.Calamari.Git.RepositoryPath")
diff --git a/src/pages/docs/infrastructure/deployment-targets/dynamic-infrastructure/new-octopustarget.mdx b/src/pages/docs/infrastructure/deployment-targets/dynamic-infrastructure/new-octopustarget.mdx
index 0692f954d1..1ff71ad3d8 100644
--- a/src/pages/docs/infrastructure/deployment-targets/dynamic-infrastructure/new-octopustarget.mdx
+++ b/src/pages/docs/infrastructure/deployment-targets/dynamic-infrastructure/new-octopustarget.mdx
@@ -61,7 +61,7 @@ Below is an example of creating an AWS ECS Cluster target with [account credenti
PowerShell
-```powershell PowerShell
+```powershell
$inputs = @"
{
"clusterName": "$($OctopusParameters["clusterName"])",
@@ -137,7 +137,7 @@ New-OctopusTarget -Name "$($OctopusParameters["target_name"])" -TargetId "aws-ec
Bash
-```bash Bash
+```bash
read -r -d '' INPUTS <PowerShell
-```powershell
+```powershell Rename a deployment target and save it
$machine = $repository.Machines.Get("machines-1");
$machine.Name = "Test Server 1";
$repository.Machines.Modify($machine);
@@ -23,7 +23,7 @@ $repository.Machines.Modify($machine);
C#
-```csharp
+```csharp Rename a deployment target and save it
// Sync
var machine = repository.Machines.Get("machines-1");
machine.Name = "Test Server 1";
@@ -37,4 +37,4 @@ await repository.Machines.Modify(machine);
-The repository methods all make direct HTTP requests. There's no "session" abstraction or transaction support.
\ No newline at end of file
+The repository methods all make direct HTTP requests. There's no "session" abstraction or transaction support.
diff --git a/src/pages/docs/octopus-rest-api/octopus.client/working-with-spaces.md b/src/pages/docs/octopus-rest-api/octopus.client/working-with-spaces.md
index beb86a3b2d..48c3e8cbd1 100644
--- a/src/pages/docs/octopus-rest-api/octopus.client/working-with-spaces.md
+++ b/src/pages/docs/octopus-rest-api/octopus.client/working-with-spaces.md
@@ -32,7 +32,7 @@ $projects = $repositoryForSpace.Projects.GetAll()
C#
-```csharp C#
+```csharp
// Create endpoint and client
var endpoint = new OctopusServerEndpoint("https://your-octopus-url", "API-YOUR-KEY");
var client = new OctopusClient(endpoint);
@@ -83,4 +83,4 @@ var repositoryForSpace = repository.ForSpace(space);
var projects = repositoryForSpace.Projects.GetAll();
```
-
\ No newline at end of file
+
diff --git a/src/pages/docs/projects/variables/certificate-variables.md b/src/pages/docs/projects/variables/certificate-variables.md
index 12d4bc3f39..e1052d0d43 100644
--- a/src/pages/docs/projects/variables/certificate-variables.md
+++ b/src/pages/docs/projects/variables/certificate-variables.md
@@ -11,38 +11,38 @@ navOrder: 60
In the variable-editor, selecting *Certificate* as the [variable](/docs/projects/variables) type allows you to create a variable with a certificate managed by Octopus as the value.
:::figure
-
+
:::
Certificate variables can be [scoped](/docs/projects/variables/#scoping-variables), similar to regular text variables.
:::figure
-
+
:::
## Expanded properties
-At deploy-time, certificate variables are expanded. For example, a variable _MyCertificate_ becomes:
-
-| Variable | Description | Example value |
-| ---------------------- | ------------------ | ------------- |
-| `MyCertificate` | The certificate ID | Certificates-1 |
-| `MyCertificate.Type` | The variable type | Certificate
-| `MyCertificate.Name` | The user-provided name | My Development Certificate
-| `MyCertificate.Thumbprint` | Thumbprint | A163E39F59560E6FE33A0299D19124B242D9B37E
-| `MyCertificate.RawOriginal` | The base64 encoded original file, exactly as it was uploaded. |
-| `MyCertificate.Password` | The password specified when the file was uploaded. |
-| `MyCertificate.Pfx` | The base64 encoded certificate in [PKCS#12](https://datatracker.ietf.org/doc/html/rfc7292#page-9) format, including the private-key if present. If the originally uploaded certificate was password-protected (i.e. `MyCertificate.Password` is not empty), then this value will also be a password-protected PFX (PKCS#12) format.
-| `MyCertificate.Certificate` | The base64 encoded DER ASN.1 certificate. |
-| `MyCertificate.PrivateKey` | The base64 encoded DER ASN.1 private key. This will be stored and transmitted as a [sensitive variable](/docs/projects/variables/sensitive-variables). |
-| `MyCertificate.CertificatePem` | The PEM representation of the certificate (i.e. the PublicKey with header\footer). |
-| `MyCertificate.PrivateKeyPem` | The PEM representation of the private key (i.e. the PrivateKey with header\footer). |
-| `MyCertificate.ChainPem` | The PEM representation of any chain certificates (intermediate or certificate-authority). This variable does not include the primary certificate. |
-| `MyCertificate.Subject` | The X.500 distinguished name of the subject |
-| `MyCertificate.SubjectCommonName` | The un-attributed subject common name |
-| `MyCertificate.Issuer` | The X.500 distinguished name of the issuer |
-| `MyCertificate.NotBefore` | NotBefore date | 2016-06-15T13:45:30.0000000-07:00
-| `MyCertificate.NotAfter` | NotAfter date | 2019-06-15T13:45:30.0000000-07:00
+At deploy-time, certificate variables are expanded. For example, a variable *MyCertificate* becomes:
+
+| Variable | Description | Example value |
+| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
+| `MyCertificate` | The certificate ID | Certificates-1 |
+| `MyCertificate.Type` | The variable type | Certificate |
+| `MyCertificate.Name` | The user-provided name | My Development Certificate |
+| `MyCertificate.Thumbprint` | Thumbprint | A163E39F59560E6FE33A0299D19124B242D9B37E |
+| `MyCertificate.RawOriginal` | The base64 encoded original file, exactly as it was uploaded. | |
+| `MyCertificate.Password` | The password specified when the file was uploaded. | |
+| `MyCertificate.Pfx` | The base64 encoded certificate in [PKCS#12](https://datatracker.ietf.org/doc/html/rfc7292#page-9) format, including the private-key if present. If the originally uploaded certificate was password-protected (i.e. `MyCertificate.Password` is not empty), then this value will also be a password-protected PFX (PKCS#12) format. | |
+| `MyCertificate.Certificate` | The base64 encoded DER ASN.1 certificate. | |
+| `MyCertificate.PrivateKey` | The base64 encoded DER ASN.1 private key. This will be stored and transmitted as a [sensitive variable](/docs/projects/variables/sensitive-variables). | |
+| `MyCertificate.CertificatePem` | The PEM representation of the certificate (i.e. the PublicKey with header/footer). | |
+| `MyCertificate.PrivateKeyPem` | The PEM representation of the private key (i.e. the PrivateKey with header/footer). | |
+| `MyCertificate.ChainPem` | The PEM representation of any chain certificates (intermediate or certificate-authority). This variable does not include the primary certificate. | |
+| `MyCertificate.Subject` | The X.500 distinguished name of the subject | |
+| `MyCertificate.SubjectCommonName` | The un-attributed subject common name | |
+| `MyCertificate.Issuer` | The X.500 distinguished name of the issuer | |
+| `MyCertificate.NotBefore` | NotBefore date | 2016-06-15T13:45:30.0000000-07:00 |
+| `MyCertificate.NotAfter` | NotAfter date | 2019-06-15T13:45:30.0000000-07:00 |
### Example usage
@@ -51,7 +51,7 @@ Given the certificate variable `MyCertificate`, you can access the certificate t
PowerShell
-```powershell PowerShell
+```powershell
Write-Host $OctopusParameters["MyCertificate.Thumbprint"]
```
diff --git a/src/pages/docs/projects/variables/output-variables.mdx b/src/pages/docs/projects/variables/output-variables.mdx
index 7844ce200f..d88e897c6d 100644
--- a/src/pages/docs/projects/variables/output-variables.mdx
+++ b/src/pages/docs/projects/variables/output-variables.mdx
@@ -114,7 +114,7 @@ testResult = get_octopusvariable("Octopus.Action[StepA].Output.TestResult")
PowerShell
-```powershell PowerShell
+```powershell
Set-OctopusVariable -name "Password" -value "correct horse battery staple" -sensitive
```
@@ -173,14 +173,14 @@ Imagine that an output variable was set by a script which ran on two deployment
In this scenario, the following output variables would be captured:
-| Name | Value | Scope |
+| Name | Value | Scope |
| ---------------------------------------- | -------- | -------------- |
-| `Octopus.Action[StepA].Output[Web01].TestResult` | `Passed` | |
-| `Octopus.Action[StepA].Output[Web02].TestResult` | `Failed` | |
+| `Octopus.Action[StepA].Output[Web01].TestResult` | `Passed` | |
+| `Octopus.Action[StepA].Output[Web02].TestResult` | `Failed` | |
| `Octopus.Action[StepA].Output.TestResult` | `Passed` | Deployment Target: Web01 |
| `Octopus.Action[StepA].Output.TestResult` | `Failed` | Deployment Target: Web02 |
-| `Octopus.Action[StepA].Output.TestResult` | `Passed` | |
-| `Octopus.Action[StepA].Output.TestResult` | `Failed` | |
+| `Octopus.Action[StepA].Output.TestResult` | `Passed` | |
+| `Octopus.Action[StepA].Output.TestResult` | `Failed` | |
Note that for each output variable/deployment target combination:
@@ -195,9 +195,9 @@ For some practical examples of using output variables, and how scoping rules are
## Output from a Deploy a Release step \{#deploy-release-output}
-Output variables from deployments triggered by a _Deploy a Release_ step are captured and exposed as output variables on the _Deploy a Release_ step.
+Output variables from deployments triggered by a *Deploy a Release* step are captured and exposed as output variables on the *Deploy a Release* step.
-To get the value of an output variable from a _Deploy a Release_ step, use the `Output.Deployment` variable on the _Deploy a Release_ step. For example, if your _Deploy a Release_ step is named "Deploy Web Project", the target step in the child project is named "Update IP Address", and the variable name is "IPAddress", you would use the following variable to access it in the parent project: `Octopus.Action[Deploy Web Project].Output.Deployment[Update IP Address].IPAddress`.
+To get the value of an output variable from a *Deploy a Release* step, use the `Output.Deployment` variable on the *Deploy a Release* step. For example, if your *Deploy a Release* step is named "Deploy Web Project", the target step in the child project is named "Update IP Address", and the variable name is "IPAddress", you would use the following variable to access it in the parent project: `Octopus.Action[Deploy Web Project].Output.Deployment[Update IP Address].IPAddress`.
## Setting output variables using scripts \{#output-variables-in-scripts}
@@ -214,9 +214,7 @@ From a PowerShell script, you can use the PowerShell CmdLet `Set-OctopusVariable
For example:
-**PowerShell**
-
-```powershell
+```powershell Set an output variable
Set-OctopusVariable -name "TestResult" -value "Passed"
```
@@ -228,9 +226,7 @@ Set-OctopusVariable -name "TestResult" -value "Passed"
From a C# script, you can use the `public static void SetVariable(string name, string value)` method to set the name and value of an output variable.
-**C#**
-
-```csharp
+```csharp Set an output variable
SetVariable("TestResult", "Passed");
```
@@ -240,9 +236,7 @@ SetVariable("TestResult", "Passed");
In a Bash script you can use the `set_octopusvariable` function to set the name and value of an output variable. This function takes two positional parameters with the same purpose as the PowerShell CmdLet.
-**Bash**
-
-```bash
+```bash Set an output variable
set_octopusvariable "TestResult" "Passed"
```
@@ -252,29 +246,25 @@ set_octopusvariable "TestResult" "Passed"
From a F# script, you can use the `setVariable : name:string -> value:string -> unit` function to collect artifacts. The function takes two parameters with the same purpose as the PowerShell CmdLet.
-**F#**
-
-```fsharp
+```fsharp Set an output variable
Octopus.setVariable "TestResult" "Passed"
```
-**Python3**
-
-```python Python3
+```python Set an output variable
set_octopusvariable("TestResult", "Passed")
```
## Best practice
-If you have multiple steps which depend on an output variable created by a previous step in your deployment process, it can be cumbersome to need to use the full variable name everywhere, e.g. `Octopus.Action[StepA].Output.TestResult`.
+If you have multiple steps which depend on an output variable created by a previous step in your deployment process, it can be cumbersome to need to use the full variable name everywhere, e.g. `Octopus.Action[StepA].Output.TestResult`.
A useful pattern is to create a project variable which evaluates to the output variable, e.g.
-| Variable name | Value |
+| Variable name | Value |
| ---------------------------------------- | -------- |
| `TestResult` | `#{Octopus.Action[StepA].Output.TestResult}` |
-This allows using `TestResult` as the variable name in dependent steps, rather than the full output variable name. In the case of the step name changing (e.g. `StepA` -> `StepX`), this also reduces the amount of places the step name in the output variable expression needs to be changed.
+This allows using `TestResult` as the variable name in dependent steps, rather than the full output variable name. In the case of the step name changing (e.g. `StepA` -> `StepX`), this also reduces the amount of places the step name in the output variable expression needs to be changed.
## Learn more
diff --git a/src/plugins/shiki-code-block.js b/src/plugins/shiki-code-block.js
new file mode 100644
index 0000000000..c8747b08bc
--- /dev/null
+++ b/src/plugins/shiki-code-block.js
@@ -0,0 +1,135 @@
+// Wraps every highlighted block in the code block shell at build time, so the
+// frame, header, label and language are on the page before any script runs.
+// code-blocks.js wires up what happens next: copying, collapsing, and folding a
+// set into one block with a language menu.
+
+const REST = 'Copy to clipboard';
+const SHOW_MORE = 'Show more';
+
+/** Display names for the fence languages used across the docs. */
+const LANGUAGE_NAMES = {
+ bash: 'Bash',
+ batch: 'Batch',
+ 'c#': 'C#',
+ cs: 'C#',
+ csharp: 'C#',
+ docker: 'Docker',
+ dockerfile: 'Dockerfile',
+ fsharp: 'F#',
+ go: 'Go',
+ hcl: 'HCL',
+ html: 'HTML',
+ ini: 'INI',
+ java: 'Java',
+ javascript: 'JavaScript',
+ js: 'JavaScript',
+ json: 'JSON',
+ log: 'Log',
+ markdown: 'Markdown',
+ nginx: 'nginx',
+ ocl: 'OCL',
+ plaintext: 'Text',
+ powershell: 'PowerShell',
+ ps: 'PowerShell',
+ python: 'Python',
+ ruby: 'Ruby',
+ sh: 'Shell',
+ shell: 'Shell',
+ sql: 'SQL',
+ text: 'Text',
+ txt: 'Text',
+ typescript: 'TypeScript',
+ xml: 'XML',
+ yaml: 'YAML',
+ yml: 'YAML',
+};
+
+function displayName(language) {
+ const key = String(language ?? '')
+ .trim()
+ .toLowerCase();
+ if (!key) return '';
+ return LANGUAGE_NAMES[key] ?? key.charAt(0).toUpperCase() + key.slice(1);
+}
+
+function h(tagName, properties, children = []) {
+ return { type: 'element', tagName, properties, children };
+}
+
+function text(value) {
+ return { type: 'text', value };
+}
+
+export default function shikiCodeBlock() {
+ return {
+ name: 'octopus:code-block',
+
+ root(node) {
+ const pre = node.children.find(
+ (child) => child.type === 'element' && child.tagName === 'pre'
+ );
+ if (!pre) return;
+
+ // langAlias rewrites what Shiki reports, so the attribute Astro set from
+ // the fence wins when it is there. ```ocl has to stay OCL, not HCL.
+ const language = displayName(
+ pre.properties?.['data-language'] ?? this.options.lang
+ );
+ const label = this.options.meta?.__raw?.trim() ?? '';
+
+ const header = h('div', { className: ['code-block__header'] }, [
+ h(
+ 'p',
+ { className: ['code-block__label'], hidden: !label },
+ label ? [text(label)] : []
+ ),
+ h('div', { className: ['code-block__actions'] }, [
+ h('span', { className: ['code-block__language'] }, [text(language)]),
+ h(
+ 'button',
+ {
+ type: 'button',
+ className: ['code-block__copy', 'btn', 'btn--small'],
+ 'data-tooltip': REST,
+ 'aria-label': 'Copy code to clipboard',
+ },
+ // Empty: the glyph is a CSS mask on the span itself.
+ [
+ h(
+ 'span',
+ { className: ['code-block__copy-icon', 'btn__icon'] },
+ []
+ ),
+ ]
+ ),
+ ]),
+ ]);
+
+ const body = h('div', { className: ['code-block__body'] }, [
+ h('div', { className: ['code-block__panel'] }, [pre]),
+ h('div', { className: ['code-block__fade'] }, []),
+ ]);
+
+ // Sits below the body rather than over it, so it stays clear of the code
+ // once the block is open. Hidden until code-blocks.js finds the block too
+ // tall to show whole.
+ const toggle = h(
+ 'button',
+ {
+ type: 'button',
+ className: ['code-block__toggle'],
+ 'aria-expanded': 'false',
+ },
+ [text(SHOW_MORE)]
+ );
+
+ // Replaced in place: assigning to node.children would drop anything else
+ // a later Shiki version puts alongside the
.
+ node.children[node.children.indexOf(pre)] = h(
+ 'div',
+ { className: ['code-block'] },
+ [header, body, toggle]
+ );
+ },
+ };
+}
diff --git a/src/scripts/main.js b/src/scripts/main.js
index cd5a85d246..49c941da5f 100644
--- a/src/scripts/main.js
+++ b/src/scripts/main.js
@@ -43,6 +43,13 @@ function enabled(settings, option) {
return settings && settings.includes(option);
}
+// Ahead of the tabs: a group whose panels are all code becomes one code block
+// with a language menu, and code-blocks.js removes it so tabs skip it.
+if (enabled(f.codeBlocks, 'copy')) {
+ const codeBlocks = await import('./modules/code-blocks.js');
+ codeBlocks.enhanceCodeBlocks();
+}
+
if (enabled(f.details, 'tabs')) {
const tabs = await import('./modules/detail-tabs.js');
tabs.enhanceDetailGroups();
@@ -53,11 +60,6 @@ if (enabled(f.youTubeLinks, 'embed')) {
youTube.enhanceYoutubeLinks();
}
-if (enabled(f.codeBlocks, 'copy')) {
- const codeBlocks = await import('./modules/code-blocks.js');
- codeBlocks.enhanceCodeBlocks();
-}
-
if (enabled(f.figures, 'enlarge')) {
const figures = await import('./modules/figures.js');
figures.enhanceFigures();
diff --git a/src/scripts/modules/code-blocks.js b/src/scripts/modules/code-blocks.js
index d045e9c350..8302d65cfc 100644
--- a/src/scripts/modules/code-blocks.js
+++ b/src/scripts/modules/code-blocks.js
@@ -1,58 +1,228 @@
+// @ts-check
import { qs, qsa } from './query.js';
+import { copyOnClick } from './copy-button.js';
-const activeClass = 'copy-button';
+// The shell around each block, its copy button included, is rendered at build
+// time by src/plugins/shiki-code-block.js. This wires up what happens next.
-const clipboard = ``;
+/** Taller than this and the block collapses until it is opened. */
+const COLLAPSE_HEIGHT = 500;
-const clipboardDone = ``;
+const SHOW_MORE = 'Show more';
+const SHOW_LESS = 'Show less';
-const clipboardError = ``;
+/**
+ * @param {HTMLElement} button
+ */
+function visibleCode(button) {
+ const code = button
+ .closest('.code-block')
+ ?.querySelector('.code-block__panel:not([hidden]) code');
+
+ // textContent because a collapsed block clips its last lines, and innerText
+ // returns only what is on screen.
+ return code?.textContent ?? null;
+}
+
+/* Language switcher ------------------------------------------------------ */
/**
- * Enables copy on code blocks (