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:
2026-09-02 18:30:32 +08:00
parent 16dfc8a88c
commit f1ddc8fb8e
7 changed files with 100 additions and 33 deletions
+3 -9
View File
@@ -436,24 +436,18 @@ data-config:
grid: 5x5
layers:
- prop: title
x1: 1
y1: 1
x2: 5
y2: 1
pos: 1,1-5,1
font: 12
- template: |
**{{name}}** — {{type}}
{{description}}
x1: 1
y1: 3
x2: 5
y2: 8
pos: 1,3-5,8
font: 3
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)
+10 -4
View File
@@ -18,7 +18,7 @@ describe("layersToConfigs", () => {
test("parses structured list with prop layers", () => {
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[0]).toMatchObject({
@@ -35,7 +35,7 @@ describe("layersToConfigs", () => {
test("parses structured list with template layers", () => {
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[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", () => {
expect(layersToConfigs()).toEqual([]);
expect(layersToConfigs("")).toEqual([]);
@@ -81,8 +87,8 @@ describe("normalizeDeckConfig", () => {
test("normalizes layers and back_layers", () => {
const cfg = normalizeDeckConfig({
layers: [
{ prop: "name", x1: 1, y1: 1, x2: 5, y2: 1 },
{ template: "{{body}}", x1: 1, y1: 2, x2: 5, y2: 8 },
{ prop: "name", pos: "1,1-5,1" },
{ template: "{{body}}", pos: "1,2-5,8" },
],
back_layers: "logo:1,1-2,2",
});
+22 -8
View File
@@ -22,10 +22,8 @@ export interface DeckConfigYaml {
export interface DeckLayerYaml {
prop?: string;
template?: string;
x1?: number;
y1?: number;
x2?: number;
y2?: number;
/** Grid placement "x1,y1-x2,y2" (1-based), same shape as the compact layers string. */
pos?: string;
font?: number;
fontSize?: number;
orientation?: "n" | "s" | "e" | "w";
@@ -70,16 +68,32 @@ function parseDimension(
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. */
function layerYamlToConfig(l: DeckLayerYaml): LayerConfig {
const pos = parsePos(l.pos);
return {
prop: l.prop,
template: l.template,
visible: l.visible ?? true,
x1: l.x1 ?? 1,
y1: l.y1 ?? 1,
x2: l.x2 ?? 2,
y2: l.y2 ?? 2,
x1: pos?.x1 ?? 1,
y1: pos?.y1 ?? 1,
x2: pos?.x2 ?? 2,
y2: pos?.y2 ?? 2,
orientation: l.orientation,
fontSize: l.fontSize ?? l.font,
align: l.align,
+55
View File
@@ -1,4 +1,5 @@
import { createStore } from "solid-js/store";
import yaml from "js-yaml";
import { calculateDimensions } from "./dimensions";
import { loadCSV, CSV } from "../../utils/csv-loader";
import { formatLayers } from "./layer-parser";
@@ -41,6 +42,8 @@ export interface DeckState {
cornerRadius: number;
shape: CardShape;
fixed: boolean;
/** True when the deck was configured via a yaml/tag codeblock (data-config). */
isYamlBlock: boolean;
src: string;
rawSrc: string;
@@ -85,6 +88,7 @@ export interface DeckActions {
setPadding: (padding: number) => void;
setCornerRadius: (cornerRadius: number) => void;
setShape: (shape: CardShape) => void;
setIsYamlBlock: (isYamlBlock: boolean) => void;
setCards: (cards: CSV<CardData>) => void;
setActiveTab: (index: number) => void;
@@ -176,6 +180,7 @@ export function createDeckStore(initialSrc: string = ""): DeckStore {
cornerRadius: DECK_DEFAULTS.CORNER_RADIUS,
shape: "rectangle",
fixed: false,
isYamlBlock: false,
src: initialSrc,
rawSrc: initialSrc,
dimensions: null,
@@ -244,6 +249,9 @@ export function createDeckStore(initialSrc: string = ""): DeckStore {
const setShape = (shape: CardShape) => {
setState({ shape });
};
const setIsYamlBlock = (isYamlBlock: boolean) => {
setState({ isYamlBlock });
};
const setCards = (cards: CSV<CardData>) => setState({ cards, activeTab: 0 });
const setActiveTab = (index: number) => setState({ activeTab: index });
@@ -483,6 +491,9 @@ export function createDeckStore(initialSrc: string = ""): DeckStore {
const clearError = () => setState({ error: null });
const generateCode = (backLayersStr?: string) => {
if (state.isYamlBlock) {
return generateYamlCode();
}
const frontLayersStr = formatLayers(state.frontLayerConfigs);
const backLayersString =
backLayersStr || formatLayers(state.backLayerConfigs);
@@ -510,6 +521,49 @@ export function createDeckStore(initialSrc: string = ""): DeckStore {
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 code = generateCode();
try {
@@ -569,6 +623,7 @@ export function createDeckStore(initialSrc: string = ""): DeckStore {
setPadding,
setCornerRadius,
setShape,
setIsYamlBlock,
setCards,
setActiveTab,
updateCardData,
+2
View File
@@ -77,6 +77,8 @@ customElement<DeckProps>(
if (dataConfigAttr) {
try {
config = normalizeDeckConfig(JSON.parse(dataConfigAttr));
// 记录来源,复制代码时输出 yaml/tag 代码块
store.actions.setIsYamlBlock(true);
} catch (e) {
console.error("Invalid data-config on md-deck:", e);
}
+4 -4
View File
@@ -26,10 +26,10 @@ export interface LayerConfig {
/** Markdown template (with {{var}} substitution) when the layer renders a template. */
template?: string;
visible: boolean;
x1?: number;
y1?: number;
x2?: number;
y2?: number;
x1: number;
y1: number;
x2: number;
y2: number;
orientation?: "n" | "s" | "e" | "w";
fontSize?: number;
align?: "l" | "c" | "r";
+4 -8
View File
@@ -29,23 +29,19 @@ data-config:
grid: 5x5
layers:
- prop: title
x1: 1
y1: 1
x2: 5
y2: 1
pos: 1,1-5,1
font: 12
- template: |
**{{name}}** — {{type}}
{{description}}
x1: 1
y1: 3
x2: 5
y2: 8
pos: 1,3-5,8
font: 3
align: l
```
````
`pos` 为网格位置 `x1,y1-x2,y2`1-based,与紧凑 layers 字符串同格式)。
## 图层格式
`layers` 属性格式为 `字段:起始行,起始列-结束列,字号`,多个图层用空格分隔: