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
|
||||
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)
|
||||
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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` 属性格式为 `字段:起始行,起始列-结束列,字号`,多个图层用空格分隔:
|
||||
|
||||
Reference in New Issue
Block a user