Compare commits
5 Commits
2b5bcfff93
...
5c26fa407d
| Author | SHA1 | Date |
|---|---|---|
|
|
5c26fa407d | |
|
|
f53dc847ca | |
|
|
a2ac902129 | |
|
|
1dca41b36a | |
|
|
19256b501f |
|
|
@ -0,0 +1,35 @@
|
|||
# yaml/tag 代码块格式测试
|
||||
|
||||
## 使用 yaml/tag 语法创建 md-deck
|
||||
|
||||
```yaml/tag
|
||||
tag: md-deck
|
||||
body: ./sparks.csv
|
||||
size: 54x86
|
||||
grid: 5x8
|
||||
bleed: 1
|
||||
padding: 2
|
||||
font-size: 3
|
||||
```
|
||||
|
||||
## 使用 body 字段添加内容
|
||||
|
||||
```yaml/tag
|
||||
tag: tag-box
|
||||
class: note
|
||||
body: |
|
||||
这是一个提示框
|
||||
```
|
||||
|
||||
## 带引号的值
|
||||
|
||||
```yaml/tag
|
||||
tag: tag-alert
|
||||
type: warning
|
||||
class: my-alert
|
||||
body: 这是一个警告信息
|
||||
```
|
||||
|
||||
## 旧的指令语法仍然可用
|
||||
|
||||
:md-deck[./sparks.csv]{size="54x86" grid="5x8"}
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
"chokidar": "^5.0.0",
|
||||
"commander": "^12.1.0",
|
||||
"csv-parse": "^5.5.6",
|
||||
"js-yaml": "^4.1.1",
|
||||
"marked": "^14.1.0",
|
||||
"marked-directive": "^1.0.7",
|
||||
"solid-element": "^1.9.1",
|
||||
|
|
@ -2251,6 +2252,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/attributes-parser": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/attributes-parser/-/attributes-parser-2.2.3.tgz",
|
||||
|
|
@ -2669,6 +2676,18 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
"chokidar": "^5.0.0",
|
||||
"commander": "^12.1.0",
|
||||
"csv-parse": "^5.5.6",
|
||||
"js-yaml": "^4.1.1",
|
||||
"marked": "^14.1.0",
|
||||
"marked-directive": "^1.0.7",
|
||||
"solid-element": "^1.9.1",
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export function CardLayer(props: CardLayerProps) {
|
|||
class="absolute flex items-center justify-center text-center prose prose-sm"
|
||||
style={{
|
||||
...getLayerStyle(layer, props.dimensions),
|
||||
'font-size': `${props.dimensions.fontSize}mm`
|
||||
'font-size': `${layer.fontSize || 3}mm`
|
||||
}}
|
||||
innerHTML={renderLayerContent(layer, props.cardData)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ export function PrintPreview(props: PrintPreviewProps) {
|
|||
gridOriginY: store.state.dimensions?.gridOriginY || 0,
|
||||
gridAreaWidth: store.state.dimensions?.gridAreaWidth || 56,
|
||||
gridAreaHeight: store.state.dimensions?.gridAreaHeight || 88,
|
||||
fontSize: store.state.dimensions?.fontSize || 3,
|
||||
visibleLayers: visibleLayers(),
|
||||
dimensions: store.state.dimensions!
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,6 +25,13 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
|
|||
}
|
||||
};
|
||||
|
||||
const updateLayerFontSize = (layerProp: string, fontSize?: number) => {
|
||||
const layer = store.state.layerConfigs.find(l => l.prop === layerProp);
|
||||
if (layer) {
|
||||
store.actions.updateLayerConfig(layerProp, { ...layer, fontSize });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="w-64 flex-shrink-0">
|
||||
<h3 class="font-bold mb-2 mt-0">图层</h3>
|
||||
|
|
@ -32,7 +39,7 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
|
|||
<div class="space-y-2">
|
||||
<For each={store.state.layerConfigs}>
|
||||
{(layer) => (
|
||||
<div class="flex flex-col gap-1 p-2 bg-gray-50 rounded">
|
||||
<div class="flex flex-row flex-wrap gap-1 p-2 bg-gray-50 rounded">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
@ -42,6 +49,16 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
|
|||
/>
|
||||
<span class="text-sm flex-1">{layer.prop}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => store.actions.setEditingLayer(store.state.editingLayer === layer.prop ? null : layer.prop)}
|
||||
class={`text-xs px-2 py-1 rounded cursor-pointer ${
|
||||
store.state.editingLayer === layer.prop
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
{store.state.editingLayer === layer.prop ? '✓ 框选' : '框选'}
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<select
|
||||
value={layer.orientation || 'n'}
|
||||
|
|
@ -54,16 +71,21 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
|
|||
)}
|
||||
</For>
|
||||
</select>
|
||||
<button
|
||||
onClick={() => store.actions.setEditingLayer(store.state.editingLayer === layer.prop ? null : layer.prop)}
|
||||
class={`text-xs px-2 py-1 rounded cursor-pointer ${
|
||||
store.state.editingLayer === layer.prop
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
{store.state.editingLayer === layer.prop ? '✓ 框选' : '框选'}
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-xs text-gray-600">字体/mm</label>
|
||||
<input
|
||||
type="number"
|
||||
value={layer.fontSize || ''}
|
||||
placeholder="默认"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
updateLayerFontSize(layer.prop, value ? Number(value) : undefined);
|
||||
}}
|
||||
class="w-16 text-xs px-2 py-1 rounded border border-gray-300 bg-white"
|
||||
step="0.1"
|
||||
min="0.1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export interface PropertiesEditorPanelProps {
|
|||
}
|
||||
|
||||
/**
|
||||
* 卡牌属性编辑面板:尺寸、网格、字体、出血、内边距
|
||||
* 卡牌属性编辑面板:尺寸、网格、出血、内边距
|
||||
*/
|
||||
export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
|
||||
const { store } = props;
|
||||
|
|
@ -56,17 +56,7 @@ export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">牌面字体 (mm)</label>
|
||||
<input
|
||||
type="number"
|
||||
class="w-full border border-gray-300 rounded px-2 py-1 text-sm"
|
||||
value={store.state.fontSize}
|
||||
onChange={(e) => store.actions.setFontSize(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">出血 (mm)</label>
|
||||
<label class="block text-sm font-medium text-gray-700">出血 / 内边距 (mm)</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="number"
|
||||
|
|
@ -75,12 +65,6 @@ export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
|
|||
onChange={(e) => store.actions.setBleed(Number(e.target.value))}
|
||||
placeholder="出血"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">内边距 (mm)</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="number"
|
||||
class="w-full border border-gray-300 rounded px-2 py-1 text-sm"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { createStore } from 'solid-js/store';
|
||||
import { calculateDimensions } from './dimensions';
|
||||
import { loadCSV } from '../../utils/csv-loader';
|
||||
import { initLayerConfigs } from './layer-parser';
|
||||
import { initLayerConfigs, formatLayers } from './layer-parser';
|
||||
import type { CardData, LayerConfig, Dimensions } from '../types';
|
||||
|
||||
/**
|
||||
|
|
@ -13,8 +13,7 @@ export const DECK_DEFAULTS = {
|
|||
GRID_W: 5,
|
||||
GRID_H: 8,
|
||||
BLEED: 1,
|
||||
PADDING: 2,
|
||||
FONT_SIZE: 3
|
||||
PADDING: 2
|
||||
} as const;
|
||||
|
||||
export interface DeckState {
|
||||
|
|
@ -25,7 +24,6 @@ export interface DeckState {
|
|||
gridH: number;
|
||||
bleed: number;
|
||||
padding: number;
|
||||
fontSize: number;
|
||||
fixed: boolean;
|
||||
src: string;
|
||||
rawSrc: string; // 原始 CSV 路径(用于生成代码时保持相对路径)
|
||||
|
|
@ -74,7 +72,6 @@ export interface DeckActions {
|
|||
setGridH: (grid: number) => void;
|
||||
setBleed: (bleed: number) => void;
|
||||
setPadding: (padding: number) => void;
|
||||
setFontSize: (size: number) => void;
|
||||
|
||||
// 数据设置
|
||||
setCards: (cards: CardData[]) => void;
|
||||
|
|
@ -138,7 +135,6 @@ export function createDeckStore(
|
|||
gridH: DECK_DEFAULTS.GRID_H,
|
||||
bleed: DECK_DEFAULTS.BLEED,
|
||||
padding: DECK_DEFAULTS.PADDING,
|
||||
fontSize: DECK_DEFAULTS.FONT_SIZE,
|
||||
fixed: false,
|
||||
src: initialSrc,
|
||||
rawSrc: initialSrc,
|
||||
|
|
@ -169,8 +165,7 @@ export function createDeckStore(
|
|||
gridW: state.gridW,
|
||||
gridH: state.gridH,
|
||||
bleed: state.bleed,
|
||||
padding: state.padding,
|
||||
fontSize: state.fontSize
|
||||
padding: state.padding
|
||||
});
|
||||
setState({ dimensions: dims });
|
||||
};
|
||||
|
|
@ -199,10 +194,6 @@ export function createDeckStore(
|
|||
setState({ padding });
|
||||
updateDimensions();
|
||||
};
|
||||
const setFontSize = (size: number) => {
|
||||
setState({ fontSize: size });
|
||||
updateDimensions();
|
||||
};
|
||||
|
||||
const setCards = (cards: CardData[]) => setState({ cards, activeTab: 0 });
|
||||
const setActiveTab = (index: number) => setState({ activeTab: index });
|
||||
|
|
@ -277,11 +268,8 @@ export function createDeckStore(
|
|||
const clearError = () => setState({ error: null });
|
||||
|
||||
const generateCode = () => {
|
||||
const layersStr = state.layerConfigs
|
||||
.filter(l => l.visible)
|
||||
.map(l => `${l.prop}:${l.x1},${l.y1}-${l.x2},${l.y2}${l.orientation && l.orientation !== 'n' ? `${l.orientation}` : ''}`)
|
||||
.join(' ');
|
||||
return `:md-deck[${state.rawSrc || state.src}]{size="${state.sizeW}x${state.sizeH}" grid="${state.gridW}x${state.gridH}" bleed="${state.bleed}" padding="${state.padding}" font-size="${state.fontSize}" layers="${layersStr}"}`;
|
||||
const layersStr = formatLayers(state.layerConfigs);
|
||||
return `:md-deck[${state.rawSrc || state.src}]{size="${state.sizeW}x${state.sizeH}" grid="${state.gridW}x${state.gridH}" bleed="${state.bleed}" padding="${state.padding}" layers="${layersStr}"}`;
|
||||
};
|
||||
|
||||
const copyCode = async () => {
|
||||
|
|
@ -326,7 +314,6 @@ export function createDeckStore(
|
|||
setGridH,
|
||||
setBleed,
|
||||
setPadding,
|
||||
setFontSize,
|
||||
setCards,
|
||||
setActiveTab,
|
||||
updateCardData,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ export interface DimensionOptions {
|
|||
padding: number;
|
||||
gridW: number;
|
||||
gridH: number;
|
||||
fontSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -41,7 +40,6 @@ export function calculateDimensions(options: DimensionOptions): Dimensions {
|
|||
gridH: options.gridH,
|
||||
gridOriginX,
|
||||
gridOriginY,
|
||||
fontSize: options.fontSize ?? 3
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import type { Layer, LayerConfig } from '../types';
|
||||
|
||||
/**
|
||||
* 解析 layers 字符串 "body:1,7-5,8 title:1,1-5,1" 或 "body:1,7-5,8,s title:1,1-5,1,e"
|
||||
* 解析 layers 字符串
|
||||
* 格式:body:1,7-5,8 title:1,1-4,1f6.6s
|
||||
* f[fontSize] 表示字体大小(可选),方向字母在最后(可选)
|
||||
*/
|
||||
export function parseLayers(layersStr: string): Layer[] {
|
||||
if (!layersStr) return [];
|
||||
|
||||
const layers: Layer[] = [];
|
||||
const regex = /(\w+):(\d+),(\d+)-(\d+),(\d+)([nsew])?/g;
|
||||
// 匹配:prop:x1,y1-x2,y2[ffontSize][direction]
|
||||
const regex = /(\w+):(\d+),(\d+)-(\d+),(\d+)(?:f([\d.]+))?([nsew])?/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(layersStr)) !== null) {
|
||||
|
|
@ -17,7 +20,8 @@ export function parseLayers(layersStr: string): Layer[] {
|
|||
y1: parseInt(match[3]),
|
||||
x2: parseInt(match[4]),
|
||||
y2: parseInt(match[5]),
|
||||
orientation: match[6] as 'n' | 's' | 'e' | 'w' | undefined
|
||||
fontSize: match[7] ? parseFloat(match[7]) : undefined,
|
||||
orientation: match[8] as 'n' | 's' | 'e' | 'w' | undefined
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -30,7 +34,16 @@ export function parseLayers(layersStr: string): Layer[] {
|
|||
export function formatLayers(layers: LayerConfig[]): string {
|
||||
return layers
|
||||
.filter(l => l.visible)
|
||||
.map(l => `${l.prop}:${l.x1},${l.y1}-${l.x2},${l.y2}${l.orientation && l.orientation !== 'n' ? `${l.orientation}` : ''}`)
|
||||
.map(l => {
|
||||
let str = `${l.prop}:${l.x1},${l.y1}-${l.x2},${l.y2}`;
|
||||
if (l.fontSize) {
|
||||
str += `f${l.fontSize}`;
|
||||
}
|
||||
if (l.orientation && l.orientation !== 'n') {
|
||||
str += l.orientation;
|
||||
}
|
||||
return str;
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +66,8 @@ export function initLayerConfigs(
|
|||
y1: existing?.y1 || 1,
|
||||
x2: existing?.x2 || 2,
|
||||
y2: existing?.y2 || 2,
|
||||
orientation: existing?.orientation
|
||||
orientation: existing?.orientation,
|
||||
fontSize: existing?.fontSize
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ export interface ExportOptions {
|
|||
gridOriginY: number;
|
||||
gridAreaWidth: number;
|
||||
gridAreaHeight: number;
|
||||
fontSize: number;
|
||||
visibleLayers: LayerConfig[];
|
||||
dimensions: Dimensions;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ interface DeckProps {
|
|||
gridH?: number;
|
||||
bleed?: number | string;
|
||||
padding?: number | string;
|
||||
fontSize?: number;
|
||||
layers?: string;
|
||||
fixed?: boolean | string;
|
||||
}
|
||||
|
|
@ -30,7 +29,6 @@ customElement<DeckProps>('md-deck', {
|
|||
gridH: 8,
|
||||
bleed: 1,
|
||||
padding: 2,
|
||||
fontSize: 3,
|
||||
layers: '',
|
||||
fixed: false
|
||||
}, (props, { element }) => {
|
||||
|
|
@ -87,12 +85,6 @@ customElement<DeckProps>('md-deck', {
|
|||
store.actions.setPadding(props.padding ?? 2);
|
||||
}
|
||||
|
||||
if (typeof props.fontSize === 'string') {
|
||||
store.actions.setFontSize(Number(props.fontSize));
|
||||
} else {
|
||||
store.actions.setFontSize(props.fontSize ?? 3);
|
||||
}
|
||||
|
||||
// 加载 CSV 数据
|
||||
store.actions.loadCardsFromPath(resolvedSrc, csvPath, (props.layers as string) || '');
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export interface Layer {
|
|||
x2: number;
|
||||
y2: number;
|
||||
orientation?: 'n' | 's' | 'e' | 'w';
|
||||
fontSize?: number;
|
||||
}
|
||||
|
||||
export interface LayerConfig {
|
||||
|
|
@ -19,6 +20,7 @@ export interface LayerConfig {
|
|||
x2: number;
|
||||
y2: number;
|
||||
orientation?: 'n' | 's' | 'e' | 'w';
|
||||
fontSize?: number;
|
||||
}
|
||||
|
||||
export interface Dimensions {
|
||||
|
|
@ -32,7 +34,6 @@ export interface Dimensions {
|
|||
gridH: number;
|
||||
gridOriginX: number;
|
||||
gridOriginY: number;
|
||||
fontSize: number;
|
||||
}
|
||||
|
||||
export interface SelectionState {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { customElement, noShadowDOM } from 'solid-element';
|
|||
import { createSignal, For, Show, createEffect, createMemo, createResource } from 'solid-js';
|
||||
import { marked } from '../markdown';
|
||||
import { resolvePath } from './utils/path';
|
||||
import { loadCSV } from './utils/csv-loader';
|
||||
import {loadCSV, CSV, processVariables} from './utils/csv-loader';
|
||||
|
||||
export interface TableProps {
|
||||
roll?: boolean;
|
||||
|
|
@ -17,7 +17,7 @@ interface TableRow {
|
|||
|
||||
customElement('md-table', { roll: false, remix: false }, (props, { element }) => {
|
||||
noShadowDOM();
|
||||
const [rows, setRows] = createSignal<TableRow[]>([]);
|
||||
const [rows, setRows] = createSignal<CSV<TableRow>>([]);
|
||||
const [activeTab, setActiveTab] = createSignal(0);
|
||||
const [activeGroup, setActiveGroup] = createSignal<string | null>(null);
|
||||
const [bodyHtml, setBodyHtml] = createSignal('');
|
||||
|
|
@ -78,21 +78,8 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
|
|||
|
||||
// 处理 body 内容中的 {{prop}} 语法并解析 markdown
|
||||
const processBody = (body: string, currentRow: TableRow): string => {
|
||||
let processedBody = body;
|
||||
|
||||
if (!props.remix) {
|
||||
// 不启用 remix 时,只替换当前行的引用
|
||||
processedBody = body.replace(/\{\{(\w+)\}\}/g, (_, key) => currentRow[key] || '');
|
||||
} else {
|
||||
// 启用 remix 时,每次引用使用随机行的内容
|
||||
processedBody = body.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
||||
const randomRow = rows()[Math.floor(Math.random() * rows().length)];
|
||||
return randomRow?.[key] || '';
|
||||
});
|
||||
}
|
||||
|
||||
// 使用 marked 解析 markdown
|
||||
return marked.parse(processedBody) as string;
|
||||
return marked.parse(processVariables(body, currentRow, rows(), filteredRows(), props.remix)) as string;
|
||||
};
|
||||
|
||||
// 更新 body 内容
|
||||
|
|
|
|||
|
|
@ -1,22 +1,61 @@
|
|||
import { parse } from 'csv-parse/browser/esm/sync';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
/**
|
||||
* 全局缓存已加载的 CSV 内容
|
||||
*/
|
||||
const csvCache = new Map<string, Record<string, string>[]>();
|
||||
|
||||
/**
|
||||
* 解析 front matter
|
||||
* @param content 包含 front matter 的内容
|
||||
* @returns 解析结果,包含 front matter 和剩余内容
|
||||
*/
|
||||
function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainingContent: string } {
|
||||
// 检查是否以 --- 开头
|
||||
if (!content.trim().startsWith('---')) {
|
||||
return { remainingContent: content };
|
||||
}
|
||||
|
||||
// 分割内容
|
||||
const parts = content.split(/(?:^|\n)---\s*\n/);
|
||||
|
||||
// 至少需要三个部分:空字符串、front matter、剩余内容
|
||||
if (parts.length < 3) {
|
||||
return { remainingContent: content };
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析 YAML front matter
|
||||
const frontmatterStr = parts[1].trim();
|
||||
const frontmatter = yaml.load(frontmatterStr) as JSONObject;
|
||||
|
||||
// 剩余内容是第三部分及之后的所有内容
|
||||
const remainingContent = parts.slice(2).join('---\n').trimStart();
|
||||
|
||||
return { frontmatter, remainingContent };
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse front matter:', error);
|
||||
return { remainingContent: content };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 CSV 文件
|
||||
* @template T 返回数据的类型,默认为 Record<string, string>
|
||||
*/
|
||||
export async function loadCSV<T = Record<string, string>>(path: string): Promise<T[]> {
|
||||
export async function loadCSV<T = Record<string, string>>(path: string): Promise<CSV<T>> {
|
||||
if (csvCache.has(path)) {
|
||||
return csvCache.get(path)! as T[];
|
||||
return csvCache.get(path)! as CSV<T>;
|
||||
}
|
||||
|
||||
const response = await fetch(path);
|
||||
const content = await response.text();
|
||||
const records = parse(content, {
|
||||
|
||||
// 解析 front matter
|
||||
const { frontmatter, remainingContent } = parseFrontMatter(content);
|
||||
|
||||
const records = parse(remainingContent, {
|
||||
columns: true,
|
||||
comment: '#',
|
||||
trim: true,
|
||||
|
|
@ -24,6 +63,36 @@ export async function loadCSV<T = Record<string, string>>(path: string): Promise
|
|||
});
|
||||
|
||||
const result = records as Record<string, string>[];
|
||||
// 添加 front matter 到结果中
|
||||
const csvResult = result as CSV<T>;
|
||||
if (frontmatter) {
|
||||
csvResult.frontmatter = frontmatter;
|
||||
}
|
||||
|
||||
csvCache.set(path, result);
|
||||
return result as T[];
|
||||
return csvResult;
|
||||
}
|
||||
|
||||
type JSONData = JSONArray | JSONObject | string | number | boolean | null;
|
||||
interface JSONArray extends Array<JSONData> {}
|
||||
interface JSONObject extends Record<string, JSONData> {}
|
||||
|
||||
export type CSV<T> = T[] & {
|
||||
frontmatter?: JSONObject;
|
||||
}
|
||||
|
||||
export function processVariables<T extends JSONObject> (body: string, currentRow: T, csv: CSV<T>, filtered?: T[], remix?: boolean): string {
|
||||
const rolled = filtered || csv;
|
||||
|
||||
function replaceProp(key: string) {
|
||||
const row = remix ?
|
||||
rolled[Math.floor(Math.random() * rolled.length)] :
|
||||
currentRow;
|
||||
const frontMatter = csv.frontmatter;
|
||||
if(key in row) return row[key];
|
||||
if(frontMatter && key in frontMatter) return frontMatter[key];
|
||||
return '';
|
||||
}
|
||||
|
||||
return body.replace(/\{\{(\w+)\}\}/g, (_, key) => `${replaceProp(key)}`);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { Marked } from 'marked';
|
||||
import {createDirectives, presetDirectiveConfigs} from 'marked-directive';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
// 使用 marked-directive 来支持指令语法
|
||||
const marked = new Marked().use(createDirectives([
|
||||
|
|
@ -23,7 +24,63 @@ const marked = new Marked().use(createDirectives([
|
|||
return false;
|
||||
}
|
||||
},
|
||||
]));
|
||||
]), {
|
||||
// 自定义代码块渲染器,支持 yaml/tag 格式
|
||||
extensions: [{
|
||||
name: 'code-block-yaml-tag',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
// 检测 ```yaml/tag 开头的代码块
|
||||
return src.match(/^```yaml\/tag\s*\n/m)?.index;
|
||||
},
|
||||
tokenizer(src: string) {
|
||||
const rule = /^```yaml\/tag\s*\n([\s\S]*?)\n```/;
|
||||
const match = rule.exec(src);
|
||||
if (match) {
|
||||
const yamlContent = match[1]?.trim() || '';
|
||||
const props = yaml.load(yamlContent) as Record<string, unknown> || {};
|
||||
|
||||
// 提取 tag 名称,默认为 tag-unknown
|
||||
const tagName = (props.tag as string) || 'tag-unknown';
|
||||
|
||||
// 移除 tag 属性,剩下的作为 HTML 属性
|
||||
const { tag, ...rest } = props;
|
||||
|
||||
// 提取 innerText 内容(如果有 body 字段)
|
||||
let content = '';
|
||||
if ('body' in rest) {
|
||||
content = String(rest.body || '');
|
||||
delete (rest as Record<string, unknown>).body;
|
||||
}
|
||||
|
||||
// 构建属性字符串
|
||||
const propsStr = Object.entries(rest)
|
||||
.map(([key, value]) => {
|
||||
const strValue = String(value);
|
||||
// 如果值包含空格或特殊字符,添加引号
|
||||
if (strValue.includes(' ') || strValue.includes('"')) {
|
||||
return `${key}="${strValue.replace(/"/g, '"')}"`;
|
||||
}
|
||||
return `${key}="${strValue}"`;
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
return {
|
||||
type: 'code-block-yaml-tag',
|
||||
raw: match[0],
|
||||
tagName,
|
||||
props: propsStr,
|
||||
content
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token: any) {
|
||||
// 渲染为自定义 HTML 标签
|
||||
const propsAttr = token.props ? ` ${token.props}` : '';
|
||||
return `<${token.tagName}${propsAttr}>${token.content || ''}</${token.tagName}>\n`;
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
export function parseMarkdown(content: string): string {
|
||||
return marked.parse(content.trimStart()) as string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue