Compare commits

..

No commits in common. "5c26fa407d5350d188e1a45cc248c1aa331793d8" and "2b5bcfff9300e73e6686ab97621e1b812be7b78a" have entirely different histories.

16 changed files with 93 additions and 257 deletions

View File

@ -1,35 +0,0 @@
# 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"}

19
package-lock.json generated
View File

@ -13,7 +13,6 @@
"chokidar": "^5.0.0", "chokidar": "^5.0.0",
"commander": "^12.1.0", "commander": "^12.1.0",
"csv-parse": "^5.5.6", "csv-parse": "^5.5.6",
"js-yaml": "^4.1.1",
"marked": "^14.1.0", "marked": "^14.1.0",
"marked-directive": "^1.0.7", "marked-directive": "^1.0.7",
"solid-element": "^1.9.1", "solid-element": "^1.9.1",
@ -2252,12 +2251,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/attributes-parser": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/attributes-parser/-/attributes-parser-2.2.3.tgz", "resolved": "https://registry.npmjs.org/attributes-parser/-/attributes-parser-2.2.3.tgz",
@ -2676,18 +2669,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/jsesc": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",

View File

@ -34,7 +34,6 @@
"chokidar": "^5.0.0", "chokidar": "^5.0.0",
"commander": "^12.1.0", "commander": "^12.1.0",
"csv-parse": "^5.5.6", "csv-parse": "^5.5.6",
"js-yaml": "^4.1.1",
"marked": "^14.1.0", "marked": "^14.1.0",
"marked-directive": "^1.0.7", "marked-directive": "^1.0.7",
"solid-element": "^1.9.1", "solid-element": "^1.9.1",

View File

@ -36,7 +36,7 @@ export function CardLayer(props: CardLayerProps) {
class="absolute flex items-center justify-center text-center prose prose-sm" class="absolute flex items-center justify-center text-center prose prose-sm"
style={{ style={{
...getLayerStyle(layer, props.dimensions), ...getLayerStyle(layer, props.dimensions),
'font-size': `${layer.fontSize || 3}mm` 'font-size': `${props.dimensions.fontSize}mm`
}} }}
innerHTML={renderLayerContent(layer, props.cardData)} innerHTML={renderLayerContent(layer, props.cardData)}
/> />

View File

@ -31,6 +31,7 @@ export function PrintPreview(props: PrintPreviewProps) {
gridOriginY: store.state.dimensions?.gridOriginY || 0, gridOriginY: store.state.dimensions?.gridOriginY || 0,
gridAreaWidth: store.state.dimensions?.gridAreaWidth || 56, gridAreaWidth: store.state.dimensions?.gridAreaWidth || 56,
gridAreaHeight: store.state.dimensions?.gridAreaHeight || 88, gridAreaHeight: store.state.dimensions?.gridAreaHeight || 88,
fontSize: store.state.dimensions?.fontSize || 3,
visibleLayers: visibleLayers(), visibleLayers: visibleLayers(),
dimensions: store.state.dimensions! dimensions: store.state.dimensions!
}; };

View File

@ -25,13 +25,6 @@ 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 ( return (
<div class="w-64 flex-shrink-0"> <div class="w-64 flex-shrink-0">
<h3 class="font-bold mb-2 mt-0"></h3> <h3 class="font-bold mb-2 mt-0"></h3>
@ -39,7 +32,7 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
<div class="space-y-2"> <div class="space-y-2">
<For each={store.state.layerConfigs}> <For each={store.state.layerConfigs}>
{(layer) => ( {(layer) => (
<div class="flex flex-row flex-wrap gap-1 p-2 bg-gray-50 rounded"> <div class="flex flex-col gap-1 p-2 bg-gray-50 rounded">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
@ -49,16 +42,6 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
/> />
<span class="text-sm flex-1">{layer.prop}</span> <span class="text-sm flex-1">{layer.prop}</span>
</div> </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"> <div class="flex items-center gap-2">
<select <select
value={layer.orientation || 'n'} value={layer.orientation || 'n'}
@ -71,21 +54,16 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
)} )}
</For> </For>
</select> </select>
</div> <button
<div class="flex items-center gap-2"> onClick={() => store.actions.setEditingLayer(store.state.editingLayer === layer.prop ? null : layer.prop)}
<label class="text-xs text-gray-600">/mm</label> class={`text-xs px-2 py-1 rounded cursor-pointer ${
<input store.state.editingLayer === layer.prop
type="number" ? 'bg-blue-500 text-white'
value={layer.fontSize || ''} : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
placeholder="默认" }`}
onChange={(e) => { >
const value = e.target.value; {store.state.editingLayer === layer.prop ? '✓ 框选' : '框选'}
updateLayerFontSize(layer.prop, value ? Number(value) : undefined); </button>
}}
class="w-16 text-xs px-2 py-1 rounded border border-gray-300 bg-white"
step="0.1"
min="0.1"
/>
</div> </div>
</div> </div>
)} )}

View File

@ -5,7 +5,7 @@ export interface PropertiesEditorPanelProps {
} }
/** /**
* *
*/ */
export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) { export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
const { store } = props; const { store } = props;
@ -56,7 +56,17 @@ export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
</div> </div>
<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>
<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>
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
type="number" type="number"
@ -65,12 +75,18 @@ export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
onChange={(e) => store.actions.setBleed(Number(e.target.value))} onChange={(e) => store.actions.setBleed(Number(e.target.value))}
placeholder="出血" placeholder="出血"
/> />
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700"> (mm)</label>
<div class="flex gap-2">
<input <input
type="number" type="number"
class="w-full border border-gray-300 rounded px-2 py-1 text-sm" class="w-full border border-gray-300 rounded px-2 py-1 text-sm"
value={store.state.padding} value={store.state.padding}
onChange={(e) => store.actions.setPadding(Number(e.target.value))} onChange={(e) => store.actions.setPadding(Number(e.target.value))}
placeholder="内边距" placeholder="内边距"
/> />
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
import { createStore } from 'solid-js/store'; import { createStore } from 'solid-js/store';
import { calculateDimensions } from './dimensions'; import { calculateDimensions } from './dimensions';
import { loadCSV } from '../../utils/csv-loader'; import { loadCSV } from '../../utils/csv-loader';
import { initLayerConfigs, formatLayers } from './layer-parser'; import { initLayerConfigs } from './layer-parser';
import type { CardData, LayerConfig, Dimensions } from '../types'; import type { CardData, LayerConfig, Dimensions } from '../types';
/** /**
@ -13,7 +13,8 @@ export const DECK_DEFAULTS = {
GRID_W: 5, GRID_W: 5,
GRID_H: 8, GRID_H: 8,
BLEED: 1, BLEED: 1,
PADDING: 2 PADDING: 2,
FONT_SIZE: 3
} as const; } as const;
export interface DeckState { export interface DeckState {
@ -24,6 +25,7 @@ export interface DeckState {
gridH: number; gridH: number;
bleed: number; bleed: number;
padding: number; padding: number;
fontSize: number;
fixed: boolean; fixed: boolean;
src: string; src: string;
rawSrc: string; // 原始 CSV 路径(用于生成代码时保持相对路径) rawSrc: string; // 原始 CSV 路径(用于生成代码时保持相对路径)
@ -72,6 +74,7 @@ export interface DeckActions {
setGridH: (grid: number) => void; setGridH: (grid: number) => void;
setBleed: (bleed: number) => void; setBleed: (bleed: number) => void;
setPadding: (padding: number) => void; setPadding: (padding: number) => void;
setFontSize: (size: number) => void;
// 数据设置 // 数据设置
setCards: (cards: CardData[]) => void; setCards: (cards: CardData[]) => void;
@ -135,6 +138,7 @@ export function createDeckStore(
gridH: DECK_DEFAULTS.GRID_H, gridH: DECK_DEFAULTS.GRID_H,
bleed: DECK_DEFAULTS.BLEED, bleed: DECK_DEFAULTS.BLEED,
padding: DECK_DEFAULTS.PADDING, padding: DECK_DEFAULTS.PADDING,
fontSize: DECK_DEFAULTS.FONT_SIZE,
fixed: false, fixed: false,
src: initialSrc, src: initialSrc,
rawSrc: initialSrc, rawSrc: initialSrc,
@ -165,7 +169,8 @@ export function createDeckStore(
gridW: state.gridW, gridW: state.gridW,
gridH: state.gridH, gridH: state.gridH,
bleed: state.bleed, bleed: state.bleed,
padding: state.padding padding: state.padding,
fontSize: state.fontSize
}); });
setState({ dimensions: dims }); setState({ dimensions: dims });
}; };
@ -194,6 +199,10 @@ export function createDeckStore(
setState({ padding }); setState({ padding });
updateDimensions(); updateDimensions();
}; };
const setFontSize = (size: number) => {
setState({ fontSize: size });
updateDimensions();
};
const setCards = (cards: CardData[]) => setState({ cards, activeTab: 0 }); const setCards = (cards: CardData[]) => setState({ cards, activeTab: 0 });
const setActiveTab = (index: number) => setState({ activeTab: index }); const setActiveTab = (index: number) => setState({ activeTab: index });
@ -268,8 +277,11 @@ export function createDeckStore(
const clearError = () => setState({ error: null }); const clearError = () => setState({ error: null });
const generateCode = () => { const generateCode = () => {
const layersStr = formatLayers(state.layerConfigs); const layersStr = 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}"}`; .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 copyCode = async () => { const copyCode = async () => {
@ -314,6 +326,7 @@ export function createDeckStore(
setGridH, setGridH,
setBleed, setBleed,
setPadding, setPadding,
setFontSize,
setCards, setCards,
setActiveTab, setActiveTab,
updateCardData, updateCardData,

View File

@ -7,6 +7,7 @@ export interface DimensionOptions {
padding: number; padding: number;
gridW: number; gridW: number;
gridH: number; gridH: number;
fontSize?: number;
} }
/** /**
@ -40,6 +41,7 @@ export function calculateDimensions(options: DimensionOptions): Dimensions {
gridH: options.gridH, gridH: options.gridH,
gridOriginX, gridOriginX,
gridOriginY, gridOriginY,
fontSize: options.fontSize ?? 3
}; };
} }

View File

@ -1,16 +1,13 @@
import type { Layer, LayerConfig } from '../types'; import type { Layer, LayerConfig } from '../types';
/** /**
* layers * layers "body:1,7-5,8 title:1,1-5,1" "body:1,7-5,8,s title:1,1-5,1,e"
* body:1,7-5,8 title:1,1-4,1f6.6s
* f[fontSize]
*/ */
export function parseLayers(layersStr: string): Layer[] { export function parseLayers(layersStr: string): Layer[] {
if (!layersStr) return []; if (!layersStr) return [];
const layers: Layer[] = []; const layers: Layer[] = [];
// 匹配prop:x1,y1-x2,y2[ffontSize][direction] const regex = /(\w+):(\d+),(\d+)-(\d+),(\d+)([nsew])?/g;
const regex = /(\w+):(\d+),(\d+)-(\d+),(\d+)(?:f([\d.]+))?([nsew])?/g;
let match; let match;
while ((match = regex.exec(layersStr)) !== null) { while ((match = regex.exec(layersStr)) !== null) {
@ -20,8 +17,7 @@ export function parseLayers(layersStr: string): Layer[] {
y1: parseInt(match[3]), y1: parseInt(match[3]),
x2: parseInt(match[4]), x2: parseInt(match[4]),
y2: parseInt(match[5]), y2: parseInt(match[5]),
fontSize: match[7] ? parseFloat(match[7]) : undefined, orientation: match[6] as 'n' | 's' | 'e' | 'w' | undefined
orientation: match[8] as 'n' | 's' | 'e' | 'w' | undefined
}); });
} }
@ -34,16 +30,7 @@ export function parseLayers(layersStr: string): Layer[] {
export function formatLayers(layers: LayerConfig[]): string { export function formatLayers(layers: LayerConfig[]): string {
return layers return layers
.filter(l => l.visible) .filter(l => l.visible)
.map(l => { .map(l => `${l.prop}:${l.x1},${l.y1}-${l.x2},${l.y2}${l.orientation && l.orientation !== 'n' ? `${l.orientation}` : ''}`)
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(' '); .join(' ');
} }
@ -66,8 +53,7 @@ export function initLayerConfigs(
y1: existing?.y1 || 1, y1: existing?.y1 || 1,
x2: existing?.x2 || 2, x2: existing?.x2 || 2,
y2: existing?.y2 || 2, y2: existing?.y2 || 2,
orientation: existing?.orientation, orientation: existing?.orientation
fontSize: existing?.fontSize
}; };
}); });
} }

View File

@ -29,6 +29,7 @@ export interface ExportOptions {
gridOriginY: number; gridOriginY: number;
gridAreaWidth: number; gridAreaWidth: number;
gridAreaHeight: number; gridAreaHeight: number;
fontSize: number;
visibleLayers: LayerConfig[]; visibleLayers: LayerConfig[];
dimensions: Dimensions; dimensions: Dimensions;
} }

View File

@ -16,6 +16,7 @@ interface DeckProps {
gridH?: number; gridH?: number;
bleed?: number | string; bleed?: number | string;
padding?: number | string; padding?: number | string;
fontSize?: number;
layers?: string; layers?: string;
fixed?: boolean | string; fixed?: boolean | string;
} }
@ -29,6 +30,7 @@ customElement<DeckProps>('md-deck', {
gridH: 8, gridH: 8,
bleed: 1, bleed: 1,
padding: 2, padding: 2,
fontSize: 3,
layers: '', layers: '',
fixed: false fixed: false
}, (props, { element }) => { }, (props, { element }) => {
@ -85,6 +87,12 @@ customElement<DeckProps>('md-deck', {
store.actions.setPadding(props.padding ?? 2); 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 数据 // 加载 CSV 数据
store.actions.loadCardsFromPath(resolvedSrc, csvPath, (props.layers as string) || ''); store.actions.loadCardsFromPath(resolvedSrc, csvPath, (props.layers as string) || '');

View File

@ -9,7 +9,6 @@ export interface Layer {
x2: number; x2: number;
y2: number; y2: number;
orientation?: 'n' | 's' | 'e' | 'w'; orientation?: 'n' | 's' | 'e' | 'w';
fontSize?: number;
} }
export interface LayerConfig { export interface LayerConfig {
@ -20,7 +19,6 @@ export interface LayerConfig {
x2: number; x2: number;
y2: number; y2: number;
orientation?: 'n' | 's' | 'e' | 'w'; orientation?: 'n' | 's' | 'e' | 'w';
fontSize?: number;
} }
export interface Dimensions { export interface Dimensions {
@ -34,6 +32,7 @@ export interface Dimensions {
gridH: number; gridH: number;
gridOriginX: number; gridOriginX: number;
gridOriginY: number; gridOriginY: number;
fontSize: number;
} }
export interface SelectionState { export interface SelectionState {

View File

@ -2,7 +2,7 @@ import { customElement, noShadowDOM } from 'solid-element';
import { createSignal, For, Show, createEffect, createMemo, createResource } from 'solid-js'; import { createSignal, For, Show, createEffect, createMemo, createResource } from 'solid-js';
import { marked } from '../markdown'; import { marked } from '../markdown';
import { resolvePath } from './utils/path'; import { resolvePath } from './utils/path';
import {loadCSV, CSV, processVariables} from './utils/csv-loader'; import { loadCSV } from './utils/csv-loader';
export interface TableProps { export interface TableProps {
roll?: boolean; roll?: boolean;
@ -17,7 +17,7 @@ interface TableRow {
customElement('md-table', { roll: false, remix: false }, (props, { element }) => { customElement('md-table', { roll: false, remix: false }, (props, { element }) => {
noShadowDOM(); noShadowDOM();
const [rows, setRows] = createSignal<CSV<TableRow>>([]); const [rows, setRows] = createSignal<TableRow[]>([]);
const [activeTab, setActiveTab] = createSignal(0); const [activeTab, setActiveTab] = createSignal(0);
const [activeGroup, setActiveGroup] = createSignal<string | null>(null); const [activeGroup, setActiveGroup] = createSignal<string | null>(null);
const [bodyHtml, setBodyHtml] = createSignal(''); const [bodyHtml, setBodyHtml] = createSignal('');
@ -78,8 +78,21 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
// 处理 body 内容中的 {{prop}} 语法并解析 markdown // 处理 body 内容中的 {{prop}} 语法并解析 markdown
const processBody = (body: string, currentRow: TableRow): string => { 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 // 使用 marked 解析 markdown
return marked.parse(processVariables(body, currentRow, rows(), filteredRows(), props.remix)) as string; return marked.parse(processedBody) as string;
}; };
// 更新 body 内容 // 更新 body 内容

View File

@ -1,61 +1,22 @@
import { parse } from 'csv-parse/browser/esm/sync'; import { parse } from 'csv-parse/browser/esm/sync';
import yaml from 'js-yaml';
/** /**
* CSV * CSV
*/ */
const csvCache = new Map<string, Record<string, string>[]>(); 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 * CSV
* @template T Record<string, string> * @template T Record<string, string>
*/ */
export async function loadCSV<T = Record<string, string>>(path: string): Promise<CSV<T>> { export async function loadCSV<T = Record<string, string>>(path: string): Promise<T[]> {
if (csvCache.has(path)) { if (csvCache.has(path)) {
return csvCache.get(path)! as CSV<T>; return csvCache.get(path)! as T[];
} }
const response = await fetch(path); const response = await fetch(path);
const content = await response.text(); const content = await response.text();
const records = parse(content, {
// 解析 front matter
const { frontmatter, remainingContent } = parseFrontMatter(content);
const records = parse(remainingContent, {
columns: true, columns: true,
comment: '#', comment: '#',
trim: true, trim: true,
@ -63,36 +24,6 @@ export async function loadCSV<T = Record<string, string>>(path: string): Promise
}); });
const result = records as Record<string, string>[]; const result = records as Record<string, string>[];
// 添加 front matter 到结果中
const csvResult = result as CSV<T>;
if (frontmatter) {
csvResult.frontmatter = frontmatter;
}
csvCache.set(path, result); csvCache.set(path, result);
return csvResult; return result as T[];
}
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)}`);
} }

View File

@ -1,6 +1,5 @@
import { Marked } from 'marked'; import { Marked } from 'marked';
import {createDirectives, presetDirectiveConfigs} from 'marked-directive'; import {createDirectives, presetDirectiveConfigs} from 'marked-directive';
import yaml from 'js-yaml';
// 使用 marked-directive 来支持指令语法 // 使用 marked-directive 来支持指令语法
const marked = new Marked().use(createDirectives([ const marked = new Marked().use(createDirectives([
@ -24,63 +23,7 @@ const marked = new Marked().use(createDirectives([
return false; 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, '&quot;')}"`;
}
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 { export function parseMarkdown(content: string): string {
return marked.parse(content.trimStart()) as string; return marked.parse(content.trimStart()) as string;