feat(code): add toggle code block functionality and enhance code tabs (#719)

* feat(markup): add toggle code block functionality with multiple format support

* fix(assets): improve filename generation for code downloads

* feat(code): add `before_tabs` attribute support for tabbed code blocks

* feat(code): implement visibility control for first tab content in code blocks
This commit is contained in:
Cell
2026-03-20 17:26:28 +08:00
committed by GitHub
parent 65ad407cd0
commit b09555981b
6 changed files with 114 additions and 8 deletions
+1
View File
@@ -30,3 +30,4 @@ $RECYCLE.BIN/
CHANGELOG.md
.stash/
docs
+12
View File
@@ -180,6 +180,18 @@ function add(a, b) {
<p>Lorem ipsum dolor sit amet, graecis denique ei vel, at duo primis mandamus. Et legere ocurreret pri, animal tacimates complectitur ad cum. Cu eum inermis inimicus efficiendi. Labore officiis his ex, soluta officiis concludaturque ei qui, vide sensibus vim ad.</p>
## Code Toggle
```toggle {before_tabs="hugo."}
[params]
description = ''
keywords = []
[params.codeblock]
mode = 'classic'
wrapper = true
```
## Code block inside other blocks
> [!NOTE]+
+11
View File
@@ -532,6 +532,11 @@ pre {
padding-block: 0.875rem;
}
}
// for code tabs, only show the first tab content before JS loaded
&[data-hidden] {
display: none;
}
}
}
@@ -628,6 +633,12 @@ pre {
position: relative;
@include border-radius(top);
.before-tabs {
padding: 0.4rem 0.6rem;
white-space: nowrap;
color: fixit-var(global-font-color);
}
.tab-item {
padding: 0.4rem 0.8rem;
cursor: pointer;
+18 -7
View File
@@ -462,15 +462,18 @@ class FixIt {
if (!downloadBtn) return;
downloadBtn.addEventListener('click', () => {
const $codeHeader = codeBlock.querySelector('.code-header');
const fileNameFromTitle = $codeHeader?.querySelector('.code-title')?.dataset.name?.trim();
const name = codeBlock.dataset.name?.trim();
const language = Array.from($codeHeader?.classList || []).find((className) => className.startsWith('language-'))?.replace('language-', '');
const fallbackName = language && language !== 'fallback' ? `code.${language}` : 'code.txt';
const fileName = (fileNameFromTitle || fallbackName).replace(/[\\/:*?"<>|\r\n]+/g, '-');
const ext = language && language !== 'fallback' ? language : 'txt';
const fallbackName = name
? (name.includes('.') ? name : `${name}.${ext}`)
: `code.${ext}`;
const fileName = codeBlock.getAttribute('filename')?.trim();
const blob = new Blob([codePreEl.innerText], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName || fallbackName;
link.download = (fileName || fallbackName).replace(/[\\/:*?"<>|\r\n]+/g, '-');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
@@ -598,18 +601,19 @@ class FixIt {
* init code tabs
*/
initCodeTabs() {
const $codeBlocks = document.querySelectorAll('.code-block[data-tab-group]');
const $codeBlocks = document.querySelectorAll('.code-block[group]');
const processed = new Set();
Util.forEach($codeBlocks, ($block) => {
if (processed.has($block)) return;
const groupName = $block.dataset.tabGroup;
const groupName = $block.getAttribute('group');
const $tabs = [];
let $curr = $block;
// collect consecutive blocks with same group
while ($curr && $curr.classList?.contains('code-block') && $curr.dataset.tabGroup === groupName) {
while ($curr && $curr.classList?.contains('code-block') && $curr.getAttribute('group') === groupName) {
delete $curr.dataset.hidden;
$tabs.push($curr);
processed.add($curr);
$curr = $curr.nextElementSibling;
@@ -641,6 +645,13 @@ class FixIt {
$firstBlock.parentNode.insertBefore($container, $firstBlock);
const activeTabIndex = $tabs.findIndex(tab => tab.classList.contains('active'));
const beforeTabs = $tabs[0]?.getAttribute('before_tabs');
if (beforeTabs) {
const $before = document.createElement('span');
$before.className = 'before-tabs';
$before.textContent = beforeTabs;
$items.appendChild($before);
}
$tabs.forEach(($tab, index) => {
const title = $tab.dataset.tabTitle || 'Code';
const defaultActiveTab = activeTabIndex === -1 && index === 0;
@@ -0,0 +1,65 @@
{{- $parsed := dict -}}
{{- $ok := false -}}
{{- with try (transform.Unmarshal .Inner) -}}
{{- with .Err -}}
{{- warnf "Toggle code block: unable to parse content at %s" $.Position -}}
{{- else with .Value -}}
{{- $parsed = . -}}
{{- $ok = true -}}
{{- end -}}
{{- end -}}
{{- if $ok -}}
{{- $pageID := .Page.RelPermalink | default .Page.File.Path -}}
{{- $seed := printf "%s|%s|%s" $pageID .Position .Inner -}}
{{- $group := printf "toggle-%s" (md5 $seed) -}}
{{- $activeFormat := "" -}}
{{- $raw := strings.TrimSpace .Inner -}}
{{- range $candidate := slice "json" "toml" "yaml" -}}
{{- if not $activeFormat -}}
{{- with try (transform.Unmarshal (dict "format" $candidate) $raw) -}}
{{- if not .Err -}}
{{- $activeFormat = $candidate -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $commonAttrsStr := printf ", group=%q" $group -}}
{{- if ne .Attributes.copyable nil -}}
{{- $commonAttrsStr = printf "%s, copyable=%v" $commonAttrsStr .Attributes.copyable -}}
{{- end -}}
{{- if ne .Attributes.downloadable nil -}}
{{- $commonAttrsStr = printf "%s, downloadable=%v" $commonAttrsStr .Attributes.downloadable -}}
{{- end -}}
{{- if ne .Attributes.fullscreen nil -}}
{{- $commonAttrsStr = printf "%s, fullscreen=%v" $commonAttrsStr .Attributes.fullscreen -}}
{{- end -}}
{{- if ne .Attributes.linenostoggler nil -}}
{{- $commonAttrsStr = printf "%s, lineNosToggler=%v" $commonAttrsStr .Attributes.linenostoggler -}}
{{- end -}}
{{- if ne .Attributes.linewraptoggler nil -}}
{{- $commonAttrsStr = printf "%s, lineWrapToggler=%v" $commonAttrsStr .Attributes.linewraptoggler -}}
{{- end -}}
{{- if ne .Attributes.editable nil -}}
{{- $commonAttrsStr = printf "%s, editable=%v" $commonAttrsStr .Attributes.editable -}}
{{- end -}}
{{- if ne .Attributes.before_tabs nil -}}
{{- $commonAttrsStr = printf "%s, before_tabs=%q" $commonAttrsStr .Attributes.before_tabs -}}
{{- end -}}
{{- $renderText := "" -}}
{{- range $format := slice "toml" "yaml" "json" -}}
{{- $content := transform.Remarshal $format $parsed -}}
{{- if eq $format "toml" -}}
{{- $content = replaceRE `(?m)^[\t ]+` "" $content -}}
{{- else if eq $format "json" -}}
{{- $content = $parsed | jsonify (dict "indent" " ") -}}
{{- end -}}
{{- $active := cond (eq $format $activeFormat) ", .active" "" -}}
{{- $jsonViewer := cond (eq $format "json") ", enable=false" "" -}}
{{- $block := printf "```%s {name=%q%s%s%s}\n%s\n```\n\n" $format $format $active $jsonViewer $commonAttrsStr $content -}}
{{- $renderText = add $renderText $block -}}
{{- end -}}
{{- $renderText | .Page.RenderString -}}
{{- else -}}
{{- partial "plugin/code-block-wrapper.html" . -}}
{{- end -}}
@@ -67,10 +67,16 @@
{{- end -}}
{{- if $config.group -}}
{{- $tabTitle := $config.name | default (strings.FirstUpper .Type) | default "Code" -}}
{{- $groupKey := printf "code-tab-group-first:%s" $config.group -}}
{{- $isFirst := not (.Page.Store.Get $groupKey) -}}
{{- if $isFirst -}}
{{- .Page.Store.Set $groupKey true -}}
{{- end -}}
{{- $hidden := cond $isFirst "" " data-hidden" -}}
{{- $wrapper =
replace $wrapper
(printf `class="%s"` $class)
(printf `class="%s" data-tab-group="%s" data-tab-title="%s"` $class $config.group $tabTitle) -}}
(printf `class="%s" data-tab-group="%s" data-tab-title="%s"%s` $class $config.group $tabTitle $hidden) -}}
{{- end -}}
{{- if ne .Options.linenostart nil -}}
{{- $wrapper =