Compare commits

..

29 Commits

Author SHA1 Message Date
Cell 92b0a6b56e chore(release): 0.4.5 2026-03-25 10:53:08 +08:00
Cell 9132b408c6 refactor(utils): migrate utility functions into common module and rewrite method calls 2026-03-24 11:36:28 +08:00
Cell a53ef80035 feat(print): add print configuration options and enhance print functionality (#720) 2026-03-22 20:06:27 +08:00
Cell 0f2712f182 fix(tooltip): update cell-tooltip version to 0.3.0 and enhance code copy btn 2026-03-21 02:38:04 +08:00
Cell d5aa013106 feat(code): remember the selected language of the code toggle 2026-03-21 00:47:12 +08:00
Cell d9e45154ff fix(code): repeated initialization of the code tab in the encryption page 2026-03-21 00:03:07 +08:00
Cell b09555981b 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
2026-03-20 17:26:28 +08:00
Cell 65ad407cd0 style: improve documentation formatting for layouts 2026-03-20 11:44:15 +08:00
Cell 9e384b666f fix(assets): update cell-tooltip v0.2.0 to fix the MIME type error for the .cjs extension (#718) 2026-03-20 11:20:25 +08:00
Cell 96b4b4512c fix(assets): code header sticky error in mobile device (#716) 2026-03-20 11:10:43 +08:00
Cell de38644d75 fix(assets): tab underline error position 2026-03-19 15:19:41 +08:00
Cell c79badcf44 chore(release): 0.4.4 2026-03-19 14:15:16 +08:00
Cell ccf17c6444 fix(assets): enhance tablist-wrapper styles 2026-03-19 12:47:17 +08:00
Cell 9bac69b2ec fix: update print handling and improve file tree interactions 2026-03-19 11:42:15 +08:00
Cell 0bf5d3a49b docs(CONTRIBUTING): update commit message guidelines and examples 2026-03-18 17:29:08 +08:00
Cell 74b80bd94d chore(libs): update Twikoo from 1.6.44 to 1.7.3 (close #711) 2026-03-18 15:21:20 +08:00
Cell 789973da78 chore(dev-deps): update dev dependencies 2026-03-18 12:29:29 +08:00
Cell d51dfd35e8 refactor(responsive): enhance responsive styles and add media mixin support (#715)
* feat(responsive): add responsive breakpoints and mixin for media queries

* refactor: print styles and add media query support

* refactor(responsive): enhance responsive styles across various components
2026-03-18 11:22:51 +08:00
Cell 8534cddc7c chore(i): some some general updates 2026-03-13 11:18:21 +08:00
Cell 02ac3f5259 feat(code): add shadow effect for code blocks (#713) 2026-03-12 17:28:15 +08:00
Cell c8d028aa0e feat(tabs): enhance tab styles and improve active code tab handling (#712)
* feat(tabs): enhance tab styles with sliding indicators and hover effects

* feat(tabs): improve active code tab handling and initialization logic
2026-03-12 11:41:52 +08:00
Cell f2c112e402 feat(code): add tabbed codeblocks support (#710) 2026-03-10 17:33:28 +08:00
Cell 8bf2fea735 fix(typeit): restore cursor color variable definition for proper styling 2026-03-08 18:07:16 +08:00
Cell fe7105a38a fix: optimize TOC initialization with a timeout and clean up button spacing in decryptor 2026-03-08 17:57:41 +08:00
Cell fda650ad58 feat(MathJax): add dark mode adaptation for MathJax rendering 2026-03-08 02:17:14 +08:00
Cell d97e69ad0a feat(tooltip): add cell-tooltip integration for footnote references a… (#709)
* feat(tooltip): add cell-tooltip integration for footnote references and initialize tooltips

* fix(code-header): add data-ct-tooltip attribute to action buttons for better accessibility
2026-03-06 19:25:35 +08:00
Cell 98a82d845a refactor(Util): convert methods to static for better accessibility and usage 2026-03-06 10:46:47 +08:00
Cell 423a7ddbf7 fix(CSS): restore the definition of the $global-font-secondary-color variable; third-party components may reference it 2026-03-04 16:02:10 +08:00
Cell da297a71f8 feat(shortcodes): add store flags for aplayer, music, mapbox, and fixit-encryptor 2026-03-04 11:26:03 +08:00
72 changed files with 2273 additions and 1118 deletions
+2 -2
View File
@@ -120,7 +120,7 @@ FixIt/
```javascript
// 使用 ES6 类
export default class Util {
copyText(text) {
static copyText(text) {
// ...
}
}
@@ -129,7 +129,7 @@ export default class Util {
class FixIt {
constructor() {
this.config = window.config
this.util = new Util()
this.scrollTop = Util.getScrollTop()
}
init() {
+1
View File
@@ -30,3 +30,4 @@ $RECYCLE.BIN/
CHANGELOG.md
.stash/
docs
+42
View File
@@ -119,3 +119,45 @@ Finally, create a new pull request at <https://github.com/hugo-fixit/FixIt/pulls
## Git Commit Guidelines
We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification for commit messages. This enables automatic changelog generation using our custom template: [conventional.hbs](https://github.com/Lruihao/auto-changelog-plus/blob/main/settings/conventional.hbs).
> [!NOTE]
>
> Commits in a PR will normally be squashed into one commit, so you don't need to rebase locally.
### Commit Message Format
```
<type>(<scope>): <subject>
^ ^ ^
| | |__ Subject: Concise description of the change (imperative mood, lowercase).
| |____________ Scope: The specific part of the codebase affected (optional but recommended).
|___________________ Type: Indicates the kind of change.
```
### Allowed Types
- `feat`: A new feature.
- `fix`: A bug fix.
- `refactor`: Code changes that neither fix a bug nor add a feature.
- `chore`: Changes to the build process, auxiliary tools, libraries, documentation generation etc.
- `docs`: Documentation only changes.
- Other conventional types like `perf`, `style`, `test`, `ci`, `build` are also acceptable.
### Allowed Scopes
- `workflow`: CI/CD workflow changes (`.github/workflows/`)
- `archetypes`: Content templates (`archetypes/`)
- `assets`: Changes to theme assets like CSS, JS (`assets/`)
- `i18n`: Internationalization and translation files (`i18n/`)
- `layouts`: Root-level Hugo template files (`layouts/*.html`)
- `config`: Theme configuration (`hugo.toml`, `theme.toml`)
- _(All top-level directories in the layouts and packages directories)_
- _(Consider adding other scopes as needed for better granularity)_
### Examples
- `feat(_shortcodes): add mapbox zoom control options`
- `fix(_partials): avoid duplicate canonical link tags`
- `docs(i18n): update translation key naming guidelines`
- `ci(workflow): optimize preview deployment cache`
- `refactor(assets): split theme initialization into smaller modules`
+166
View File
@@ -0,0 +1,166 @@
---
title: Code Tabs Test
date: 2026-01-25T07:50:22+08:00
collections:
- Tests
categories:
- Markdown
tags:
- Code Tabs
---
Code tabs test cases for grouped code blocks.
<!--more-->
## Default Name
```python {group=tab2}
print("Python")
```
```go {group=tab2}
fmt.Println("Go")
```
## Custom Name
```js {group=tab-actions name="[JavaScript]"}
function sum(a, b) {
return a + b
}
console.log(sum(1, 2))
```
```python {group=tab-actions name="[Python]"}
def sum(a, b):
return a + b
print(sum(1, 2))
```
## Active Tab
```js {group=tab-active name="Inactive Tab"}
function greet(name) {
return `Hello, ${name}!`
}
```
```python {group=tab-active name="Active Tab" .active}
def greet(name):
return f"Hello, {name}!"
```
## Shadow
```js {group=tab-shadow name="Always" shadow="always"}
function greet(name) {
return `Hello, ${name}!`
}
```
```python {group=tab-shadow name="Hover" shadow="hover"}
def greet(name):
return f"Hello, {name}!"
```
```go {group=tab-shadow name="Never" shadow="never"}
func greet(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}
```
## Mode
```ts {group=tab-mode name="Classic"}
interface User {
id: number
name: string
}
```
```ts {group=tab-mode name="Mac" mode="mac"}
interface User {
id: number
name: string
}
```
```ts {group=tab-mode name="Simple" mode="simple"}
interface User {
id: number
name: string
}
```
## Actions
```bash {group=tab-actions name="lineNos=true" lineNos=true}
echo "line 1"
echo "line 2"
echo "line 3"
```
```bash {group=tab-actions name="lineNos=false" lineNos=false}
echo "line 1"
echo "line 2"
echo "line 3"
```
```bash {group=tab-actions name="wrapping" .line-wrapping}
printf "this is a very very very very very very very very very very very very very very very very long line"
```
```js {group=tab-actions name="editable" editable=true}
function greet(name) {
return `Hello, ${name}!`
}
```
## Expanded/Collapsed
```js {group=tab-expand name="Expanded"}
function longFunction() {
console.log('This is a long function.')
console.log('It has many lines of code.')
console.log('The code block should be expanded by default.')
console.log('Lorem ipsum dolor sit amet, consectetur adipiscing elit.')
console.log('Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.')
console.log('Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.')
console.log('Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.')
console.log('Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.')
console.log('The end of the long function.')
}
```
```js {group=tab-expand name="Collapsed" .is-collapsed}
function shortFunction() {
console.log('This is a short function.')
}
```
## Code tabs inside other blocks
> [!NOTE]+
>
> ```python {group=tab-nested}
> print("Python")
> ```
>
> ```go {group=tab-nested}
> fmt.Println("Go")
> ```
{{< details open=true >}}
```python {group=tab2}
print("Python")
```
```go {group=tab2}
fmt.Println("Go")
```
{{< /details >}}
+44
View File
@@ -64,6 +64,38 @@ hello('FixIt')
// Hello, FixIt!
```
## Shadow
{{< tabs >}}
{{% tab title="Always" %}}
```js {shadow="always"}
function hello(x = 'world') {
console.log(`Hello, ${x}!`)
}
```
{{% /tab %}}
{{% tab title="Hover" %}}
```js {shadow="hover"}
function hello(x = 'world') {
console.log(`Hello, ${x}!`)
}
```
{{% /tab %}}
{{% tab title="Never" %}}
```js {shadow="never"}
function hello(x = 'world') {
console.log(`Hello, ${x}!`)
}
```
{{% /tab %}}
{{< /tabs >}}
## Collapsed/Expanded
```
@@ -148,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]+
+8
View File
@@ -71,3 +71,11 @@ $$ \ce{CO2 + C -> 2 CO} $$
$$ \ce{Hg^2+ ->[I-] HgI2 ->[I-] [Hg^{II}I4]^2-} $$
$$C_p[\ce{H2O(l)}] = \pu{75.3 J // mol K}$$
## $\KaTeX$ in headings
This is a bug:
```latex
## $\KaTeX$ in headings
```
+26
View File
@@ -117,3 +117,29 @@ U \ar@/_/[ddr]_y \ar@{.>}[dr]|{\langle x,y \rangle} \ar@/^/[drr]^x \\
}
\end{xy}
$$
## \$\\text{MathJax}\$ in headings
```latex
## \$\\text{MathJax}\$ in headings
```
## More
### Dark Mode Adaptation
Use `.auto-dark-mode` class to automatically adapt to dark mode by inverting the color and hue:
For inline formula:
$\bbox[border: solid .4pt magenta, pink]{x^2=4}$
{.auto-dark-mode}
For formula blocks:
{{< style "[data-theme=dark] & { filter: invert(1) hue-rotate(0.5turn); }" >}}
$$
\bbox[border: solid .4pt magenta, pink]{x^2=4}
$$
{.auto-dark-mode}
{{< /style >}}
@@ -216,3 +216,16 @@ type = "file"
{{% /tab %}}
{{< /tabs >}}
### MathJax inside tabs
{{< tabs >}}
{{% tab title="Inline" %}}
$c = \pm\sqrt{a^2 + b^2}$ and \(f(x)=\int_{-\infty}^{\infty} \hat{f}(\xi) e^{2 \pi i \xi x} d \xi\)
{{% /tab %}}
{{% tab title="Block" %}}
$$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$
\[ f(a) = \frac{1}{2\pi i} \oint\frac{f(z)}{z-a}dz \]
{{% /tab %}}
{{< /tabs >}}
+1 -3
View File
@@ -9,10 +9,8 @@ html {
width: 100%;
scroll-behavior: smooth;
overflow: auto;
}
@media screen and (prefers-reduced-motion: reduce) {
html {
@include media('reduce-motion') {
scroll-behavior: auto;
}
}
+39
View File
@@ -58,3 +58,42 @@
.rounded-full {
@include border-radius(full);
}
// auto adapt to dark mode by inverting the color and hue
.auto-dark-mode {
[data-theme=dark] & {
filter: invert(1) hue-rotate(0.5turn);
}
}
// print view helper
.page-break-before {
break-before: page;
}
.page-break-after {
break-after: page;
}
@include media('xs', 'up') {
.d-none-desktop {
display: none;
}
}
@include media('xs') {
.d-none-mobile {
display: none !important;
}
}
@include media('print') {
@page {
size: A4 portrait;
margin: 1.27cm;
}
.d-none-print {
display: none !important;
}
}
+28 -1
View File
@@ -5,7 +5,6 @@
display: flex;
gap: 0.5rem;
@include z-index(sticky);
@extend .print-d-none;
&:has(~ main[data-reverse]) {
flex-direction: row-reverse;
@@ -16,11 +15,35 @@
content: '';
flex: 1;
padding-inline: 0.5rem;
@include media('sm', 'down') {
display: none;
}
}
&.sticky {
position: sticky;
top: fixit-var(header-height);
@include media('sm', 'up') {
body:not([data-header-desktop='sticky']) & {
top: 0;
}
// adjust the scroll margin top of the content anchors on the page
body:not([data-header-desktop='auto']) &+.fi-container .content [id] {
scroll-margin-top: calc(#{fixit-var(scroll-mt)} + #{fixit-var(breadcrumb-height)});
}
}
@include media('xs') {
body:not([data-header-mobile='sticky']) & {
top: 0;
}
// adjust the scroll margin top of the content anchors on the page
body:not([data-header-mobile='auto']) &+.fi-container .content [id] {
scroll-margin-top: calc(#{fixit-var(scroll-mt)} + #{fixit-var(breadcrumb-height)});
}
}
}
.breadcrumb {
@@ -47,4 +70,8 @@
}
}
}
@include media('print') {
display: none !important;
}
}
+8 -1
View File
@@ -4,7 +4,6 @@ footer {
text-align: center;
line-height: 1.25rem;
padding: 1rem 0;
@extend .print-d-none;
.footer-container {
display: flex;
@@ -25,9 +24,17 @@ footer {
animation: icon-animate 1.33s ease-in-out infinite;
}
}
@include media('xs') {
font-size: 0.618rem;
}
}
@include blur;
@include media('print') {
display: none !important;
}
}
@keyframes icon-animate {
+25 -2
View File
@@ -1,9 +1,8 @@
header {
width: 100%;
background-color: fixit-var(header-background-color);
@include z-index(fixed);
transition: box-shadow 0.3s ease;
@extend .print-d-none;
@include z-index(fixed);
&:hover {
box-shadow: 0 0 1.5rem 0 rgba(0, 0, 0, 0.1);
@@ -12,6 +11,10 @@ header {
box-shadow: 0 0 1.5rem 0 rgba(255, 255, 255, 0.1);
}
}
@include media('print') {
display: none !important;
}
}
.header-wrapper {
@@ -262,6 +265,14 @@ header {
}
}
}
@include media('md', 'down') {
padding-inline-end: 1rem;
}
@include media('sm', 'down') {
padding-inline-start: 1rem;
}
}
&.open .header-wrapper .menu .menu-item.search {
@@ -269,6 +280,10 @@ header {
width: 24rem;
}
}
@include media('xs') {
display: none;
}
}
#header-mobile {
@@ -480,6 +495,10 @@ header {
}
}
}
@include media('xs') {
display: block;
}
}
.search-dropdown {
@@ -495,6 +514,10 @@ header {
&.desktop {
right: 2rem;
width: 30rem;
@include media('md', 'down') {
right: 1rem;
}
}
&.mobile {
+42 -2
View File
@@ -1,4 +1,4 @@
/** Layout **/
// Responsive layout
.wrapper {
display: flex;
flex-direction: column;
@@ -16,6 +16,10 @@
&:not(:has(~ aside)) {
display: none;
}
@include media('sm', 'down') {
display: none !important;
}
}
&.fi-container {
@@ -34,9 +38,45 @@
}
}
}
@include media('md') {
main {
// hide empty aside and expand article when only one aside is empty
aside:first-child:empty:has(~ aside:not(:empty)),
aside:first-child:not(:empty) ~ aside#toc-auto:empty {
display: none;
}
aside:first-child:empty:has(~ aside:not(:empty)) ~ article,
aside:first-child:not(:empty) ~ article:has(~ aside:empty) {
flex: 3;
}
}
// adjust breadcrumb flex when asides are empty
&:has(main > aside:first-child:empty ~ aside:not(:empty)) .breadcrumb-container {
.breadcrumb {
flex: 3;
}
&::before {
display: none;
}
}
&:has(main > aside:first-child:not(:empty) ~ aside#toc-auto:empty) .breadcrumb-container {
.breadcrumb {
flex: 3;
}
&::after {
display: none;
}
}
}
}
@import "header";
@import "breadcrumb";
@import "footer";
@import "page-style";
@import "pagination";
@import "footer";
+47
View File
@@ -0,0 +1,47 @@
%page-style {
@include media('xl') {
width: ROUND(60%, 2px);
max-width: 1200px;
}
@include media('lg') {
width: ROUND(56%, 2px);
}
@include media('md') {
width: ROUND(52%, 2px);
}
@include media('sm') {
width: ROUND(80%, 2px);
}
@include media('xs') {
width: 100%;
}
}
[data-page-style='wide'] {
%page-style {
@include media('xl') {
width: ROUND(64%, 2px);
max-width: 1600px;
}
@include media('lg') {
width: ROUND(60%, 2px);
}
@include media('md') {
width: ROUND(56%, 2px);
}
}
}
[data-page-style='narrow'] {
%page-style {
@include media('xl') {
max-width: 800px;
}
}
}
@@ -69,5 +69,9 @@
width: 50%;
}
}
@include media('xs') {
margin: 0 5px;
}
}
}
+26
View File
@@ -0,0 +1,26 @@
/// Mixin to apply box-shadow with timing control
/// @param {String} $when - When to show shadow: always | hover | never (default: always)
/// @param {List} $shadow - The box-shadow value
/// @example
/// @include box-shadow;
/// @include box-shadow(hover);
/// @include box-shadow(never);
/// @include box-shadow(always, 0 4px 8px rgba(0, 0, 0, 0.15));
@mixin box-shadow(
$when: always,
$shadow: (0 1px 2px -2px rgba(0, 0, 0, 0.08), 0 3px 6px 0 rgba(0, 0, 0, 0.06), 0 5px 12px 4px rgba(0, 0, 0, 0.04))
) {
@if $when == always {
box-shadow: $shadow;
} @else if $when == hover {
transition: box-shadow 0.3s fixit-var(bezier);
&:hover {
box-shadow: $shadow;
}
} @else if $when == never {
box-shadow: none;
} @else {
@warn "box-shadow mixin: unknown $when value '#{$when}'. Expected always, hover, or never.";
}
}
+3 -1
View File
@@ -1,8 +1,10 @@
@import 'theme-vars';
@import 'blur';
@import 'border-radius';
@import 'box-shadow';
@import 'compatibility';
@import 'link';
@import 'loading';
@import 'media';
@import 'scrollbar-width';
@import 'theme-vars';
@import 'z-index';
+100
View File
@@ -0,0 +1,100 @@
// Responsive breakpoints matching the project's media query design
// ┌──────────────────────────────────────────────────────┐
// │ xs │ sm │ md │ lg │ xl │
// │ ≤680 │ 681960 │ 9611200 │ 12011440 │ ≥1441 │
// └──────────────────────────────────────────────────────┘
$breakpoints: (
'xs': (
'min': null,
'max': 680px,
),
'sm': (
'min': 681px,
'max': 960px,
),
'md': (
'min': 961px,
'max': 1200px,
),
'lg': (
'min': 1201px,
'max': 1440px,
),
'xl': (
'min': 1441px,
'max': null,
),
) !default;
/// Apply styles within a named media target.
/// @param {String} $target - Target name: xs | sm | md | lg | xl | print | reduce-motion
/// @param {String} $direction - Range direction: only | up | down [default: only]
///
/// @example
/// // xs: ≤ 680px
/// @include media('xs') { ... }
/// // sm: 681px 960px
/// @include media('sm') { ... }
/// // md and above: ≥ 961px
/// @include media('md', 'up') { ... }
/// // lg and below: ≤ 1440px
/// @include media('lg', 'down') { ... }
/// // print media
/// @include media('print') { ... }
/// // reduced motion
/// @include media('reduce-motion') { ... }
@mixin media($target, $direction: 'only') {
@if $target == 'print' {
@media only print {
@content;
}
} @else if $target == 'reduce-motion' {
@media screen and (prefers-reduced-motion: reduce) {
@content;
}
} @else {
$range: map-get($breakpoints, $target);
$min: if($range != null, map-get($range, 'min'), null);
$max: if($range != null, map-get($range, 'max'), null);
@if $range == null {
@warn "Unknown media target `#{$target}`. Expected: xs, sm, md, lg, xl, print, reduce-motion.";
} @else if $direction == 'only' {
@if $min == null {
// xs: max-width only, no lower bound
@media only screen and (max-width: $max) {
@content;
}
} @else if $max == null {
// xl: min-width only, no upper bound
@media only screen and (min-width: $min) {
@content;
}
} @else {
@media only screen and (min-width: $min) and (max-width: $max) {
@content;
}
}
} @else if $direction == 'up' {
@if $min == null {
// xs-up: no lower bound, always applies
@content;
} @else {
@media only screen and (min-width: $min) {
@content;
}
}
} @else if $direction == 'down' {
@if $max == null {
// xl-down: no upper bound, always applies
@content;
} @else {
@media only screen and (max-width: $max) {
@content;
}
}
} @else {
@warn "Unknown direction `#{$direction}`. Expected: only, up, down.";
}
}
}
+4
View File
@@ -55,6 +55,9 @@
scrollbar-width: thin,
scrollbar-width-legacy: 12px,
// Animations
bezier: cubic-bezier(.4, 0, .2, 1),
// Other utilities
breadcrumb-height: 0px,
divider-edge-weak: linear-gradient(to right, transparent, fixit-var(divider-bg, #d0d0d5), transparent),
@@ -74,6 +77,7 @@
// Global
global-background-color: (light: $global-background-color, dark: $global-background-color-dark),
global-font-color: (light: $global-font-color, dark: $global-font-color-dark),
global-font-secondary-color: (light: $global-font-secondary-color, dark: $global-font-secondary-color-dark),
global-placeholder-color: (light: $global-placeholder-color, dark: $global-placeholder-color-dark),
global-link-color: (light: $global-link-color, dark: $global-link-color-dark),
global-link-hover-color: (light: $global-link-hover-color, dark: $global-link-hover-color-dark),
@@ -0,0 +1,11 @@
body {
.cell-tooltip {
--tooltip-bg: #ffffff;
--tooltip-color: #0f172a;
[data-theme=dark] & {
--tooltip-bg: #39393c;
--tooltip-color: #e2e2e6;
}
}
}
+4 -1
View File
@@ -17,7 +17,6 @@
transition: color 0.4s ease;
@include border-radius(full);
@include blur;
@extend .print-d-none;
[data-theme=dark] & {
background-color: darken($header-background-color-dark, 3%);
@@ -34,6 +33,10 @@
&:hover {
outline: none;
}
@include media('print') {
display: none !important;
}
}
.back-to-top {
@@ -17,11 +17,11 @@
position: fixed;
top: 0;
@include z-index(fixed);
@extend .print-d-none;
:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
svg {
width: fixit-var(header-height);
height: fixit-var(header-height);
@@ -29,6 +29,7 @@
color: fixit-var(github-corner-color);
fill: fixit-var(github-corner-fill);
}
&.left {
left: 0;
@@ -36,7 +37,12 @@
transform: scale(-1, 1);
}
}
&.right {
right: 0;
}
@include media('print') {
display: none !important;
}
}
+1
View File
@@ -1,3 +1,4 @@
@import 'cell-tooltip';
@import 'cookieconsent';
@import 'details';
@import 'fixed-button';
+3 -1
View File
@@ -1,4 +1,6 @@
#postChat_button,
.post-TianliGPT {
@extend .print-d-none;
@include media('print') {
display: none !important;
}
}
@@ -4,9 +4,12 @@
background-color: fixit-var(bg-progress, fixit-var(success));
position: fixed;
@include z-index(fixed);
@extend .print-d-none;
[data-theme=dark] & {
background-color: fixit-var(bg-progress-dark, fixit-var(global-font-color));
}
@include media('print') {
display: none !important;
}
}
+9 -1
View File
@@ -1,8 +1,12 @@
/* Modern browsers with `scrollbar-*` support (high priority) */
@supports (scrollbar-width: auto) {
* {
scrollbar-color: fixit-var(scrollbar-thumb-color) fixit-var(scrollbar-track-color);;
scrollbar-color: fixit-var(scrollbar-thumb-color) fixit-var(scrollbar-track-color);
scrollbar-width: fixit-var(scrollbar-width);
@include media('print') {
scrollbar-width: none;
}
}
}
@@ -11,6 +15,10 @@
height: fixit-var(scrollbar-width-legacy);
width: fixit-var(scrollbar-width-legacy);
overflow: visible;
@include media('print') {
display: none;
}
}
::-webkit-scrollbar-button {
height: 0;
+1
View File
@@ -8,6 +8,7 @@
white-space: pre-wrap;
}
--ti-cursor-color: fixit-var(global-font-color);
--ti-cursor-font-family: fixit-var(global-font-family);
--ti-cursor-transform: translateX(0);
}
+4
View File
@@ -38,6 +38,10 @@
padding-right: 1rem;
}
}
@include media('xs') {
width: 100%;
}
}
}
+17 -1
View File
@@ -5,10 +5,27 @@
.content {
[id] {
scroll-margin-top: fixit-var(scroll-mt);
[data-header-desktop='normal'] & {
@include media('xs', 'up') {
@include set-fixit-var(scroll-mt, fixit-var(global-scroll-margin-top));
}
}
[data-header-mobile='normal'] & {
@include media('xs') {
@include set-fixit-var(scroll-mt, fixit-var(global-scroll-margin-top));
}
}
}
}
@include blur;
@include media('print') {
width: 100% !important;
padding-top: 0 !important;
}
}
@import '_single';
@@ -17,5 +34,4 @@
@import 'home';
@import '404';
@import 'offline';
@import "media";
@import "patch";
-272
View File
@@ -1,272 +0,0 @@
@media only screen and (min-width: 1441px) {
%page-style {
width: ROUND(60%, 2px);
max-width: 1200px;
[data-page-style='wide'] & {
width: ROUND(64%, 2px);
max-width: 1600px;
}
[data-page-style='narrow'] & {
max-width: 800px;
}
}
}
@media only screen and (max-width: 1440px) {
%page-style {
width: ROUND(56%, 2px);
[data-page-style='wide'] & {
width: ROUND(60%, 2px);
}
}
}
@media only screen and (min-width: 1201px) {
.single .content {
:is(.code-block, .diagram-container) {
:is(.code-copy-btn, .diagram-copy-btn) {
display: none;
opacity: 0;
transform: scale(0.9);
transition:
display 0.25s allow-discrete,
opacity 0.25s,
transform 0.25s,
color 0.1s,
background-color 0.1s,
box-shadow 0.1s,
border-color 0.1s;
}
&:hover {
:is(.code-copy-btn, .diagram-copy-btn) {
display: block;
opacity: 1;
transform: scale(1);
@starting-style {
opacity: 0;
transform: scale(0.9);
}
}
}
}
}
}
@media only screen and (max-width: 1200px) {
%page-style {
width: ROUND(52%, 2px);
[data-page-style='wide'] & {
width: ROUND(56%, 2px);
}
}
#header-desktop .header-wrapper {
padding-right: 1rem;
}
.search-dropdown.desktop {
right: 1rem;
}
}
@media only screen and (max-width: 1200px) and (min-width: 961px) {
.wrapper {
main {
// Hide empty aside and expand article when only one aside is empty
aside:first-child:empty:has(~ aside:not(:empty)),
aside:first-child:not(:empty) ~ aside#toc-auto:empty {
display: none;
}
aside:first-child:empty:has(~ aside:not(:empty)) ~ article,
aside:first-child:not(:empty) ~ article:has(~ aside:empty) {
flex: 3;
}
}
// Adjust breadcrumb flex when asides are empty
&:has(main > aside:first-child:empty ~ aside:not(:empty)) .breadcrumb-container {
.breadcrumb {
flex: 3;
}
&::before {
display: none;
}
}
&:has(main > aside:first-child:not(:empty) ~ aside#toc-auto:empty) .breadcrumb-container {
.breadcrumb {
flex: 3;
}
&::after {
display: none;
}
}
}
}
@media only screen and (max-width: 960px) {
%page-style {
width: ROUND(80%, 2px) !important;
}
aside,
.breadcrumb-container::before,
.breadcrumb-container::after {
display: none !important;
}
#toc-static {
display: block;
}
#header-desktop .header-wrapper {
padding-left: 1rem;
}
}
@media only screen and (min-width: 681px) {
.d-none-desktop {
display: none;
}
.breadcrumb-container.sticky {
body:not([data-header-desktop='sticky']) & {
top: 0;
}
// adjust the scroll margin top of the content anchors on the page
body:not([data-header-desktop='auto']) &+.fi-container .content [id] {
scroll-margin-top: calc(#{fixit-var(scroll-mt)} + #{fixit-var(breadcrumb-height)});
}
}
[data-header-desktop='normal'] .page .content [id] {
@include set-fixit-var(scroll-mt, fixit-var(global-scroll-margin-top));
}
[data-header-desktop='normal'] {
#toc-auto,
.aside-collection {
top: 0;
}
}
[data-header-desktop='sticky'] {
.page .content .highlight.code-block .code-header {
top: calc(#{fixit-var(header-height)} + #{fixit-var(breadcrumb-height)});
}
}
}
@media only screen and (max-width: 680px) {
#header-desktop {
display: none;
}
#header-mobile {
display: block;
}
.d-none-mobile {
display: none !important;
}
.breadcrumb-container.sticky {
body:not([data-header-mobile='sticky']) & {
top: 0;
}
// adjust the scroll margin top of the content anchors on the page
body:not([data-header-mobile='auto']) &+.fi-container .content [id] {
scroll-margin-top: calc(#{fixit-var(scroll-mt)} + #{fixit-var(breadcrumb-height)});
}
}
[data-header-mobile='normal'] .page .content [id] {
@include set-fixit-var(scroll-mt, fixit-var(global-scroll-margin-top));
}
[data-header-mobile='normal'] {
#toc-auto,
.aside-collection {
top: 0;
}
}
[data-header-mobile='sticky'] {
.page .content .highlight.code-block .code-header {
top: calc(#{fixit-var(header-height)} + #{fixit-var(breadcrumb-height)});
}
}
%page-style {
width: 100% !important;
}
.page {
.taxonomy-cards {
.card-item {
width: 100%;
}
}
}
footer {
.footer-container {
font-size: 0.618rem;
}
}
.pagination .page-item {
margin: 0 5px;
}
}
@media only print {
// 确保打印时没有 scrollbar
:root {
@include set-fixit-var(scrollbar-width, none !important);
}
*::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
.scrollbar-thin {
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
@page {
size: A4 portrait;
margin: 1.27cm;
}
.page {
width: 100% !important;
padding-top: 0 !important;
&.single {
.single-title,
.single-subtitle,
.post-meta {
text-align: center;
}
}
h1:not(.single-title),
.page-break-before {
page-break-before: always;
}
.page-break-after {
page-break-after: always;
}
}
.print-d-none {
display: none !important;
}
}
-50
View File
@@ -19,56 +19,6 @@
animation-name: #{$prefix}pulse !important;
}
/**
* After the CSS round() function exceeds 90% browser support in the future, this code can be removed
* See https://caniuse.com/mdn-css_types_round
*/
@supports not (width: ROUND(60%, 2px)) {
@media only screen and (min-width: 1441px) {
%page-style {
width: 60%;
max-width: 1200px;
[data-page-style='wide'] & {
width: 64%;
max-width: 1600px;
}
[data-page-style='narrow'] & {
max-width: 800px;
}
}
}
@media only screen and (max-width: 1440px) {
%page-style {
width: 56%;
[data-page-style='wide'] & {
width: 60%;
}
}
}
@media only screen and (max-width: 1200px) {
%page-style {
width: 52%;
[data-page-style='wide'] & {
width: 56%;
}
}
}
@media only screen and (max-width: 960px) {
%page-style {
width: 80% !important;
}
}
@media only screen and (max-width: 680px) {
%page-style {
width: 100% !important;
}
}
}
// fix ZenUML intersection height error
#zenuml-intersection-detector-container {
display: none;
+180 -6
View File
@@ -132,6 +132,14 @@ pre {
// code fences blocks wrapped
&.code-block {
&[data-shadow='always'] {
@include box-shadow(always);
}
&[data-shadow='hover'] {
@include box-shadow(hover);
}
&.instant-height .code-wrapper {
transition: height 0s !important;
}
@@ -229,7 +237,10 @@ pre {
[role='button'] {
padding: 0.4rem;
@extend .print-d-none;
@include media('print') {
display: none !important;
}
}
.copy-btn[data-copied] .fa-clone {
@@ -256,6 +267,22 @@ pre {
@include set-fixit-var(code-type, '"#{$text}"');
}
}
[data-header-desktop='sticky'] & {
@include media('sm', 'up') {
top: calc(#{fixit-var(header-height)} + #{fixit-var(breadcrumb-height)});
}
}
[data-header-mobile='sticky'] & {
@include media('xs') {
top: calc(#{fixit-var(header-height)} + #{fixit-var(breadcrumb-height)});
}
}
@include media('print') {
top: initial !important;
}
}
&.line-wrapping .line-wrap-btn,
@@ -267,10 +294,7 @@ pre {
&.is-fullscreen {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
inset: 0;
margin: 0 !important;
overflow: auto;
@include z-index(fixed, 1);
@@ -359,7 +383,7 @@ pre {
display: inline;
}
.code-header [role='button']:not(.ellipses-btn),
.code-header .action-btn,
.code-expand-btn {
display: none;
}
@@ -559,3 +583,153 @@ pre {
@import './_code-syntax/github-dark-dimmed';
}
}
// Code Tabs
.code-tabs {
margin: 0.5rem 0;
background-color: fixit-var(code-block-background-color);
border: 1px solid fixit-var(global-border-color);
@include border-radius;
&[data-shadow='always'] {
@include box-shadow(always);
}
&[data-shadow='hover'] {
@include box-shadow(hover);
}
&:has(.code-block.active .code-expand-btn) {
margin-bottom: 1rem;
}
&:has(.code-block.active.line-wrapping) .tabs-actions .line-wrap-btn,
&:has(.code-block.active:not(.line-nos-hidden)) .tabs-actions .line-nos-btn,
&:has(.code-block.active [contenteditable='true']) .tabs-actions .edit-btn {
color: fixit-var(global-link-hover-color);
}
.tabs-header {
display: flex;
justify-content: space-between;
align-items: center;
line-height: 1.4em;
font-size: fixit-var(code-block-font-size);
color: fixit-var(code-header-color);
background-color: fixit-var(code-header-background-color);
position: sticky;
top: fixit-var(breadcrumb-height);
@include z-index(base);
@include border-radius(top);
.tabs-items {
display: flex;
overflow-x: auto;
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;
font-weight: bold;
color: fixit-var(secondary);
white-space: nowrap;
@include user-select(none);
@include border-radius;
@include media('xs') {
padding-inline: 0.6rem;
}
}
&:not(:has(.tab-item:hover)) .tab-item.active,
.tab-item:hover {
anchor-name: #{$rootPrefix}tab-hover;
color: fixit-var(global-font-color);
}
&::before {
content: '';
position-anchor: #{$rootPrefix}tab-hover;
position: absolute;
inset: anchor(inside);
translate: 2px 2px;
width: calc(anchor-size(width) - 0.25rem);
height: calc(anchor-size(height) - 0.25rem);
box-shadow: inset 0 0 1rem 0.25rem color-mix(in srgb, fixit-var(secondary) 30%, transparent);
pointer-events: none;
transition: 0.3s ease-in-out;
@include border-radius;
}
}
.tabs-actions {
display: flex;
flex-shrink: 0;
.action-btn {
padding: 0.4rem;
&:hover {
color: fixit-var(global-link-hover-color);
}
&.copy-btn[data-copied] .fa-clone {
--fa: "\f00c";
font-weight: 900;
color: fixit-var(global-link-hover-color);
}
&.download-btn[data-downloaded] .fa-download {
--fa: "\f1ce";
font-weight: 900;
color: fixit-var(global-link-hover-color);
}
&.fullscreen-btn {
display: none;
}
}
}
[data-header-desktop='sticky'] & {
@include media('sm', 'up') {
top: calc(#{fixit-var(header-height)} + #{fixit-var(breadcrumb-height)});
}
}
[data-header-mobile='sticky'] & {
@include media('xs') {
top: calc(#{fixit-var(header-height)} + #{fixit-var(breadcrumb-height)});
}
}
@include media('print') {
top: initial !important;
}
}
.tabs-content {
.code-block {
display: none;
margin: 0;
@include border-radius(bottom);
@include box-shadow(never);
&.active {
display: block;
}
// hide original header
.code-header {
display: none;
}
}
}
}
+8 -2
View File
@@ -7,7 +7,6 @@
position: relative;
@include border-radius(0.5rem);
@include user-select(none);
@extend .print-d-none;
[data-theme=dark] & {
background-color: lighten($global-background-color-dark, 3%);
@@ -81,6 +80,10 @@
}
}
}
@include media('print') {
display: none !important;
}
}
// Collection aside container
@@ -90,7 +93,10 @@
box-sizing: border-box;
@include overflow-wrap(break-word);
@include blur;
@extend .print-d-none;
@include media('print') {
display: none !important;
}
}
// Collection List
+5 -2
View File
@@ -1,8 +1,11 @@
>#comments {
> #comments {
padding: 2rem 0;
@extend .print-d-none;
iframe {
width: 100%;
}
@include media('print') {
display: none !important;
}
}
@@ -13,17 +13,14 @@
right: 0.5rem;
line-height: 1;
padding: 0.5rem;
user-select: none;
border: 1px solid;
color: fixit-var(dcb-color, #25292e);
border-color: fixit-var(dcb-border-color, #d1d9e0);
background-color: fixit-var(dcb-background-color, #f6f8fa);
box-shadow: fixit-var(dcb-box-shadow, 0px 1px 0px 0px #1f23280a);
transition-duration: 0.1s;
transition-property: color, background-color, box-shadow, border-color;
@include user-select(none);
@include border-radius;
@include z-index(base);
@extend .print-d-none;
[data-theme=dark] & {
@include set-fixit-vars((
@@ -59,4 +56,8 @@
color: fixit-var(global-link-hover-color);
}
}
@include media('print') {
display: none !important;
}
}
+11 -3
View File
@@ -42,10 +42,13 @@
}
.post-info-share {
@extend .print-d-none;
a * {
vertical-align: text-bottom;
}
@include media('print') {
display: none !important;
}
}
}
}
@@ -57,7 +60,9 @@
font-size: 0.9rem;
section:last-child {
@extend .print-d-none;
@include media('print') {
display: none !important;
}
}
}
@@ -81,7 +86,6 @@
justify-content: space-between;
gap: 1.5rem;
padding-block: 0.5rem;
@extend .print-d-none;
.post-nav-item {
flex: 1;
@@ -101,5 +105,9 @@
transform: translateX(4px);
}
}
@include media('print') {
display: none !important;
}
}
}
+57 -9
View File
@@ -26,6 +26,10 @@
margin-right: 0.25em;
color: fixit-var(success);
}
@include media('print') {
text-align: center;
}
}
.single-subtitle {
@@ -33,17 +37,16 @@
font-size: 1.2rem;
font-weight: normal;
line-height: 1.15;
@include media('print') {
text-align: center;
}
}
.post-meta {
font-size: 0.875rem;
color: fixit-var(secondary);
.comment-visitors,
.comment-count {
@extend .print-d-none;
}
span {
display: inline-block;
}
@@ -64,6 +67,15 @@
object-fit: cover;
}
}
@include media('print') {
text-align: center;
.comment-visitors,
.comment-count {
display: none;
}
}
}
.featured-image {
@@ -125,6 +137,7 @@
.content {
margin-block: 1rem;
> h1,
> h2 {
font-size: 1.5em;
@@ -458,6 +471,9 @@
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
// neutralize the margin-right setting of .katex .vlist-t2
// to prevent scrollbars from appearing when the width is not full
padding-right: 2px;
}
.katex-error {
padding: 0.14em 0.28em;
@@ -467,10 +483,13 @@
}
// MathJax styles
mjx-container[jax='CHTML'][display='true'] {
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
mjx-container[jax='CHTML'] {
// formula Blocks
&[display='true'] {
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
}
// fix the color of xypic diagrams
mjx-xypic {
@@ -487,6 +506,35 @@
json-viewer[boxed] + json-viewer[boxed] {
margin-top: 0.5rem;
}
@include media('lg', 'up') {
:is(.code-block, .diagram-container) {
:is(.code-copy-btn, .diagram-copy-btn) {
display: none;
opacity: 0;
transform: scale(0.9);
transition:
display 0.25s allow-discrete,
opacity 0.25s,
transform 0.25s,
color 0.1s,
background-color 0.1s,
box-shadow 0.1s,
border-color 0.1s;
}
&:hover {
:is(.code-copy-btn, .diagram-copy-btn) {
display: block;
opacity: 1;
transform: scale(1);
@starting-style {
opacity: 0;
transform: scale(0.9);
}
}
}
}
}
}
@import '_shortcodes';
+4 -1
View File
@@ -1,7 +1,6 @@
.post-reward {
padding: 1rem;
text-align: center;
@extend .print-d-none;
&:has(.reward-ways:empty) {
display: none;
@@ -85,4 +84,8 @@
border-color: lighten($reward-color, 5%);
}
}
@include media('print') {
display: none !important;
}
}
@@ -60,7 +60,9 @@
margin: 0;
}
.code-block .code-header {
// reset sticky header position in code block when inside admonition
.code-block .code-header,
.code-tabs .tabs-header {
top: initial !important;
}
}
@@ -56,6 +56,10 @@
flex-shrink: 0;
}
.file-tree-label__inner {
word-break: break-all;
}
&.is-highlighted {
color: fixit-var(global-link-hover-color);
}
@@ -43,10 +43,10 @@
padding: 0;
}
@media only print {
@include media('print') {
.mermaid,
.mermaid-dark {
display: none;
display: none !important;
}
.mermaid-neutral {
height: auto;
+121 -19
View File
@@ -107,17 +107,15 @@ tab-container {
// ========================================
// Style 1: Underline Style (Default)
// Clean, minimal with bottom border
// Clean, minimal with sliding indicator
// ========================================
&[type='underline'] {
> .tab-button {
@include set-fixit-var(tab-button-border-color, transparent);
@include tab-button-size(0);
&[aria-selected='true'],
&[aria-selected='false']:hover {
color: fixit-var(primary);
@include set-fixit-var(tab-button-border-color, fixit-var(primary));
}
&[aria-selected='false']:hover {
@@ -125,6 +123,29 @@ tab-container {
}
}
// Anchor tracking: follows hover, rests on active tab
&:not(:has(> .tab-button:hover)) > .tab-button[aria-selected='true'],
> .tab-button:hover {
anchor-name: #{$rootPrefix}tab-underline-active;
}
&::part(tablist-wrapper) {
position: relative;
// Sliding box-shadow indicator via anchor positioning
&::before {
content: '';
position: absolute;
position-anchor: #{$rootPrefix}tab-underline-active;
inset: anchor(inside);
pointer-events: none;
}
}
&:has(> .tab-button[aria-selected='false']:hover)::part(tablist-wrapper)::before {
opacity: 0.7;
}
&[placement='top'],
&[placement='bottom'] {
gap: 0.5rem;
@@ -134,9 +155,13 @@ tab-container {
}
&::part(tablist-wrapper) {
overflow-x: scroll;
overflow-x: auto;
overflow-y: hidden;
}
&::part(tablist-wrapper)::before {
transition: all 0.3s ease-in-out, top, bottom 0s;
}
}
&[placement='left'],
@@ -144,6 +169,10 @@ tab-container {
> .tab-button {
@include tab-button-size(1rem, 0);
}
&::part(tablist-wrapper)::before {
transition: all 0.3s ease-in-out, left, right 0s;
}
}
// Top placement
@@ -152,9 +181,9 @@ tab-container {
border-bottom: 2px solid fixit-var(global-border-color);
}
> .tab-button {
&::part(tablist-wrapper)::before {
box-shadow: inset 0 -2px 0 fixit-var(primary);
translate: 0 2px;
border-bottom: 2px solid fixit-var(tab-button-border-color);
}
}
@@ -164,9 +193,9 @@ tab-container {
border-top: 2px solid fixit-var(global-border-color);
}
> .tab-button {
&::part(tablist-wrapper)::before {
box-shadow: inset 0 2px 0 fixit-var(primary);
translate: 0 -2px;
border-top: 2px solid fixit-var(tab-button-border-color);
}
}
@@ -176,10 +205,13 @@ tab-container {
border-right: 2px solid fixit-var(global-border-color);
}
&::part(tablist-wrapper)::before {
box-shadow: inset -2px 0 0 fixit-var(primary);
translate: 2px 0;
}
> .tab-button {
text-align: right;
translate: 2px 0;
border-right: 2px solid fixit-var(tab-button-border-color);
}
}
@@ -189,10 +221,13 @@ tab-container {
border-left: 2px solid fixit-var(global-border-color);
}
&::part(tablist-wrapper)::before {
box-shadow: inset 2px 0 0 fixit-var(primary);
translate: -2px 0;
}
> .tab-button {
text-align: left;
translate: -2px 0;
border-left: 2px solid fixit-var(tab-button-border-color);
}
}
}
@@ -326,7 +361,7 @@ tab-container {
&[placement='top'],
&[placement='bottom'] {
&::part(tablist-wrapper) {
overflow-x: scroll;
overflow-x: auto;
overflow-y: hidden;
}
@@ -436,7 +471,7 @@ tab-container {
// ========================================
&[type='segment'] {
&::part(tablist-wrapper) {
overflow-x: scroll;
overflow-x: auto;
overflow-y: hidden;
}
@@ -451,8 +486,10 @@ tab-container {
}
> .tab-button {
position: relative;
font-weight: 400;
flex-grow: 1;
@include z-index(base);
@include tab-button-size;
@include border-radius;
@@ -463,12 +500,26 @@ tab-container {
}
&[aria-selected='true'] {
background-color: fixit-var(global-background-color);
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1);
anchor-name: #{$rootPrefix}tab-segment-active;
background-color: transparent;
box-shadow: none;
}
}
[data-theme=dark] & {
box-shadow: 0 0.125rem 0.25rem rgba(255, 255, 255, 0.1);
}
// Sliding active indicator via anchor positioning
&::part(tablist-tab-wrapper)::before {
content: '';
position: absolute;
pointer-events: none;
position-anchor: #{$rootPrefix}tab-segment-active;
inset: anchor(inside);
background-color: fixit-var(global-background-color);
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1);
transition: all 0.15s ease, top, bottom 0s;
@include border-radius;
[data-theme=dark] & {
box-shadow: 0 0.125rem 0.25rem rgba(255, 255, 255, 0.1);
}
}
}
@@ -531,3 +582,54 @@ tab-container {
tab-container .tab-panel:not([hidden]) {
animation: tabSlideIn 0.3s ease-out;
}
// Firefox fallback: `::part(...)::before` is not supported.
// We use a CSS variable to control the border color of the active tab button,
// and apply the sliding effect directly on the button itself.
@supports (-moz-appearance: none) {
tab-container {
&[type='underline'] {
> .tab-button {
@include set-fixit-var(tab-button-border-color, transparent);
&[aria-selected='true'],
&[aria-selected='false']:hover {
@include set-fixit-var(tab-button-border-color, fixit-var(primary));
}
}
&[placement='top'] > .tab-button {
translate: 0 2px;
border-bottom: 2px solid fixit-var(tab-button-border-color);
}
&[placement='bottom'] > .tab-button {
translate: 0 -2px;
border-top: 2px solid fixit-var(tab-button-border-color);
}
&[placement='left'] > .tab-button {
translate: 2px 0;
border-right: 2px solid fixit-var(tab-button-border-color);
}
&[placement='right'] > .tab-button {
translate: -2px 0;
border-left: 2px solid fixit-var(tab-button-border-color);
}
}
&[type='segment'] {
> .tab-button {
&[aria-selected='true'] {
background-color: fixit-var(global-background-color);
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.1);
[data-theme=dark] & {
box-shadow: 0 0.125rem 0.25rem rgba(255, 255, 255, 0.1);
}
}
}
}
}
}
+8 -1
View File
@@ -94,7 +94,6 @@
visibility: hidden;
@include overflow-wrap(break-word);
@include blur;
@extend .print-d-none;
[data-header-desktop='normal'] & {
@include set-fixit-var(scroll-mt, fixit-var(global-scroll-margin-top));
@@ -157,6 +156,10 @@
}
}
}
@include media('print') {
display: none !important;
}
}
#toc-static {
@@ -211,6 +214,10 @@
}
}
}
@include media('sm', 'down') {
display: block;
}
}
#toc-dialog {
+4
View File
@@ -28,6 +28,10 @@ $global-background-color-dark: #292a2e !default;
$global-font-color: #161209 !default;
$global-font-color-dark: #b1b1ba !default;
// Color of the secondary text
$global-font-secondary-color: #8b949e !default;
$global-font-secondary-color-dark: #7d8792 !default;
// Color of the placeholder
$global-placeholder-color: #b1b1ba !default;
$global-placeholder-color-dark: #909092 !default;
+4 -2
View File
@@ -19,6 +19,8 @@ libFiles:
# autocomplete-js@0.38.1 https://github.com/algolia/autocomplete
# TODO update autocompleteJS: '@algolia/autocomplete-js@1.7.1/dist/umd/index.production.js'
autocompleteJS: autocomplete.js@0.38.1/dist/autocomplete.min.js
# cell-tooltip@0.3.0 https://github.com/Lruihao/cell-tooltip
cellTooltipJS: cell-tooltip@0.3.0/dist/cell-tooltip.umd.js
# cell-watermark@1.0.3 https://github.com/Lruihao/watermark
cellWatermarkJS: cell-watermark@1.0.3/src/watermark.min.js
# cookieconsent@3.1.1 https://github.com/osano/cookieconsent
@@ -67,8 +69,8 @@ libFiles:
tabContainerElementJS: '@github/tab-container-element@4.8.2/dist/index.min.js'
# twemoji@14.0.2 https://github.com/twitter/twemoji
twemojiJS: twemoji@14.0.2/dist/twemoji.min.js
# twikoo@1.6.44 https://github.com/imaegoo/twikoo
twikooJS: twikoo@1.6.44/dist/twikoo.all.min.js
# twikoo@1.7.3 https://github.com/imaegoo/twikoo
twikooJS: twikoo@1.7.3/dist/twikoo.all.min.js
# typeit@8.8.4 https://github.com/alexmacarthur/typeit
typeitJS: typeit@8.8.4/dist/index.umd.js
# valine@1.5.2 https://github.com/xCss/Valine
+4 -2
View File
@@ -19,6 +19,8 @@ libFiles:
# autocomplete-js@0.38.1 https://github.com/algolia/autocomplete
# TODO update autocompleteJS: '@algolia/autocomplete-js@1.7.1/dist/umd/index.production.js'
autocompleteJS: autocomplete.js@0.38.1/dist/autocomplete.min.js
# cell-tooltip@0.3.0 https://github.com/Lruihao/cell-tooltip
cellTooltipJS: cell-tooltip@0.3.0/dist/cell-tooltip.umd.js
# cell-watermark@1.0.3 https://github.com/Lruihao/watermark
cellWatermarkJS: cell-watermark@1.0.3/src/watermark.min.js
# cookieconsent@3.1.1 https://github.com/osano/cookieconsent
@@ -67,8 +69,8 @@ libFiles:
tabContainerElementJS: '@github/tab-container-element@4.8.2/dist/index.js'
# twemoji@14.0.2 https://github.com/twitter/twemoji
twemojiJS: twemoji@14.0.2/dist/twemoji.min.js
# twikoo@1.6.44 https://github.com/imaegoo/twikoo
twikooJS: twikoo@1.6.44/dist/twikoo.all.min.js
# twikoo@1.7.3 https://github.com/imaegoo/twikoo
twikooJS: twikoo@1.7.3/dist/twikoo.all.min.js
# typeit@8.8.4 https://github.com/alexmacarthur/typeit
typeitJS: typeit@8.8.4/dist/index.umd.js
# valine@1.5.2 https://github.com/xCss/Valine
+12 -2
View File
@@ -2,7 +2,7 @@
* FileTree class to handle file tree interactions
*/
export default class FileTree {
init(target = document) {
static init(target = document) {
target.querySelectorAll('.file-tree-toggle:not([data-init])').forEach((label) => {
label.addEventListener('click', (e) => {
e.stopPropagation()
@@ -18,7 +18,7 @@ export default class FileTree {
this.updateLineHeight(target)
}
updateLineHeight(target = document) {
static updateLineHeight(target = document) {
const uls = target.querySelectorAll('.file-tree .file-tree')
uls.forEach((ul) => {
const parentItem = ul.closest('.file-tree-item.is-collapsed')
@@ -47,4 +47,14 @@ export default class FileTree {
ul.style.setProperty('--fi-file-tree-line-height', `${height}px`)
})
}
static expandAll(target = document) {
target.querySelectorAll('.file-tree-folder').forEach(folder => folder.classList.remove('is-collapsed'))
this.updateLineHeight(target)
}
static collapseAll(target = document) {
target.querySelectorAll('.file-tree-folder').forEach(folder => folder.classList.add('is-collapsed'))
this.updateLineHeight(target)
}
}
+320 -76
View File
@@ -1,21 +1,33 @@
// TODO use ESLint to check the code style
// 按需加载主题内部库和功能模块
import Util from './util';
import {
forEach,
getScrollTop,
isMobile,
isTocStatic,
animateCSS,
isValidDate,
scrollIntoView,
getStagingDOM,
createCopyText,
isObjectLiteral,
HTMLEscape,
} from './utils/common';
import FileTree from './lib/file-tree.js'
const copyText = createCopyText();
class FixIt {
constructor() {
this.config = window.config;
this.isDark = document.documentElement.dataset.theme === 'dark';
this.util = new Util();
this.fileTree = new FileTree();
this.newScrollTop = this.util.getScrollTop();
this.newScrollTop = getScrollTop();
this.oldScrollTop = this.newScrollTop;
this.scrollEventSet = new Set();
this.resizeEventSet = new Set();
this.switchThemeEventSet = new Set();
this.clickMaskEventSet = new Set();
this.beforeprintEventSet = new Set();
this.afterprintEventSet = new Set();
this.disableScrollEvent = false;
window.objectFitImages && objectFitImages();
}
@@ -31,7 +43,7 @@ class FixIt {
}
initSVGIcon() {
this.util.forEach(document.querySelectorAll('[data-svg-src]'), ($icon) => {
forEach(document.querySelectorAll('[data-svg-src]'), ($icon) => {
fetch($icon.dataset.svgSrc)
.then((response) => response.text())
.then((svg) => {
@@ -60,7 +72,7 @@ class FixIt {
}
initMenuDesktop() {
this.util.forEach(document.querySelectorAll('.has-children'), ($item) => {
forEach(document.querySelectorAll('.has-children'), ($item) => {
$item.querySelector('.sub-menu').style.minWidth = `${$item.offsetWidth - 8}px`;
});
}
@@ -80,7 +92,7 @@ class FixIt {
});
this.clickMaskEventSet.add(this._menuMobileOnClickMask);
// add nested menu toggler
this.util.forEach(document.querySelectorAll('.menu-item>.nested-item'), ($nestedItem) => {
forEach(document.querySelectorAll('.menu-item>.nested-item'), ($nestedItem) => {
$nestedItem.addEventListener('click', function () {
this.parentNode.querySelector('.sub-menu').classList.toggle('open');
this.querySelector('.dropdown-icon').classList.toggle('open');
@@ -89,7 +101,7 @@ class FixIt {
}
initSwitchTheme() {
this.util.forEach(document.getElementsByClassName('theme-switch'), ($themeSwitch) => {
forEach(document.getElementsByClassName('theme-switch'), ($themeSwitch) => {
$themeSwitch.addEventListener('click', () => {
document.documentElement.dataset.theme = this.isDark ? 'light' : 'dark';
document.documentElement.style.setProperty('color-scheme', this.isDark ? 'light' : 'dark');
@@ -136,11 +148,11 @@ class FixIt {
initSearch() {
const searchConfig = this.config.search;
const isMobile = this.util.isMobile();
const _isMobile = isMobile();
if (
!searchConfig ||
(isMobile && this._searchMobileOnce) ||
(!isMobile && this._searchDesktopOnce)
(_isMobile && this._searchMobileOnce) ||
(!_isMobile && this._searchDesktopOnce)
)
return;
// Initialize default search config
@@ -156,7 +168,7 @@ class FixIt {
const ignoreLocation = searchConfig.ignoreLocation ?? false;
const useExtendedSearch = searchConfig.useExtendedSearch ?? false;
const ignoreFieldNorm = searchConfig.ignoreFieldNorm ?? false;
const suffix = isMobile ? 'mobile' : 'desktop';
const suffix = _isMobile ? 'mobile' : 'desktop';
const $header = document.getElementById(`header-${suffix}`);
const $searchInput = document.getElementById(`search-input-${suffix}`);
const $searchToggle = document.getElementById(`search-toggle-${suffix}`);
@@ -166,7 +178,7 @@ class FixIt {
// goto the PostChat panel rather than search results
if (searchConfig.type === 'post-chat' && window.postChatUser) {
if (isMobile) {
if (_isMobile) {
$searchInput.addEventListener('focus', () => {
window.postChatUser.setSearchInput('');
}, false);
@@ -178,7 +190,7 @@ class FixIt {
return;
}
if (isMobile) {
if (_isMobile) {
this._searchMobileOnce = true;
$searchInput.addEventListener('focus', () => {
this.disableScrollEvent = true;
@@ -343,7 +355,7 @@ class FixIt {
templates: {
suggestion: ({ title, uri, date, context }) =>
`<div><a href="${uri}"><span class="suggestion-title">${title}</span></a><span class="suggestion-date">${date}</span></div><div class="suggestion-context">${context}</div>`,
empty: ({ query }) => `<div class="search-empty">${searchConfig.noResultsFound}: <span class="search-query">"${this.util.HTMLEscape(query)}"</span></div>`,
empty: ({ query }) => `<div class="search-empty">${searchConfig.noResultsFound}: <span class="search-query">"${HTMLEscape(query)}"</span></div>`,
footer: ({ }) => {
let searchType, icon, href;
switch (searchConfig.type) {
@@ -378,7 +390,7 @@ class FixIt {
document.getElementById('mask')?.click();
window.location.assign(suggestion.uri);
});
if (isMobile) {
if (_isMobile) {
this._searchMobile = autosearch;
} else {
this._searchDesktop = autosearch;
@@ -396,7 +408,7 @@ class FixIt {
}
initDetails(target = document) {
this.util.forEach(target.querySelectorAll('.details:not(.disabled)'), ($details) => {
forEach(target.querySelectorAll('.details:not(.disabled)'), ($details) => {
const $summary = $details.querySelector('.details-summary');
$summary.addEventListener('click', () => {
$details.classList.toggle('open');
@@ -438,14 +450,21 @@ class FixIt {
const iswWrap = codeBlock.classList.contains('line-wrapping');
const highlightLines = codeBlock.querySelectorAll('.hl');
iswWrap && codeBlock.classList.toggle('line-wrapping');
this.util.forEach(highlightLines, $hl => $hl.classList.toggle('hl'));
this.util.copyText(codePreEl.innerText.trim()).then(() => {
this.util.animateCSS(codePreEl, 'animate__flash');
forEach(highlightLines, $hl => $hl.classList.toggle('hl'));
copyText(codePreEl.innerText.trim()).then(() => {
animateCSS(codePreEl, 'animate__flash');
iswWrap && codeBlock.classList.toggle('line-wrapping');
this.util.forEach(highlightLines, $hl => $hl.classList.toggle('hl'));
forEach(highlightLines, $hl => $hl.classList.toggle('hl'));
const copiedText = copyBtn.dataset.copiedText;
const originalTitle = copyBtn.dataset.ctOriginalTitle;
copyBtn.toggleAttribute('data-copied', true);
copyBtn.dataset.ctTitle = copiedText;
const instance = window.CellTooltip.getOrCreateInstance(copyBtn);
instance.refresh();
setTimeout(() => {
copyBtn.toggleAttribute('data-copied', false);
copyBtn.dataset.ctTitle = originalTitle;
instance.hide();
}, 2000);
}, () => {
console.error('Clipboard write failed!', 'Your browser does not support clipboard API!');
@@ -464,15 +483,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);
@@ -543,7 +565,7 @@ class FixIt {
*/
initCodeWrapper() {
const $codeBlocks = document.querySelectorAll('.code-block.highlight:not([data-init])');
this.util.forEach($codeBlocks, ($codeBlock) => {
forEach($codeBlocks, ($codeBlock) => {
const $preElements = $codeBlock.querySelectorAll('pre.chroma');
if (!$preElements.length) return;
const $codePreEl = $preElements[$preElements.length - 1];
@@ -583,7 +605,7 @@ class FixIt {
$codePreEl.setAttribute('contenteditable', false);
$codePreEl.blur();
} else {
this.util.forEach($codeBlock.querySelectorAll('.hl'), ($hl) => {
forEach($codeBlock.querySelectorAll('.hl'), ($hl) => {
$hl.classList.remove('hl');
});
$codeBlock.classList.add('is-expanded');
@@ -596,19 +618,152 @@ class FixIt {
});
}
/**
* init code tabs
*/
initCodeTabs() {
const $codeBlocks = document.querySelectorAll('.code-block[group]:not([data-tab-init])');
const processed = new Set();
forEach($codeBlocks, ($block) => {
if (processed.has($block)) return;
const groupName = $block.getAttribute('group');
const $tabs = [];
let $curr = $block;
// collect consecutive blocks with same group
while ($curr && $curr.classList?.contains('code-block') && $curr.getAttribute('group') === groupName) {
$tabs.push($curr);
processed.add($curr);
$curr = $curr.nextElementSibling;
}
if ($tabs.length < 2) return;
// create DOM structure
const $container = document.createElement('div');
$container.className = 'code-tabs';
const $header = document.createElement('div');
$header.className = 'tabs-header';
const $items = document.createElement('div');
$items.className = 'tabs-items';
const $actions = document.createElement('div');
$actions.className = 'tabs-actions';
$header.appendChild($items);
$header.appendChild($actions);
const $content = document.createElement('div');
$content.className = 'tabs-content';
// insert container before the first block
const $firstBlock = $tabs[0];
$firstBlock.parentNode.insertBefore($container, $firstBlock);
const activeTabIndex = $tabs.findIndex(tab => tab.classList.contains('active'));
const langPref = window.localStorage.getItem('config_lang_perf');
const hasCodeToggle = $tabs.some(tab => tab.dataset.codeToggle === 'true');
const langPrefIndex = (langPref && hasCodeToggle) ? $tabs.findIndex(tab => tab.dataset.tabTitle.toLowerCase() === langPref) : -1;
const resolvedIndex = langPrefIndex !== -1 ? langPrefIndex : activeTabIndex;
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 = resolvedIndex === -1 && index === 0;
// tab button
const $btn = document.createElement('span');
$btn.className = 'tab-item';
if (defaultActiveTab) $btn.classList.add('active');
$btn.textContent = title;
$btn.dataset.index = index;
$btn.title = title;
$btn.addEventListener('click', () => {
// 1. restore buttons to the currently active tab
const $activeTab = $tabs.find(t => t.classList.contains('active'));
if ($activeTab) {
const $activeHeader = $activeTab.querySelector('.code-header');
if ($activeHeader) {
Array.from($actions.children).forEach(btn => $activeHeader.appendChild(btn));
}
}
// 2. switch active tab UI
$items.querySelectorAll('.tab-item').forEach(b => b.classList.remove('active'));
$btn.classList.add('active');
if ($tab.dataset.codeToggle === 'true') {
window.localStorage.setItem('config_lang_perf', $tab.dataset.tabTitle.toLowerCase());
const activeItems = document.querySelectorAll(`
.tab-item[title="${$tab.dataset.tabTitle.toLowerCase()}"]:not(.active),
.tab-item[title="${$tab.dataset.tabTitle.toUpperCase()}"]:not(.active)`
);
forEach(activeItems, t => t.click());
}
// 3. switch content
$tabs.forEach(b => b.classList.remove('active'));
$tab.classList.add('active');
// 4. sync shadow mode data attribute
const shadowMode = $tab?.dataset.shadow;
if (shadowMode) {
$container.dataset.shadow = shadowMode;
} else {
delete $container.dataset.shadow;
}
// 5. move new buttons to actions
const $codeHeader = $tab.querySelector('.code-header');
if ($codeHeader) {
$codeHeader.querySelectorAll('.action-btn').forEach(btn => $actions.appendChild(btn));
}
});
$items.appendChild($btn);
// move block to content
$tab.classList.toggle('active', resolvedIndex === index || defaultActiveTab);
$tab.classList.remove('is-collapsed');
$tab.classList.remove('d-none');
$tab.dataset.tabInit = 'true';
$content.appendChild($tab);
});
$container.appendChild($header);
$container.appendChild($content);
// initialize actions for the active tab
if (resolvedIndex !== -1) {
const $activeBtn = $items.querySelector(`.tab-item[data-index="${resolvedIndex}"]`);
if ($activeBtn) $activeBtn.click();
} else {
$items.firstElementChild.click();
}
});
}
/**
* init diagram copy button
*/
initDiagramCopyBtn() {
const stagingDOM = this.util.getStagingDOM()
this.util.forEach(document.querySelectorAll('.diagram-copy-btn'), ($btn) => {
const stagingDOM = getStagingDOM()
forEach(document.querySelectorAll('.diagram-copy-btn'), ($btn) => {
$btn.addEventListener('click', () => {
stagingDOM.stage($btn.parentElement.querySelector('template').content.cloneNode(true))
let code = stagingDOM.contentAsText();
try {
code = JSON.stringify(JSON.parse(code), null, 2);
} catch { }
this.util.copyText(code).then(() => {
copyText(code).then(() => {
$btn.toggleAttribute('data-copied', true);
setTimeout(() => {
$btn.toggleAttribute('data-copied', false);
@@ -632,10 +787,10 @@ class FixIt {
const $tocLiElements = $tocContainer.getElementsByTagName('li');
// Remove all active classes
this.util.forEach($tocLinkElements, ($tocLink) => {
forEach($tocLinkElements, ($tocLink) => {
$tocLink.classList.remove('active');
});
this.util.forEach($tocLiElements, ($tocLi) => {
forEach($tocLiElements, ($tocLi) => {
$tocLi.classList.remove('has-active');
});
@@ -674,10 +829,10 @@ class FixIt {
// TOC Drawer Button Visibility
const openButton = document.querySelector("#toc-drawer-button");
if (openButton) {
openButton.classList.toggle('d-none', !this.util.isTocStatic());
openButton.classList.toggle('d-none', !isTocStatic());
}
// TOC Static and TOC Dialog
if (this.util.isTocStatic()) {
if (isTocStatic()) {
const $tocContentStatic = document.getElementById('toc-content-static');
if ($tocCore.parentElement !== $tocContentStatic) {
$tocCore.parentElement.removeChild($tocCore);
@@ -704,7 +859,7 @@ class FixIt {
}
const $toc = document.getElementById('toc-auto');
$toc.style.visibility = 'visible';
this.util.animateCSS($toc, ['animate__fadeIn', 'animate__faster'], true);
animateCSS($toc, ['animate__fadeIn', 'animate__faster'], true);
const $postMeta = document.querySelector('.post-meta');
$toc.style.marginTop = `${$postMeta.offsetTop + $postMeta.clientHeight}px`;
@@ -733,7 +888,7 @@ class FixIt {
} else {
$tocContentAuto.classList.remove('animate__fadeIn');
}
this.util.animateCSS($tocContentAuto, animation, true, () => {
animateCSS($tocContentAuto, animation, true, () => {
$tocContentAuto.classList.contains('animate__fadeOut') && $tocContentAuto.classList.add('d-none');
});
$toc.classList.toggle('toc-hidden');
@@ -768,7 +923,7 @@ class FixIt {
$tocCore = $newTocCore;
}
// remove APlayer click event listener of the heading mark
this.util.forEach(document.querySelectorAll('.heading-mark'), ($headingMark) => {
forEach(document.querySelectorAll('.heading-mark'), ($headingMark) => {
const $newHeadingMark = $headingMark.cloneNode(true);
$headingMark.parentElement.replaceChild($newHeadingMark, $headingMark);
});
@@ -785,8 +940,8 @@ class FixIt {
this._echartsArr[i].dispose();
}
this._echartsArr = [];
const stagingDOM = this.util.getStagingDOM()
this.util.forEach(document.getElementsByClassName('echarts'), ($echarts) => {
const stagingDOM = getStagingDOM()
forEach(document.getElementsByClassName('echarts'), ($echarts) => {
const $dataEl = $echarts.nextElementSibling;
if ($dataEl.tagName !== 'TEMPLATE') return;
const chart = echarts.init($echarts, this.isDark ? 'dark' : 'light', { renderer: 'svg' });
@@ -818,7 +973,7 @@ class FixIt {
* @returns {Object|Promise} ECharts option or Promise
*/
const _getOption = new Function('fixit', 'chart',
this.util.isObjectLiteral(jsCodes) ? `return ${jsCodes}` : jsCodes
isObjectLiteral(jsCodes) ? `return ${jsCodes}` : jsCodes
);
if ($dataEl.dataset.async === 'true') {
return Promise.resolve(_getOption(this, chart)).then(option => {
@@ -852,7 +1007,7 @@ class FixIt {
mapboxgl.setRTLTextPlugin(this.config.mapbox.RTLTextPlugin);
this._mapboxArr = this._mapboxArr || [];
}
this.util.forEach(document.querySelectorAll('.mapbox:empty'), ($mapbox) => {
forEach(document.querySelectorAll('.mapbox:empty'), ($mapbox) => {
const { lng, lat, zoom, lightStyle, darkStyle, marked, markers, navigation, geolocate, scale, fullscreen } = JSON.parse($mapbox.dataset.options);
const mapbox = new mapboxgl.Map({
container: $mapbox,
@@ -901,7 +1056,7 @@ class FixIt {
this._mapboxArr.push(mapbox);
});
this._mapboxOnSwitchTheme = this._mapboxOnSwitchTheme || (() => {
this.util.forEach(this._mapboxArr, (mapbox) => {
forEach(this._mapboxArr, (mapbox) => {
const $mapbox = mapbox.getContainer();
const { lightStyle, darkStyle } = JSON.parse($mapbox.dataset.options);
mapbox.setStyle(this.isDark ? darkStyle : lightStyle);
@@ -928,7 +1083,7 @@ class FixIt {
acc[group].push(ele);
return acc;
}, {});
const stagingDOM = this.util.getStagingDOM()
const stagingDOM = getStagingDOM()
Object.values(groupMap).forEach((group) => {
const typeone = (i) => {
@@ -1002,7 +1157,7 @@ class FixIt {
$viewCommentsBtn.classList.remove('d-none');
// view comments button click event
$viewCommentsBtn.addEventListener('click', () => {
this.util.scrollIntoView('#comments');
scrollIntoView('#comments');
}, false);
}
this.config.comment.expired && document.querySelector('#comments').remove();
@@ -1119,7 +1274,7 @@ class FixIt {
let now = new Date();
let run = new Date(this.config.siteTime);
let $runTimes = document.querySelector('.run-times');
if (!this.util.isValidDate(run) || !$runTimes) {
if (!isValidDate(run) || !$runTimes) {
clearInterval(this.siteTime);
$runTimes && $runTimes.parentNode.remove();
return;
@@ -1197,7 +1352,7 @@ class FixIt {
initJsonViewer() {
if (!window.JsonViewerElement) return;
this._jsonViewerOnSwitchTheme = this._jsonViewerOnSwitchTheme || (() => {
this.util.forEach(document.getElementsByTagName('json-viewer'), ($el) => {
forEach(document.getElementsByTagName('json-viewer'), ($el) => {
$el.setAttribute('theme', this.isDark ? 'dark' : 'light');
});
});
@@ -1207,11 +1362,50 @@ class FixIt {
initTabEvents(target = document) {
target.addEventListener('tab-container-changed', () => {
this.fileTree.updateLineHeight(target);
FileTree.updateLineHeight(target);
window.FixItMermaid?.init?.();
}, false);
}
initFootnotes() {
const $footnoteRefs = document.querySelectorAll('#content sup[id^="fnref:"]');
const $footnotes = document.querySelector('.footnotes[role="doc-endnotes"]');
if (!$footnoteRefs.length || !$footnotes) return;
const footnoteMap = new Map();
$footnoteRefs.forEach(($ref) => {
if (this.config.tooltip) {
const $link = $ref.querySelector('a.footnote-ref');
if ($link) {
$link.addEventListener('click', (e) => {
e.preventDefault();
}, false);
}
}
const id = $ref.id.replace('fnref:', '');
const $footnoteContent = $footnotes.querySelector(`[id="fn:${id}"]`);
if ($footnoteContent) {
const $clonedContent = $footnoteContent.cloneNode(true);
const $backref = $clonedContent.querySelector('.footnote-backref');
if ($backref) {
$backref.remove();
}
footnoteMap.set($ref, $clonedContent);
}
});
footnoteMap.forEach(($content, $ref) => {
if ($ref.hasAttribute('title')) return;
$ref.setAttribute('title', $content.textContent.trim());
if (this.config.tooltip) {
window.CellTooltip.getOrCreateInstance($ref);
}
});
}
initTooltip() {
if (!this.config.tooltip) return;
window.CellTooltip.initAll('[data-ct-tooltip]');
}
/**
* Helper method to initialize content components
* @param {Element} target - The target element (optional, defaults to document)
@@ -1222,21 +1416,26 @@ class FixIt {
this.initDetails(target);
this.initLightGallery();
this.initCodeWrapper();
this.initCodeTabs();
this.initDiagramCopyBtn();
this.initEcharts();
this.initTypeit(target);
this.initMapbox();
this.initFootnotes();
this.initTooltip();
if (includeToc) {
this.fixTocScroll();
this.initToc();
this.initTocListener();
this.initTocDialog();
window.setTimeout(() => {
this.fixTocScroll();
this.initToc();
this.initTocListener();
this.initTocDialog();
}, 100);
}
this.initPangu();
this.initMathJax();
this.initJsonViewer();
this.initTabEvents(target);
this.fileTree.init(target);
FileTree.init(target);
window.FixItMermaid?.init?.();
window.FixItAPlayer?.init?.();
}
@@ -1249,7 +1448,7 @@ class FixIt {
_toggleEncryptedClass(container, show) {
const fromClass = show ? 'encrypted-hidden' : 'decrypted-shown';
const toClass = show ? 'decrypted-shown' : 'encrypted-hidden';
this.util.forEach(container.querySelectorAll(`.${fromClass}`), ($element) => {
forEach(container.querySelectorAll(`.${fromClass}`), ($element) => {
$element.classList.replace(fromClass, toClass);
});
}
@@ -1274,7 +1473,7 @@ class FixIt {
initAutoMark() {
if (!this.config.autoBookmark) return;
window.addEventListener('beforeunload', () => {
window.sessionStorage?.setItem(`fixit-bookmark/#${location.pathname}`, this.util.getScrollTop());
window.sessionStorage?.setItem(`fixit-bookmark/#${location.pathname}`, getScrollTop());
});
const scrollTop = Number(window.sessionStorage?.getItem(`fixit-bookmark/#${location.pathname}`));
// If the page opens with a specific hash, just jump out
@@ -1290,15 +1489,15 @@ class FixIt {
const $rewards = document.querySelectorAll('.post-reward [data-mode="fixed"]');
if (!$rewards.length) return;
// `fixed` mode only supports desktop
if (this.util.isMobile()) {
this.util.forEach($rewards, ($reward) => {
if (isMobile()) {
forEach($rewards, ($reward) => {
$reward.removeAttribute('data-mode');
});
return;
}
// Close post reward images exclude special id
const _closeRewardExclude = (id) => {
this.util.forEach($rewards, ($reward) => {
forEach($rewards, ($reward) => {
const $rewardInput = $reward.parentElement.querySelector('.reward-input');
if ($rewardInput.id !== id) {
$rewardInput.checked = false;
@@ -1306,7 +1505,7 @@ class FixIt {
});
};
// Add additional click event to reward buttons
this.util.forEach($rewards, ($reward) => {
forEach($rewards, ($reward) => {
$reward.previousElementSibling.addEventListener('click', function () {
_closeRewardExclude(this.getAttribute('for'));
}, false)
@@ -1339,7 +1538,7 @@ class FixIt {
$headers.push(document.getElementById('header-mobile'));
}
$backToTop?.addEventListener('click', () => {
this.util.scrollIntoView('body');
scrollIntoView('body');
});
window.addEventListener('scroll', (event) => {
if (this.disableScrollEvent) {
@@ -1347,17 +1546,17 @@ class FixIt {
return;
}
const $mask = document.getElementById('mask');
this.newScrollTop = this.util.getScrollTop();
this.newScrollTop = getScrollTop();
const scroll = this.newScrollTop - this.oldScrollTop;
// header animation
this.util.forEach($headers, ($header) => {
forEach($headers, ($header) => {
if (scroll > ACCURACY) {
$header.classList.remove('animate__fadeInDown');
this.util.animateCSS($header, ['animate__fadeOutUp'], true);
animateCSS($header, ['animate__fadeOutUp'], true);
$mask.click();
} else if (scroll < -ACCURACY) {
$header.classList.remove('animate__fadeOutUp');
this.util.animateCSS($header, ['animate__fadeInDown'], true);
animateCSS($header, ['animate__fadeInDown'], true);
$mask.click();
}
});
@@ -1370,10 +1569,10 @@ class FixIt {
if ($backToTop) {
if (scrollPercent > 1) {
$backToTop.classList.remove('d-none', 'animate__fadeOut');
this.util.animateCSS($backToTop, ['animate__fadeIn'], true);
animateCSS($backToTop, ['animate__fadeIn'], true);
} else {
$backToTop.classList.remove('animate__fadeIn');
this.util.animateCSS($backToTop, ['animate__fadeOut'], true, () => {
animateCSS($backToTop, ['animate__fadeOut'], true, () => {
$backToTop.classList.contains('animate__fadeOut') && $backToTop.classList.add('d-none');
});
}
@@ -1393,7 +1592,7 @@ class FixIt {
}
onResize() {
let resizeBefore = this.util.isMobile();
let resizeBefore = isMobile();
window.addEventListener('resize', () => {
if (!this._resizeTimeout) {
this._resizeTimeout = window.setTimeout(() => {
@@ -1404,10 +1603,10 @@ class FixIt {
this.initToc();
this.initSearch();
const isMobile = this.util.isMobile()
if (isMobile !== resizeBefore) {
const _isMobile = isMobile();
if (_isMobile !== resizeBefore) {
document.getElementById('mask').click();
resizeBefore = isMobile;
resizeBefore = _isMobile;
}
}, 100);
}
@@ -1425,14 +1624,60 @@ class FixIt {
}, false);
}
beforeprint() {
initPrint() {
window.addEventListener('beforeprint', () => {
this.util.forEach(document.querySelectorAll('.chroma'), ($el) => {
$el.classList.toggle('open', true)
});
const $content = document.getElementById('content');
const printConfig = this.config.print || {};
if (printConfig.expandAdmonition) {
forEach($content.querySelectorAll('.admonition'), ($el) => $el.classList.add('open'));
}
if (printConfig.expandCode) {
// revert code tabs to code blocks for better printing support
forEach($content.querySelectorAll('.code-tabs'), ($codeTabs) => {
// restore action buttons to the active tab's code-header before reverting
const $actions = $codeTabs.querySelector('.tabs-actions');
const $activeBlock = $codeTabs.querySelector('.code-block.active');
if ($actions && $activeBlock) {
const $codeHeader = $activeBlock.querySelector('.code-header');
if ($codeHeader) {
Array.from($actions.children).forEach(btn => $codeHeader.appendChild(btn));
}
}
const $codeBlocks = $codeTabs.querySelectorAll('.code-block');
$codeBlocks.forEach(($codeBlock) => {
delete $codeBlock.dataset.tabInit;
$codeTabs.parentElement.insertBefore($codeBlock, $codeTabs);
});
$codeTabs.parentElement.removeChild($codeTabs);
});
forEach($content.querySelectorAll('.code-block'), ($el) => {
// line wrapping
$el.classList.add('line-wrapping');
// expand all code blocks
$el.classList.remove('is-collapsed');
// expand code preview
if ($el.querySelector('.code-expand-btn')) {
$el.classList.add('is-expanded');
}
});
}
if (printConfig.expandDetails) {
forEach($content.querySelectorAll('details'), ($el) => $el.setAttribute('open', ''));
}
for (let event of this.beforeprintEventSet) {
event();
}
if (printConfig.expandFileTree) {
FileTree.expandAll($content);
}
}, false);
window.addEventListener('afterprint', () => {
this.initCodeTabs();
for (let event of this.afterprintEventSet) {
event();
}
}, false);
}
@@ -1458,7 +1703,6 @@ class FixIt {
this.initReward();
this.initPostChatUser();
// [todo] refactor async init toc
window.setTimeout(() => {
this.initComment();
if (!this.config.encryption?.all) {
@@ -1470,7 +1714,7 @@ class FixIt {
this.onScroll();
this.onResize();
this.onClickMask();
this.beforeprint();
this.initPrint();
}, 100);
} catch (err) {
console.error(err);
-146
View File
@@ -1,146 +0,0 @@
export default class Util {
forEach(elements, handler) {
elements = elements || [];
const promises = [];
for (let i = 0; i < elements.length; i++) {
const result = handler(elements[i], i);
if (result instanceof Promise) {
promises.push(result);
}
}
return Promise.all(promises);
}
getScrollTop() {
return (document.documentElement ?? document.body).scrollTop;
}
isMobile() {
return window.matchMedia('only screen and (max-width: 680px)').matches;
}
isTocStatic() {
return document.getElementById('toc-static').dataset.kept === 'true' || window.matchMedia('only screen and (max-width: 960px)').matches;
}
/**
* add animate to element
* @param {Element} element animate element
* @param {String|Array<String>} animation animation name
* @param {Boolean} reserved reserved animation
* @param {Function} callback remove callback
*/
animateCSS(element, animation, reserved, callback) {
!Array.isArray(animation) && (animation = [animation]);
element.classList.add('animate__animated', ...animation);
element.addEventListener('animationend', () => {
!reserved && element.classList.remove('animate__animated', ...animation);
typeof callback === 'function' && callback();
}, { once: true });
}
/**
* date validator
* @param {*} date may be date or not
* @returns {Boolean}
*/
isValidDate(date) {
return date instanceof Date && !isNaN(date.getTime());
}
/**
* scroll some element into view
* @param {String} selector element to scroll
*/
scrollIntoView(selector) {
const element = selector.startsWith('#')
? document.getElementById(selector.slice(1))
: document.querySelector(selector);
element?.scrollIntoView({
behavior: 'smooth'
});
}
/**
* get a hidden element for temporary use
* @returns {Object} { $el: Element, destroy: Function }
*/
getStagingDOM() {
const stagingElement = document.createElement('div')
stagingElement.style.display = 'none';
stagingElement.dataset.stagingId = Math.random().toString(36).slice(2);
document.body.appendChild(stagingElement);
return {
$el: stagingElement,
stage(dom) {
stagingElement.innerHTML = '';
stagingElement.appendChild(dom);
},
contentAsHtml() {
return stagingElement.innerHTML;
},
contentAsText() {
return stagingElement.innerText;
},
contentAsJson() {
return JSON.parse(stagingElement.innerHTML);
},
destroy() {
document.body.removeChild(stagingElement);
}
}
}
/**
* copy text to clipboard
* @param {String} text text to copy
* @returns {Promise} promise
*/
copyText(text) {
if (navigator.clipboard) {
this.copyText = (text) => navigator.clipboard.writeText(text);
return this.copyText(text);
}
this.copyText = (text) => new Promise((resolve, reject) => {
const input = document.createElement('input');
input.value = text;
document.body.appendChild(input);
input.select();
if (document.execCommand('copy')) {
document.body.removeChild(input);
resolve();
} else {
reject();
}
});
return this.copyText(text);
}
/**
* check if a string is a JS object string
* @example isObjectLiteral("{a:1,b:2}") // true
* @param {String} str string to check
* @returns {Boolean} whether the string is a JS object string
*/
isObjectLiteral(str) {
if (typeof str !== 'string') {
return false;
}
str = str.replace(/\s+/g, ' ').trim().replace(/;$/, '')
if (str.startsWith('{') && str.endsWith('}')) {
return true;
}
return false;
}
HTMLEscape(str) {
return str.replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
})[char]);
}
}
+141
View File
@@ -0,0 +1,141 @@
export function forEach(elements, handler) {
elements = elements || [];
const promises = [];
for (let i = 0; i < elements.length; i++) {
const result = handler(elements[i], i);
if (result instanceof Promise) {
promises.push(result);
}
}
return Promise.all(promises);
}
export function getScrollTop() {
return (document.documentElement ?? document.body).scrollTop;
}
export function isMobile() {
return window.matchMedia('only screen and (max-width: 680px)').matches;
}
export function isTocStatic() {
return document.getElementById('toc-static').dataset.kept === 'true' || window.matchMedia('only screen and (max-width: 960px)').matches;
}
/**
* add animate to element
* @param {Element} element animate element
* @param {String|Array<String>} animation animation name
* @param {Boolean} reserved reserved animation
* @param {Function} callback remove callback
*/
export function animateCSS(element, animation, reserved, callback) {
!Array.isArray(animation) && (animation = [animation]);
element.classList.add('animate__animated', ...animation);
element.addEventListener('animationend', () => {
!reserved && element.classList.remove('animate__animated', ...animation);
typeof callback === 'function' && callback();
}, { once: true });
}
/**
* date validator
* @param {*} date may be date or not
* @returns {Boolean}
*/
export function isValidDate(date) {
return date instanceof Date && !isNaN(date.getTime());
}
/**
* scroll some element into view
* @param {String} selector element to scroll
*/
export function scrollIntoView(selector) {
const element = selector.startsWith('#')
? document.getElementById(selector.slice(1))
: document.querySelector(selector);
element?.scrollIntoView({
behavior: 'smooth'
});
}
/**
* get a hidden element for temporary use
* @returns {Object} { $el: Element, destroy: Function }
*/
export function getStagingDOM() {
const stagingElement = document.createElement('div')
stagingElement.style.display = 'none';
stagingElement.dataset.stagingId = Math.random().toString(36).slice(2);
document.body.appendChild(stagingElement);
return {
$el: stagingElement,
stage(dom) {
stagingElement.innerHTML = '';
stagingElement.appendChild(dom);
},
contentAsHtml() {
return stagingElement.innerHTML;
},
contentAsText() {
return stagingElement.innerText;
},
contentAsJson() {
return JSON.parse(stagingElement.innerHTML);
},
destroy() {
document.body.removeChild(stagingElement);
}
}
}
/**
* create a copy text function with fallback
* @returns {Function} copy text function
*/
export function createCopyText() {
if (navigator.clipboard) {
return (text) => navigator.clipboard.writeText(text);
}
return (text) => new Promise((resolve, reject) => {
const input = document.createElement('input');
input.value = text;
document.body.appendChild(input);
input.select();
if (document.execCommand('copy')) {
document.body.removeChild(input);
resolve();
} else {
reject();
}
});
}
/**
* check if a string is a JS object string
* @example isObjectLiteral("{a:1,b:2}") // true
* @param {String} str string to check
* @returns {Boolean} whether the string is a JS object string
*/
export function isObjectLiteral(str) {
if (typeof str !== 'string') {
return false;
}
str = str.replace(/\s+/g, ' ').trim().replace(/;$/, '')
if (str.startsWith('{') && str.endsWith('}')) {
return true;
}
return false;
}
export function HTMLEscape(str) {
return str.replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
})[char]);
}
+1 -1
View File
@@ -24,7 +24,7 @@ sharer.js@0.5.1 https://github.com/ellisonleao/sharer.js
simple-icons@9.19.0 https://github.com/simple-icons/simple-icons
tab-container-element@4.8.2 https://github.com/github/tab-container-element
twemoji@14.0.2 https://github.com/twitter/twemoji
twikoo@1.6.44 https://github.com/imaegoo/twikoo
twikoo@1.7.3 https://github.com/imaegoo/twikoo
typeit@8.8.4 https://github.com/alexmacarthur/typeit
valine@1.5.2 https://github.com/xCss/Valine
waline@3.12.1 https://github.com/walinejs/waline
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+15
View File
@@ -453,6 +453,8 @@ summaryPlainify = false
# default is false for better performance, especially for single-language sites
# set to true if your site has multiple languages and you want to show all content
enableTranslationMerge = false
# FixIt 0.4.4 | NEW whether to enable tooltip replacement for elements with title attribute, such as footnote references
tooltip = true
# FixIt 0.2.14 | NEW FixIt will, by default, inject a theme meta tag in the HTML head on the home page only.
# You can turn it off, but we would really appreciate if you dont, as this is a good way to watch FixIt's popularity on the rise.
disableThemeInject = false
@@ -1142,6 +1144,8 @@ mode = "classic"
wrapperClass = ""
# the maximum number of lines to show in the code block preview
maxShownLines = 10
# whether to show shadow effect for code blocks, available values: ["always", "hover", "never"]
shadow = "never"
# whether to enable code copy button
copyable = true
# [classic] whether to enable code download button in the code block header
@@ -1174,6 +1178,17 @@ folderSlash = false
# list of file or folder names to ignore
ignoreList = []
# FixIt 0.4.5 | NEW Print config
[params.print]
# whether to expand all admonitions before printing
expand_admonition = true
# whether to expand all code blocks and code tabs before printing
expand_code = true
# whether to expand all details elements before printing
expand_details = true
# whether to expand all file trees before printing
expand_file_tree = false
# FixIt 0.3.12 | NEW Custom partials config
# Custom partials must be stored in the /layouts/_partials/ directory.
# Depends on open custom blocks https://fixit.lruihao.cn/references/blocks/
@@ -0,0 +1,64 @@
{{- $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 -}}
{{- $groupId := printf "tab-%s" (md5 (dict "Page" .Page | partial "function/id.html")) -}}
{{- $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" $groupId -}}
{{- 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 -}}
{{- with .Attributes.before_tabs -}}
{{- $commonAttrsStr = printf "%s, before_tabs=%q" $commonAttrsStr . -}}
{{- 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" "" -}}
{{- $labelName := cond (ne $.Attributes.before_tabs nil) $format (upper $format) -}}
{{- $block := printf "```%s {data-code-toggle=true, name=%q%s%s%s}\n%s\n```\n\n" $format $labelName $active $jsonViewer $commonAttrsStr $content -}}
{{- $renderText = add $renderText $block -}}
{{- end -}}
{{- $renderText | .Page.RenderString -}}
{{- else -}}
{{- partial "plugin/code-block-wrapper.html" . -}}
{{- end -}}
+16 -5
View File
@@ -156,7 +156,7 @@ FixIt theme assets partial
{{- end -}}
{{- /* Mapbox GL */ -}}
{{- if .HasShortcode "mapbox" -}}
{{- if .Store.Get "hasMapbox" -}}
{{- $source := $cdn.mapboxGLCSS | default "lib/mapbox-gl/mapbox-gl.css" -}}
{{- dict "Source" $source "Minify" true "Fingerprint" $fingerprint "Preload" true | dict "Page" . "Data" | partial "store/style.html" -}}
{{- $source = $cdn.mapboxGLJS | default "lib/mapbox-gl/mapbox-gl.js" -}}
@@ -166,7 +166,7 @@ FixIt theme assets partial
{{- end -}}
{{- /* Music */ -}}
{{- if (.HasShortcode "aplayer") | or (.HasShortcode "music") -}}
{{- if (.Store.Get "hasAplayer") | or (.Store.Get "hasMusic") -}}
{{- /* APlayer */ -}}
{{- $source := $cdn.aplayerCSS | default "lib/aplayer/APlayer.min.css" -}}
{{- dict "Source" $source "Fingerprint" $fingerprint "Preload" true | dict "Page" . "Data" | partial "store/style.html" -}}
@@ -175,14 +175,14 @@ FixIt theme assets partial
{{- $source := $cdn.aplayerJS | default "lib/aplayer/APlayer.min.js" -}}
{{- dict "Source" $source "Fingerprint" $fingerprint "Defer" true | dict "Page" . "Data" | partial "store/script.html" -}}
{{- if .HasShortcode "aplayer" -}}
{{- if .Store.Get "hasAplayer" -}}
{{- $options := dict "targetPath" "js/lib/aplayer.min.js" "minify" hugo.IsProduction -}}
{{- if not hugo.IsProduction -}}
{{- $options = dict "sourceMap" "external" | merge $options -}}
{{- end -}}
{{- dict "Source" (resources.Get "js/lib/aplayer.js") "Build" $options "Fingerprint" $fingerprint "Defer" true | dict "Page" . "Data" | partial "store/script.html" -}}
{{- end -}}
{{- if .HasShortcode "music" -}}
{{- if .Store.Get "hasMusic" -}}
{{- /* MetingJS */ -}}
{{- $source := $cdn.metingJS | default "lib/meting/Meting.min.js" -}}
{{- dict "Source" $source "Fingerprint" $fingerprint "Defer" true | dict "Page" . "Data" | partial "store/script.html" -}}
@@ -214,6 +214,14 @@ FixIt theme assets partial
{{- $config = dict "pangu" .Site.Params.pangu | merge $config -}}
{{- end -}}
{{- /* Cell Tooltip */ -}}
{{- if eq .Site.Params.tooltip true -}}
{{- /* [todo] 临时可行性验证,需要寻找一个更稳定的替代品(Floating UI */ -}}
{{- $source := $cdn.cellTooltipJS | default "lib/cell-tooltip/cell-tooltip.umd.js" -}}
{{- dict "Source" $source "Fingerprint" $fingerprint "Defer" true | dict "Page" . "Data" | partial "store/script.html" -}}
{{- $config = dict "tooltip" true | merge $config -}}
{{- end -}}
{{- /* Watermark */ -}}
{{- if eq .Site.Params.watermark.enable true -}}
{{- $source := $cdn.cellWatermarkJS | default "lib/cell-watermark/watermark.min.js" -}}
@@ -232,7 +240,7 @@ FixIt theme assets partial
{{- end -}}
{{- /* Content Decryption */ -}}
{{- $encryptPartial := .HasShortcode "fixit-encryptor" -}}
{{- $encryptPartial := .Store.Get "hasEncryptor" -}}
{{- if $params.password | or $encryptPartial -}}
{{- $cryptoCoreJS := $cdn.cryptoCoreJS | default "lib/crypto-js/core.js" -}}
{{- $cryptoEncBase64JS := $cdn.cryptoEncBase64JS | default "lib/crypto-js/enc-base64.js" -}}
@@ -300,6 +308,9 @@ FixIt theme assets partial
{{- end -}}
{{- end -}}
{{- /* Print */ -}}
{{- $config = partial "function/snake2camel.html" .Site.Params.print | dict "print" | merge $config -}}
{{- /* PostChat */ -}}
{{- partial "plugin/post-chat-ai.html" . -}}
+6 -6
View File
@@ -18,27 +18,27 @@
{{- end -}}
{{- if $config.linenostoggler -}}
{{- $lineNosIcon := dict "Class" "fa-solid fa-list-ol" | partial "plugin/icon.html" -}}
{{- $lineNosBtn = printf `<span class="line-nos-btn" aria-label="%s" role="button" title="%s">%s</span>` (T "assets.toggleLineNumbers") (T "assets.toggleLineNumbers") $lineNosIcon -}}
{{- $lineNosBtn = printf `<span class="action-btn line-nos-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (T "assets.toggleLineNumbers") (T "assets.toggleLineNumbers") $lineNosIcon -}}
{{- end -}}
{{- if $config.linewraptoggler -}}
{{- $lineWrapIcon := dict "Class" "fa-solid fa-right-left" | partial "plugin/icon.html" -}}
{{- $lineWrapBtn = printf `<span class="line-wrap-btn" aria-label="%s" role="button" title="%s">%s</span>` (T "assets.toggleLineWrap") (T "assets.toggleLineWrap") $lineWrapIcon -}}
{{- $lineWrapBtn = printf `<span class="action-btn line-wrap-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (T "assets.toggleLineWrap") (T "assets.toggleLineWrap") $lineWrapIcon -}}
{{- end -}}
{{- if $config.editable -}}
{{- $editIcon := dict "Class" "fa-solid fa-pen-to-square" | partial "plugin/icon.html" -}}
{{- $editBtn = printf `<span class="edit-btn" aria-label="%s" role="button" title="%s">%s</span>` (T "assets.toggleCodeEditable") (T "assets.toggleCodeEditable") $editIcon -}}
{{- $editBtn = printf `<span class="action-btn edit-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (T "assets.toggleCodeEditable") (T "assets.toggleCodeEditable") $editIcon -}}
{{- end -}}
{{- if $config.copyable -}}
{{- $copyIcon := dict "Class" "fa-regular fa-clone" | partial "plugin/icon.html" -}}
{{- $copyBtn = printf `<span class="copy-btn" aria-label="%s" role="button" title="%s">%s</span>` (T "assets.copyToClipboard") (T "assets.copyToClipboard") $copyIcon -}}
{{- $copyBtn = printf `<span class="action-btn copy-btn" aria-label="%s" role="button" title="%s" data-copied-text="%s" data-ct-tooltip>%s</span>` (T "assets.copyToClipboard") (T "assets.copyToClipboard") (T "assets.copiedText") $copyIcon -}}
{{- end -}}
{{- if $config.downloadable -}}
{{- $downloadIcon := dict "Class" "fa-solid fa-download" | partial "plugin/icon.html" -}}
{{- $downloadBtn = printf `<span class="download-btn" aria-label="%s" role="button" title="%s">%s</span>` (T "assets.downloadCode") (T "assets.downloadCode") $downloadIcon -}}
{{- $downloadBtn = printf `<span class="action-btn download-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (T "assets.downloadCode") (T "assets.downloadCode") $downloadIcon -}}
{{- end -}}
{{- if $config.fullscreen -}}
{{- $fullscreenIcon := dict "Class" "fa-solid fa-expand" | partial "plugin/icon.html" -}}
{{- $fullscreenBtn = printf `<span class="fullscreen-btn" aria-label="%s" role="button" title="%s">%s</span>` (T "assets.toggleCodeFullscreen") (T "assets.toggleCodeFullscreen") $fullscreenIcon -}}
{{- $fullscreenBtn = printf `<span class="action-btn fullscreen-btn" aria-label="%s" role="button" title="%s" data-ct-tooltip>%s</span>` (T "assets.toggleCodeFullscreen") (T "assets.toggleCodeFullscreen") $fullscreenIcon -}}
{{- end -}}
{{- with $config.name -}}
{{- $titleEl = replace $titleEl `<span class="code-title">` (printf `<span class="code-title" data-name="%s">` .) -}}
@@ -0,0 +1,91 @@
{{- /*
Convert snake_case config keys to camelCase recursively.
This is mainly used to avoid problems caused by Hugo's case-insensitive config key parsing.
Keep config keys in snake_case, then convert them here for stable access in templates and JS.
@param {map} . - The input map/object with snake_case keys
@return {map} A new map with camelCase keys, or the input if it's not a map
@example
// Simple Map conversion
Input: dict "code_block" true "max_shown_lines" 10 "line_nos" false
Output: dict "codeBlock" true "maxShownLines" 10 "lineNos" false
@example
// Nested Map conversion
Input: dict "code_block" (dict "enable_wrapper" true)
Output: dict "codeBlock" (dict "enableWrapper" true)
@example
// Array of Maps conversion
Input: slice (dict "user_name" "John") (dict "user_age" 30)
Output: slice (dict "userName" "John") (dict "userAge" 30)
@example
// Practical usage in templates
{{- $siteParams := partial "function/snake2camel.html" .Site.Params }}
// Now access parameters with camelCase: $siteParams.codeBlock.enableWrapper
// [todo] refactor all config to use snake_case in Hugo config files, and camelCase in templates and JS (v1.0 breaking change)
*/ -}}
{{- $input := . -}}
{{- $output := dict -}}
{{- if reflect.IsMap $input -}}
{{- /* Process map: iterate through all key-value pairs */ -}}
{{- range $key, $value := $input -}}
{{- /*
Convert key from snake_case to camelCase
Example: "max_shown_lines" -> "maxShownLines"
Strategy: split by "_", capitalize first letter of each part (except first), then concatenate
*/ -}}
{{- $parts := split $key "_" -}}
{{- $newKey := index $parts 0 -}}
{{- range $i, $part := $parts -}}
{{- if gt $i 0 -}}
{{- /* Capitalize first letter, preserve the rest */ -}}
{{- $first := upper (substr $part 0 1) -}}
{{- $rest := substr $part 1 -}}
{{- $newKey = printf "%s%s%s" $newKey $first $rest -}}
{{- end -}}
{{- end -}}
{{- /*
Recursively process value based on its type
- For nested maps: recursively convert all nested keys
- For arrays: process each map element in the array
- For scalar values: keep as-is
*/ -}}
{{- $newValue := $value -}}
{{- if reflect.IsMap $value -}}
{{- $newValue = partial "function/snake2camel.html" $value -}}
{{- else if reflect.IsSlice $value -}}
{{- $newValue = slice -}}
{{- range $item := $value -}}
{{- if reflect.IsMap $item -}}
{{- $newValue = $newValue | append (partial "function/snake2camel.html" $item) -}}
{{- else -}}
{{- $newValue = $newValue | append $item -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- /* Add the converted key-value pair to output */ -}}
{{- $output = merge $output (dict $newKey $newValue) -}}
{{- end -}}
{{- return $output -}}
{{- else if reflect.IsSlice $input -}}
{{- /* Process array: recursively convert each map element */ -}}
{{- $output := slice -}}
{{- range $item := $input -}}
{{- if reflect.IsMap $item -}}
{{- $output = $output | append (partial "function/snake2camel.html" $item) -}}
{{- else -}}
{{- $output = $output | append $item -}}
{{- end -}}
{{- end -}}
{{- return $output -}}
{{- else -}}
{{- /* Return scalar values unchanged */ -}}
{{- return $input -}}
{{- end -}}
+1 -1
View File
@@ -1,4 +1,4 @@
{{- hugo.Store.Set "version" "v0.4.3" -}}
{{- hugo.Store.Set "version" "v0.4.5" -}}
{{- .Store.Set "this" dict -}}
{{- partial "init/detection-env.html" . -}}
+7 -7
View File
@@ -1,12 +1,12 @@
{{- /*
* The extended syntax of alert is compatible with Obsidian and FixIt admonition shortcode.
* @param {String} .Text the content of the admonition box
* @param {String} [.Type] the type of the admonition box
* @param {String} [.Title] the title of the admonition box
* @param {Boolean} [.Open] whether the admonition box is open, default is true
* @param {Boolean} [.Foldable] whether the admonition box is foldable, default is true
* For custom admonitions, see https://fixit.lruihao.cn/documentation/content-management/shortcodes/extended/admonition/#customize-admonitions
The extended syntax of alert is compatible with Obsidian and FixIt admonition shortcode.
@param {String} .Text the content of the admonition box
@param {String} [.Type] the type of the admonition box
@param {String} [.Title] the title of the admonition box
@param {Boolean} [.Open] whether the admonition box is open, default is true
@param {Boolean} [.Foldable] whether the admonition box is foldable, default is true
For custom admonitions, see https://fixit.lruihao.cn/documentation/content-management/shortcodes/extended/admonition/#customize-admonitions
*/ -}}
{{- $iconMap := dict
+5 -5
View File
@@ -1,9 +1,9 @@
{{- /*
* The basic syntax of alert is compatible with GitHub, Obsidian, and Typora.
* @param {String} .Type the type of the alert box
* @param {String} .Text the content of the alert box
* @param {Map} [.Attributes] the attributes of the alert box
* @example {{- dict "Text" .Text "Type" .AlertType "Attributes" .Attributes | partial "plugin/alert.html" -}}
The basic syntax of alert is compatible with GitHub, Obsidian, and Typora.
@param {String} .Type the type of the alert box
@param {String} .Text the content of the alert box
@param {Map} [.Attributes] the attributes of the alert box
@example {{- dict "Text" .Text "Type" .AlertType "Attributes" .Attributes | partial "plugin/alert.html" -}}
*/ -}}
{{- $iconMap := dict
@@ -58,11 +58,33 @@
(printf `class="%s" data-copyable="%v"` $class $config.copyable)
-}}
{{- end -}}
{{- if ne $config.shadow "never" -}}
{{- $wrapper =
replace $wrapper
(printf `class="%s"` $class)
(printf `class="%s" data-shadow="%s"` $class $config.shadow)
-}}
{{- 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 -}}
{{- else -}}
{{- $targetClass = add $targetClass " d-none" -}}
{{- end -}}
{{- $wrapper =
replace $wrapper
(printf `class="%s"` $class)
(printf `class="%s" data-tab-title="%s"` $class $tabTitle)
-}}
{{- end -}}
{{- if ne .Options.linenostart nil -}}
{{- $wrapper =
replace $wrapper
`class="code-wrapper"`
(printf `class="code-wrapper" data-line-start="%v"` .)
(printf `class="code-wrapper" data-line-start="%v"` .Options.linenostart)
-}}
{{- end -}}
{{- $wrapperStyle := printf "--fi-max-shown-lines:%v;--fi-line-digit:%v;--fi-line-start:%v;" $maxShownLines $lineNosDigit $lineNoStart -}}
@@ -16,11 +16,11 @@
<input type="password" id="{{ $id }}" class="fixit-decryptor-input" placeholder="🔑 {{ $message }}" />
</label>
<button class="fixit-decryptor-btn">
{{- dict "Class" "fa-solid fa-unlock" | partial "plugin/icon.html" }} {{ T "single.enterBtn" -}}
{{- dict "Class" "fa-solid fa-unlock" | partial "plugin/icon.html" }}{{ T "single.enterBtn" -}}
</button>
{{- if not .IsPartial -}}
<button class="fixit-encryptor-btn">
{{- dict "Class" "fa-solid fa-lock" | partial "plugin/icon.html" }} {{ T "single.encryptyAgain" -}}
{{- dict "Class" "fa-solid fa-lock" | partial "plugin/icon.html" }}{{ T "single.encryptyAgain" -}}
</button>
{{- end -}}
</div>
@@ -9,10 +9,10 @@
<input type="password" id="fixit-decryptor-input" class="fixit-decryptor-input d-none" placeholder="🔑 {{ $msg }}" />
</label>
<button class="fixit-decryptor-btn d-none">
{{- dict "Class" "fa-solid fa-unlock" | partial "plugin/icon.html" }} {{ T "single.enterBtn" -}}
{{- dict "Class" "fa-solid fa-unlock" | partial "plugin/icon.html" }}{{ T "single.enterBtn" -}}
</button>
<button class="fixit-encryptor-btn d-none">
{{- dict "Class" "fa-solid fa-lock" | partial "plugin/icon.html" }} {{ T "single.encryptyAgain" -}}
{{- dict "Class" "fa-solid fa-lock" | partial "plugin/icon.html" }}{{ T "single.encryptyAgain" -}}
</button>
</div>
{{- end -}}
+1
View File
@@ -18,6 +18,7 @@
{{- $audio = replace $audio "}" "}," | printf "[%s]" -}}
{{- $audio = replace $audio ",]" "]" -}}
<div class="aplayer-shortcode" data-audio="{{ $audio }}" data-options="{{ $options }}"></div>
{{- .Page.Store.Set "hasAplayer" true -}}
{{- else -}}
{{- errorf "Only named params is supported: %s" .Position -}}
{{- end -}}
+35 -36
View File
@@ -1,54 +1,53 @@
{{- /*
Renders an HTML details element.
Renders an HTML details element.
@param {string} [class] The value of the element's class attribute.
@param {string} [name] The value of the element's name attribute.
@param {string} [summary] The content of the child summary element.
@param {string} [title] The value of the element's title attribute.
@param {bool} [open=false] Whether to initially display the content of the details element.
@param {string} [class] The value of the element's class attribute.
@param {string} [name] The value of the element's name attribute.
@param {string} [summary] The content of the child summary element.
@param {string} [title] The value of the element's title attribute.
@param {bool} [open=false] Whether to initially display the content of the details element.
@reference https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details
@reference https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details
@examples
@examples
{{< details >}}
A basic collapsible section.
{{< /details >}}
{{< details >}}
A basic collapsible section.
{{< /details >}}
{{< details summary="Custom Summary Text" >}}
Showing custom `summary` text.
{{< /details >}}
{{< details summary="Custom Summary Text" >}}
Showing custom `summary` text.
{{< /details >}}
{{< details summary="Open Details" open=true >}}
Contents displayed initially by using `open`.
{{< /details >}}
{{< details summary="Open Details" open=true >}}
Contents displayed initially by using `open`.
{{< /details >}}
{{< details summary="Styled Content" class="my-custom-class" >}}
Content can be styled with CSS by specifying a `class`.
{{< details summary="Styled Content" class="my-custom-class" >}}
Content can be styled with CSS by specifying a `class`.
Target details element:
Target details element:
```css
details.my-custom-class { }
```
```css
details.my-custom-class { }
```
Target summary element:
Target summary element:
```css
details.my-custom-class > summary > * { }
```
```css
details.my-custom-class > summary > * { }
```
Target inner content:
Target inner content:
```css
details.my-custom-class > :not(summary) { }
```
{{< /details >}}
{{< details summary="Grouped Details" name="my-details" >}}
Specifying a `name` allows elements to be connected, with only one able to be open at a time.
{{< /details >}}
```css
details.my-custom-class > :not(summary) { }
```
{{< /details >}}
{{< details summary="Grouped Details" name="my-details" >}}
Specifying a `name` allows elements to be connected, with only one able to be open at a time.
{{< /details >}}
*/}}
{{- /* Get arguments. */}}
+1
View File
@@ -6,6 +6,7 @@
{{- $content := .Inner | .Page.RenderString -}}
{{- /* Content Encryption */ -}}
{{- dict "Content" $content "Password" $password "Message" $message "IsPartial" true "Page" .Page | partial "plugin/fixit-encryptor.html" -}}
{{- .Page.Store.Set "hasEncryptor" true -}}
{{- else -}}
{{- .Inner -}}
{{- end -}}
+1 -1
View File
@@ -33,4 +33,4 @@
{{- $attrs := printf `style="width: %v; height: %v;"` $width $height -}}
<div class="mapbox" data-options="{{ $options | jsonify }}" {{ $attrs | safeHTMLAttr }}></div>
{{- /* EOF */ -}}
{{- .Page.Store.Set "hasMapbox" true -}}
+1
View File
@@ -47,3 +47,4 @@
{{- else -}}
<meting-js server="{{ .Get 0 }}" type="{{ .Get 1 }}" id="{{ .Get 2 }}" theme="{{ $theme }}"></meting-js>
{{- end -}}
{{- .Page.Store.Set "hasMusic" true -}}
+7 -7
View File
@@ -1,9 +1,9 @@
{
"name": "@hugo-fixit/core",
"type": "module",
"version": "0.4.3",
"version": "0.4.5",
"private": true,
"packageManager": "pnpm@10.22.0",
"packageManager": "pnpm@10.32.1",
"description": "Hugo FixIt core theme component source files",
"author": {
"name": "Lruihao",
@@ -42,12 +42,12 @@
"prepare": "simple-git-hooks"
},
"devDependencies": {
"@antfu/eslint-config": "^7.6.1",
"@types/node": "^24.11.0",
"@antfu/eslint-config": "^7.7.3",
"@types/node": "^25.5.0",
"auto-changelog-plus": "^1.2.3",
"eslint": "^9.39.2",
"lint-staged": "^16.3.0",
"serve": "^14.2.5",
"eslint": "^10.0.3",
"lint-staged": "^16.4.0",
"serve": "^14.2.6",
"simple-git-hooks": "^2.13.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
+408 -424
View File
File diff suppressed because it is too large Load Diff