diff --git a/docs/markdown.md b/docs/markdown.md index eb43cfa..5afa1f6 100644 --- a/docs/markdown.md +++ b/docs/markdown.md @@ -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) diff --git a/src/components/md-deck/config.test.ts b/src/components/md-deck/config.test.ts index 314d558..5e7f30c 100644 --- a/src/components/md-deck/config.test.ts +++ b/src/components/md-deck/config.test.ts @@ -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", }); diff --git a/src/components/md-deck/config.ts b/src/components/md-deck/config.ts index 6013707..2ffd8d1 100644 --- a/src/components/md-deck/config.ts +++ b/src/components/md-deck/config.ts @@ -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, diff --git a/src/components/md-deck/hooks/deckStore.ts b/src/components/md-deck/hooks/deckStore.ts index 6b8546d..ad46a74 100644 --- a/src/components/md-deck/hooks/deckStore.ts +++ b/src/components/md-deck/hooks/deckStore.ts @@ -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) => 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) => 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 = {}; + 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 = { + 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, diff --git a/src/components/md-deck/index.tsx b/src/components/md-deck/index.tsx index 7219622..98cd5ff 100644 --- a/src/components/md-deck/index.tsx +++ b/src/components/md-deck/index.tsx @@ -77,6 +77,8 @@ customElement( 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); } diff --git a/src/components/md-deck/types.ts b/src/components/md-deck/types.ts index b9d3f17..8cf418e 100644 --- a/src/components/md-deck/types.ts +++ b/src/components/md-deck/types.ts @@ -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"; diff --git a/src/doc-entries/md-deck.md b/src/doc-entries/md-deck.md index e4c41b6..80a2a47 100644 --- a/src/doc-entries/md-deck.md +++ b/src/doc-entries/md-deck.md @@ -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` 属性格式为 `字段:起始行,起始列-结束列,字号`,多个图层用空格分隔: