@stonedeck/core 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/emitter.d.ts +6 -0
- package/dist/emitter.d.ts.map +1 -0
- package/dist/emitter.js +107 -0
- package/dist/emitter.js.map +1 -0
- package/dist/emitter.test.d.ts +2 -0
- package/dist/emitter.test.d.ts.map +1 -0
- package/dist/emitter.test.js +70 -0
- package/dist/emitter.test.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/interface/export-plugin.d.ts +5 -0
- package/dist/interface/export-plugin.d.ts.map +1 -0
- package/dist/interface/export-plugin.js +2 -0
- package/dist/interface/export-plugin.js.map +1 -0
- package/dist/layouts/layout-generator.d.ts +14 -0
- package/dist/layouts/layout-generator.d.ts.map +1 -0
- package/dist/layouts/layout-generator.js +110 -0
- package/dist/layouts/layout-generator.js.map +1 -0
- package/dist/layouts/registry.json +290 -0
- package/dist/layouts/validator.d.ts +7 -0
- package/dist/layouts/validator.d.ts.map +1 -0
- package/dist/layouts/validator.js +27 -0
- package/dist/layouts/validator.js.map +1 -0
- package/dist/layouts/validator.test.d.ts +2 -0
- package/dist/layouts/validator.test.d.ts.map +1 -0
- package/dist/layouts/validator.test.js +19 -0
- package/dist/layouts/validator.test.js.map +1 -0
- package/dist/models/ir.d.ts +91 -0
- package/dist/models/ir.d.ts.map +1 -0
- package/dist/models/ir.js +2 -0
- package/dist/models/ir.js.map +1 -0
- package/dist/parser/image-mapper.d.ts +8 -0
- package/dist/parser/image-mapper.d.ts.map +1 -0
- package/dist/parser/image-mapper.js +15 -0
- package/dist/parser/image-mapper.js.map +1 -0
- package/dist/parser/list-mapper.d.ts +3 -0
- package/dist/parser/list-mapper.d.ts.map +1 -0
- package/dist/parser/list-mapper.js +24 -0
- package/dist/parser/list-mapper.js.map +1 -0
- package/dist/parser/list-mapper.test.d.ts +2 -0
- package/dist/parser/list-mapper.test.d.ts.map +1 -0
- package/dist/parser/list-mapper.test.js +26 -0
- package/dist/parser/list-mapper.test.js.map +1 -0
- package/dist/parser/manifesto.d.ts +9 -0
- package/dist/parser/manifesto.d.ts.map +1 -0
- package/dist/parser/manifesto.js +40 -0
- package/dist/parser/manifesto.js.map +1 -0
- package/dist/parser/manifesto.test.d.ts +2 -0
- package/dist/parser/manifesto.test.d.ts.map +1 -0
- package/dist/parser/manifesto.test.js +32 -0
- package/dist/parser/manifesto.test.js.map +1 -0
- package/dist/parser/table-mapper.d.ts +7 -0
- package/dist/parser/table-mapper.d.ts.map +1 -0
- package/dist/parser/table-mapper.js +33 -0
- package/dist/parser/table-mapper.js.map +1 -0
- package/dist/parser/tokenizer.d.ts +4 -0
- package/dist/parser/tokenizer.d.ts.map +1 -0
- package/dist/parser/tokenizer.js +103 -0
- package/dist/parser/tokenizer.js.map +1 -0
- package/dist/parser/tokenizer.test.d.ts +2 -0
- package/dist/parser/tokenizer.test.d.ts.map +1 -0
- package/dist/parser/tokenizer.test.js +31 -0
- package/dist/parser/tokenizer.test.js.map +1 -0
- package/dist/processor.d.ts +3 -0
- package/dist/processor.d.ts.map +1 -0
- package/dist/processor.js +5 -0
- package/dist/processor.js.map +1 -0
- package/dist/resolver/metrics.d.ts +27 -0
- package/dist/resolver/metrics.d.ts.map +1 -0
- package/dist/resolver/metrics.js +43 -0
- package/dist/resolver/metrics.js.map +1 -0
- package/dist/resolver/metrics.test.d.ts +2 -0
- package/dist/resolver/metrics.test.d.ts.map +1 -0
- package/dist/resolver/metrics.test.js +23 -0
- package/dist/resolver/metrics.test.js.map +1 -0
- package/dist/resolver/theme-loader.d.ts +17 -0
- package/dist/resolver/theme-loader.d.ts.map +1 -0
- package/dist/resolver/theme-loader.js +110 -0
- package/dist/resolver/theme-loader.js.map +1 -0
- package/dist/resolver/theme-loader.test.d.ts +2 -0
- package/dist/resolver/theme-loader.test.d.ts.map +1 -0
- package/dist/resolver/theme-loader.test.js +60 -0
- package/dist/resolver/theme-loader.test.js.map +1 -0
- package/package.json +19 -0
- package/src/emitter.ts +118 -0
- package/src/index.ts +7 -0
- package/src/interface/export-plugin.ts +5 -0
- package/src/layouts/layout-generator.ts +116 -0
- package/src/layouts/registry.json +290 -0
- package/src/layouts/validator.ts +31 -0
- package/src/models/ir.ts +98 -0
- package/src/parser/image-mapper.ts +14 -0
- package/src/parser/list-mapper.ts +28 -0
- package/src/parser/manifesto.ts +49 -0
- package/src/parser/table-mapper.ts +38 -0
- package/src/parser/tokenizer.ts +126 -0
- package/src/processor.ts +6 -0
- package/src/resolver/metrics.ts +64 -0
- package/src/resolver/theme-loader.ts +122 -0
- package/src/themes/academico.yaml +25 -0
- package/src/themes/corporativo.yaml +25 -0
- package/src/themes/dark_mode.yaml +26 -0
- package/src/themes/minimalista.yaml +25 -0
- package/src/themes/moderno.yaml +26 -0
- package/test/cycle2_markdown.test.ts +36 -0
- package/test/cycle2_slides.test.ts +52 -0
- package/tsconfig.json +17 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/emitter.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { parseManifesto } from './parser/manifesto.js';
|
|
2
|
+
import { parseSlides } from './parser/tokenizer.js';
|
|
3
|
+
import { ThemeLoader } from './resolver/theme-loader.js';
|
|
4
|
+
import { validateLayout, getLayout } from './layouts/validator.js';
|
|
5
|
+
import { StoneDeckIR, Slide, SlotContent, ListItem, TableCell } from './models/ir.js';
|
|
6
|
+
import { metricsCalculator } from './resolver/metrics.js';
|
|
7
|
+
import { mapMarkdownLists } from './parser/list-mapper.js';
|
|
8
|
+
import { mapMarkdownTable } from './parser/table-mapper.js';
|
|
9
|
+
import { mapMarkdownImage } from './parser/image-mapper.js';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Emitter orchestrates the parsing and theme resolution to produce the final IR.
|
|
14
|
+
*/
|
|
15
|
+
export function emitIR(content: string, filePath: string, themeOverride?: string): StoneDeckIR {
|
|
16
|
+
const basePath = path.dirname(filePath);
|
|
17
|
+
const globalWarnings: string[] = [];
|
|
18
|
+
|
|
19
|
+
// 1. Parse Manifesto
|
|
20
|
+
const { manifesto, remainingContent } = parseManifesto(content);
|
|
21
|
+
|
|
22
|
+
// 2. Parse Slides
|
|
23
|
+
const slides = parseSlides(remainingContent);
|
|
24
|
+
|
|
25
|
+
// 4. Resolve Temas e Validar Layouts
|
|
26
|
+
const themeToLoad = themeOverride || manifesto.theme;
|
|
27
|
+
const theme = ThemeLoader.load(themeToLoad, basePath);
|
|
28
|
+
|
|
29
|
+
const resolvedSlides = slides.map((slide, slideIdx) => {
|
|
30
|
+
// Validate Layout
|
|
31
|
+
const validation = validateLayout(slide.layout_id, slide.slots.length);
|
|
32
|
+
const warnings: string[] = [];
|
|
33
|
+
if (!validation.valid) {
|
|
34
|
+
warnings.push(validation.error!);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const layout = getLayout(slide.layout_id, slide.slots.length);
|
|
38
|
+
// Merge Theme Defaults
|
|
39
|
+
const defaults = theme.defaults || {};
|
|
40
|
+
const mergedStyle = { ...defaults, ...slide.style };
|
|
41
|
+
|
|
42
|
+
// Resolve Style with Theme
|
|
43
|
+
const resolvedStyle = ThemeLoader.resolveStyle(mergedStyle, theme);
|
|
44
|
+
|
|
45
|
+
// Process Slots (List, Table, Image detection)
|
|
46
|
+
const processedSlots: SlotContent[] = slide.slots.map(slot => {
|
|
47
|
+
if (slot.type === 'markdown') {
|
|
48
|
+
// Try Table
|
|
49
|
+
const tableRows = mapMarkdownTable(slot.raw);
|
|
50
|
+
if (tableRows) return { type: 'table', rows: tableRows };
|
|
51
|
+
|
|
52
|
+
// Try Image
|
|
53
|
+
const img = mapMarkdownImage(slot.raw);
|
|
54
|
+
if (img) return { type: 'image', src: img.src, ...(img.alt ? { alt: img.alt } : {}) } as SlotContent;
|
|
55
|
+
|
|
56
|
+
// Try List - but only convert if ALL content is list items
|
|
57
|
+
const listItems = mapMarkdownLists(slot.raw);
|
|
58
|
+
if (listItems.length > 0) {
|
|
59
|
+
// Count non-empty, non-list lines
|
|
60
|
+
const lines = slot.raw.split(/\r?\n/).filter(l => l.trim().length > 0);
|
|
61
|
+
const hasOnlyListItems = lines.every(line => {
|
|
62
|
+
// Check if line matches list pattern
|
|
63
|
+
return /^\s*([-*]|\d+\.)\s+/.test(line);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (hasOnlyListItems) {
|
|
67
|
+
return { type: 'list', items: listItems };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return slot;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Calculate Metrics and Overflow
|
|
75
|
+
const heights: number[] = [];
|
|
76
|
+
if (layout) {
|
|
77
|
+
processedSlots.forEach((slot, slotIdx) => {
|
|
78
|
+
const slotDef = layout.slots[slotIdx];
|
|
79
|
+
if (slotDef) {
|
|
80
|
+
let contentToMeasure = '';
|
|
81
|
+
if (slot.type === 'list') {
|
|
82
|
+
contentToMeasure = slot.items.map((i: ListItem) => i.text).join('\n');
|
|
83
|
+
} else if (slot.type === 'markdown') {
|
|
84
|
+
contentToMeasure = slot.raw;
|
|
85
|
+
} else if (slot.type === 'table') {
|
|
86
|
+
contentToMeasure = slot.rows.map((r: TableCell[]) => r.map((c: TableCell) => c.text).join(' ')).join('\n');
|
|
87
|
+
} else if (slot.type === 'image') {
|
|
88
|
+
contentToMeasure = slot.alt || slot.src;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const estimatedHeight = metricsCalculator.estimateMarkdownHeight(contentToMeasure, resolvedStyle, slotDef.width);
|
|
92
|
+
heights.push(estimatedHeight);
|
|
93
|
+
|
|
94
|
+
if (estimatedHeight > slotDef.height) {
|
|
95
|
+
warnings.push(`Overflow Warning: Content in slot "${slotDef.id}" (slide ${slideIdx + 1}) exceeds defined height (${estimatedHeight.toFixed(1)}pt > ${slotDef.height}pt)`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
...slide,
|
|
103
|
+
style: resolvedStyle,
|
|
104
|
+
slots: processedSlots,
|
|
105
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
106
|
+
metrics: {
|
|
107
|
+
heights
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
manifesto,
|
|
114
|
+
slides: resolvedSlides,
|
|
115
|
+
basePath,
|
|
116
|
+
warnings: globalWarnings.length > 0 ? globalWarnings : undefined
|
|
117
|
+
};
|
|
118
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './models/ir.js';
|
|
2
|
+
export * from './emitter.js';
|
|
3
|
+
export * from './parser/tokenizer.js';
|
|
4
|
+
export * from './parser/manifesto.js';
|
|
5
|
+
export * from './layouts/validator.js';
|
|
6
|
+
export * from './interface/export-plugin.js';
|
|
7
|
+
export * from './processor.js'; // Will create this next
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
|
|
2
|
+
import { Layout, LayoutSlot } from '../models/ir.js';
|
|
3
|
+
|
|
4
|
+
export class LayoutGenerator {
|
|
5
|
+
private static CANVAS_WIDTH = 720;
|
|
6
|
+
private static CANVAS_HEIGHT = 405;
|
|
7
|
+
private static SAFETY_MARGIN = 30;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generates a dynamic layout based on valid base types.
|
|
11
|
+
*/
|
|
12
|
+
static generate(layoutId: string, slotCount: number): Layout | null {
|
|
13
|
+
if (layoutId === 'content') {
|
|
14
|
+
return this.generateContentGrid(slotCount);
|
|
15
|
+
}
|
|
16
|
+
if (layoutId === 'title-and-content') {
|
|
17
|
+
return this.generateTitleAndContent(slotCount);
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private static generateContentGrid(count: number): Layout {
|
|
23
|
+
// Full canvas usage
|
|
24
|
+
const bounds = {
|
|
25
|
+
x: this.SAFETY_MARGIN,
|
|
26
|
+
y: this.SAFETY_MARGIN,
|
|
27
|
+
w: this.CANVAS_WIDTH - (this.SAFETY_MARGIN * 2),
|
|
28
|
+
h: this.CANVAS_HEIGHT - (this.SAFETY_MARGIN * 2)
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
id: 'content',
|
|
33
|
+
slots: this.calculateGridSlots(count, bounds)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private static generateTitleAndContent(count: number): Layout {
|
|
38
|
+
// Slot 0 is always Title (Top)
|
|
39
|
+
// Slots 1..N are Body Content (Bottom)
|
|
40
|
+
|
|
41
|
+
const titleHeight = 120;
|
|
42
|
+
const spacing = 20;
|
|
43
|
+
|
|
44
|
+
const titleSlot: LayoutSlot = {
|
|
45
|
+
id: 'title',
|
|
46
|
+
x: this.SAFETY_MARGIN,
|
|
47
|
+
y: 30,
|
|
48
|
+
width: this.CANVAS_WIDTH - (this.SAFETY_MARGIN * 2),
|
|
49
|
+
height: titleHeight
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (count <= 1) {
|
|
53
|
+
return {
|
|
54
|
+
id: 'title-and-content',
|
|
55
|
+
slots: [titleSlot]
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const bodyBounds = {
|
|
60
|
+
x: this.SAFETY_MARGIN,
|
|
61
|
+
y: 30 + titleHeight + spacing,
|
|
62
|
+
w: this.CANVAS_WIDTH - (this.SAFETY_MARGIN * 2),
|
|
63
|
+
h: this.CANVAS_HEIGHT - (30 + titleHeight + spacing) - 30
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const bodySlots = this.calculateGridSlots(count - 1, bodyBounds, 1); // offset index by 1
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
id: 'title-and-content',
|
|
70
|
+
slots: [titleSlot, ...bodySlots]
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private static calculateGridSlots(count: number, bounds: { x: number, y: number, w: number, h: number }, indexOffset: number = 0): LayoutSlot[] {
|
|
75
|
+
const slots: LayoutSlot[] = [];
|
|
76
|
+
if (count === 0) return slots;
|
|
77
|
+
|
|
78
|
+
// Simple Grid Logic
|
|
79
|
+
// 1: 1x1
|
|
80
|
+
// 2: 2 cols
|
|
81
|
+
// 3: 3 cols
|
|
82
|
+
// 4: 2x2
|
|
83
|
+
// 5: 3x2 (last row 2 center? No, simple grid for now)
|
|
84
|
+
// 6: 3x2
|
|
85
|
+
// 7-8: 4x2
|
|
86
|
+
// 9: 3x3
|
|
87
|
+
|
|
88
|
+
let cols = 1;
|
|
89
|
+
let rows = 1;
|
|
90
|
+
|
|
91
|
+
if (count === 2) { cols = 2; }
|
|
92
|
+
else if (count === 3) { cols = 3; }
|
|
93
|
+
else if (count === 4) { cols = 2; rows = 2; }
|
|
94
|
+
else if (count === 5 || count === 6) { cols = 3; rows = 2; }
|
|
95
|
+
else if (count > 6) { cols = Math.ceil(Math.sqrt(count)); rows = Math.ceil(count / cols); }
|
|
96
|
+
|
|
97
|
+
const cellW = (bounds.w - (cols - 1) * 20) / cols;
|
|
98
|
+
const cellH = (bounds.h - (rows - 1) * 20) / rows;
|
|
99
|
+
const gap = 20;
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < count; i++) {
|
|
102
|
+
const r = Math.floor(i / cols);
|
|
103
|
+
const c = i % cols;
|
|
104
|
+
|
|
105
|
+
slots.push({
|
|
106
|
+
id: `slot_${i + indexOffset}`,
|
|
107
|
+
x: bounds.x + c * (cellW + gap),
|
|
108
|
+
y: bounds.y + r * (cellH + gap),
|
|
109
|
+
width: cellW,
|
|
110
|
+
height: cellH
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return slots;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
{
|
|
2
|
+
"layouts": [
|
|
3
|
+
{
|
|
4
|
+
"id": "section-header",
|
|
5
|
+
"slots": [
|
|
6
|
+
{
|
|
7
|
+
"id": "title",
|
|
8
|
+
"x": 50,
|
|
9
|
+
"y": 180,
|
|
10
|
+
"width": 620,
|
|
11
|
+
"height": 60
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "title",
|
|
17
|
+
"slots": [
|
|
18
|
+
{
|
|
19
|
+
"id": "title",
|
|
20
|
+
"x": 50,
|
|
21
|
+
"y": 120,
|
|
22
|
+
"width": 620,
|
|
23
|
+
"height": 100
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "subtitle",
|
|
27
|
+
"x": 50,
|
|
28
|
+
"y": 230,
|
|
29
|
+
"width": 620,
|
|
30
|
+
"height": 80
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "title-and-content",
|
|
36
|
+
"slots": [
|
|
37
|
+
{
|
|
38
|
+
"id": "title",
|
|
39
|
+
"x": 50,
|
|
40
|
+
"y": 30,
|
|
41
|
+
"width": 620,
|
|
42
|
+
"height": 60
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "body",
|
|
46
|
+
"x": 50,
|
|
47
|
+
"y": 100,
|
|
48
|
+
"width": 620,
|
|
49
|
+
"height": 280
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "title-and-content",
|
|
55
|
+
"slots": [
|
|
56
|
+
{
|
|
57
|
+
"id": "title",
|
|
58
|
+
"x": 50,
|
|
59
|
+
"y": 30,
|
|
60
|
+
"width": 620,
|
|
61
|
+
"height": 60
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "col1",
|
|
65
|
+
"x": 50,
|
|
66
|
+
"y": 100,
|
|
67
|
+
"width": 300,
|
|
68
|
+
"height": 280
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "col2",
|
|
72
|
+
"x": 370,
|
|
73
|
+
"y": 100,
|
|
74
|
+
"width": 300,
|
|
75
|
+
"height": 280
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "title-and-content",
|
|
81
|
+
"slots": [
|
|
82
|
+
{
|
|
83
|
+
"id": "title",
|
|
84
|
+
"x": 50,
|
|
85
|
+
"y": 30,
|
|
86
|
+
"width": 620,
|
|
87
|
+
"height": 60
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"id": "col1",
|
|
91
|
+
"x": 50,
|
|
92
|
+
"y": 100,
|
|
93
|
+
"width": 190,
|
|
94
|
+
"height": 280
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"id": "col2",
|
|
98
|
+
"x": 260,
|
|
99
|
+
"y": 100,
|
|
100
|
+
"width": 190,
|
|
101
|
+
"height": 280
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"id": "col3",
|
|
105
|
+
"x": 470,
|
|
106
|
+
"y": 100,
|
|
107
|
+
"width": 190,
|
|
108
|
+
"height": 280
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"id": "title-and-content",
|
|
114
|
+
"slots": [
|
|
115
|
+
{
|
|
116
|
+
"id": "title",
|
|
117
|
+
"x": 50,
|
|
118
|
+
"y": 30,
|
|
119
|
+
"width": 620,
|
|
120
|
+
"height": 60
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"id": "card1",
|
|
124
|
+
"x": 50,
|
|
125
|
+
"y": 100,
|
|
126
|
+
"width": 300,
|
|
127
|
+
"height": 135
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"id": "card2",
|
|
131
|
+
"x": 370,
|
|
132
|
+
"y": 100,
|
|
133
|
+
"width": 300,
|
|
134
|
+
"height": 135
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"id": "card3",
|
|
138
|
+
"x": 50,
|
|
139
|
+
"y": 245,
|
|
140
|
+
"width": 300,
|
|
141
|
+
"height": 135
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"id": "card4",
|
|
145
|
+
"x": 370,
|
|
146
|
+
"y": 245,
|
|
147
|
+
"width": 300,
|
|
148
|
+
"height": 135
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"id": "content",
|
|
154
|
+
"slots": [
|
|
155
|
+
{
|
|
156
|
+
"id": "body",
|
|
157
|
+
"x": 50,
|
|
158
|
+
"y": 50,
|
|
159
|
+
"width": 620,
|
|
160
|
+
"height": 300
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"id": "content",
|
|
166
|
+
"slots": [
|
|
167
|
+
{
|
|
168
|
+
"id": "col1",
|
|
169
|
+
"x": 50,
|
|
170
|
+
"y": 50,
|
|
171
|
+
"width": 300,
|
|
172
|
+
"height": 300
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"id": "col2",
|
|
176
|
+
"x": 370,
|
|
177
|
+
"y": 50,
|
|
178
|
+
"width": 300,
|
|
179
|
+
"height": 300
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"id": "content",
|
|
185
|
+
"slots": [
|
|
186
|
+
{
|
|
187
|
+
"id": "col1",
|
|
188
|
+
"x": 50,
|
|
189
|
+
"y": 50,
|
|
190
|
+
"width": 190,
|
|
191
|
+
"height": 300
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"id": "col2",
|
|
195
|
+
"x": 260,
|
|
196
|
+
"y": 50,
|
|
197
|
+
"width": 190,
|
|
198
|
+
"height": 300
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"id": "col3",
|
|
202
|
+
"x": 470,
|
|
203
|
+
"y": 50,
|
|
204
|
+
"width": 190,
|
|
205
|
+
"height": 300
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"id": "content",
|
|
211
|
+
"slots": [
|
|
212
|
+
{
|
|
213
|
+
"id": "card1",
|
|
214
|
+
"x": 50,
|
|
215
|
+
"y": 50,
|
|
216
|
+
"width": 300,
|
|
217
|
+
"height": 140
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"id": "card2",
|
|
221
|
+
"x": 370,
|
|
222
|
+
"y": 50,
|
|
223
|
+
"width": 300,
|
|
224
|
+
"height": 140
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"id": "card3",
|
|
228
|
+
"x": 50,
|
|
229
|
+
"y": 210,
|
|
230
|
+
"width": 300,
|
|
231
|
+
"height": 140
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"id": "card4",
|
|
235
|
+
"x": 370,
|
|
236
|
+
"y": 210,
|
|
237
|
+
"width": 300,
|
|
238
|
+
"height": 140
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"id": "content",
|
|
244
|
+
"slots": [
|
|
245
|
+
{
|
|
246
|
+
"id": "card1",
|
|
247
|
+
"x": 50,
|
|
248
|
+
"y": 50,
|
|
249
|
+
"width": 190,
|
|
250
|
+
"height": 140
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"id": "card2",
|
|
254
|
+
"x": 260,
|
|
255
|
+
"y": 50,
|
|
256
|
+
"width": 190,
|
|
257
|
+
"height": 140
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"id": "card3",
|
|
261
|
+
"x": 470,
|
|
262
|
+
"y": 50,
|
|
263
|
+
"width": 190,
|
|
264
|
+
"height": 140
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"id": "card4",
|
|
268
|
+
"x": 50,
|
|
269
|
+
"y": 210,
|
|
270
|
+
"width": 190,
|
|
271
|
+
"height": 140
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"id": "card5",
|
|
275
|
+
"x": 260,
|
|
276
|
+
"y": 210,
|
|
277
|
+
"width": 190,
|
|
278
|
+
"height": 140
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"id": "card6",
|
|
282
|
+
"x": 470,
|
|
283
|
+
"y": 210,
|
|
284
|
+
"width": 190,
|
|
285
|
+
"height": 140
|
|
286
|
+
}
|
|
287
|
+
]
|
|
288
|
+
}
|
|
289
|
+
]
|
|
290
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Layout } from '../models/ir.js';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { LayoutGenerator } from './layout-generator.js';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
const registryPath = path.join(__dirname, 'registry.json');
|
|
11
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8'));
|
|
12
|
+
|
|
13
|
+
export function validateLayout(id: string, slotCount: number): { valid: boolean; error?: string } {
|
|
14
|
+
const layout = getLayout(id, slotCount);
|
|
15
|
+
if (!layout) {
|
|
16
|
+
return { valid: false, error: `Layout '${id}' not found for ${slotCount} slots.` };
|
|
17
|
+
}
|
|
18
|
+
return { valid: true };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getLayout(id: string, slotCount: number): Layout | undefined {
|
|
22
|
+
// 1. Try Registry First (Static definitions override dynamic)
|
|
23
|
+
const layout = registry.layouts.find((l: any) => l.id === id && l.slots.length === slotCount);
|
|
24
|
+
if (layout) return layout;
|
|
25
|
+
|
|
26
|
+
// 2. Try Dynamic Generator
|
|
27
|
+
const dynamic = LayoutGenerator.generate(id, slotCount);
|
|
28
|
+
if (dynamic) return dynamic;
|
|
29
|
+
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
package/src/models/ir.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export interface StoneDeckManifesto {
|
|
2
|
+
StoneDeck: boolean;
|
|
3
|
+
title: string;
|
|
4
|
+
subtitle?: string;
|
|
5
|
+
theme: string;
|
|
6
|
+
author?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ListItem {
|
|
10
|
+
text: string;
|
|
11
|
+
level: number;
|
|
12
|
+
bullet_type: 'dot' | 'square' | 'arrow' | 'dash' | 'number' | string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TableCell {
|
|
16
|
+
text: string;
|
|
17
|
+
isHeader: boolean;
|
|
18
|
+
align?: 'left' | 'center' | 'right';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SlideStyle {
|
|
22
|
+
background?: {
|
|
23
|
+
type: 'color' | 'image' | 'gradient';
|
|
24
|
+
value?: string;
|
|
25
|
+
src?: string;
|
|
26
|
+
opacity?: number;
|
|
27
|
+
fit?: 'cover' | 'contain';
|
|
28
|
+
colors?: string[];
|
|
29
|
+
direction?: string;
|
|
30
|
+
};
|
|
31
|
+
content_align?: {
|
|
32
|
+
horizontal?: 'left' | 'center' | 'right';
|
|
33
|
+
vertical?: 'top' | 'middle' | 'bottom';
|
|
34
|
+
};
|
|
35
|
+
card?: {
|
|
36
|
+
background?: string;
|
|
37
|
+
radius?: string;
|
|
38
|
+
border?: string;
|
|
39
|
+
shadow?: boolean;
|
|
40
|
+
content_align?: {
|
|
41
|
+
horizontal?: 'left' | 'center' | 'right';
|
|
42
|
+
vertical?: 'top' | 'middle' | 'bottom';
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
list?: {
|
|
46
|
+
bullet_type?: string;
|
|
47
|
+
bullet_color?: string;
|
|
48
|
+
spacing?: string;
|
|
49
|
+
indent?: string;
|
|
50
|
+
};
|
|
51
|
+
[key: string]: any;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type SlotContent = {
|
|
55
|
+
type: 'markdown';
|
|
56
|
+
raw: string;
|
|
57
|
+
} | {
|
|
58
|
+
type: 'list';
|
|
59
|
+
items: ListItem[];
|
|
60
|
+
} | {
|
|
61
|
+
type: 'table';
|
|
62
|
+
rows: TableCell[][];
|
|
63
|
+
} | {
|
|
64
|
+
type: 'image';
|
|
65
|
+
src: string;
|
|
66
|
+
alt?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export interface Slide {
|
|
70
|
+
layout_id: string;
|
|
71
|
+
title?: string; // Optional slide title
|
|
72
|
+
style: SlideStyle;
|
|
73
|
+
slots: SlotContent[]; // Content of each slot (Structured)
|
|
74
|
+
warnings?: string[] | undefined;
|
|
75
|
+
metrics?: {
|
|
76
|
+
heights: number[]; // Height of each slot in pt
|
|
77
|
+
} | undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface StoneDeckIR {
|
|
81
|
+
manifesto: StoneDeckManifesto;
|
|
82
|
+
slides: Slide[];
|
|
83
|
+
basePath: string; // Base directory for resource resolution (images, themes)
|
|
84
|
+
warnings?: string[] | undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface LayoutSlot {
|
|
88
|
+
id: string;
|
|
89
|
+
x: number;
|
|
90
|
+
y: number;
|
|
91
|
+
width: number;
|
|
92
|
+
height: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface Layout {
|
|
96
|
+
id: string;
|
|
97
|
+
slots: LayoutSlot[];
|
|
98
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects a Markdown image in a string.
|
|
3
|
+
*/
|
|
4
|
+
export function mapMarkdownImage(markdown: string): { src: string; alt?: string } | null {
|
|
5
|
+
// Regex for 
|
|
6
|
+
const match = markdown.trim().match(/^!\[(.*?)\]\((.*?)\)$/);
|
|
7
|
+
if (match && match[1] !== undefined && match[2] !== undefined) {
|
|
8
|
+
return {
|
|
9
|
+
alt: match[1],
|
|
10
|
+
src: match[2]
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|