mirror of
https://github.com/saicaca/fuwari.git
synced 2026-01-10 14:22:51 +01:00
feat: introduce Expressive Code for code block rendering (#476)
* feat: improve code block feature with Expressive Code * docs: add example Markdown file for visual review * feat: improve styles of Expressive Code * fix: errors from the merging * chore: formatting * chore: update config * chore: update dependencies * fix: fix build error * fix: minor fixes * fix: style tweaks * docs: update example post * fix: isolate copy button timeouts to avoid cross-button interference --------- Co-authored-by: Hasenpfote <Hasenpfote36@gmail.com>
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import svelte from "@astrojs/svelte";
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections";
|
||||
import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
|
||||
import swup from "@swup/astro";
|
||||
import expressiveCode from "astro-expressive-code";
|
||||
import icon from "astro-icon";
|
||||
import { defineConfig } from "astro/config";
|
||||
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||
@@ -12,11 +15,14 @@ import remarkDirective from "remark-directive"; /* Handle directives */
|
||||
import remarkGithubAdmonitionsToDirectives from "remark-github-admonitions-to-directives";
|
||||
import remarkMath from "remark-math";
|
||||
import remarkSectionize from "remark-sectionize";
|
||||
import { expressiveCodeConfig } from "./src/config.ts";
|
||||
import { pluginLanguageBadge } from "./src/plugins/expressive-code/language-badge.ts";
|
||||
import { AdmonitionComponent } from "./src/plugins/rehype-component-admonition.mjs";
|
||||
import { GithubCardComponent } from "./src/plugins/rehype-component-github-card.mjs";
|
||||
import { parseDirectiveNode } from "./src/plugins/remark-directive-rehype.js";
|
||||
import { remarkExcerpt } from "./src/plugins/remark-excerpt.js";
|
||||
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs";
|
||||
import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-copy-button.js";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
@@ -49,6 +55,50 @@ export default defineConfig({
|
||||
"fa6-solid": ["*"],
|
||||
},
|
||||
}),
|
||||
expressiveCode({
|
||||
themes: [expressiveCodeConfig.theme, expressiveCodeConfig.theme],
|
||||
plugins: [
|
||||
pluginCollapsibleSections(),
|
||||
pluginLineNumbers(),
|
||||
pluginLanguageBadge(),
|
||||
pluginCustomCopyButton()
|
||||
],
|
||||
defaultProps: {
|
||||
wrap: true,
|
||||
overridesByLang: {
|
||||
'shellsession': {
|
||||
showLineNumbers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
styleOverrides: {
|
||||
codeBackground: "var(--codeblock-bg)",
|
||||
borderRadius: "0.75rem",
|
||||
borderColor: "none",
|
||||
codeFontSize: "0.875rem",
|
||||
codeFontFamily: "'JetBrains Mono Variable', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||
codeLineHeight: "1.5rem",
|
||||
frames: {
|
||||
editorBackground: "var(--codeblock-bg)",
|
||||
terminalBackground: "var(--codeblock-bg)",
|
||||
terminalTitlebarBackground: "var(--codeblock-topbar-bg)",
|
||||
editorTabBarBackground: "var(--codeblock-topbar-bg)",
|
||||
editorActiveTabBackground: "none",
|
||||
editorActiveTabIndicatorBottomColor: "var(--primary)",
|
||||
editorActiveTabIndicatorTopColor: "none",
|
||||
editorTabBarBorderBottomColor: "var(--codeblock-topbar-bg)",
|
||||
terminalTitlebarBorderBottomColor: "none"
|
||||
},
|
||||
textMarkers: {
|
||||
delHue: 0,
|
||||
insHue: 180,
|
||||
markHue: 250
|
||||
}
|
||||
},
|
||||
frames: {
|
||||
showCopyToClipboardButton: false,
|
||||
}
|
||||
}),
|
||||
svelte(),
|
||||
sitemap(),
|
||||
],
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
"@astrojs/sitemap": "^3.4.0",
|
||||
"@astrojs/svelte": "7.1.0",
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
"@expressive-code/core": "^0.41.2",
|
||||
"@expressive-code/plugin-collapsible-sections": "^0.41.2",
|
||||
"@expressive-code/plugin-line-numbers": "^0.41.2",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.5",
|
||||
"@fontsource/roboto": "^5.2.5",
|
||||
"@iconify-json/fa6-brands": "^1.2.5",
|
||||
@@ -31,6 +34,7 @@
|
||||
"@swup/astro": "^1.6.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"astro": "5.8.1",
|
||||
"astro-expressive-code": "^0.41.2",
|
||||
"astro-icon": "^1.1.5",
|
||||
"hastscript": "^9.0.1",
|
||||
"katex": "^0.16.22",
|
||||
@@ -61,6 +65,7 @@
|
||||
"@astrojs/ts-plugin": "^1.10.4",
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@rollup/plugin-yaml": "^4.1.2",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
|
||||
1361
pnpm-lock.yaml
generated
1361
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -14,51 +14,30 @@ const className = Astro.props.class;
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const observer = new MutationObserver(addPreCopyButton);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
document.addEventListener("click", function (e: MouseEvent) {
|
||||
const target = e.target as Element | null;
|
||||
if (target && target.classList.contains("copy-btn")) {
|
||||
const preEle = target.closest("pre");
|
||||
const codeEle = preEle?.querySelector("code");
|
||||
const code = Array.from(codeEle?.querySelectorAll(".code:not(summary *)") ?? [])
|
||||
.map(el => el.textContent)
|
||||
.map(t => t === "\n" ? "" : t)
|
||||
.join("\n");
|
||||
navigator.clipboard.writeText(code);
|
||||
|
||||
function addPreCopyButton() {
|
||||
observer.disconnect();
|
||||
|
||||
let codeBlocks = Array.from(document.querySelectorAll("pre"));
|
||||
|
||||
for (let codeBlock of codeBlocks) {
|
||||
if (codeBlock.parentElement?.nodeName === "DIV" && codeBlock.parentElement?.classList.contains("code-block")) continue
|
||||
|
||||
let wrapper = document.createElement("div");
|
||||
wrapper.className = "relative code-block";
|
||||
|
||||
let copyButton = document.createElement("button");
|
||||
copyButton.className = "copy-btn btn-regular-dark absolute active:scale-90 h-8 w-8 top-2 right-2 opacity-75 text-sm p-1.5 rounded-lg transition-all ease-in-out";
|
||||
|
||||
codeBlock.setAttribute("tabindex", "0");
|
||||
if (codeBlock.parentNode) {
|
||||
codeBlock.parentNode.insertBefore(wrapper, codeBlock);
|
||||
const timeoutId = target.getAttribute("data-timeout-id");
|
||||
if (timeoutId) {
|
||||
clearTimeout(parseInt(timeoutId));
|
||||
}
|
||||
|
||||
let copyIcon = `<svg class="copy-btn-icon copy-icon" xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px"><path d="M368.37-237.37q-34.48 0-58.74-24.26-24.26-24.26-24.26-58.74v-474.26q0-34.48 24.26-58.74 24.26-24.26 58.74-24.26h378.26q34.48 0 58.74 24.26 24.26 24.26 24.26 58.74v474.26q0 34.48-24.26 58.74-24.26 24.26-58.74 24.26H368.37Zm0-83h378.26v-474.26H368.37v474.26Zm-155 238q-34.48 0-58.74-24.26-24.26-24.26-24.26-58.74v-515.76q0-17.45 11.96-29.48 11.97-12.02 29.33-12.02t29.54 12.02q12.17 12.03 12.17 29.48v515.76h419.76q17.45 0 29.48 11.96 12.02 11.97 12.02 29.33t-12.02 29.54q-12.03 12.17-29.48 12.17H213.37Zm155-238v-474.26 474.26Z"/></svg>`
|
||||
let successIcon = `<svg class="copy-btn-icon success-icon" xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px"><path d="m389-377.13 294.7-294.7q12.58-12.67 29.52-12.67 16.93 0 29.61 12.67 12.67 12.68 12.67 29.53 0 16.86-12.28 29.14L419.07-288.41q-12.59 12.67-29.52 12.67-16.94 0-29.62-12.67L217.41-430.93q-12.67-12.68-12.79-29.45-.12-16.77 12.55-29.45 12.68-12.67 29.62-12.67 16.93 0 29.28 12.67L389-377.13Z"/></svg>`
|
||||
copyButton.innerHTML = `<div>${copyIcon} ${successIcon}</div>
|
||||
`
|
||||
target.classList.add("success");
|
||||
|
||||
wrapper.appendChild(codeBlock);
|
||||
wrapper.appendChild(copyButton);
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
copyButton.addEventListener("click", async () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
let text = codeBlock?.querySelector("code")?.innerText;
|
||||
if (text === undefined) return;
|
||||
await navigator.clipboard.writeText(text);
|
||||
copyButton.classList.add("success");
|
||||
timeout = setTimeout(() => {
|
||||
copyButton.classList.remove("success");
|
||||
// 设置新的timeout并保存ID到按钮的自定义属性中
|
||||
const newTimeoutId = setTimeout(() => {
|
||||
target.classList.remove("success");
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
target.setAttribute("data-timeout-id", newTimeoutId.toString());
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
ExpressiveCodeConfig,
|
||||
LicenseConfig,
|
||||
NavBarConfig,
|
||||
ProfileConfig,
|
||||
@@ -81,3 +82,9 @@ export const licenseConfig: LicenseConfig = {
|
||||
name: "CC BY-NC-SA 4.0",
|
||||
url: "https://creativecommons.org/licenses/by-nc-sa/4.0/",
|
||||
};
|
||||
|
||||
export const expressiveCodeConfig: ExpressiveCodeConfig = {
|
||||
// Note: Some styles (such as background color) are being overridden, see the astro.config.mjs file.
|
||||
// Please select a dark theme, as this blog theme currently only supports dark background color
|
||||
theme: "github-dark",
|
||||
};
|
||||
|
||||
311
src/content/posts/expressive-code.md
Normal file
311
src/content/posts/expressive-code.md
Normal file
@@ -0,0 +1,311 @@
|
||||
---
|
||||
title: Expressive Code Example
|
||||
published: 2024-04-10
|
||||
description: How code blocks look in Markdown using Expressive Code.
|
||||
tags: [Markdown, Blogging, Demo]
|
||||
category: Examples
|
||||
draft: false
|
||||
---
|
||||
|
||||
Here, we'll explore how code blocks look using [Expressive Code](https://expressive-code.com/). The provided examples are based on the official documentation, which you can refer to for further details.
|
||||
|
||||
## Expressive Code
|
||||
|
||||
### Syntax Highlighting
|
||||
|
||||
[Syntax Highlighting](https://expressive-code.com/key-features/syntax-highlighting/)
|
||||
|
||||
#### Regular syntax highlighting
|
||||
|
||||
```js
|
||||
console.log('This code is syntax highlighted!')
|
||||
```
|
||||
|
||||
#### Rendering ANSI escape sequences
|
||||
|
||||
```ansi
|
||||
ANSI colors:
|
||||
- Regular: [31mRed[0m [32mGreen[0m [33mYellow[0m [34mBlue[0m [35mMagenta[0m [36mCyan[0m
|
||||
- Bold: [1;31mRed[0m [1;32mGreen[0m [1;33mYellow[0m [1;34mBlue[0m [1;35mMagenta[0m [1;36mCyan[0m
|
||||
- Dimmed: [2;31mRed[0m [2;32mGreen[0m [2;33mYellow[0m [2;34mBlue[0m [2;35mMagenta[0m [2;36mCyan[0m
|
||||
|
||||
256 colors (showing colors 160-177):
|
||||
[38;5;160m160 [38;5;161m161 [38;5;162m162 [38;5;163m163 [38;5;164m164 [38;5;165m165[0m
|
||||
[38;5;166m166 [38;5;167m167 [38;5;168m168 [38;5;169m169 [38;5;170m170 [38;5;171m171[0m
|
||||
[38;5;172m172 [38;5;173m173 [38;5;174m174 [38;5;175m175 [38;5;176m176 [38;5;177m177[0m
|
||||
|
||||
Full RGB colors:
|
||||
[38;2;34;139;34mForestGreen - RGB(34, 139, 34)[0m
|
||||
|
||||
Text formatting: [1mBold[0m [2mDimmed[0m [3mItalic[0m [4mUnderline[0m
|
||||
```
|
||||
|
||||
### Editor & Terminal Frames
|
||||
|
||||
[Editor & Terminal Frames](https://expressive-code.com/key-features/frames/)
|
||||
|
||||
#### Code editor frames
|
||||
|
||||
```js title="my-test-file.js"
|
||||
console.log('Title attribute example')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```html
|
||||
<!-- src/content/index.html -->
|
||||
<div>File name comment example</div>
|
||||
```
|
||||
|
||||
#### Terminal frames
|
||||
|
||||
```bash
|
||||
echo "This terminal frame has no title"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```powershell title="PowerShell terminal example"
|
||||
Write-Output "This one has a title!"
|
||||
```
|
||||
|
||||
#### Overriding frame types
|
||||
|
||||
```sh frame="none"
|
||||
echo "Look ma, no frame!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```ps frame="code" title="PowerShell Profile.ps1"
|
||||
# Without overriding, this would be a terminal frame
|
||||
function Watch-Tail { Get-Content -Tail 20 -Wait $args }
|
||||
New-Alias tail Watch-Tail
|
||||
```
|
||||
|
||||
### Text & Line Markers
|
||||
|
||||
[Text & Line Markers](https://expressive-code.com/key-features/text-markers/)
|
||||
|
||||
#### Marking full lines & line ranges
|
||||
|
||||
```js {1, 4, 7-8}
|
||||
// Line 1 - targeted by line number
|
||||
// Line 2
|
||||
// Line 3
|
||||
// Line 4 - targeted by line number
|
||||
// Line 5
|
||||
// Line 6
|
||||
// Line 7 - targeted by range "7-8"
|
||||
// Line 8 - targeted by range "7-8"
|
||||
```
|
||||
|
||||
#### Selecting line marker types (mark, ins, del)
|
||||
|
||||
```js title="line-markers.js" del={2} ins={3-4} {6}
|
||||
function demo() {
|
||||
console.log('this line is marked as deleted')
|
||||
// This line and the next one are marked as inserted
|
||||
console.log('this is the second inserted line')
|
||||
|
||||
return 'this line uses the neutral default marker type'
|
||||
}
|
||||
```
|
||||
|
||||
#### Adding labels to line markers
|
||||
|
||||
```jsx {"1":5} del={"2":7-8} ins={"3":10-12}
|
||||
// labeled-line-markers.jsx
|
||||
<button
|
||||
role="button"
|
||||
{...props}
|
||||
value={value}
|
||||
className={buttonClassName}
|
||||
disabled={disabled}
|
||||
active={active}
|
||||
>
|
||||
{children &&
|
||||
!active &&
|
||||
(typeof children === 'string' ? <span>{children}</span> : children)}
|
||||
</button>
|
||||
```
|
||||
|
||||
#### Adding long labels on their own lines
|
||||
|
||||
```jsx {"1. Provide the value prop here:":5-6} del={"2. Remove the disabled and active states:":8-10} ins={"3. Add this to render the children inside the button:":12-15}
|
||||
// labeled-line-markers.jsx
|
||||
<button
|
||||
role="button"
|
||||
{...props}
|
||||
|
||||
value={value}
|
||||
className={buttonClassName}
|
||||
|
||||
disabled={disabled}
|
||||
active={active}
|
||||
>
|
||||
|
||||
{children &&
|
||||
!active &&
|
||||
(typeof children === 'string' ? <span>{children}</span> : children)}
|
||||
</button>
|
||||
```
|
||||
|
||||
#### Using diff-like syntax
|
||||
|
||||
```diff
|
||||
+this line will be marked as inserted
|
||||
-this line will be marked as deleted
|
||||
this is a regular line
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```diff
|
||||
--- a/README.md
|
||||
+++ b/README.md
|
||||
@@ -1,3 +1,4 @@
|
||||
+this is an actual diff file
|
||||
-all contents will remain unmodified
|
||||
no whitespace will be removed either
|
||||
```
|
||||
|
||||
#### Combining syntax highlighting with diff-like syntax
|
||||
|
||||
```diff lang="js"
|
||||
function thisIsJavaScript() {
|
||||
// This entire block gets highlighted as JavaScript,
|
||||
// and we can still add diff markers to it!
|
||||
- console.log('Old code to be removed')
|
||||
+ console.log('New and shiny code!')
|
||||
}
|
||||
```
|
||||
|
||||
#### Marking individual text inside lines
|
||||
|
||||
```js "given text"
|
||||
function demo() {
|
||||
// Mark any given text inside lines
|
||||
return 'Multiple matches of the given text are supported';
|
||||
}
|
||||
```
|
||||
|
||||
#### Regular expressions
|
||||
|
||||
```ts /ye[sp]/
|
||||
console.log('The words yes and yep will be marked.')
|
||||
```
|
||||
|
||||
#### Escaping forward slashes
|
||||
|
||||
```sh /\/ho.*\//
|
||||
echo "Test" > /home/test.txt
|
||||
```
|
||||
|
||||
#### Selecting inline marker types (mark, ins, del)
|
||||
|
||||
```js "return true;" ins="inserted" del="deleted"
|
||||
function demo() {
|
||||
console.log('These are inserted and deleted marker types');
|
||||
// The return statement uses the default marker type
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Word Wrap
|
||||
|
||||
[Word Wrap](https://expressive-code.com/key-features/word-wrap/)
|
||||
|
||||
#### Configuring word wrap per block
|
||||
|
||||
```js wrap
|
||||
// Example with wrap
|
||||
function getLongString() {
|
||||
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```js wrap=false
|
||||
// Example with wrap=false
|
||||
function getLongString() {
|
||||
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
|
||||
}
|
||||
```
|
||||
|
||||
#### Configuring indentation of wrapped lines
|
||||
|
||||
```js wrap preserveIndent
|
||||
// Example with preserveIndent (enabled by default)
|
||||
function getLongString() {
|
||||
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```js wrap preserveIndent=false
|
||||
// Example with preserveIndent=false
|
||||
function getLongString() {
|
||||
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
|
||||
}
|
||||
```
|
||||
|
||||
## Collapsible Sections
|
||||
|
||||
[Collapsible Sections](https://expressive-code.com/plugins/collapsible-sections/)
|
||||
|
||||
```js collapse={1-5, 12-14, 21-24}
|
||||
// All this boilerplate setup code will be collapsed
|
||||
import { someBoilerplateEngine } from '@example/some-boilerplate'
|
||||
import { evenMoreBoilerplate } from '@example/even-more-boilerplate'
|
||||
|
||||
const engine = someBoilerplateEngine(evenMoreBoilerplate())
|
||||
|
||||
// This part of the code will be visible by default
|
||||
engine.doSomething(1, 2, 3, calcFn)
|
||||
|
||||
function calcFn() {
|
||||
// You can have multiple collapsed sections
|
||||
const a = 1
|
||||
const b = 2
|
||||
const c = a + b
|
||||
|
||||
// This will remain visible
|
||||
console.log(`Calculation result: ${a} + ${b} = ${c}`)
|
||||
return c
|
||||
}
|
||||
|
||||
// All this code until the end of the block will be collapsed again
|
||||
engine.closeConnection()
|
||||
engine.freeMemory()
|
||||
engine.shutdown({ reason: 'End of example boilerplate code' })
|
||||
```
|
||||
|
||||
## Line Numbers
|
||||
|
||||
[Line Numbers](https://expressive-code.com/plugins/line-numbers/)
|
||||
|
||||
### Displaying line numbers per block
|
||||
|
||||
```js showLineNumbers
|
||||
// This code block will show line numbers
|
||||
console.log('Greetings from line 2!')
|
||||
console.log('I am on line 3')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```js showLineNumbers=false
|
||||
// Line numbers are disabled for this block
|
||||
console.log('Hello?')
|
||||
console.log('Sorry, do you know what line I am on?')
|
||||
```
|
||||
|
||||
### Changing the starting line number
|
||||
|
||||
```js showLineNumbers startLineNumber=5
|
||||
console.log('Greetings from line 5!')
|
||||
console.log('I am on line 6')
|
||||
```
|
||||
@@ -305,17 +305,6 @@ function initCustomScrollbar() {
|
||||
autoHideSuspend: false,
|
||||
},
|
||||
});
|
||||
const preElements = document.querySelectorAll('pre');
|
||||
preElements.forEach((ele) => {
|
||||
OverlayScrollbars(ele, {
|
||||
scrollbars: {
|
||||
theme: 'scrollbar-base scrollbar-dark px-2',
|
||||
autoHide: 'leave',
|
||||
autoHideDelay: 500,
|
||||
autoHideSuspend: false
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const katexElements = document.querySelectorAll('.katex-display') as NodeListOf<HTMLElement>;
|
||||
|
||||
|
||||
90
src/plugins/expressive-code/custom-copy-button.ts
Normal file
90
src/plugins/expressive-code/custom-copy-button.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { definePlugin } from "@expressive-code/core";
|
||||
import type { Element } from "hast";
|
||||
|
||||
export function pluginCustomCopyButton() {
|
||||
return definePlugin({
|
||||
name: "Custom Copy Button",
|
||||
hooks: {
|
||||
postprocessRenderedBlock: (context) => {
|
||||
function traverse(node: Element) {
|
||||
if (node.type === "element" && node.tagName === "pre") {
|
||||
processCodeBlock(node);
|
||||
return;
|
||||
}
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
if (child.type === "element") traverse(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function processCodeBlock(node: Element) {
|
||||
const copyButton = {
|
||||
type: "element" as const,
|
||||
tagName: "button",
|
||||
properties: {
|
||||
className: ["copy-btn"],
|
||||
"aria-label": "Copy code",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: "element" as const,
|
||||
tagName: "div",
|
||||
properties: {
|
||||
className: ["copy-btn-icon"],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: "element" as const,
|
||||
tagName: "svg",
|
||||
properties: {
|
||||
viewBox: "0 -960 960 960",
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
className: ["copy-btn-icon", "copy-icon"],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: "element" as const,
|
||||
tagName: "path",
|
||||
properties: {
|
||||
d: "M368.37-237.37q-34.48 0-58.74-24.26-24.26-24.26-24.26-58.74v-474.26q0-34.48 24.26-58.74 24.26-24.26 58.74-24.26h378.26q34.48 0 58.74 24.26 24.26 24.26 24.26 58.74v474.26q0 34.48-24.26 58.74-24.26 24.26-58.74 24.26H368.37Zm0-83h378.26v-474.26H368.37v474.26Zm-155 238q-34.48 0-58.74-24.26-24.26-24.26-24.26-58.74v-515.76q0-17.45 11.96-29.48 11.97-12.02 29.33-12.02t29.54 12.02q12.17 12.03 12.17 29.48v515.76h419.76q17.45 0 29.48 11.96 12.02 11.97 12.02 29.33t-12.02 29.54q-12.03 12.17-29.48 12.17H213.37Zm155-238v-474.26 474.26Z",
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "element" as const,
|
||||
tagName: "svg",
|
||||
properties: {
|
||||
viewBox: "0 -960 960 960",
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
className: ["copy-btn-icon", "success-icon"],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: "element" as const,
|
||||
tagName: "path",
|
||||
properties: {
|
||||
d: "m389-377.13 294.7-294.7q12.58-12.67 29.52-12.67 16.93 0 29.61 12.67 12.67 12.68 12.67 29.53 0 16.86-12.28 29.14L419.07-288.41q-12.59 12.67-29.52 12.67-16.94 0-29.62-12.67L217.41-430.93q-12.67-12.68-12.79-29.45-.12-16.77 12.55-29.45 12.68-12.67 29.62-12.67 16.93 0 29.28 12.67L389-377.13Z",
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as Element;
|
||||
|
||||
if (!node.children) {
|
||||
node.children = [];
|
||||
}
|
||||
node.children.push(copyButton);
|
||||
}
|
||||
|
||||
traverse(context.renderData.blockAst);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
48
src/plugins/expressive-code/language-badge.ts
Normal file
48
src/plugins/expressive-code/language-badge.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Based on the discussion at https://github.com/expressive-code/expressive-code/issues/153#issuecomment-2282218684
|
||||
*/
|
||||
import { definePlugin } from "@expressive-code/core";
|
||||
|
||||
export function pluginLanguageBadge() {
|
||||
return definePlugin({
|
||||
name: "Language Badge",
|
||||
baseStyles: ({ cssVar }) => `
|
||||
[data-language]::before {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
padding: 0.1rem 0.5rem;
|
||||
content: attr(data-language);
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: oklch(0.75 0.1 var(--hue));
|
||||
background: oklch(0.33 0.035 var(--hue));
|
||||
border-radius: 0.5rem;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s;
|
||||
opacity: 0;
|
||||
}
|
||||
.frame:not(.has-title):not(.is-terminal) {
|
||||
@media (hover: none) {
|
||||
& [data-language]::before {
|
||||
opacity: 1;
|
||||
margin-right: 3rem;
|
||||
}
|
||||
& [data-language]:active::before {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@media (hover: hover) {
|
||||
& [data-language]::before {
|
||||
opacity: 1;
|
||||
}
|
||||
&:hover [data-language]::before {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
}
|
||||
3
src/styles/expressive-code.css
Normal file
3
src/styles/expressive-code.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.expressive-code .frame {
|
||||
@apply !shadow-none;
|
||||
}
|
||||
@@ -110,22 +110,6 @@
|
||||
.btn-regular-dark.success {
|
||||
@apply bg-[oklch(0.75_0.14_var(--hue))] dark:bg-[oklch(0.75_0.14_var(--hue))]
|
||||
}
|
||||
|
||||
.copy-btn-icon {
|
||||
@apply absolute top-1/2 left-1/2 transition -translate-x-1/2 -translate-y-1/2
|
||||
}
|
||||
.copy-btn .copy-icon {
|
||||
@apply opacity-100 fill-white dark:fill-white/75
|
||||
}
|
||||
.copy-btn.success .copy-icon {
|
||||
@apply opacity-0 fill-[var(--deep-text)]
|
||||
}
|
||||
.copy-btn .success-icon {
|
||||
@apply opacity-0
|
||||
}
|
||||
.copy-btn.success .success-icon {
|
||||
@apply opacity-100
|
||||
}
|
||||
}
|
||||
|
||||
.custom-md img, #post-cover img {
|
||||
|
||||
@@ -58,18 +58,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply bg-[var(--codeblock-bg)] !important;
|
||||
@apply rounded-xl px-5;
|
||||
.copy-btn {
|
||||
all: initial;
|
||||
@apply btn-regular-dark opacity-0 shadow-lg shadow-black/50 absolute active:scale-90 h-8 w-8 top-3 right-3 text-sm rounded-lg transition-all ease-in-out z-20 cursor-pointer;
|
||||
}
|
||||
.frame:hover .copy-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
code {
|
||||
@apply bg-transparent text-inherit text-sm p-0;
|
||||
.copy-btn-icon {
|
||||
@apply absolute top-1/2 left-1/2 transition -translate-x-1/2 -translate-y-1/2 w-4 h-4 fill-white pointer-events-none;
|
||||
}
|
||||
.copy-btn .copy-icon {
|
||||
@apply opacity-100 fill-white dark:fill-white/75;
|
||||
}
|
||||
.copy-btn.success .copy-icon {
|
||||
@apply opacity-0 fill-[var(--deep-text)]
|
||||
}
|
||||
.copy-btn .success-icon {
|
||||
@apply opacity-0 fill-white;
|
||||
}
|
||||
.copy-btn.success .success-icon {
|
||||
@apply opacity-100
|
||||
}
|
||||
|
||||
.expressive-code {
|
||||
@apply my-4;
|
||||
::selection {
|
||||
@apply bg-[var(--codeblock-selection)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ul, ol {
|
||||
li::marker {
|
||||
|
||||
@@ -56,7 +56,8 @@ define({
|
||||
--inline-code-color: var(--btn-content)
|
||||
--selection-bg: oklch(0.90 0.05 var(--hue)) oklch(0.40 0.08 var(--hue))
|
||||
--codeblock-selection: oklch(0.40 0.08 var(--hue))
|
||||
--codeblock-bg: oklch(0.2 0.015 var(--hue)) oklch(0.17 0.015 var(--hue))
|
||||
--codeblock-bg: oklch(0.17 0.015 var(--hue)) oklch(0.17 0.015 var(--hue))
|
||||
--codeblock-topbar-bg: oklch(0.3 0.02 var(--hue)) oklch(0.12 0.015 var(--hue))
|
||||
|
||||
--license-block-bg: black(0.03) var(--codeblock-bg)
|
||||
|
||||
|
||||
@@ -86,3 +86,7 @@ export type BlogPostData = {
|
||||
nextTitle?: string;
|
||||
nextSlug?: string;
|
||||
};
|
||||
|
||||
export type ExpressiveCodeConfig = {
|
||||
theme: string;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { expressiveCodeConfig } from "@/config";
|
||||
import type { LIGHT_DARK_MODE } from "@/types/config";
|
||||
import {
|
||||
AUTO_MODE,
|
||||
@@ -42,6 +43,12 @@ export function applyThemeToDocument(theme: LIGHT_DARK_MODE) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Set the theme for Expressive Code
|
||||
document.documentElement.setAttribute(
|
||||
"data-theme",
|
||||
expressiveCodeConfig.theme,
|
||||
);
|
||||
}
|
||||
|
||||
export function setTheme(theme: LIGHT_DARK_MODE): void {
|
||||
|
||||
Reference in New Issue
Block a user