Skip to content
Open
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
1 change: 1 addition & 0 deletions layouts/develop/single.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{{ define "head" }}
<script src='{{ relURL "js/codetabs.js" }}' defer></script>
<script src='{{ relURL "js/cli.js" }}' defer></script>
{{ end }}

{{ define "main" }}
Expand Down
34 changes: 33 additions & 1 deletion layouts/partials/tabbed-clients-example.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@
{{ warnf "[tabbed-clients-example] Example not found %q for %q" $id $.Page }}
{{ end }}

{{/* Extract executable CLI commands (lines starting with "> " or "redis> ") for Try It button */}}
{{ $tryItCommands := slice }}
{{ if (ne (trim $redisCommands "\n") "") }}
{{ range $line := split (trim $redisCommands "\n") "\n" }}
{{ $trimmedLine := trim $line " " }}
{{ if hasPrefix $trimmedLine "redis> " }}
{{ $tryItCommands = $tryItCommands | append (strings.TrimPrefix "redis> " $trimmedLine) }}
{{ else if hasPrefix $trimmedLine "> " }}
{{ $tryItCommands = $tryItCommands | append (strings.TrimPrefix "> " $trimmedLine) }}
{{ end }}
{{ end }}
{{ end }}


{{ $tabs := slice }}
{{/* Render redis-cli example from inner content if any */}}
{{ if (ne (trim $redisCommands "\n") "") }}
Expand Down Expand Up @@ -66,7 +80,22 @@
{{ if and (gt $redisCommandsLineLimitInt 0) (gt $estimatedVisualLineCount $redisCommandsLineLimitInt) }}
{{ $hasCliTrim = true }}
{{ end }}
{{ $redisCliContent := highlight $trimmedRedisCommands "plaintext" "linenos=false" }}
{{/* Render the redis-cli tab as an interactive terminal (form.redis-cli is
picked up by cli.js and executed inline). Only redis-cli is interactive;
other language tabs stay as static highlighted code. */}}
{{ $cliCommandLines := $tryItCommands }}
{{ if eq (len $cliCommandLines) 0 }}
{{ range $line := $redisCommandLines }}
{{ $trimmedCliLine := trim $line " \t\r" }}
{{ if $trimmedCliLine }}{{ $cliCommandLines = $cliCommandLines | append $trimmedCliLine }}{{ end }}
{{ end }}
{{ end }}
{{ $cliJoined := delimit $cliCommandLines "\n" }}
{{ $cliFormInner := htmlEscape $cliJoined }}
{{ $cliSourceAttr := replace (htmlEscape $cliJoined) "\n" "&#10;" }}
{{ $redisCliContent := printf "<form class=\"redis-cli overflow-y-auto max-h-80\" data-cli-source=\"%s\">\n%s\n</form>" $cliSourceAttr $cliFormInner | safeHTML }}
{{/* Trimming applies to static code blocks; the interactive terminal manages its own height. */}}
{{ $hasCliTrim = false }}
{{ $cliTab := dict "title" "redis-cli" "displayName" $cliTabName "content" $redisCliContent "limit" $redisCommandsLineLimit "limitInt" $redisCommandsLineLimitInt "lineCount" $redisCommandLineCount "visualLineCount" $estimatedVisualLineCount "hasCliTrim" $hasCliTrim "customFooterLinkText" $cliFooterLinkText "customFooterLinkUrl" $cliFooterLinkUrl }}
{{ if gt (len $commands) 0 }}
{{ $cliTab = merge $cliTab (dict "commands" $commands) }}
Expand Down Expand Up @@ -131,4 +160,7 @@
{{ if gt (len $buildsUpon) 0 }}
{{ $params = merge $params (dict "buildsUpon" $buildsUpon) }}
{{ end }}
{{ if gt (len $tryItCommands) 0 }}
{{ $params = merge $params (dict "tryItCommands" $tryItCommands) }}
{{ end }}
{{ partial "tabs/wrapper.html" $params }}
66 changes: 64 additions & 2 deletions layouts/partials/tabs/wrapper.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
{{ $description := .description }}
{{ $difficulty := .difficulty }}
{{ $buildsUpon := .buildsUpon }}
{{ $tryItCommands := .tryItCommands }}

{{- /* Build metadata map for each language/client combination */ -}}
{{- $codetabsMeta := dict -}}
Expand Down Expand Up @@ -73,7 +74,7 @@
{{- if $exampleId -}}
<a id="{{ $exampleId }}" class="relative"{{ if $description }} data-description="{{ $description | htmlEscape }}"{{ end }}{{ if $difficulty }} data-difficulty="{{ $difficulty | htmlEscape }}"{{ end }}{{ if $buildsUpon }} data-builds-upon="{{ delimit $buildsUpon "," }}"{{ end }} data-codetabs-id="{{ $id }}"></a>
{{- end -}}
<div class="codetabs cli group box-border rounded-lg mt-0 mb-0 mx-auto bg-slate-900" id="{{ $id }}" data-codetabs-meta="{{ $codetabsMetaJson | htmlEscape }}">
<div class="codetabs cli group box-border rounded-lg mt-0 mb-0 mx-auto bg-slate-900" id="{{ $id }}" data-codetabs-meta="{{ $codetabsMetaJson | htmlEscape }}"{{ if $tryItCommands }} {{ printf "data-tryit-commands=\"%s\"" ($tryItCommands | jsonify | htmlEscape) | safeHTMLAttr }}{{ end }}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unused data-tryit-commands HTML data attribute

Low Severity

The data-tryit-commands attribute is written to the HTML div but never read by any JavaScript on the page. The actual mechanism uses window._tryItCommands[codetabsId] (populated at line 486) and openTryItCli reads from that registry — not from the DOM attribute. This is unused/dead output that adds JSON payload bloat to every codetabs container with Try It commands.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b1580b3. Configure here.

<!-- Language selector dropdown with controls -->
<div class="codetabs-header flex flex-col px-4 py-2 bg-slate-900 rounded-t-lg">
<div class="flex items-start justify-between">
Expand Down Expand Up @@ -101,6 +102,21 @@
</div>

<div class="flex items-center gap-2">
{{/* "Try it" button - opens redis.io/cli with pre-populated commands */}}
{{ if $tryItCommands }}
<button tabindex="1"
type="button"
class="tryit-button flex items-center gap-1 text-slate-300 hover:text-white bg-red-700 hover:bg-red-600 h-7 px-3 rounded text-xs font-medium transition duration-150 ease-in-out"
title="Run these commands in the Redis CLI"
data-codetabs-id="{{ $id }}"
onclick="openTryItCli(this)"
aria-label="Try these Redis commands in an interactive CLI">
<svg class="w-3.5 h-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M8 5v14l11-7z"/>
</svg>
<span>Try it</span>
</button>
{{ end }}
{{/* BinderHub "Run in browser" link - shown conditionally based on current tab's binderId */}}
<div id="binder-link-container-{{ $id }}" class="flex items-center">
{{/* Link will be shown/hidden by JavaScript based on selected tab */}}
Expand Down Expand Up @@ -251,6 +267,25 @@
<!-- spellchecker-enable -->

<script>
// Registry for Try It CLI commands (populated per codetabs instance)
window._tryItCommands = window._tryItCommands || {};

// Global function to open Try It CLI with pre-populated commands
window.openTryItCli = window.openTryItCli || function(button) {
const codetabsId = button.getAttribute('data-codetabs-id');
const commands = window._tryItCommands[codetabsId];
if (!commands || !commands.length) return;

// URL-safe base64 of the JSON payload, no padding. Avoids the %5C%22
// patterns produced by raw JSON+encodeURIComponent, which trigger
// Cloudflare WAF SQLi/XSS-bypass rules.
const json = JSON.stringify(commands);
const b64 = btoa(unescape(encodeURIComponent(json)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const url = 'https://redis.io/cli?commands=' + b64 + '&autorun=true';
window.open(url, '_blank', 'noopener,noreferrer');
};

// Global function to toggle commands foldout
window.toggleCommandsFoldout = window.toggleCommandsFoldout || function(button) {
const details = button.nextElementSibling;
Expand Down Expand Up @@ -299,6 +334,28 @@
}
};

// Global function to show/hide Try It buttons based on selected language
// The button should only be visible when the Redis CLI tab is active
window.updateAllTryItButtons = window.updateAllTryItButtons || function() {
const buttons = document.querySelectorAll('.tryit-button');
buttons.forEach((button) => {
const codetabsId = button.getAttribute('data-codetabs-id');
const langSelect = document.getElementById('lang-select-' + codetabsId);
if (!langSelect) return;

const selectedOption = langSelect.options[langSelect.selectedIndex];
const selectedLang = selectedOption ? selectedOption.value : '';

if (selectedLang === 'redis-cli') {
button.classList.remove('hidden');
button.classList.add('flex');
} else {
button.classList.add('hidden');
button.classList.remove('flex');
}
});
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing Try It button update in tab switching path

Medium Severity

The updatePanelVisibility function in codetabs.js calls window.updateAllCliOutputToggles() and window.updateAllBinderLinks() but not the new window.updateAllTryItButtons(). When codetabs.js restores a saved language preference from localStorage/URL during initialization, it calls updatePanelVisibility — which won't update Try It button visibility. The 100ms setTimeout in wrapper.html may fire before codetabs.js (which is deferred) runs, leaving the button in the wrong visibility state after the tab is restored.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit aac21dd. Configure here.


// Global function to update all binder links on the page
// This is called whenever the language selection changes
window.updateAllBinderLinks = window.updateAllBinderLinks || function() {
Expand Down Expand Up @@ -423,15 +480,19 @@
};

(function() {
// Initialize BinderHub link for this codetabs instance
// Register Try It commands for this codetabs instance
const codetabsId = '{{ $id }}';
{{ if $tryItCommands }}
window._tryItCommands[codetabsId] = {{ $tryItCommands | jsonify | safeJS }};
{{ end }}
const langSelect = document.getElementById('lang-select-' + codetabsId);

// Initialize on page load - use a delay to allow codetabs.js to restore localStorage selection
// codetabs.js is deferred, so it runs after DOM is ready, but we need to wait for it to complete
setTimeout(() => {
window.updateAllBinderLinks();
window.updateAllCliOutputToggles();
window.updateAllTryItButtons();
}, 100);

// Update all binder links when language changes
Expand All @@ -441,6 +502,7 @@
setTimeout(() => {
window.updateAllBinderLinks();
window.updateAllCliOutputToggles();
window.updateAllTryItButtons();
}, 10);
});
}
Expand Down
34 changes: 31 additions & 3 deletions layouts/shortcodes/redis-cli.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,36 @@
<div class="bg-slate-900 border-b border-slate-700 rounded-t-xl px-4 py-3 w-full flex">
{{- $commands := slice -}}
{{- range split (trim .Inner " \n\t\r") "\n" -}}
{{- $line := trim . " \t\r" -}}
{{- if $line -}}
{{- $commands = $commands | append $line -}}
{{- end -}}
{{- end -}}
{{- $id := printf "redis-cli-%s" (substr (.Inner | md5) 0 8) -}}

<div class="bg-slate-900 border-b border-slate-700 rounded-t-xl px-4 py-3 w-full flex items-center">
{{ partial "icons/cli-circle" (dict "class" "shrink-0 h-[1.0625rem] w-[1.0625rem] fill-slate-50") }}
{{ partial "icons/cli-triangle" (dict "class" "shrink-0 h-[1.0625rem] w-[1.0625rem] fill-slate-50") }}
{{ partial "icons/cli-star" (dict "class" "shrink-0 h-[1.0625rem] w-[1.0625rem] fill-slate-50") }}
</div>
{{- if $commands -}}
<button tabindex="1" type="button" class="tryit-button ml-auto flex items-center gap-1 text-slate-300 hover:text-white bg-red-700 hover:bg-red-600 h-7 px-3 rounded text-xs font-medium transition duration-150 ease-in-out" title="Run these commands in the Redis CLI" data-codetabs-id="{{ $id }}" onclick="openTryItCli(this)" aria-label="Try these Redis commands in an interactive CLI"><svg class="w-3.5 h-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg><span>Try it</span></button>
{{- end -}}
</div>
<form class="redis-cli overflow-y-auto max-h-80">
{{- .Inner -}}
</form>
</form>
{{- if $commands -}}
<script>
window._tryItCommands = window._tryItCommands || {};
window._tryItCommands['{{ $id }}'] = {{ $commands | jsonify | safeJS }};
window.openTryItCli = window.openTryItCli || function(button) {
const codetabsId = button.getAttribute('data-codetabs-id');
const commands = window._tryItCommands[codetabsId];
if (!commands || !commands.length) return;
const json = JSON.stringify(commands);
const b64 = btoa(unescape(encodeURIComponent(json)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const url = 'https://redis.io/cli?commands=' + b64 + '&autorun=true';
window.open(url, '_blank', 'noopener,noreferrer');
};
</script>
{{- end -}}
23 changes: 23 additions & 0 deletions static/js/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,35 @@ function shouldAnimate(cli) {
}

function getCommandsToExecute(cli) {
const fromUrl = getCommandsFromUrl();
if (fromUrl) return fromUrl;

const textContent = cli.textContent.trim();
if (!textContent) return;

return textContent.split('\n').map(x => x.trim());
}

function getCommandsFromUrl() {
try {
const params = new URLSearchParams(window.location.search);
const raw = params.get('commands');
if (!raw) return;
if (params.get('autorun') !== 'true') return;

const b64 = raw.replace(/-/g, '+').replace(/_/g, '/');
const padded = b64 + '==='.slice((b64.length + 3) % 4);
const json = decodeURIComponent(escape(atob(padded)));
const commands = JSON.parse(json);
if (!Array.isArray(commands)) return;

const cleaned = commands.map(c => String(c).trim()).filter(Boolean);
return cleaned.length ? cleaned : undefined;
} catch (err) {
console.warn('cli: failed to parse commands from URL', err);
}
}

function createPre(cli) {
const pre = document.createElement('pre');
pre.setAttribute('tabindex', '0');
Expand Down
15 changes: 15 additions & 0 deletions static/js/codetabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ function copyCodeToClipboardForCodetabs(button) {
const visiblePanel = codetabsContainer.querySelector('.panel:not(.panel-hidden)');
if (!visiblePanel) return;

// The redis-cli panel is an interactive terminal (form.redis-cli) with no
// <code> element, so copy the registered commands instead of reading the DOM.
const cliForm = visiblePanel.querySelector('form.redis-cli');
if (cliForm) {
const cliCode = cliForm.getAttribute('data-cli-source') || cliForm.textContent;
navigator.clipboard.writeText(cliCode.trim());

const cliTooltip = button.querySelector('.tooltiptext');
if (cliTooltip) {
cliTooltip.style.display = 'block';
setTimeout(() => cliTooltip.style.display = 'none', 1000);
}
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Try It button visibility not updated on URL navigation

Medium Severity

The updatePanelVisibility function in codetabs.js calls window.updateAllCliOutputToggles() and window.updateAllBinderLinks() but never calls window.updateAllTryItButtons(). When language is changed via applyLanguageFromUrl (triggered by hashchange, popstate, or URL polling), dropdown values are set programmatically without dispatching change events, so the wrapper.html event listener won't fire. This leaves the Try It button visible when a non-CLI language is active, or hidden when redis-cli becomes active through URL-based navigation.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d69cbd1. Configure here.

let code;
const isCliTrimmed = visiblePanel.getAttribute('data-cli-trimmable') === 'true';
const cliPreviewLines = parseInt(visiblePanel.getAttribute('data-cli-preview-lines') || '0', 10);
Expand Down
Loading