refactor(md-deck): pos string and yaml copy output
Replace per-axis x1/y1/x2/y2 layer fields with a single pos string (x1,y1-x2,y2) matching the compact layers format. When a deck is configured via a yaml/tag codeblock (data-config), the copy button now emits a yaml/tag block serialized from current store state so template layers round-trip.
This commit is contained in:
+3
-9
@@ -436,24 +436,18 @@ data-config:
|
|||||||
grid: 5x5
|
grid: 5x5
|
||||||
layers:
|
layers:
|
||||||
- prop: title
|
- prop: title
|
||||||
x1: 1
|
pos: 1,1-5,1
|
||||||
y1: 1
|
|
||||||
x2: 5
|
|
||||||
y2: 1
|
|
||||||
font: 12
|
font: 12
|
||||||
- template: |
|
- template: |
|
||||||
**{{name}}** — {{type}}
|
**{{name}}** — {{type}}
|
||||||
{{description}}
|
{{description}}
|
||||||
x1: 1
|
pos: 1,3-5,8
|
||||||
y1: 3
|
|
||||||
x2: 5
|
|
||||||
y2: 8
|
|
||||||
font: 3
|
font: 3
|
||||||
align: l
|
align: l
|
||||||
```
|
```
|
||||||
````
|
````
|
||||||
|
|
||||||
每个图层通过 `prop`(读取 CSV 字段)或 `template`(渲染带 `{{变量}}` 替换的 markdown 模板)定义,支持 `x1/y1/x2/y2`、`font`、`orientation`、`align`。`back_layers` 用于背面图层。
|
每个图层通过 `prop`(读取 CSV 字段)或 `template`(渲染带 `{{变量}}` 替换的 markdown 模板)定义,`pos` 为网格位置 `x1,y1-x2,y2`(1-based,与紧凑 layers 字符串同格式),支持 `font`、`orientation`、`align`。`back_layers` 用于背面图层。
|
||||||
|
|
||||||
### 🧶 叙事线组件 (md-yarn-spinner)
|
### 🧶 叙事线组件 (md-yarn-spinner)
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe("layersToConfigs", () => {
|
|||||||
|
|
||||||
test("parses structured list with prop layers", () => {
|
test("parses structured list with prop layers", () => {
|
||||||
const layers = layersToConfigs([
|
const layers = layersToConfigs([
|
||||||
{ prop: "name", x1: 1, y1: 1, x2: 5, y2: 2, font: 12 },
|
{ prop: "name", pos: "1,1-5,2", font: 12 },
|
||||||
]);
|
]);
|
||||||
expect(layers).toHaveLength(1);
|
expect(layers).toHaveLength(1);
|
||||||
expect(layers[0]).toMatchObject({
|
expect(layers[0]).toMatchObject({
|
||||||
@@ -35,7 +35,7 @@ describe("layersToConfigs", () => {
|
|||||||
|
|
||||||
test("parses structured list with template layers", () => {
|
test("parses structured list with template layers", () => {
|
||||||
const layers = layersToConfigs([
|
const layers = layersToConfigs([
|
||||||
{ template: "**{{name}}**", x1: 1, y1: 3, x2: 5, y2: 8, align: "l" },
|
{ template: "**{{name}}**", pos: "1,3-5,8", align: "l" },
|
||||||
]);
|
]);
|
||||||
expect(layers).toHaveLength(1);
|
expect(layers).toHaveLength(1);
|
||||||
expect(layers[0]).toMatchObject({
|
expect(layers[0]).toMatchObject({
|
||||||
@@ -50,6 +50,12 @@ describe("layersToConfigs", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("defaults placement when pos is missing or invalid", () => {
|
||||||
|
const layers = layersToConfigs([{ prop: "x" }, { prop: "y", pos: "bogus" }]);
|
||||||
|
expect(layers[0]).toMatchObject({ x1: 1, y1: 1, x2: 2, y2: 2 });
|
||||||
|
expect(layers[1]).toMatchObject({ x1: 1, y1: 1, x2: 2, y2: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
test("returns [] for absent/empty input", () => {
|
test("returns [] for absent/empty input", () => {
|
||||||
expect(layersToConfigs()).toEqual([]);
|
expect(layersToConfigs()).toEqual([]);
|
||||||
expect(layersToConfigs("")).toEqual([]);
|
expect(layersToConfigs("")).toEqual([]);
|
||||||
@@ -81,8 +87,8 @@ describe("normalizeDeckConfig", () => {
|
|||||||
test("normalizes layers and back_layers", () => {
|
test("normalizes layers and back_layers", () => {
|
||||||
const cfg = normalizeDeckConfig({
|
const cfg = normalizeDeckConfig({
|
||||||
layers: [
|
layers: [
|
||||||
{ prop: "name", x1: 1, y1: 1, x2: 5, y2: 1 },
|
{ prop: "name", pos: "1,1-5,1" },
|
||||||
{ template: "{{body}}", x1: 1, y1: 2, x2: 5, y2: 8 },
|
{ template: "{{body}}", pos: "1,2-5,8" },
|
||||||
],
|
],
|
||||||
back_layers: "logo:1,1-2,2",
|
back_layers: "logo:1,1-2,2",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,10 +22,8 @@ export interface DeckConfigYaml {
|
|||||||
export interface DeckLayerYaml {
|
export interface DeckLayerYaml {
|
||||||
prop?: string;
|
prop?: string;
|
||||||
template?: string;
|
template?: string;
|
||||||
x1?: number;
|
/** Grid placement "x1,y1-x2,y2" (1-based), same shape as the compact layers string. */
|
||||||
y1?: number;
|
pos?: string;
|
||||||
x2?: number;
|
|
||||||
y2?: number;
|
|
||||||
font?: number;
|
font?: number;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
orientation?: "n" | "s" | "e" | "w";
|
orientation?: "n" | "s" | "e" | "w";
|
||||||
@@ -70,16 +68,32 @@ function parseDimension(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Parse a "x1,y1-x2,y2" placement string into grid coordinates. */
|
||||||
|
export function parsePos(
|
||||||
|
pos?: string,
|
||||||
|
): { x1: number; y1: number; x2: number; y2: number } | undefined {
|
||||||
|
if (!pos) return undefined;
|
||||||
|
const m = /^(\d+)\s*,\s*(\d+)\s*-\s*(\d+)\s*,\s*(\d+)$/.exec(pos.trim());
|
||||||
|
if (!m) return undefined;
|
||||||
|
return {
|
||||||
|
x1: Number(m[1]),
|
||||||
|
y1: Number(m[2]),
|
||||||
|
x2: Number(m[3]),
|
||||||
|
y2: Number(m[4]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Map a structured YAML layer to a LayerConfig. */
|
/** Map a structured YAML layer to a LayerConfig. */
|
||||||
function layerYamlToConfig(l: DeckLayerYaml): LayerConfig {
|
function layerYamlToConfig(l: DeckLayerYaml): LayerConfig {
|
||||||
|
const pos = parsePos(l.pos);
|
||||||
return {
|
return {
|
||||||
prop: l.prop,
|
prop: l.prop,
|
||||||
template: l.template,
|
template: l.template,
|
||||||
visible: l.visible ?? true,
|
visible: l.visible ?? true,
|
||||||
x1: l.x1 ?? 1,
|
x1: pos?.x1 ?? 1,
|
||||||
y1: l.y1 ?? 1,
|
y1: pos?.y1 ?? 1,
|
||||||
x2: l.x2 ?? 2,
|
x2: pos?.x2 ?? 2,
|
||||||
y2: l.y2 ?? 2,
|
y2: pos?.y2 ?? 2,
|
||||||
orientation: l.orientation,
|
orientation: l.orientation,
|
||||||
fontSize: l.fontSize ?? l.font,
|
fontSize: l.fontSize ?? l.font,
|
||||||
align: l.align,
|
align: l.align,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
|
import yaml from "js-yaml";
|
||||||
import { calculateDimensions } from "./dimensions";
|
import { calculateDimensions } from "./dimensions";
|
||||||
import { loadCSV, CSV } from "../../utils/csv-loader";
|
import { loadCSV, CSV } from "../../utils/csv-loader";
|
||||||
import { formatLayers } from "./layer-parser";
|
import { formatLayers } from "./layer-parser";
|
||||||
@@ -41,6 +42,8 @@ export interface DeckState {
|
|||||||
cornerRadius: number;
|
cornerRadius: number;
|
||||||
shape: CardShape;
|
shape: CardShape;
|
||||||
fixed: boolean;
|
fixed: boolean;
|
||||||
|
/** True when the deck was configured via a yaml/tag codeblock (data-config). */
|
||||||
|
isYamlBlock: boolean;
|
||||||
src: string;
|
src: string;
|
||||||
rawSrc: string;
|
rawSrc: string;
|
||||||
|
|
||||||
@@ -85,6 +88,7 @@ export interface DeckActions {
|
|||||||
setPadding: (padding: number) => void;
|
setPadding: (padding: number) => void;
|
||||||
setCornerRadius: (cornerRadius: number) => void;
|
setCornerRadius: (cornerRadius: number) => void;
|
||||||
setShape: (shape: CardShape) => void;
|
setShape: (shape: CardShape) => void;
|
||||||
|
setIsYamlBlock: (isYamlBlock: boolean) => void;
|
||||||
|
|
||||||
setCards: (cards: CSV<CardData>) => void;
|
setCards: (cards: CSV<CardData>) => void;
|
||||||
setActiveTab: (index: number) => void;
|
setActiveTab: (index: number) => void;
|
||||||
@@ -176,6 +180,7 @@ export function createDeckStore(initialSrc: string = ""): DeckStore {
|
|||||||
cornerRadius: DECK_DEFAULTS.CORNER_RADIUS,
|
cornerRadius: DECK_DEFAULTS.CORNER_RADIUS,
|
||||||
shape: "rectangle",
|
shape: "rectangle",
|
||||||
fixed: false,
|
fixed: false,
|
||||||
|
isYamlBlock: false,
|
||||||
src: initialSrc,
|
src: initialSrc,
|
||||||
rawSrc: initialSrc,
|
rawSrc: initialSrc,
|
||||||
dimensions: null,
|
dimensions: null,
|
||||||
@@ -244,6 +249,9 @@ export function createDeckStore(initialSrc: string = ""): DeckStore {
|
|||||||
const setShape = (shape: CardShape) => {
|
const setShape = (shape: CardShape) => {
|
||||||
setState({ shape });
|
setState({ shape });
|
||||||
};
|
};
|
||||||
|
const setIsYamlBlock = (isYamlBlock: boolean) => {
|
||||||
|
setState({ isYamlBlock });
|
||||||
|
};
|
||||||
|
|
||||||
const setCards = (cards: CSV<CardData>) => setState({ cards, activeTab: 0 });
|
const setCards = (cards: CSV<CardData>) => setState({ cards, activeTab: 0 });
|
||||||
const setActiveTab = (index: number) => setState({ activeTab: index });
|
const setActiveTab = (index: number) => setState({ activeTab: index });
|
||||||
@@ -483,6 +491,9 @@ export function createDeckStore(initialSrc: string = ""): DeckStore {
|
|||||||
const clearError = () => setState({ error: null });
|
const clearError = () => setState({ error: null });
|
||||||
|
|
||||||
const generateCode = (backLayersStr?: string) => {
|
const generateCode = (backLayersStr?: string) => {
|
||||||
|
if (state.isYamlBlock) {
|
||||||
|
return generateYamlCode();
|
||||||
|
}
|
||||||
const frontLayersStr = formatLayers(state.frontLayerConfigs);
|
const frontLayersStr = formatLayers(state.frontLayerConfigs);
|
||||||
const backLayersString =
|
const backLayersString =
|
||||||
backLayersStr || formatLayers(state.backLayerConfigs);
|
backLayersStr || formatLayers(state.backLayerConfigs);
|
||||||
@@ -510,6 +521,49 @@ export function createDeckStore(initialSrc: string = ""): DeckStore {
|
|||||||
return parts.join("");
|
return parts.join("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Serialize the deck back to a ```yaml/tag codeblock (round-trips templates). */
|
||||||
|
const generateYamlCode = () => {
|
||||||
|
const toYamlLayer = (l: LayerConfig) => {
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
if (l.template) {
|
||||||
|
out.template = l.template;
|
||||||
|
} else {
|
||||||
|
out.prop = l.prop ?? "";
|
||||||
|
}
|
||||||
|
out.pos = `${l.x1},${l.y1}-${l.x2},${l.y2}`;
|
||||||
|
if (l.fontSize) out.font = l.fontSize;
|
||||||
|
if (l.orientation && l.orientation !== "n") out.orientation = l.orientation;
|
||||||
|
if (l.align) out.align = l.align;
|
||||||
|
if (!l.visible) out.visible = false;
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config: Record<string, unknown> = {
|
||||||
|
size: `${state.sizeW}x${state.sizeH}`,
|
||||||
|
grid: `${state.gridW}x${state.gridH}`,
|
||||||
|
layers: state.frontLayerConfigs.map(toYamlLayer),
|
||||||
|
};
|
||||||
|
if (state.bleed !== DECK_DEFAULTS.BLEED) config.bleed = state.bleed;
|
||||||
|
if (state.padding !== DECK_DEFAULTS.PADDING) config.padding = state.padding;
|
||||||
|
if (state.shape !== "rectangle") config.shape = state.shape;
|
||||||
|
if (state.backLayerConfigs.length > 0) {
|
||||||
|
config.back_layers = state.backLayerConfigs.map(toYamlLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = {
|
||||||
|
tag: "md-deck",
|
||||||
|
body: state.rawSrc || state.src,
|
||||||
|
"data-config": config,
|
||||||
|
};
|
||||||
|
const yamlStr = yaml.dump(doc, {
|
||||||
|
indent: 2,
|
||||||
|
lineWidth: -1,
|
||||||
|
noRefs: true,
|
||||||
|
});
|
||||||
|
const fence = "```";
|
||||||
|
return `${fence}yaml/tag\n${yamlStr}${fence}`;
|
||||||
|
};
|
||||||
|
|
||||||
const copyCode = async (fallback?: (code: string) => void) => {
|
const copyCode = async (fallback?: (code: string) => void) => {
|
||||||
const code = generateCode();
|
const code = generateCode();
|
||||||
try {
|
try {
|
||||||
@@ -569,6 +623,7 @@ export function createDeckStore(initialSrc: string = ""): DeckStore {
|
|||||||
setPadding,
|
setPadding,
|
||||||
setCornerRadius,
|
setCornerRadius,
|
||||||
setShape,
|
setShape,
|
||||||
|
setIsYamlBlock,
|
||||||
setCards,
|
setCards,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
updateCardData,
|
updateCardData,
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ customElement<DeckProps>(
|
|||||||
if (dataConfigAttr) {
|
if (dataConfigAttr) {
|
||||||
try {
|
try {
|
||||||
config = normalizeDeckConfig(JSON.parse(dataConfigAttr));
|
config = normalizeDeckConfig(JSON.parse(dataConfigAttr));
|
||||||
|
// 记录来源,复制代码时输出 yaml/tag 代码块
|
||||||
|
store.actions.setIsYamlBlock(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Invalid data-config on md-deck:", e);
|
console.error("Invalid data-config on md-deck:", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ export interface LayerConfig {
|
|||||||
/** Markdown template (with {{var}} substitution) when the layer renders a template. */
|
/** Markdown template (with {{var}} substitution) when the layer renders a template. */
|
||||||
template?: string;
|
template?: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
x1?: number;
|
x1: number;
|
||||||
y1?: number;
|
y1: number;
|
||||||
x2?: number;
|
x2: number;
|
||||||
y2?: number;
|
y2: number;
|
||||||
orientation?: "n" | "s" | "e" | "w";
|
orientation?: "n" | "s" | "e" | "w";
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
align?: "l" | "c" | "r";
|
align?: "l" | "c" | "r";
|
||||||
|
|||||||
@@ -29,23 +29,19 @@ data-config:
|
|||||||
grid: 5x5
|
grid: 5x5
|
||||||
layers:
|
layers:
|
||||||
- prop: title
|
- prop: title
|
||||||
x1: 1
|
pos: 1,1-5,1
|
||||||
y1: 1
|
|
||||||
x2: 5
|
|
||||||
y2: 1
|
|
||||||
font: 12
|
font: 12
|
||||||
- template: |
|
- template: |
|
||||||
**{{name}}** — {{type}}
|
**{{name}}** — {{type}}
|
||||||
{{description}}
|
{{description}}
|
||||||
x1: 1
|
pos: 1,3-5,8
|
||||||
y1: 3
|
|
||||||
x2: 5
|
|
||||||
y2: 8
|
|
||||||
font: 3
|
font: 3
|
||||||
align: l
|
align: l
|
||||||
```
|
```
|
||||||
````
|
````
|
||||||
|
|
||||||
|
`pos` 为网格位置 `x1,y1-x2,y2`(1-based,与紧凑 layers 字符串同格式)。
|
||||||
|
|
||||||
## 图层格式
|
## 图层格式
|
||||||
|
|
||||||
`layers` 属性格式为 `字段:起始行,起始列-结束列,字号`,多个图层用空格分隔:
|
`layers` 属性格式为 `字段:起始行,起始列-结束列,字号`,多个图层用空格分隔:
|
||||||
|
|||||||
Reference in New Issue
Block a user