@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.
Files changed (110) hide show
  1. package/dist/emitter.d.ts +6 -0
  2. package/dist/emitter.d.ts.map +1 -0
  3. package/dist/emitter.js +107 -0
  4. package/dist/emitter.js.map +1 -0
  5. package/dist/emitter.test.d.ts +2 -0
  6. package/dist/emitter.test.d.ts.map +1 -0
  7. package/dist/emitter.test.js +70 -0
  8. package/dist/emitter.test.js.map +1 -0
  9. package/dist/index.d.ts +8 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +8 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/interface/export-plugin.d.ts +5 -0
  14. package/dist/interface/export-plugin.d.ts.map +1 -0
  15. package/dist/interface/export-plugin.js +2 -0
  16. package/dist/interface/export-plugin.js.map +1 -0
  17. package/dist/layouts/layout-generator.d.ts +14 -0
  18. package/dist/layouts/layout-generator.d.ts.map +1 -0
  19. package/dist/layouts/layout-generator.js +110 -0
  20. package/dist/layouts/layout-generator.js.map +1 -0
  21. package/dist/layouts/registry.json +290 -0
  22. package/dist/layouts/validator.d.ts +7 -0
  23. package/dist/layouts/validator.d.ts.map +1 -0
  24. package/dist/layouts/validator.js +27 -0
  25. package/dist/layouts/validator.js.map +1 -0
  26. package/dist/layouts/validator.test.d.ts +2 -0
  27. package/dist/layouts/validator.test.d.ts.map +1 -0
  28. package/dist/layouts/validator.test.js +19 -0
  29. package/dist/layouts/validator.test.js.map +1 -0
  30. package/dist/models/ir.d.ts +91 -0
  31. package/dist/models/ir.d.ts.map +1 -0
  32. package/dist/models/ir.js +2 -0
  33. package/dist/models/ir.js.map +1 -0
  34. package/dist/parser/image-mapper.d.ts +8 -0
  35. package/dist/parser/image-mapper.d.ts.map +1 -0
  36. package/dist/parser/image-mapper.js +15 -0
  37. package/dist/parser/image-mapper.js.map +1 -0
  38. package/dist/parser/list-mapper.d.ts +3 -0
  39. package/dist/parser/list-mapper.d.ts.map +1 -0
  40. package/dist/parser/list-mapper.js +24 -0
  41. package/dist/parser/list-mapper.js.map +1 -0
  42. package/dist/parser/list-mapper.test.d.ts +2 -0
  43. package/dist/parser/list-mapper.test.d.ts.map +1 -0
  44. package/dist/parser/list-mapper.test.js +26 -0
  45. package/dist/parser/list-mapper.test.js.map +1 -0
  46. package/dist/parser/manifesto.d.ts +9 -0
  47. package/dist/parser/manifesto.d.ts.map +1 -0
  48. package/dist/parser/manifesto.js +40 -0
  49. package/dist/parser/manifesto.js.map +1 -0
  50. package/dist/parser/manifesto.test.d.ts +2 -0
  51. package/dist/parser/manifesto.test.d.ts.map +1 -0
  52. package/dist/parser/manifesto.test.js +32 -0
  53. package/dist/parser/manifesto.test.js.map +1 -0
  54. package/dist/parser/table-mapper.d.ts +7 -0
  55. package/dist/parser/table-mapper.d.ts.map +1 -0
  56. package/dist/parser/table-mapper.js +33 -0
  57. package/dist/parser/table-mapper.js.map +1 -0
  58. package/dist/parser/tokenizer.d.ts +4 -0
  59. package/dist/parser/tokenizer.d.ts.map +1 -0
  60. package/dist/parser/tokenizer.js +103 -0
  61. package/dist/parser/tokenizer.js.map +1 -0
  62. package/dist/parser/tokenizer.test.d.ts +2 -0
  63. package/dist/parser/tokenizer.test.d.ts.map +1 -0
  64. package/dist/parser/tokenizer.test.js +31 -0
  65. package/dist/parser/tokenizer.test.js.map +1 -0
  66. package/dist/processor.d.ts +3 -0
  67. package/dist/processor.d.ts.map +1 -0
  68. package/dist/processor.js +5 -0
  69. package/dist/processor.js.map +1 -0
  70. package/dist/resolver/metrics.d.ts +27 -0
  71. package/dist/resolver/metrics.d.ts.map +1 -0
  72. package/dist/resolver/metrics.js +43 -0
  73. package/dist/resolver/metrics.js.map +1 -0
  74. package/dist/resolver/metrics.test.d.ts +2 -0
  75. package/dist/resolver/metrics.test.d.ts.map +1 -0
  76. package/dist/resolver/metrics.test.js +23 -0
  77. package/dist/resolver/metrics.test.js.map +1 -0
  78. package/dist/resolver/theme-loader.d.ts +17 -0
  79. package/dist/resolver/theme-loader.d.ts.map +1 -0
  80. package/dist/resolver/theme-loader.js +110 -0
  81. package/dist/resolver/theme-loader.js.map +1 -0
  82. package/dist/resolver/theme-loader.test.d.ts +2 -0
  83. package/dist/resolver/theme-loader.test.d.ts.map +1 -0
  84. package/dist/resolver/theme-loader.test.js +60 -0
  85. package/dist/resolver/theme-loader.test.js.map +1 -0
  86. package/package.json +19 -0
  87. package/src/emitter.ts +118 -0
  88. package/src/index.ts +7 -0
  89. package/src/interface/export-plugin.ts +5 -0
  90. package/src/layouts/layout-generator.ts +116 -0
  91. package/src/layouts/registry.json +290 -0
  92. package/src/layouts/validator.ts +31 -0
  93. package/src/models/ir.ts +98 -0
  94. package/src/parser/image-mapper.ts +14 -0
  95. package/src/parser/list-mapper.ts +28 -0
  96. package/src/parser/manifesto.ts +49 -0
  97. package/src/parser/table-mapper.ts +38 -0
  98. package/src/parser/tokenizer.ts +126 -0
  99. package/src/processor.ts +6 -0
  100. package/src/resolver/metrics.ts +64 -0
  101. package/src/resolver/theme-loader.ts +122 -0
  102. package/src/themes/academico.yaml +25 -0
  103. package/src/themes/corporativo.yaml +25 -0
  104. package/src/themes/dark_mode.yaml +26 -0
  105. package/src/themes/minimalista.yaml +25 -0
  106. package/src/themes/moderno.yaml +26 -0
  107. package/test/cycle2_markdown.test.ts +36 -0
  108. package/test/cycle2_slides.test.ts +52 -0
  109. package/tsconfig.json +17 -0
  110. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,28 @@
1
+ import { ListItem } from '../models/ir.js';
2
+
3
+ export function mapMarkdownLists(markdown: string): ListItem[] {
4
+ const lines = markdown.split(/\r?\n/);
5
+ const items: ListItem[] = [];
6
+
7
+ for (const line of lines) {
8
+ // Regex matches: leading spaces, asterisk/dash/number, then the text
9
+ const match = line.match(/^(\s*)([-*+]|\d+\.)\s+(.*)$/);
10
+ if (match) {
11
+ const indentation = match[1]!.length;
12
+ const bullet = match[2]!;
13
+ const text = match[3]!;
14
+
15
+ // RFC 9.2: Hierarquia até 3 níveis.
16
+ // Assuming 2 spaces per level.
17
+ const level = Math.min(Math.floor(indentation / 2) + 1, 3);
18
+
19
+ let bullet_type = 'dot';
20
+ if (bullet === '-') bullet_type = 'dash';
21
+ if (/^\d+\./.test(bullet)) bullet_type = 'number';
22
+
23
+ items.push({ text, level, bullet_type });
24
+ }
25
+ }
26
+
27
+ return items;
28
+ }
@@ -0,0 +1,49 @@
1
+ import * as YAML from 'yaml';
2
+ import { StoneDeckManifesto } from '../models/ir.js';
3
+
4
+ export class ManifestoValidationError extends Error {
5
+ constructor(message: string) {
6
+ super(message);
7
+ this.name = 'ManifestoValidationError';
8
+ }
9
+ }
10
+
11
+ export function parseManifesto(content: string): { manifesto: StoneDeckManifesto; remainingContent: string } {
12
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
13
+
14
+ if (!match) {
15
+ throw new ManifestoValidationError('Manifesto block (--- ... ---) not found at the beginning of the file.');
16
+ }
17
+
18
+ const yamlContent = match[1]!;
19
+ const remainingContent = content.slice(match[0].length).trim();
20
+
21
+ let data: any;
22
+ try {
23
+ data = YAML.parse(yamlContent);
24
+ } catch (e: any) {
25
+ throw new ManifestoValidationError(`Failed to parse Manifesto YAML: ${e.message}`);
26
+ }
27
+
28
+ if (data?.StoneDeck !== true) {
29
+ throw new ManifestoValidationError('Manifesto must contain "StoneDeck: true" to be processed.');
30
+ }
31
+
32
+ if (!data.title) {
33
+ throw new ManifestoValidationError('Manifesto must contain a "title".');
34
+ }
35
+
36
+ if (!data.theme) {
37
+ throw new ManifestoValidationError('Manifesto must contain a "theme".');
38
+ }
39
+
40
+ const manifesto: StoneDeckManifesto = {
41
+ StoneDeck: true,
42
+ title: data.title,
43
+ subtitle: data.subtitle,
44
+ theme: data.theme,
45
+ author: data.author,
46
+ };
47
+
48
+ return { manifesto, remainingContent };
49
+ }
@@ -0,0 +1,38 @@
1
+ import { TableCell } from '../models/ir.js';
2
+
3
+ /**
4
+ * Simple Markdown table parser.
5
+ * It identifies tables and converts them into a 2D array of TableCells.
6
+ */
7
+ export function mapMarkdownTable(markdown: string): TableCell[][] | null {
8
+ const lines = markdown.split(/\r?\n/).map(l => l.trim()).filter(l => l.length > 0);
9
+
10
+ // Minimum table has 2 lines: header and delimiter
11
+ if (lines.length < 2) return null;
12
+
13
+ // Check if the first line is a table header
14
+ const firstLine = lines[0];
15
+ if (!firstLine || !firstLine.startsWith('|') || !firstLine.endsWith('|')) return null;
16
+
17
+ // Check for delimiter line (|---|---|)
18
+ const delimiterLine = lines[1];
19
+ if (!delimiterLine || !delimiterLine.includes('|') || !delimiterLine.includes('-')) return null;
20
+
21
+ const rows: TableCell[][] = [];
22
+
23
+ // Helper to split row correctly
24
+ const splitRow = (row: string) => row.replace(/^\||\|$/g, '').split('|').map(c => c.trim());
25
+
26
+ const headers = splitRow(firstLine);
27
+ rows.push(headers.map(h => ({ text: h, isHeader: true })));
28
+
29
+ // Parse Body
30
+ for (let i = 2; i < lines.length; i++) {
31
+ const line = lines[i];
32
+ if (!line || !line.startsWith('|')) break;
33
+ const cells = splitRow(line);
34
+ rows.push(cells.map(c => ({ text: c, isHeader: false })));
35
+ }
36
+
37
+ return rows.length > 1 ? rows : null;
38
+ }
@@ -0,0 +1,126 @@
1
+ import * as YAML from 'yaml';
2
+ import { Slide, SlotContent } from '../models/ir.js';
3
+ import { mapMarkdownImage } from './image-mapper.js';
4
+
5
+ export function tokenizeSlides(remainingContent: string): Slide[] {
6
+ // Slides are separated by ---
7
+ // Note: Since the content starts after the manifesto ---, the first slide delimiter might already be consumed or not exist.
8
+ // The RFC says: "O motor divide o restante do documento por ---."
9
+
10
+ const slideBlocks = remainingContent.split(/\r?\n---\r?\n/);
11
+ const slides: Slide[] = [];
12
+
13
+ for (const block of slideBlocks) {
14
+ if (!block.trim()) continue;
15
+
16
+ // Each slide block: Toggle between A (Yaml) and B (Markdown)
17
+ // Actually the RFC says:
18
+ // Estado A (Config): YAML define layout e style
19
+ // Estado B (Conteúdo): Markdown injetado nos slots
20
+
21
+ // Pattern:
22
+ // layout: ...
23
+ // style: ...
24
+ // ---
25
+ // # Title
26
+ // ***
27
+ // Body
28
+
29
+ // In our case, the slide block itself contains BOTH parts separated by --- internally?
30
+ // RFC 8.3: "O motor divide o restante do documento por ---."
31
+ // "Estado A (Config): YAML"
32
+ // "Estado B (Conteúdo): Markdown"
33
+
34
+ // Wait, the RFC says "O motor divide o restante do documento por ---"
35
+ // And then says "Alternância de Contexto (Toggle): A (Config) -> B (Conteúdo)"
36
+ // This means Slide 1 Config --- Slide 1 Content --- Slide 2 Config --- Slide 2 Content
37
+
38
+ // Let's re-read carefully:
39
+ // 2. Iteração de Slides: O motor divide o restante do documento por ---.
40
+ // 3. Alternância de Contexto (Toggle): A (Config) -> B (Conteùdo)
41
+
42
+ // This implies that slide 1 is block 1 (config) and block 2 (content).
43
+ // Slide 2 is block 3 (config) and block 4 (content).
44
+ }
45
+
46
+ return slides;
47
+ }
48
+
49
+ export function parseSlides(content: string): Slide[] {
50
+ // Strict V2 syntax: Content must be inside :::slide blocks
51
+ return parseSlidesV2(content);
52
+ }
53
+
54
+ function parseSlidesV2(content: string): Slide[] {
55
+ const slides: Slide[] = [];
56
+ const slideRegex = /:::slide([\s\S]*?):::/g;
57
+ let match;
58
+
59
+ while ((match = slideRegex.exec(content)) !== null) {
60
+ const slideBlock = match[1]?.trim() || '';
61
+
62
+ // Extract Front Matter
63
+ // Pattern: ^--- (yaml) --- (content)$
64
+ const fmRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
65
+ const fmMatch = slideBlock.match(fmRegex);
66
+
67
+ let config: any = {};
68
+ let rawContent = slideBlock;
69
+
70
+ if (fmMatch) {
71
+ const yamlStr = fmMatch[1] || '';
72
+ rawContent = fmMatch[2] || '';
73
+ try {
74
+ config = YAML.parse(yamlStr);
75
+ } catch (e) {
76
+ console.warn('Invalid YAML in slide:', e);
77
+ }
78
+ }
79
+
80
+ const layout_id = config.layout || 'blank';
81
+ const title = config.title;
82
+ const style = config.style || {};
83
+
84
+ const slots = parseSlots(rawContent);
85
+
86
+ slides.push({
87
+ layout_id,
88
+ title,
89
+ style,
90
+ slots
91
+ });
92
+ }
93
+
94
+ return slides;
95
+ }
96
+
97
+ function parseSlots(content: string): SlotContent[] {
98
+ let processedContent = content.trim();
99
+
100
+ // Handle comments inside content (Cycle 2 requirement)
101
+ // Removed here or handled by plugins? Actually comments should be removed BEFORE splitting slots?
102
+ // Let's assume content is pure markdown for now.
103
+
104
+ // Remove *-* at start/end
105
+ processedContent = processedContent.replace(/^\*-\*\s*[\r\n]+/, '');
106
+ processedContent = processedContent.replace(/[\r\n]+\s*\*-\*\s*$/, '');
107
+
108
+ const slots_content = processedContent
109
+ ? processedContent.split(/\r?\n\*-\*\r?\n/).map(s => s.trim()).filter(s => s.length > 0)
110
+ : [];
111
+
112
+ // If no delimiters found but content exists, treat as single slot
113
+ if (slots_content.length === 0 && processedContent.length > 0) {
114
+ const img = mapMarkdownImage(processedContent);
115
+ if (img) return [{ type: 'image', src: img.src, alt: img.alt }];
116
+ return [{ type: 'markdown', raw: processedContent }];
117
+ }
118
+
119
+ return slots_content.map(raw => {
120
+ const img = mapMarkdownImage(raw);
121
+ if (img) {
122
+ return { type: 'image', src: img.src, alt: img.alt };
123
+ }
124
+ return { type: 'markdown', raw };
125
+ });
126
+ }
@@ -0,0 +1,6 @@
1
+ import { emitIR } from './emitter.js';
2
+ import { StoneDeckIR } from './models/ir.js';
3
+
4
+ export function processStoneDeck(content: string, filePath: string, themeOverride?: string): StoneDeckIR {
5
+ return emitIR(content, filePath, themeOverride);
6
+ }
@@ -0,0 +1,64 @@
1
+ import PDFDocument from 'pdfkit';
2
+ import { SlideStyle } from '../models/ir.js';
3
+
4
+ export interface LayoutSlot {
5
+ id: string;
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ }
11
+
12
+ export interface LayoutDefinition {
13
+ id: string;
14
+ slots: LayoutSlot[];
15
+ }
16
+
17
+ export class MetricsCalculator {
18
+ private doc: PDFKit.PDFDocument;
19
+
20
+ constructor() {
21
+ // Create a headless PDF document for measurements
22
+ this.doc = new PDFDocument({ size: [720, 405] });
23
+ }
24
+
25
+ /**
26
+ * Calculates the height of a string given a font and width.
27
+ */
28
+ calculateTextHeight(text: string, style: SlideStyle, maxWidth: number): number {
29
+ const fontSize = style.font_size || 18; // Default font size
30
+ const font = style.font_family || 'Helvetica'; // Default font
31
+
32
+ try {
33
+ this.doc.font(font).fontSize(fontSize);
34
+ return this.doc.heightOfString(text, { width: maxWidth });
35
+ } catch (e) {
36
+ // Fallback font if the requested one fails
37
+ this.doc.font('Helvetica').fontSize(fontSize);
38
+ return this.doc.heightOfString(text, { width: maxWidth });
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Estimates height for Markdown content.
44
+ * For now, this is a simplified version that handles basic text and lists.
45
+ */
46
+ estimateMarkdownHeight(markdown: string, style: SlideStyle, maxWidth: number): number {
47
+ // Simple heuristic: split by lines and sum heights
48
+ // In a real implementation, this would handle actual markdown parsing.
49
+ const lines = markdown.split('\n');
50
+ let totalHeight = 0;
51
+
52
+ for (const line of lines) {
53
+ if (!line.trim()) {
54
+ totalHeight += (style.font_size || 18) * 0.5; // Half line height for empty lines
55
+ continue;
56
+ }
57
+ totalHeight += this.calculateTextHeight(line, style, maxWidth);
58
+ }
59
+
60
+ return totalHeight;
61
+ }
62
+ }
63
+
64
+ export const metricsCalculator = new MetricsCalculator();
@@ -0,0 +1,122 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as YAML from 'yaml';
4
+ import { fileURLToPath } from 'url';
5
+ import { SlideStyle } from '../models/ir.js';
6
+
7
+ export class ThemeLoader {
8
+ private static __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ private static presets: Record<string, any> = {
11
+ dark: {
12
+ name: "Dark Preset",
13
+ tokens: {
14
+ colors: { primary: "#1A1A2E", secondary: "#16213E", accent: "#E94560", surface: "#0F3460", text: "#FFFFFF" },
15
+ fonts: { heading: "Montserrat", body: "Open Sans", mono: "Fira Code" }
16
+ }
17
+ },
18
+ standard: {
19
+ name: "Standard Preset",
20
+ tokens: {
21
+ colors: { primary: "#FFFFFF", secondary: "#F0F0F0", accent: "#007BFF", surface: "#FFFFFF", text: "#000000" },
22
+ fonts: { heading: "Inter", body: "Roboto", mono: "Courier New" }
23
+ }
24
+ }
25
+ };
26
+
27
+ static load(themeRef: string, basePath: string): any {
28
+ if (this.presets[themeRef]) {
29
+ return this.presets[themeRef];
30
+ }
31
+
32
+ const potentialPaths = [
33
+ path.isAbsolute(themeRef) ? themeRef : path.join(basePath, themeRef),
34
+ path.isAbsolute(themeRef) ? themeRef : path.join(basePath, 'themes', themeRef + '.yaml'),
35
+ path.join(this.__dirname, '../../themes', themeRef + '.yaml'), // When running from built/source
36
+ path.join(this.__dirname, '../../src/themes', themeRef + '.yaml'), // Fallback to src if not copied to dist
37
+ path.join(process.cwd(), 'src/themes', themeRef + '.yaml') // Development fall back
38
+ ];
39
+
40
+ for (const p of potentialPaths) {
41
+ if (fs.existsSync(p)) {
42
+ try {
43
+ const fileContent = fs.readFileSync(p, 'utf8');
44
+ return YAML.parse(fileContent);
45
+ } catch (e) {
46
+ console.warn(`Failed to parse theme at ${p}:`, e);
47
+ }
48
+ }
49
+ }
50
+
51
+ console.warn(`Theme "${themeRef}" not found. Falling back to standard.`);
52
+ return this.presets.standard;
53
+ }
54
+
55
+ static resolveStyle(slideStyle: SlideStyle, theme: any): SlideStyle {
56
+ const tokens = theme.tokens || {};
57
+ return this.deepResolve(slideStyle, tokens);
58
+ }
59
+
60
+ private static deepResolve(obj: any, tokens: any): any {
61
+ if (typeof obj !== 'object' || obj === null) {
62
+ // Token resolution for strings
63
+ if (typeof obj === 'string') {
64
+ return this.resolveTokenValue(obj, tokens);
65
+ }
66
+ return obj;
67
+ }
68
+
69
+ if (Array.isArray(obj)) {
70
+ return obj.map(item => this.deepResolve(item, tokens));
71
+ }
72
+
73
+ const resolved: any = {};
74
+ for (const key in obj) {
75
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
76
+ resolved[key] = this.deepResolve(obj[key], tokens);
77
+ }
78
+ }
79
+ return resolved;
80
+ }
81
+
82
+ private static resolveTokenValue(value: string, tokens: any): any {
83
+ // 1. Try resolving as a color token (e.g., "primary")
84
+ if (tokens.colors?.[value]) return tokens.colors[value];
85
+
86
+ // 2. Try resolving as a font token (e.g., "heading")
87
+ if (tokens.fonts?.[value]) return tokens.fonts[value];
88
+
89
+ // 3. Try resolving as a spacing token
90
+ if (tokens.spacing?.[value]) return this.convertToPt(tokens.spacing[value]);
91
+
92
+ // 4. Try converting the string itself if it's a unit (e.g., "20px")
93
+ return this.convertToPt(value);
94
+ }
95
+
96
+ /**
97
+ * Converts various units into Points (pt).
98
+ * 1px ≈ 0.75pt
99
+ * 1em ≈ 12pt (assuming 16px base)
100
+ * RFC 4.1: "Todas as unidades finais devem ser em pt (Pontos)."
101
+ */
102
+ private static convertToPt(value: string | number): any {
103
+ if (typeof value === 'number') return value;
104
+
105
+ const match = value.match(/^([\d.]+)(px|em|rem|pt|%)?$/);
106
+ if (!match) return value;
107
+
108
+ const numStr = match[1];
109
+ if (!numStr) return value;
110
+
111
+ const num = parseFloat(numStr);
112
+ const unit = match[2];
113
+
114
+ switch (unit) {
115
+ case 'px': return num * 0.75;
116
+ case 'em':
117
+ case 'rem': return num * 12; // Simple approximation for 1em = 16px
118
+ case 'pt': return num;
119
+ default: return num;
120
+ }
121
+ }
122
+ }
@@ -0,0 +1,25 @@
1
+ name: "Academico"
2
+ tokens:
3
+ colors:
4
+ primary: "#1a237e"
5
+ secondary: "#f3e5f5"
6
+ accent: "#b71c1c"
7
+ surface: "#fdfbf7"
8
+ text: "#212121"
9
+ heading: "#1a237e"
10
+ table_header: "#e8eaf6"
11
+ fonts:
12
+ heading: "Times-Bold"
13
+ body: "Times-Roman"
14
+ mono: "Courier"
15
+
16
+ defaults:
17
+ background:
18
+ type: "color"
19
+ value: "surface"
20
+ color: "text"
21
+ font_family: "body"
22
+ font_size: 20
23
+ content_align:
24
+ horizontal: "left"
25
+ vertical: "top"
@@ -0,0 +1,25 @@
1
+ name: "Corporativo"
2
+ tokens:
3
+ colors:
4
+ primary: "#2980b9"
5
+ secondary: "#dfe6e9"
6
+ accent: "#3498db"
7
+ surface: "#f0f2f5"
8
+ text: "#2c3e50"
9
+ heading: "#2980b9"
10
+ border: "#34495e"
11
+ fonts:
12
+ heading: "Helvetica-Bold"
13
+ body: "Helvetica"
14
+ mono: "Courier New"
15
+
16
+ defaults:
17
+ background:
18
+ type: "color"
19
+ value: "surface"
20
+ color: "text"
21
+ font_family: "body"
22
+ font_size: 20
23
+ content_align:
24
+ horizontal: "left"
25
+ vertical: "top"
@@ -0,0 +1,26 @@
1
+ name: "Dark Mode"
2
+ tokens:
3
+ colors:
4
+ primary: "#64ffda"
5
+ secondary: "#112240"
6
+ accent: "#64ffda"
7
+ surface: "#0a192f"
8
+ text: "#e6f1ff"
9
+ heading: "#64ffda"
10
+ muted: "#8892b0"
11
+ code: "#ff9f43"
12
+ fonts:
13
+ heading: "Helvetica-Bold"
14
+ body: "Helvetica"
15
+ mono: "Courier New"
16
+
17
+ defaults:
18
+ background:
19
+ type: "color"
20
+ value: "surface"
21
+ color: "text"
22
+ font_family: "body"
23
+ font_size: 20
24
+ content_align:
25
+ horizontal: "left"
26
+ vertical: "top"
@@ -0,0 +1,25 @@
1
+ name: "Minimalista"
2
+ tokens:
3
+ colors:
4
+ primary: "#000000"
5
+ secondary: "#ffffff"
6
+ accent: "#000000"
7
+ surface: "#ffffff"
8
+ text: "#333333"
9
+ heading: "#000000"
10
+ muted: "#555555"
11
+ fonts:
12
+ heading: "Helvetica-Bold"
13
+ body: "Helvetica"
14
+ mono: "Courier New"
15
+
16
+ defaults:
17
+ background:
18
+ type: "color"
19
+ value: "surface"
20
+ color: "text"
21
+ font_family: "body"
22
+ font_size: 24
23
+ content_align:
24
+ horizontal: "left"
25
+ vertical: "top"
@@ -0,0 +1,26 @@
1
+ name: "Moderno"
2
+ tokens:
3
+ colors:
4
+ primary: "#2c3e50"
5
+ secondary: "#ecf0f1"
6
+ accent: "#3498db"
7
+ surface: "#f5f7fa"
8
+ text: "#34495e"
9
+ heading: "#2c3e50"
10
+ muted: "#7f8c8d"
11
+ quote_bg: "#e1f5fe"
12
+ fonts:
13
+ heading: "Helvetica-Bold"
14
+ body: "Helvetica"
15
+ mono: "Courier New"
16
+
17
+ defaults:
18
+ background:
19
+ type: "color"
20
+ value: "surface"
21
+ color: "text"
22
+ font_family: "body"
23
+ font_size: 20
24
+ content_align:
25
+ horizontal: "left"
26
+ vertical: "top"
@@ -0,0 +1,36 @@
1
+ import { mapMarkdownLists } from '../src/parser/list-mapper';
2
+
3
+ describe('Cycle 2 Markdown Support', () => {
4
+ it('should support + bullet points', () => {
5
+ const markdown = '+ Item 1\n+ Item 2';
6
+ const result = mapMarkdownLists(markdown);
7
+ expect(result).toHaveLength(2);
8
+ expect(result[0]?.text).toBe('Item 1');
9
+ // bullet_type default is dot if not dash/number
10
+ expect(result[0]?.bullet_type).toBe('dot');
11
+ });
12
+
13
+ it('should support * bullet points', () => {
14
+ const markdown = '* Item 1\n* Item 2';
15
+ const result = mapMarkdownLists(markdown);
16
+ expect(result).toHaveLength(2);
17
+ expect(result[0]?.bullet_type).toBe('dot');
18
+ });
19
+
20
+ it('should support mixed lists', () => {
21
+ const markdown = '- Dash\n+ Plus\n* Star';
22
+ const result = mapMarkdownLists(markdown);
23
+ expect(result).toHaveLength(3);
24
+ expect(result[0]?.bullet_type).toBe('dash');
25
+ expect(result[1]?.bullet_type).toBe('dot');
26
+ expect(result[2]?.bullet_type).toBe('dot');
27
+ });
28
+
29
+ it('should support numbered lists', () => {
30
+ const markdown = '1. First\n2. Second';
31
+ const result = mapMarkdownLists(markdown);
32
+ expect(result).toHaveLength(2);
33
+ expect(result[0]?.bullet_type).toBe('number');
34
+ expect(result[1]?.bullet_type).toBe('number');
35
+ });
36
+ });
@@ -0,0 +1,52 @@
1
+ import { parseSlides } from '../src/parser/tokenizer';
2
+
3
+ describe('Cycle 2 Slide Contract (V2)', () => {
4
+ it('should parse :::slide block format', () => {
5
+ const content = `
6
+ :::slide
7
+ ---
8
+ layout: title-slide
9
+ title: Hello V2
10
+ ---
11
+ # Content Here
12
+ :::
13
+ `;
14
+ const slides = parseSlides(content);
15
+ expect(slides).toHaveLength(1);
16
+ expect(slides[0]?.layout_id).toBe('title-slide');
17
+ expect(slides[0]?.title).toBe('Hello V2');
18
+ expect((slides[0]?.slots[0] as any)?.raw.trim()).toBe('# Content Here');
19
+ });
20
+
21
+ it('should detect front matter inside :::slide', () => {
22
+ const content = `
23
+ :::slide
24
+ ---
25
+ layout: two-columns
26
+ style:
27
+ color: red
28
+ ---
29
+ Left
30
+ *-*
31
+ Right
32
+ :::
33
+ `;
34
+ const slides = parseSlides(content);
35
+ expect(slides).toHaveLength(1);
36
+ expect(slides[0]?.layout_id).toBe('two-columns');
37
+ expect(slides[0]?.style.color).toBe('red');
38
+ expect(slides[0]?.slots).toHaveLength(2);
39
+ expect((slides[0]?.slots[0] as any)?.raw.trim()).toBe('Left');
40
+ expect((slides[0]?.slots[1] as any)?.raw.trim()).toBe('Right');
41
+ });
42
+
43
+ it('should ignore content outside :::slide blocks (V1 format)', () => {
44
+ const content = `
45
+ layout: simple
46
+ ---
47
+ # Old Format
48
+ `;
49
+ const slides = parseSlides(content);
50
+ expect(slides).toHaveLength(0);
51
+ });
52
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "composite": true,
7
+ "resolveJsonModule": true
8
+ },
9
+ "include": [
10
+ "src/**/*",
11
+ "src/**/*.json"
12
+ ],
13
+ "exclude": [
14
+ "**/*.test.ts",
15
+ "**/*.test.tsx"
16
+ ]
17
+ }