@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
|
@@ -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
|
+
}
|
package/src/processor.ts
ADDED
|
@@ -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
|
+
}
|