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",
|
"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",
|
||||||
|
|
@ -2251,6 +2252,12 @@
|
||||||
"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",
|
||||||
|
|
@ -2669,6 +2676,18 @@
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -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': `${props.dimensions.fontSize}mm`
|
'font-size': `${layer.fontSize || 3}mm`
|
||||||
}}
|
}}
|
||||||
innerHTML={renderLayerContent(layer, props.cardData)}
|
innerHTML={renderLayerContent(layer, props.cardData)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ 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!
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
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>
|
||||||
|
|
@ -32,7 +39,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-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">
|
<div class="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
@ -42,6 +49,16 @@ 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'}
|
||||||
|
|
@ -54,16 +71,21 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</select>
|
</select>
|
||||||
<button
|
</div>
|
||||||
onClick={() => store.actions.setEditingLayer(store.state.editingLayer === layer.prop ? null : layer.prop)}
|
<div class="flex items-center gap-2">
|
||||||
class={`text-xs px-2 py-1 rounded cursor-pointer ${
|
<label class="text-xs text-gray-600">字体/mm</label>
|
||||||
store.state.editingLayer === layer.prop
|
<input
|
||||||
? 'bg-blue-500 text-white'
|
type="number"
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
value={layer.fontSize || ''}
|
||||||
}`}
|
placeholder="默认"
|
||||||
>
|
onChange={(e) => {
|
||||||
{store.state.editingLayer === layer.prop ? '✓ 框选' : '框选'}
|
const value = e.target.value;
|
||||||
</button>
|
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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ export interface PropertiesEditorPanelProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 卡牌属性编辑面板:尺寸、网格、字体、出血、内边距
|
* 卡牌属性编辑面板:尺寸、网格、出血、内边距
|
||||||
*/
|
*/
|
||||||
export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
|
export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
|
||||||
const { store } = props;
|
const { store } = props;
|
||||||
|
|
@ -56,17 +56,7 @@ 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"
|
||||||
|
|
@ -75,12 +65,6 @@ 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"
|
||||||
|
|
|
||||||
|
|
@ -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 } from './layer-parser';
|
import { initLayerConfigs, formatLayers } from './layer-parser';
|
||||||
import type { CardData, LayerConfig, Dimensions } from '../types';
|
import type { CardData, LayerConfig, Dimensions } from '../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -13,8 +13,7 @@ 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 {
|
||||||
|
|
@ -25,7 +24,6 @@ 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 路径(用于生成代码时保持相对路径)
|
||||||
|
|
@ -74,7 +72,6 @@ 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;
|
||||||
|
|
@ -138,7 +135,6 @@ 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,
|
||||||
|
|
@ -169,8 +165,7 @@ 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 });
|
||||||
};
|
};
|
||||||
|
|
@ -199,10 +194,6 @@ 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 });
|
||||||
|
|
@ -277,11 +268,8 @@ export function createDeckStore(
|
||||||
const clearError = () => setState({ error: null });
|
const clearError = () => setState({ error: null });
|
||||||
|
|
||||||
const generateCode = () => {
|
const generateCode = () => {
|
||||||
const layersStr = state.layerConfigs
|
const layersStr = formatLayers(state.layerConfigs);
|
||||||
.filter(l => l.visible)
|
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}"}`;
|
||||||
.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 () => {
|
||||||
|
|
@ -326,7 +314,6 @@ export function createDeckStore(
|
||||||
setGridH,
|
setGridH,
|
||||||
setBleed,
|
setBleed,
|
||||||
setPadding,
|
setPadding,
|
||||||
setFontSize,
|
|
||||||
setCards,
|
setCards,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
updateCardData,
|
updateCardData,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ export interface DimensionOptions {
|
||||||
padding: number;
|
padding: number;
|
||||||
gridW: number;
|
gridW: number;
|
||||||
gridH: number;
|
gridH: number;
|
||||||
fontSize?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -41,7 +40,6 @@ export function calculateDimensions(options: DimensionOptions): Dimensions {
|
||||||
gridH: options.gridH,
|
gridH: options.gridH,
|
||||||
gridOriginX,
|
gridOriginX,
|
||||||
gridOriginY,
|
gridOriginY,
|
||||||
fontSize: options.fontSize ?? 3
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
import type { Layer, LayerConfig } from '../types';
|
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[] {
|
export function parseLayers(layersStr: string): Layer[] {
|
||||||
if (!layersStr) return [];
|
if (!layersStr) return [];
|
||||||
|
|
||||||
const layers: Layer[] = [];
|
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;
|
let match;
|
||||||
|
|
||||||
while ((match = regex.exec(layersStr)) !== null) {
|
while ((match = regex.exec(layersStr)) !== null) {
|
||||||
|
|
@ -17,7 +20,8 @@ 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]),
|
||||||
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 {
|
export function formatLayers(layers: LayerConfig[]): string {
|
||||||
return layers
|
return layers
|
||||||
.filter(l => l.visible)
|
.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(' ');
|
.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +66,8 @@ 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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -30,7 +29,6 @@ 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 }) => {
|
||||||
|
|
@ -87,12 +85,6 @@ 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) || '');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ 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 {
|
||||||
|
|
@ -19,6 +20,7 @@ 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 {
|
||||||
|
|
@ -32,7 +34,6 @@ export interface Dimensions {
|
||||||
gridH: number;
|
gridH: number;
|
||||||
gridOriginX: number;
|
gridOriginX: number;
|
||||||
gridOriginY: number;
|
gridOriginY: number;
|
||||||
fontSize: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectionState {
|
export interface SelectionState {
|
||||||
|
|
|
||||||
|
|
@ -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 } from './utils/csv-loader';
|
import {loadCSV, CSV, processVariables} 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<TableRow[]>([]);
|
const [rows, setRows] = createSignal<CSV<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,21 +78,8 @@ 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(processedBody) as string;
|
return marked.parse(processVariables(body, currentRow, rows(), filteredRows(), props.remix)) as string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新 body 内容
|
// 更新 body 内容
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,61 @@
|
||||||
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<T[]> {
|
export async function loadCSV<T = Record<string, string>>(path: string): Promise<CSV<T>> {
|
||||||
if (csvCache.has(path)) {
|
if (csvCache.has(path)) {
|
||||||
return csvCache.get(path)! as T[];
|
return csvCache.get(path)! as CSV<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,
|
||||||
|
|
@ -24,6 +63,36 @@ 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 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 { 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([
|
||||||
|
|
@ -23,7 +24,63 @@ 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, '"')}"`;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue