大更新
This commit is contained in:
@@ -19,6 +19,7 @@ const blogsClubBadge = `https://www.blogsclub.org/badge/${blogDomain}/theme`;
|
||||
</a>
|
||||
</div>
|
||||
{siteConfig.footer.links.length > 0 && <nav aria-label="页脚导航">{siteConfig.footer.links.map((item, index) => <Fragment>{index > 0 && ' · '}<a data-text-link href={item.href} target={item.external ? '_blank' : undefined} rel={item.external ? 'noreferrer' : undefined}><span class:list={{ 'footer-chinese': containsChinese(item.label) }} lang={containsChinese(item.label) ? 'zh-CN' : undefined}>{item.label}</span></a></Fragment>)}</nav>}
|
||||
<p class="m-0">Powered By Astro · Theme <a data-text-link href="https://src.luming.cool/riseforever2026/Eidolon">Eidolon</a></p>
|
||||
{siteConfig.footer.copyright && <p class="m-0">{siteConfig.footer.copyright}</p>}
|
||||
{siteConfig.footer.note && <p class="footer-chinese m-0" lang="zh-CN">{siteConfig.footer.note}</p>}
|
||||
{siteConfig.footer.theme && <p class="m-0"><a data-text-link href={siteConfig.footer.theme.href} target={siteConfig.footer.theme.external ? '_blank' : undefined} rel={siteConfig.footer.theme.external ? 'noreferrer' : undefined}>{siteConfig.footer.theme.label}</a></p>}
|
||||
|
||||
@@ -131,11 +131,15 @@ GitHub:[virelyx258](https://github.com/virelyx258)
|
||||
|
||||
### 主题
|
||||
|
||||
使用自己制作的 Astro 主题 Mirages。
|
||||
使用自己制作的 Astro 主题 Eidolon。
|
||||
|
||||
它极大程度上复刻了 Typecho 主题 Mirages 的风格和功能,又在此基础上使用了更先进的 TailWind CSS,使得站点响应速度更加优秀。
|
||||
|
||||
因为 Mirages 是一款付费主题,如果贸然开源会影响到原主题的销售,且本站主题仍处于开发状态,所以本主题暂不开源。
|
||||
~~因为 Mirages 是一款付费主题,如果贸然开源会影响到原主题的销售,且本站主题仍处于开发状态,所以本主题暂不开源。~~
|
||||
|
||||
仔细想想,发现我们其实不是同一个赛道。我做的是 Astro 主题,受众是 Astro 用户,不耽误 Typecho 主题的销售。况且,并不是所有 Typecho 用户都能无感切换到静态博客。
|
||||
|
||||
所以,本主题准备开源。
|
||||
|
||||
[collapse title="在此之前"]
|
||||
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
---
|
||||
title: "Eidolon 主题短代码演示"
|
||||
pubDate: "2026-08-12T01:10:32.630Z"
|
||||
categories: ['科技']
|
||||
tags: ['主题','Astro']
|
||||
draft: false
|
||||
comments: true
|
||||
---
|
||||
|
||||
Eidolon 主题支持短代码。除兼容 [Mirages](https://get233.com/archives/mirages-intro.html) 的全部短代码外,额外增加了“Tools”组件。
|
||||
|
||||
善于运用短代码可以使文章内容更丰富。
|
||||
|
||||
---
|
||||
|
||||
短代码的统一书写形式:
|
||||
|
||||
```
|
||||
[shortcode param="value"]内容[/shortcode]
|
||||
[shortcode param="value"/]
|
||||
```
|
||||
|
||||
|
||||
|
||||
直接在 MarkDown 源代码里书写即可,博客构建时会自动将其转换为对应样式。
|
||||
|
||||
---
|
||||
|
||||
## 链接按钮
|
||||
|
||||
短代码名称:`button`
|
||||
|
||||
[tabs]
|
||||
[tab name="说明"]
|
||||
生成一个按钮样式的超链接。
|
||||
[/tab]
|
||||
[tab name="示例代码"]
|
||||
|
||||
```
|
||||
[button href="https://src.luming.cool/riseforever2026/Eidolon"]Eidolon 主题[/button]
|
||||
```
|
||||
|
||||
[/tab]
|
||||
[tab name="效果" selected]
|
||||
[button href="https://src.luming.cool/riseforever2026/Eidolon"]Eidolon 主题[/button]
|
||||
[/tab]
|
||||
[tab name="参数"]
|
||||
|
||||
| 参数名 | 含义 |
|
||||
| -----: | :------------- |
|
||||
| href | 欲跳转到的链接 |
|
||||
| 内容 | 按钮标题 |
|
||||
|
||||
[/tab]
|
||||
[/tabs]
|
||||
|
||||
## 选项卡
|
||||
|
||||
短代码名称:`tabs`
|
||||
|
||||
[tabs]
|
||||
[tab name="说明"]
|
||||
生成一个多标签页的卡片。
|
||||
[/tab]
|
||||
[tab name="示例代码"]
|
||||
|
||||
```
|
||||
[tabs]
|
||||
[tab name="标签页1标题"]
|
||||
我是标签页1的内容
|
||||
[/tab]
|
||||
[tab name="标签页2标题"]
|
||||
我是标签页2的内容
|
||||
[/tab]
|
||||
[tab name="标签页3标题" selected]
|
||||
我是标签页3的内容
|
||||
[/tab]
|
||||
[tab name="标签页4标题"]
|
||||
我是标签页4的内容
|
||||
[/tab]
|
||||
[/tabs]
|
||||
```
|
||||
|
||||
[/tab]
|
||||
[tab name="效果" selected]
|
||||
如你所见。
|
||||
[/tab]
|
||||
[tab name="参数"]
|
||||
|
||||
| 参数名 | 含义 |
|
||||
| -------: | :----------------- |
|
||||
| name | 标签页的名称 |
|
||||
| selected | 默认选中的标签页 |
|
||||
| 内容 | 标签页内显示的内容 |
|
||||
|
||||
[/tab]
|
||||
[/tabs]
|
||||
|
||||
## 提示框、警告框
|
||||
|
||||
短代码名称:`hint`,`tip`
|
||||
|
||||
[tabs]
|
||||
[tab name="说明"]
|
||||
生成一个提示或警告卡片。
|
||||
[/tab]
|
||||
[tab name="示例代码"]
|
||||
|
||||
```
|
||||
[hint]提示内容[/hint]
|
||||
|
||||
[hint warn]警告内容[/hint]
|
||||
|
||||
[hint type="danger" title="提示标题"]
|
||||
提示内容
|
||||
[/hint]
|
||||
```
|
||||
|
||||
[/tab]
|
||||
[tab name="效果" selected]
|
||||
[hint]提示内容[/hint]
|
||||
|
||||
[hint warn]警告内容[/hint]
|
||||
|
||||
[hint type="danger" title="提示标题"]
|
||||
提示内容
|
||||
[/hint]
|
||||
[/tab]
|
||||
[tab name="参数"]
|
||||
|
||||
| 参数名 | 含义 |
|
||||
| -----: | :----------------------------------------------------------- |
|
||||
| type | 提示类型,默认为 `info`,在没有警告标题的情况下,可以直接将提示类型的值写到参数处。可选的提示类型为 `info`,`warning`(同 `warn`), `danger`(同 `error`),`success` |
|
||||
| title | 提示标题 |
|
||||
| 内容 | 提示内容 |
|
||||
|
||||
[/tab]
|
||||
[/tabs]
|
||||
|
||||
## 下载文件
|
||||
|
||||
短代码名称:`file`
|
||||
|
||||
[tabs]
|
||||
[tab name="说明"]
|
||||
生成一个下载文件的卡片。
|
||||
[/tab]
|
||||
[tab name="示例代码"]
|
||||
|
||||
```
|
||||
[file url="https://example.com/download.zip"]示例文件名[/file]
|
||||
```
|
||||
|
||||
[/tab]
|
||||
[tab name="效果" selected]
|
||||
[file url="https://example.com/download.zip"]示例文件名[/file]
|
||||
[/tab]
|
||||
[tab name="参数"]
|
||||
|
||||
| 参数名 | 含义 |
|
||||
| -----: | :------------- |
|
||||
| href | 下载文件的链接 |
|
||||
| 内容 | 文件名 |
|
||||
|
||||
[/tab]
|
||||
[/tabs]
|
||||
|
||||
## 标签
|
||||
|
||||
短代码名称:`tag`,`label`
|
||||
|
||||
[tabs]
|
||||
[tab name="说明"]
|
||||
生成一个标签。
|
||||
[/tab]
|
||||
[tab name="示例代码"]
|
||||
|
||||
```
|
||||
[tag]默认 Tag[/tag]
|
||||
[tag type="primary"]文字的颜色[/tag]
|
||||
[tag type="info"]文字的颜色[/tag]
|
||||
[tag type="warn"]文字的颜色[/tag]
|
||||
[tag type="danger"]文字的颜色[/tag]
|
||||
[tag type="success"]文字的颜色[/tag]
|
||||
|
||||
[tag outline]文字的颜色[/tag]
|
||||
[tag type="primary" outline]文字的颜色[/tag]
|
||||
[tag type="info" outline]文字的颜色[/tag]
|
||||
[tag type="warn" outline]文字的颜色[/tag]
|
||||
[tag type="danger" outline]文字的颜色[/tag]
|
||||
[tag type="success" outline="1"]文字的颜色[/tag]
|
||||
```
|
||||
|
||||
[/tab]
|
||||
[tab name="效果" selected]
|
||||
[tag]默认 Tag[/tag]
|
||||
[tag type="primary"]文字的颜色[/tag]
|
||||
[tag type="info"]文字的颜色[/tag]
|
||||
[tag type="warn"]文字的颜色[/tag]
|
||||
[tag type="danger"]文字的颜色[/tag]
|
||||
[tag type="success"]文字的颜色[/tag]
|
||||
|
||||
[tag outline]文字的颜色[/tag]
|
||||
[tag type="primary" outline]文字的颜色[/tag]
|
||||
[tag type="info" outline]文字的颜色[/tag]
|
||||
[tag type="warn" outline]文字的颜色[/tag]
|
||||
[tag type="danger" outline]文字的颜色[/tag]
|
||||
[tag type="success" outline="1"]文字的颜色[/tag]
|
||||
[/tab]
|
||||
[tab name="参数"]
|
||||
|
||||
| 参数名 | 含义 |
|
||||
| ------: | :----------------------------------------------------------- |
|
||||
| type | 默认为 `default`,可选值为 `info`,`warning`(同 `warn`), `danger`(同 `error`),`success`,`primary`, `default` |
|
||||
| outline | 是否使用 outline 样式的标签 |
|
||||
| 内容 | 标签展示的文字 |
|
||||
|
||||
[/tab]
|
||||
[/tabs]
|
||||
|
||||
## 快速输入警告符: [!/]
|
||||
|
||||
短代码名称:`!`
|
||||
|
||||
[tabs]
|
||||
[tab name="说明"]
|
||||
快速输入: [!/]
|
||||
[/tab]
|
||||
[tab name="示例代码"]
|
||||
|
||||
```
|
||||
[!/]
|
||||
```
|
||||
|
||||
[/tab]
|
||||
[tab name="效果" selected]
|
||||
[!/]
|
||||
[/tab]
|
||||
[/tabs]
|
||||
|
||||
## 折叠框
|
||||
|
||||
短代码名称:`collapse`
|
||||
|
||||
[tabs]
|
||||
[tab name="说明"]
|
||||
可以点击标题展开 / 折叠的内容。
|
||||
[/tab]
|
||||
[tab name="示例代码"]
|
||||
|
||||
```
|
||||
[collapse title="标题"]
|
||||
折叠内容
|
||||
[/collapse]
|
||||
```
|
||||
|
||||
[/tab]
|
||||
[tab name="效果" selected]
|
||||
[collapse title="标题"]
|
||||
折叠内容
|
||||
[/collapse]
|
||||
[/tab]
|
||||
[tab name="参数"]
|
||||
|
||||
| 参数名 | 含义 |
|
||||
| -----: | :--------- |
|
||||
| title | 折叠框标题 |
|
||||
| 内容 | 折叠框内容 |
|
||||
|
||||
[/tab]
|
||||
[/tabs]
|
||||
|
||||
## GitHub
|
||||
|
||||
短代码名称:`github`
|
||||
|
||||
[tabs]
|
||||
[tab name="说明"]
|
||||
放置一个 Github 仓库控件。
|
||||
[/tab]
|
||||
[tab name="示例代码"]
|
||||
|
||||
```
|
||||
[github repo="virelyx258/Astro-Theme-Eidolon" /]
|
||||
```
|
||||
|
||||
[/tab]
|
||||
[tab name="效果" selected]
|
||||
[github repo="virelyx258/Astro-Theme-Eidolon" /]
|
||||
[/tab]
|
||||
[tab name="参数"]
|
||||
|
||||
| 参数名 | 含义 |
|
||||
| -----: | :----------------------------------------------- |
|
||||
| repo | Github 仓库所属用户及仓库名。格式:用户名/仓库名 |
|
||||
|
||||
[/tab]
|
||||
[/tabs]
|
||||
|
||||
## 工具列表
|
||||
|
||||
短代码名称:`tools`
|
||||
|
||||
[tabs]
|
||||
[tab name="说明"]
|
||||
放置一个工具列表卡片。
|
||||
[/tab]
|
||||
[tab name="示例代码"]
|
||||
|
||||
```
|
||||
[tools title = "设计"]
|
||||
[Canva](https://www.canva.cn)+(https://image.luming.cool/i/2026/08/07/6a75ba231df9b.webp)/(图像设计)
|
||||
[即时设计](https://js.design)+(https://image.luming.cool/i/2026/08/07/6a75c0b9bbf29.webp)/(UI 设计)
|
||||
[Microsoft 365](https://m365.cloud.microsoft/)+(https://image.luming.cool/i/2026/08/07/6a75bd0318e8e.webp)/(文档处理)
|
||||
[剪映](https://www.capcut.cn/)+(https://image.luming.cool/i/2026/08/07/6a75bf6a337d1.webp)/(视频剪辑)
|
||||
[/tools]
|
||||
```
|
||||
|
||||
[/tab]
|
||||
[tab name="效果" selected]
|
||||
[tools title = "设计"]
|
||||
[Canva](https://www.canva.cn)+(https://image.luming.cool/i/2026/08/07/6a75ba231df9b.webp)/(图像设计)
|
||||
[即时设计](https://js.design)+(https://image.luming.cool/i/2026/08/07/6a75c0b9bbf29.webp)/(UI 设计)
|
||||
[Microsoft 365](https://m365.cloud.microsoft/)+(https://image.luming.cool/i/2026/08/07/6a75bd0318e8e.webp)/(文档处理)
|
||||
[剪映](https://www.capcut.cn/)+(https://image.luming.cool/i/2026/08/07/6a75bf6a337d1.webp)/(视频剪辑)
|
||||
[/tools]
|
||||
[/tab]
|
||||
[tab name="参数"]
|
||||
|
||||
| 参数名 | 含义 |
|
||||
| -----: | :--------- |
|
||||
| title | 工具集名称 |
|
||||
|
||||
[/tab]
|
||||
[/tabs]
|
||||
@@ -0,0 +1,52 @@
|
||||
const NOTICE_ICON = {
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: { className: ['shortcode-notice'], ariaLabel: '警告' },
|
||||
children: [{
|
||||
type: 'element',
|
||||
tagName: 'svg',
|
||||
properties: {
|
||||
ariaHidden: 'true',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
strokeWidth: 2,
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
},
|
||||
children: [
|
||||
{ type: 'element', tagName: 'circle', properties: { cx: 12, cy: 12, r: 10 }, children: [] },
|
||||
{ type: 'element', tagName: 'path', properties: { d: 'M12 8v4' }, children: [] },
|
||||
{ type: 'element', tagName: 'path', properties: { d: 'M12 16h.01' }, children: [] },
|
||||
],
|
||||
}],
|
||||
};
|
||||
|
||||
function cloneNoticeIcon() {
|
||||
return structuredClone(NOTICE_ICON);
|
||||
}
|
||||
|
||||
export function applyNoticeShortcodes(tree: any) {
|
||||
const transform = (node: any) => {
|
||||
if (!Array.isArray(node.children) || node.tagName === 'code' || node.tagName === 'pre') return;
|
||||
const output: any[] = [];
|
||||
for (const child of node.children) {
|
||||
if (child.type !== 'text' || !child.value.includes('[!/]')) {
|
||||
transform(child);
|
||||
output.push(child);
|
||||
continue;
|
||||
}
|
||||
const parts = child.value.split('[!/]');
|
||||
parts.forEach((part: string, index: number) => {
|
||||
if (part) output.push({ type: 'text', value: part });
|
||||
if (index < parts.length - 1) output.push({ type: 'text', value: '\u00a0' }, cloneNoticeIcon(), { type: 'text', value: '\u00a0' });
|
||||
});
|
||||
}
|
||||
node.children = output;
|
||||
};
|
||||
transform(tree);
|
||||
}
|
||||
|
||||
export default function noticeShortcode() {
|
||||
return (tree: any) => applyNoticeShortcodes(tree);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { SKIP, visit } from 'unist-util-visit';
|
||||
|
||||
export function applyResponsiveTables(tree: any) {
|
||||
visit(tree, 'element', (node: any, index: number | undefined, parent: any) => {
|
||||
if (node.tagName !== 'table' || index === undefined || !parent) return;
|
||||
const classes = Array.isArray(node.properties?.className) ? node.properties.className : [];
|
||||
if (classes.includes('responsive-table')) return;
|
||||
|
||||
const section = node.children?.find((child: any) => child.tagName === 'thead' || child.tagName === 'tbody');
|
||||
const row = section?.children?.find((child: any) => child.tagName === 'tr');
|
||||
const columns = Math.max(1, row?.children?.filter((child: any) => child.tagName === 'th' || child.tagName === 'td').length ?? 1);
|
||||
node.properties = { ...node.properties, className: [...classes, 'responsive-table'] };
|
||||
parent.children[index] = {
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
properties: { className: ['table-scroll'], style: `--table-columns:${columns}` },
|
||||
children: [node],
|
||||
};
|
||||
return SKIP;
|
||||
});
|
||||
}
|
||||
|
||||
export default function responsiveTables() {
|
||||
return (tree: any) => applyResponsiveTables(tree);
|
||||
}
|
||||
+72
-16
@@ -5,10 +5,18 @@ import { toHast } from 'mdast-util-to-hast';
|
||||
import { gfm } from 'micromark-extension-gfm';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import { applyExternalLinks } from './external-links';
|
||||
import { applyResponsiveTables } from './responsive-tables';
|
||||
import { applyNoticeShortcodes } from './notice-shortcode';
|
||||
|
||||
const URL_SCHEME = /^(https?:|mailto:|tel:)/i;
|
||||
const TAG_TYPES = new Set(['primary', 'success', 'warning', 'danger', 'info', 'default']);
|
||||
const HINT_TYPES = new Set(['warn', 'warning', 'error', 'danger', 'success', 'info']);
|
||||
const HINT_ICONS: Record<string, string> = {
|
||||
info: '<circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path>',
|
||||
warning: '<circle cx="12" cy="12" r="10"></circle><path d="M12 8v4"></path><path d="M12 16h.01"></path>',
|
||||
danger: '<path d="m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 8.54 21h6.92a2 2 0 0 0 1.73-3Z"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path>',
|
||||
success: '<path d="m9 12 2 2 4-4"></path><circle cx="12" cy="12" r="10"></circle>',
|
||||
};
|
||||
const BLOCK_NAMES = new Set(['hint', 'tip', 'collapse', 'tabs', 'tools']);
|
||||
const INLINE_NAMES = new Set(['button', 'btn', 'file', 'tag', 'label']);
|
||||
type GithubData = { stars?: string; forks?: string; description?: string; commitDate?: string; htmlUrl?: string; defaultBranch?: string };
|
||||
@@ -61,12 +69,37 @@ function normalizeMarkdownHeadings(source: string): string {
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
function markdown(source: string, preserveHtml = false): string {
|
||||
function markdown(source: string, preserveHtml = false, lineNumbers = false): string {
|
||||
const normalized = normalizeMarkdownHeadings(source);
|
||||
const tree = fromMarkdown(normalized, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] });
|
||||
transform(tree, normalized);
|
||||
if (!preserveHtml) visit(tree, 'html', (node: any) => { if (!node.data?.shortcode) node.value = ''; });
|
||||
const hast = toHast(tree, { allowDangerousHtml: true }) as any;
|
||||
if (lineNumbers) {
|
||||
visit(hast, 'element', (node: any) => {
|
||||
if (node.tagName !== 'pre') return;
|
||||
const code = node.children?.find((child: any) => child.type === 'element' && child.tagName === 'code');
|
||||
if (!code || code.children?.some((child: any) => child.type !== 'text')) return;
|
||||
const value = code.children.map((child: any) => child.value).join('');
|
||||
const lines = value.endsWith('\n') ? value.slice(0, -1).split('\n') : value.split('\n');
|
||||
const classes = Array.isArray(node.properties?.className) ? node.properties.className : [];
|
||||
node.properties = { ...node.properties, className: [...classes, 'shortcode-code'] };
|
||||
code.children = lines.flatMap((line: string, index: number) => [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: { className: ['line'] },
|
||||
children: [
|
||||
{ type: 'element', tagName: 'span', properties: { className: ['shortcode-line-number'], ariaHidden: 'true' }, children: [{ type: 'text', value: String(index + 1) }] },
|
||||
...(line ? [{ type: 'text', value: line }] : []),
|
||||
],
|
||||
},
|
||||
...(index < lines.length - 1 ? [{ type: 'text', value: '\n' }] : []),
|
||||
]);
|
||||
});
|
||||
}
|
||||
applyNoticeShortcodes(hast);
|
||||
applyResponsiveTables(hast);
|
||||
if (siteUrl) applyExternalLinks(hast, siteUrl);
|
||||
return toHtml(hast, { allowDangerousHtml: true });
|
||||
}
|
||||
@@ -77,24 +110,32 @@ export function renderMarkdown(source: string): string {
|
||||
|
||||
function renderBlock(name: string, args: Record<string, string>, body = ''): string {
|
||||
const label = args.title ?? args.name ?? args.text ?? body.trim().split('\n')[0] ?? '';
|
||||
if (name === 'button' || name === 'btn' || name === 'file') {
|
||||
if (name === 'button' || name === 'btn') {
|
||||
const url = safeUrl(args.href ?? args.url ?? args.link);
|
||||
if (!url) return `<span class="shortcode-invalid">${escape(label || '链接不可用')}</span>`;
|
||||
return `<a class="shortcode-button shortcode-${name}" href="${escape(url)}" data-no-text-link>${inline(label || url)}</a>`;
|
||||
}
|
||||
if (name === 'file') {
|
||||
const url = safeUrl(args.href ?? args.url ?? args.link);
|
||||
if (!url) return `<span class="shortcode-invalid">${escape(label || '文件链接不可用')}</span>`;
|
||||
const icon = '<svg class="content-file-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline><path d="M10 12v-1"></path><path d="M10 18v-2"></path><path d="M10 7v1"></path><path d="M10 15h.01"></path></svg>';
|
||||
return `<a class="shortcode-file content-file" href="${escape(url)}" target="_blank" rel="noopener noreferrer" data-no-text-link>${icon}<span class="content-file-filename">${inline(label || url)}</span></a>`;
|
||||
}
|
||||
if (name === 'tag' || name === 'label') {
|
||||
const type = TAG_TYPES.has(args.type ?? '') ? args.type : 'default';
|
||||
const outline = args.outline !== undefined ? ' shortcode-tag-outline' : '';
|
||||
return `<span class="shortcode-tag shortcode-tag-${type}${outline}">${inline(label)}</span>`;
|
||||
}
|
||||
if (name === 'hint' || name === 'tip') {
|
||||
const type = HINT_TYPES.has(args.type ?? '') ? args.type : 'info';
|
||||
const title = args.title ? `<strong>${inline(args.title)}</strong>` : '';
|
||||
return `<aside class="shortcode-hint shortcode-hint-${type}" role="note">${title}${markdown(body)}</aside>`;
|
||||
const requestedType = args.type ?? [...HINT_TYPES].find((candidate) => args[candidate] !== undefined);
|
||||
const type = requestedType === 'warn' ? 'warning' : requestedType === 'error' ? 'danger' : HINT_TYPES.has(requestedType ?? '') ? requestedType : 'info';
|
||||
const icon = `<svg class="content-hint-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${HINT_ICONS[type]}</svg>`;
|
||||
const heading = args.title ? `<div class="content-hint-heading">${icon}<p class="content-hint-title">${inline(args.title)}</p></div>` : icon;
|
||||
return `<aside class="shortcode-hint content-hint shortcode-${name} shortcode-hint-${type} hint-${type}${args.title ? ' content-hint-has-title' : ''}" role="note">${heading}${markdown(body)}</aside>`;
|
||||
}
|
||||
if (name === 'collapse') {
|
||||
const open = args.open !== undefined || args.expanded !== undefined ? ' open' : '';
|
||||
return `<details class="shortcode-collapse"${open}><summary><span class="shortcode-collapse-marker" aria-hidden="true"></span><span>${inline(label || '展开内容')}</span></summary><div class="shortcode-body"><div class="shortcode-body-inner">${markdown(body, true)}</div></div></details>`;
|
||||
return `<details class="shortcode-collapse"${open}><summary><span class="shortcode-collapse-marker" aria-hidden="true"></span><span>${inline(label || '展开内容')}</span></summary><div class="shortcode-body"><div class="shortcode-body-inner">${markdown(body, true, true)}</div></div></details>`;
|
||||
}
|
||||
if (name === 'tabs') {
|
||||
const parsed = parseBlocks(body);
|
||||
@@ -151,8 +192,28 @@ function renderBlock(name: string, args: Record<string, string>, body = ''): str
|
||||
|
||||
function parseBlocks(source: string): Array<{ name: string; args: Record<string, string>; body: string }> {
|
||||
const result: Array<{ name: string; args: Record<string, string>; body: string }> = [];
|
||||
const pattern = /^\[tab([^\]]*)\]\s*\n?([\s\S]*?)^\[\/tab\]\s*$/gm;
|
||||
for (const match of source.matchAll(pattern)) result.push({ name: 'tab', args: attrs(match[1]), body: match[2] });
|
||||
let fence: string | null = null;
|
||||
let opening: { args: Record<string, string>; bodyStart: number } | null = null;
|
||||
for (const line of source.matchAll(/^[^\n]*(?:\n|$)/gm)) {
|
||||
const text = line[0].trim();
|
||||
const start = line.index!;
|
||||
const fenceMatch = text.match(/^(`{3,}|~{3,})/);
|
||||
if (fenceMatch) {
|
||||
if (!fence) fence = fenceMatch[1][0];
|
||||
else if (fenceMatch[1][0] === fence) fence = null;
|
||||
continue;
|
||||
}
|
||||
if (fence) continue;
|
||||
|
||||
const closing = text.match(/^\[\/tab\]\s*$/i);
|
||||
if (closing && opening) {
|
||||
result.push({ name: 'tab', args: opening.args, body: source.slice(opening.bodyStart, start) });
|
||||
opening = null;
|
||||
continue;
|
||||
}
|
||||
const openingMatch = text.match(/^\[tab([^\]]*)\]\s*$/i);
|
||||
if (openingMatch && !opening) opening = { args: attrs(openingMatch[1]), bodyStart: start + line[0].length };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -220,15 +281,9 @@ function findBlocks(source: string): Array<{ name: string; args: Record<string,
|
||||
}
|
||||
|
||||
function replaceInline(source: string): string | null {
|
||||
const noticePattern = /^\[!(?:\/\s*)?\]\s*(.+)$/gm;
|
||||
let replacedNotice = false;
|
||||
source = source.replace(noticePattern, (_match, body) => {
|
||||
replacedNotice = true;
|
||||
return `<span class="shortcode-notice" role="note">${inline(body)}</span>`;
|
||||
});
|
||||
const shortcode = /\[(button|btn|file|tag|label)([^\]]*)\]([\s\S]*?)\[\/\1\]/gi;
|
||||
const shortcode = /\[(button|btn|file|tag|label|hint|tip)([^\]]*)\]([\s\S]*?)\[\/\1\]/gi;
|
||||
const replacedShortcode = shortcode.test(source);
|
||||
if (!replacedNotice && !replacedShortcode) return null;
|
||||
if (!replacedShortcode) return null;
|
||||
return source.replace(shortcode, (_match, name, rawArgs, body) => renderBlock(name.toLowerCase(), attrs(rawArgs), body));
|
||||
}
|
||||
|
||||
@@ -268,6 +323,7 @@ function transform(tree: any, raw = '') {
|
||||
const output: any[] = [];
|
||||
for (const node of root.children) {
|
||||
if (node.type !== 'paragraph' && node.type !== 'html') { output.push(node); continue; }
|
||||
if (node.type === 'html' && node.data?.shortcode) { output.push(node); continue; }
|
||||
const source = node.type === 'html' ? node.value : node.position ? raw.slice(node.position.start.offset, node.position.end.offset) : '';
|
||||
const replaced = replaceInline(source.trim());
|
||||
output.push(replaced ? { type: 'html', value: replaced, data: { shortcode: true } } : node);
|
||||
|
||||
@@ -210,14 +210,66 @@
|
||||
background: var(--color-code);
|
||||
}
|
||||
|
||||
.shortcode-code {
|
||||
--shortcode-code-gutter: color-mix(in srgb, var(--color-code) 78%, var(--color-accent));
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.shortcode-code code {
|
||||
padding-block: 1rem;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.shortcode-code .line {
|
||||
display: flex;
|
||||
min-height: 1.45em;
|
||||
font-size: .875rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.shortcode-code .shortcode-line-number {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
min-width: 3rem;
|
||||
flex: 0 0 3rem;
|
||||
margin-right: 1rem;
|
||||
padding-inline: 1rem;
|
||||
background: var(--shortcode-code-gutter);
|
||||
color: var(--color-muted);
|
||||
font-size: .875rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
opacity: 0.72;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.shortcode-code .line:first-of-type .shortcode-line-number { box-shadow: 0 -1rem var(--shortcode-code-gutter); }
|
||||
.shortcode-code .line:last-of-type .shortcode-line-number { box-shadow: 0 1rem var(--shortcode-code-gutter); }
|
||||
.shortcode-code .line:only-of-type .shortcode-line-number { box-shadow: 0 -1rem var(--shortcode-code-gutter), 0 1rem var(--shortcode-code-gutter); }
|
||||
|
||||
.prose .katex-display { max-width: 100%; overflow-x: auto; overflow-y: hidden; padding-block: .35em; }
|
||||
|
||||
.prose .mermaid { max-width: 100%; overflow-x: auto; overflow-y: hidden; padding: 1rem 0; }
|
||||
.prose .mermaid svg { max-width: none; height: auto; }
|
||||
|
||||
.prose .table-scroll {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 1.25em 0;
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-inline: contain;
|
||||
scrollbar-color: color-mix(in srgb, var(--color-muted) 60%, transparent) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.prose .table-scroll::-webkit-scrollbar { height: 8px; }
|
||||
.prose .table-scroll::-webkit-scrollbar-thumb { border-radius: 4px; background: color-mix(in srgb, var(--color-muted) 60%, transparent); }
|
||||
|
||||
.prose table {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.prose thead {
|
||||
@@ -239,6 +291,14 @@
|
||||
.prose :where(th, td) {
|
||||
padding: 0.55em 0.8em;
|
||||
border: 1px solid var(--color-border);
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.prose .responsive-table {
|
||||
width: max(100%, calc(var(--table-columns, 1) * 8rem));
|
||||
}
|
||||
}
|
||||
|
||||
.prose :where(th, td)[align='left'] { text-align: left; }
|
||||
|
||||
+102
-22
@@ -14,9 +14,54 @@
|
||||
color: var(--color-bg);
|
||||
}
|
||||
.prose .shortcode-file {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
min-height: 3.5rem;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 0.25rem 0 0.25rem 0.5rem;
|
||||
overflow: hidden;
|
||||
padding: 0 1.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font-family: var(--mirages-font-ui);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: border-color 0.6s ease;
|
||||
}
|
||||
.prose .shortcode-file:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
.prose .content-file-icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
flex: 0 0 1.5rem;
|
||||
color: var(--color-text);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.prose .content-file-filename {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--color-text);
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.prose .shortcode-file:hover {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.prose .shortcode-file:hover .content-file-icon,
|
||||
.prose .shortcode-file:hover .content-file-filename {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.prose .shortcode-file,
|
||||
.prose .content-file-icon,
|
||||
.prose .content-file-filename { transition: none; }
|
||||
}
|
||||
.shortcode-tag {
|
||||
display: inline-block;
|
||||
margin: 0.15em 0.3em 0.15em 0;
|
||||
@@ -44,29 +89,60 @@
|
||||
background: #35769b;
|
||||
}
|
||||
.shortcode-hint {
|
||||
--hint-color: var(--color-accent);
|
||||
position: relative;
|
||||
margin: 1.2em 0;
|
||||
padding: 0.7em 1em;
|
||||
border-left: 4px solid var(--color-accent);
|
||||
background: var(--color-surface);
|
||||
padding: 1.5rem 1.25rem 1.5rem 3.5rem;
|
||||
border: 1px solid color-mix(in srgb, var(--hint-color) 28%, var(--color-border));
|
||||
border-left: 0.25rem solid var(--hint-color);
|
||||
border-radius: var(--radius-sm);
|
||||
background: color-mix(in srgb, var(--hint-color) 7%, var(--color-surface));
|
||||
}
|
||||
.shortcode-hint strong {
|
||||
.content-hint-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 1rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
transform: translateY(-50%);
|
||||
color: var(--hint-color);
|
||||
}
|
||||
.content-hint-heading {
|
||||
display: grid;
|
||||
min-height: 1.5rem;
|
||||
grid-template-columns: 1.5rem minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 0 0 0.5rem -2.5rem;
|
||||
}
|
||||
.content-hint-heading .content-hint-icon {
|
||||
position: static;
|
||||
transform: none;
|
||||
}
|
||||
.prose .content-hint-title {
|
||||
display: block;
|
||||
margin-bottom: 0.2em;
|
||||
font-family: var(--mirages-font-ui);
|
||||
margin: 0;
|
||||
color: var(--hint-color);
|
||||
font-size: 1.2em;
|
||||
line-height: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.shortcode-hint p {
|
||||
.shortcode-hint p:not(.content-hint-title) {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
.shortcode-hint-warning,
|
||||
.shortcode-hint-warn {
|
||||
border-color: #a36b16;
|
||||
.shortcode-hint-warn,
|
||||
.hint-warning {
|
||||
--hint-color: #d99b00;
|
||||
}
|
||||
.shortcode-hint-error,
|
||||
.shortcode-hint-danger {
|
||||
border-color: #ad3d48;
|
||||
.shortcode-hint-danger,
|
||||
.hint-danger {
|
||||
--hint-color: #d34b50;
|
||||
}
|
||||
.shortcode-hint-success {
|
||||
border-color: #287d5b;
|
||||
.shortcode-hint-success,
|
||||
.hint-success {
|
||||
--hint-color: #209b61;
|
||||
}
|
||||
.shortcode-collapse {
|
||||
margin: 1.2em 0;
|
||||
@@ -132,10 +208,6 @@
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.shortcode-body { transition: none; }
|
||||
}
|
||||
.shortcode-body-inner table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.shortcode-body .shortcode-collapse {
|
||||
margin: 1rem 0 0.25rem;
|
||||
}
|
||||
@@ -420,9 +492,17 @@
|
||||
font-style: italic;
|
||||
}
|
||||
.shortcode-notice {
|
||||
display: inline-block;
|
||||
padding: 0.1em 0.45em;
|
||||
border-left: 2px solid var(--color-accent);
|
||||
background: color-mix(in srgb, var(--color-accent) 8%, transparent);
|
||||
color: var(--color-accent);
|
||||
display: inline-flex;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: inherit;
|
||||
line-height: 1;
|
||||
vertical-align: -0.125em;
|
||||
}
|
||||
.shortcode-notice svg {
|
||||
display: block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user