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
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2026 znai maintainers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.testingisdocumenting.znai.extensions.urlqueryvalue;

import org.testingisdocumenting.znai.core.ComponentsRegistry;
import org.testingisdocumenting.znai.extensions.PluginParams;
import org.testingisdocumenting.znai.extensions.PluginResult;
import org.testingisdocumenting.znai.extensions.inlinedcode.InlinedCodePlugin;
import org.testingisdocumenting.znai.search.SearchText;

import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class UrlQueryValueInlinedCodePlugin implements InlinedCodePlugin {
@Override
public String id() {
return "url-query-value";
}

@Override
public InlinedCodePlugin create() {
return new UrlQueryValueInlinedCodePlugin();
}

@Override
public PluginResult process(ComponentsRegistry componentsRegistry, Path markupPath, PluginParams pluginParams) {
String queryParam = pluginParams.getFreeParam();

Map<String, Object> props = new HashMap<>(pluginParams.getOpts().toMap());
props.put("queryParam", queryParam);

return PluginResult.docElement("UrlQueryValue", props);
}

@Override
public List<SearchText> textForSearch() {
return List.of();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class DocUrl {
private String dirName = "";
private String fileNameWithoutExtension = "";
private String anchorId = "";
private String queryString = "";
private String tocItemFilePath = "";

private String url;
Expand Down Expand Up @@ -74,11 +75,13 @@ private boolean handleLocalFile(String url) {

private boolean handleBasedOnFilePath(String url) {
String withoutAnchor = UrlUtils.removeAnchor(url);
String extension = FilePathUtils.fileExtension(withoutAnchor);
String withoutAnchorAndQuery = removeQueryString(withoutAnchor);
String extension = FilePathUtils.fileExtension(withoutAnchorAndQuery);

if (extension.startsWith("md")) {
tocItemFilePath = withoutAnchor;
tocItemFilePath = withoutAnchorAndQuery;
anchorId = UrlUtils.extractAnchor(url);
queryString = extractQueryString(withoutAnchor);
return true;
}

Expand Down Expand Up @@ -108,7 +111,7 @@ private boolean handleAnchorOnly() {
}

private boolean handleLocal() {
String[] parts = url.split("/");
String[] parts = extractUrlWithoutQueryString().split("/");
if (parts.length != 2 && parts.length != 3) {
throw new IllegalArgumentException("Unexpected url pattern: <" + url + "> " + LINK_TO_SECTION_INSTRUCTION);
}
Expand Down Expand Up @@ -172,7 +175,31 @@ public String getAnchorIdWithHash() {
return anchorId.isEmpty() ? "" : "#" + anchorId;
}

public String getQueryAndAnchorSuffix() {
return queryString + getAnchorIdWithHash();
}

public String getUrl() {
return url;
}

private String extractUrlWithoutQueryString() {
int idxOfQuery = url.indexOf('?');
if (idxOfQuery == -1) {
return url;
}

queryString = url.substring(idxOfQuery);
return url.substring(0, idxOfQuery);
}

private static String removeQueryString(String url) {
int idxOfQuery = url.indexOf('?');
return idxOfQuery == -1 ? url : url.substring(0, idxOfQuery);
}

private static String extractQueryString(String url) {
int idxOfQuery = url.indexOf('?');
return idxOfQuery == -1 ? "" : url.substring(idxOfQuery);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ org.testingisdocumenting.znai.extensions.file.FileInlinedCodePlugin
org.testingisdocumenting.znai.extensions.inlinedcode.IdentifierInlinedCodePlugin
org.testingisdocumenting.znai.extensions.textbadge.TextBadgeInlinedCodePlugin
org.testingisdocumenting.znai.extensions.latex.LatexInlinedCodePlugin
org.testingisdocumenting.znai.extensions.urlqueryvalue.UrlQueryValueInlinedCodePlugin
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ class DocUrlTest {
url.should == [anchorId: "page-section", tocItemFilePath: "./chapter/name.md", dirName: "", fileNameWithoutExtension: ""]
}

@Test
void "parse with query params"() {
def url = new DocUrl("./chapter/name.md?key1=value1&key2=value2#page-section")
url.should == [anchorId: "page-section", tocItemFilePath: "./chapter/name.md", dirName: "", fileNameWithoutExtension: ""]
}

@Test
void "parse with mdx extension"() {
def url = new DocUrl("./chapter/name.mdx#page-section")
Expand Down
111 changes: 111 additions & 0 deletions znai-docs/znai/layout/runtime-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# URL Query Value

Use `url-query-value` inline plugin to display a value from URL query parameter.
This can be useful for onboarding pages or runbooks where commands or settings change based on user or some runtime
information, and you can generate a link, providing additional information.

# Inline Syntax

`:url-query-value: officeName {default: "NYC-15"}`

Renders as: `:url-query-value: officeName {default: "NYC-15"}`

The `officeName` is the query parameter name to read from the URL.

Click [SF office](layout/runtime-templates?officeName=SF) to see the value above change.

# Missing Value

When no `default` is specified and the query parameter is not present, an error is displayed:

`:url-query-value: clusterName`

Renders as: `:url-query-value: clusterName`

# Template Syntax In Code Snippets And CLI Commands

Use `${paramName}` or `${paramName:defaultValue}` syntax inside code snippets to substitute values from URL query parameters.
Enable it by setting `templateUseQueryParam: true` on the code block.

```shell {templateUseQueryParam: true}
ssh ${userName:admin}@server-${officeName:NYC}.example.com
```

```shell {templateUseQueryParam: true}
ssh ${userName:admin}@server-${officeName:NYC}.example.com
```

```cli {templateUseQueryParam: true}
kubectl config use-context ${officeName:NYC}-cluster
```

```cli {templateUseQueryParam: true}
kubectl config use-context ${officeName:NYC}-cluster
```

Without query parameters, default values are used. When the page URL contains `?userName=jdoe&officeName=SF`, the substituted values will appear.

Without default values, you get error message when no query parameter is supplied:

```cli {templateUseQueryParam: true}
kubectl config use-context ${myQueryParam}-cluster
```

```cli {templateUseQueryParam: true}
kubectl config use-context ${myQueryParam}-cluster
```

# Tables With Inline Values

You can use inline `url-query-value` anywhere were text is expected, for example in table cells:

```markdown

| Setting | Value |
|---------------|-----------------------------------------------------------------|
| Office | `:url-query-value: officeName {default: "NYC"}` |
| Floor | `:url-query-value: floorNumber {default: "5"}` |
| Wi-Fi Network | `:url-query-value: officeName {default: "NYC"}`-internal |
| VPN Server | vpn-`:url-query-value: officeName {default: "NYC"}`.example.com |
```
| Setting | Value |
|---------------|-----------------------------------------------------------------|
| Office | `:url-query-value: officeName {default: "NYC"}` |
| Floor | `:url-query-value: floorNumber {default: "5"}` |
| Wi-Fi Network | `:url-query-value: officeName {default: "NYC"}`-internal |
| VPN Server | vpn-`:url-query-value: officeName {default: "NYC"}`.example.com |

# Full Example

Click one of the links below to see how values on this page change:

* [SF office, floor 3](layout/runtime-templates?officeName=SF&floorNumber=3&userName=jdoe)
* [NYC office, floor 12](layout/runtime-templates?officeName=NYC&floorNumber=12&userName=admin)
* [London office, floor 7](layout/runtime-templates?officeName=London&floorNumber=7&userName=alice)
* [Default values](layout/runtime-templates)

## Setup Instructions

Welcome to the `:url-query-value: officeName {default: "NYC"}` office!

Connect to the office VPN:

```cli {templateUseQueryParam: true}
sudo vpn connect ${officeName:NYC}-gateway.example.com --user ${userName:admin}
```

Configure your local environment:

```shell {templateUseQueryParam: true}
export OFFICE=${officeName:NYC}
export FLOOR=${floorNumber:5}
export PRINTER=printer-${officeName:NYC}-${floorNumber:5}
```

Print a test page:

```cli {templateUseQueryParam: true}
lp -d printer-${officeName:NYC}-${floorNumber:5} /etc/motd
```

Note: If you have a predetermined set of values, consider using [Page Tabs](layout/page-tabs) or [Tabs](layout/tabs) instead
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Add: [Runtime Templates](layout/runtime-templates) to substitute values at runtime
1 change: 1 addition & 0 deletions znai-docs/znai/toc
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ layout
tables
columns
templates
runtime-templates
two-sides-pages
two-sides-tabs
jupyter-notebook-two-sides
Expand Down
2 changes: 2 additions & 0 deletions znai-reactjs/src/doc-elements/DefaultElementsLibrary.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import { FootnoteBackLinks } from "./footnote/FootnotesList";
import { EmbeddedHtml } from "./html/EmbeddedHtml";
import { Asciinema } from "./asciinema/Asciinema";
import { ReadMore } from "./read-more/ReadMore.js";
import { UrlQueryValue } from "./url-query-value/UrlQueryValue";
import { withDisplayName } from "./components.ts";

const library = {}
Expand Down Expand Up @@ -142,6 +143,7 @@ library.Icon = Icon
library.KeyboardShortcut = KeyboardShortcut

library.TextBadge = TextBadge
library.UrlQueryValue = UrlQueryValue

library.OrderedList = OrderedList

Expand Down
10 changes: 7 additions & 3 deletions znai-reactjs/src/doc-elements/cli/CliCommand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import React, { useState, useEffect, useMemo } from "react";
import CliCommandToken from "./CliCommandToken";
import { splitParts } from "../../utils/strings";
import { DocElementPayload } from "../default-elements/DocElement";
import { resolveTemplateText } from "../url-query-value/queryParamTemplate";
import "./CliCommand.css";

interface Token {
Expand All @@ -37,6 +38,7 @@ interface CliCommandProps {
threshold?: number;
presentationThreshold?: number;
splitAfter?: string[];
templateUseQueryParam?: boolean;
}

const CliCommand: React.FC<CliCommandProps> = ({
Expand All @@ -49,8 +51,10 @@ const CliCommand: React.FC<CliCommandProps> = ({
threshold = 100,
presentationThreshold = 40,
splitAfter = [],
templateUseQueryParam = false,
}) => {
const tokens = useMemo(() => tokenize(command), [command]);
const resolvedCommand = templateUseQueryParam ? resolveTemplateText(command) : command;
const tokens = useMemo(() => tokenize(resolvedCommand), [resolvedCommand]);

const [lastTokenIdx, setLastTokenIdx] = useState(
isPresentation && !isPresentationDisplayed ? 1 : tokens.length
Expand Down Expand Up @@ -136,10 +140,10 @@ const CliCommand: React.FC<CliCommandProps> = ({
(isPrevCliCommand ? " prev-present" : "");

return (
<div key={command} className={className}>
<div key={resolvedCommand} className={className}>
<pre>
<span className="prompt">$ </span>
<span key={command}>{renderTokens()}</span>
<span key={resolvedCommand}>{renderTokens()}</span>
</pre>
</div>
);
Expand Down
4 changes: 3 additions & 1 deletion znai-reactjs/src/doc-elements/code-snippets/Snippet.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { parseCode } from "./codeParser";
import { countNumberOfLines } from "../../utils/strings";

import { SnippetBulletExplanations } from "./explanations/SnippetBulletExplanations";
import { resolveTemplateText } from "../url-query-value/queryParamTemplate";

import "./Snippet.css";

Expand All @@ -44,7 +45,8 @@ const BULLETS_COMMENT_TYPE = "inline";
const REMOVE_COMMENT_TYPE = "remove";

const Snippet = (props) => {
const tokensToUse = parseCodeWithCompatibility({ lang: props.lang, snippet: props.snippet, tokens: props.tokens });
const snippet = props.templateUseQueryParam ? resolveTemplateText(props.snippet) : props.snippet;
const tokensToUse = parseCodeWithCompatibility({ lang: props.lang, snippet, tokens: props.tokens });

const renderBulletComments =
props.commentsType === BULLETS_COMMENT_TYPE || (props.callouts && Object.keys(props.callouts).length > 0);
Expand Down
23 changes: 23 additions & 0 deletions znai-reactjs/src/doc-elements/url-query-value/UrlQueryValue.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2026 znai maintainers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

.znai-url-query-value-error {
color: var(--znai-color-red);
}

.znai-url-query-value-error code {
color: var(--znai-color-red);
}
Loading
Loading