yumiamd 0.1.5 → 0.1.6
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/README.md +39 -15
- package/dist/bin.js +1 -1
- package/dist/{chunk-MKQASLAK.js → chunk-FHM6V4JH.js} +583 -90
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# YumiaMD (`yumiamd`)
|
|
2
2
|
|
|
3
3
|
> **A Markdown-based presentation language & compiler designed for humans and AI.**
|
|
4
|
-
> Author decks in clean Markdown and compile directly to **100% native, editable PowerPoint (.pptx)** presentations, vector PDFs, and interactive HTML5 slides.
|
|
4
|
+
> Author slide decks in clean Markdown and compile directly to **100% native, editable PowerPoint (.pptx)** presentations, vector PDFs, and interactive HTML5 slides.
|
|
5
5
|
|
|
6
6
|
[](https://www.npmjs.com/package/yumiamd)
|
|
7
7
|
[](https://github.com/biagio-scaglia/Yumia-MD/blob/main/LICENSE)
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
## ⚡ Quick Start (Zero Install)
|
|
12
12
|
|
|
13
|
-
You can run YumiaMD immediately without installing anything
|
|
13
|
+
You can run YumiaMD immediately without installing anything via `npx`:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
# Initialize a starter presentation
|
|
17
17
|
npx yumiamd init my-deck
|
|
18
18
|
|
|
19
|
-
# Compile presentation to an editable PowerPoint (.pptx)
|
|
19
|
+
# Compile presentation directly to an editable PowerPoint (.pptx)
|
|
20
20
|
npx yumiamd build presentation.yumia.md --out presentation.pptx
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -44,7 +44,7 @@ pnpm add yumiamd
|
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
47
|
-
## 🚀 CLI Commands
|
|
47
|
+
## 🚀 CLI Commands & AI Tooling
|
|
48
48
|
|
|
49
49
|
Once installed globally or locally, the `yumia` / `yumiamd` commands are available in your terminal:
|
|
50
50
|
|
|
@@ -52,16 +52,19 @@ Once installed globally or locally, the `yumia` / `yumiamd` commands are availab
|
|
|
52
52
|
# Initialize a new presentation template
|
|
53
53
|
yumia init my-presentation
|
|
54
54
|
|
|
55
|
-
# Validate markdown syntax and AST structure
|
|
56
|
-
yumia validate presentation.yumia.md
|
|
55
|
+
# Validate markdown syntax and AST structure (supports --json for CI & AI agents)
|
|
56
|
+
yumia validate presentation.yumia.md --json
|
|
57
57
|
|
|
58
58
|
# Lint presentation for vertical overflow and layout issues
|
|
59
|
-
yumia lint presentation.yumia.md
|
|
59
|
+
yumia lint presentation.yumia.md --json
|
|
60
60
|
|
|
61
61
|
# Inspect parsed AST and computed layout bounding boxes
|
|
62
62
|
yumia inspect presentation.yumia.md --layout
|
|
63
63
|
|
|
64
|
-
#
|
|
64
|
+
# Export machine-readable JSON schema for LLMs & AI prompt generation
|
|
65
|
+
yumia schema
|
|
66
|
+
|
|
67
|
+
# Compile to native editable PowerPoint (.pptx)
|
|
65
68
|
yumia build presentation.yumia.md --out dist/presentation.pptx
|
|
66
69
|
```
|
|
67
70
|
|
|
@@ -71,8 +74,9 @@ yumia build presentation.yumia.md --out dist/presentation.pptx
|
|
|
71
74
|
|
|
72
75
|
```markdown
|
|
73
76
|
---
|
|
74
|
-
title: System Architecture &
|
|
77
|
+
title: System Architecture & Capabilities
|
|
75
78
|
theme: default
|
|
79
|
+
aspectRatio: '16:9'
|
|
76
80
|
author: Biagio Scaglia
|
|
77
81
|
---
|
|
78
82
|
|
|
@@ -109,37 +113,57 @@ Opening slide introducing the core architectural vision.
|
|
|
109
113
|
:::
|
|
110
114
|
|
|
111
115
|
:::
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
# Feature Comparison
|
|
120
|
+
|
|
121
|
+
| Feature | YumiaMD | Legacy HTML Deck Tools |
|
|
122
|
+
| :----------------------- | :------------------------------- | :----------------------------------- |
|
|
123
|
+
| **Output Type** | Native OpenXML Shapes & Text | Flat rasterized images / screenshots |
|
|
124
|
+
| **Full Editability** | ✅ 100% Editable in PowerPoint | ❌ Read-only image slides |
|
|
125
|
+
| **Deterministic Layout** | ✅ Pixel-exact bounding boxes | ❌ Browser rendering variance |
|
|
126
|
+
| **AI / Agent Tooling** | ✅ Machine-readable schema & CLI | ❌ Complex DOM scraping |
|
|
112
127
|
```
|
|
113
128
|
|
|
114
129
|
---
|
|
115
130
|
|
|
116
131
|
## 💻 JavaScript / TypeScript API
|
|
117
132
|
|
|
118
|
-
You can also use `yumiamd` programmatically inside your Node.js or TypeScript backend
|
|
133
|
+
You can also use `yumiamd` programmatically inside your Node.js or TypeScript backend, CLI tools, and AI pipelines:
|
|
119
134
|
|
|
120
135
|
```typescript
|
|
121
|
-
import { compile, parse,
|
|
136
|
+
import { compile, parse, YumiaCompiler } from 'yumiamd';
|
|
122
137
|
import fs from 'node:fs';
|
|
123
138
|
|
|
124
139
|
const markdown = fs.readFileSync('presentation.yumia.md', 'utf-8');
|
|
125
140
|
|
|
126
|
-
// 1.
|
|
141
|
+
// 1. Validate syntax and collect diagnostics
|
|
142
|
+
const compiler = new YumiaCompiler();
|
|
143
|
+
const validation = compiler.validate(markdown);
|
|
144
|
+
console.log(`Valid: ${validation.valid}, Slides: ${validation.slideCount}`);
|
|
145
|
+
|
|
146
|
+
// 2. Compile directly to PPTX buffer
|
|
127
147
|
const { buffer, errors } = await compile(markdown, { format: 'pptx' });
|
|
128
148
|
|
|
129
149
|
if (errors.length === 0) {
|
|
130
150
|
fs.writeFileSync('output.pptx', buffer);
|
|
131
151
|
console.log('✅ Presentation generated successfully!');
|
|
132
152
|
}
|
|
153
|
+
|
|
154
|
+
// 3. Export JSON schema for LLM generation
|
|
155
|
+
const schema = compiler.getSchema();
|
|
133
156
|
```
|
|
134
157
|
|
|
135
158
|
---
|
|
136
159
|
|
|
137
160
|
## ✨ Key Features
|
|
138
161
|
|
|
139
|
-
- 🎯 **Native Object Principle**: Slides compiled to PowerPoint are **NOT** rasterized screenshot images. Headings, bullet points, cards, and speaker notes are generated as **100% native vector PowerPoint shapes & textboxes** that you can click, re-format, and edit in Microsoft PowerPoint or Google Slides.
|
|
140
|
-
-
|
|
162
|
+
- 🎯 **Native Object Principle**: Slides compiled to PowerPoint are **NOT** rasterized screenshot images. Headings, bullet points, cards, tables, and speaker notes are generated as **100% native vector PowerPoint shapes, tables & textboxes** that you can click, re-format, and edit in Microsoft PowerPoint or Google Slides.
|
|
163
|
+
- 📊 **Native Tables & Multicolumn**: Full support for Markdown tables and semantic responsive multi-column layouts with customizable ratio splits (`ratios="50:50"` or `"30:70"`).
|
|
164
|
+
- 📐 **Deterministic Layout Engine**: Exact coordinate calculations for stack, columns, cards, and automatic overflow detection.
|
|
141
165
|
- 🎨 **Semantic Design Tokens**: Built-in themes with typography scales, color palettes, and contrast-safe themes.
|
|
142
|
-
- 🤖 **AI-Friendly Format**: Clean Markdown syntax designed for LLM prompts and agentic workflows.
|
|
166
|
+
- 🤖 **AI-Friendly Format**: Clean Markdown syntax designed for LLM prompts and agentic workflows, complete with `yumia schema` and `--json` CLI diagnostics.
|
|
143
167
|
|
|
144
168
|
---
|
|
145
169
|
|
package/dist/bin.js
CHANGED
|
@@ -103,26 +103,75 @@ function createLayoutDirective(mode, attributes) {
|
|
|
103
103
|
|
|
104
104
|
// ../parser/dist/parser.js
|
|
105
105
|
var DefaultYumiaParser = class {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
diagnostics = [];
|
|
107
|
+
parse(source, options) {
|
|
108
|
+
this.diagnostics = [];
|
|
109
|
+
if (!source || typeof source !== "string") {
|
|
110
|
+
return createPresentation({}, []);
|
|
111
|
+
}
|
|
112
|
+
const normalized = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
113
|
+
const { metadata, content, lineOffset } = this.extractFrontmatter(normalized);
|
|
114
|
+
const slideChunks = this.splitSlides(content, lineOffset);
|
|
115
|
+
const slides = [];
|
|
116
|
+
for (const chunk of slideChunks) {
|
|
117
|
+
const slide = this.parseSlide(chunk.content, chunk.startLine);
|
|
118
|
+
if (slide.elements.length > 0 || slide.notes !== void 0 || slide.background !== void 0) {
|
|
119
|
+
slides.push(slide);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const presentation = createPresentation(metadata, slides);
|
|
123
|
+
if (this.diagnostics.length > 0) {
|
|
124
|
+
presentation.diagnostics = [...this.diagnostics];
|
|
125
|
+
}
|
|
126
|
+
if (options?.strict && this.diagnostics.some((d) => d.severity === "error")) {
|
|
127
|
+
const firstError = this.diagnostics.find((d) => d.severity === "error");
|
|
128
|
+
throw new Error(`[Strict Parse Error] ${firstError?.message || "Syntax error"}`);
|
|
129
|
+
}
|
|
130
|
+
return presentation;
|
|
112
131
|
}
|
|
113
132
|
extractFrontmatter(source) {
|
|
114
133
|
const trimmed = source.trimStart();
|
|
115
134
|
if (!trimmed.startsWith("---")) {
|
|
116
|
-
return { metadata: {}, content: source };
|
|
135
|
+
return { metadata: {}, content: source, lineOffset: 1 };
|
|
117
136
|
}
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
return { metadata: {}, content: source };
|
|
137
|
+
const firstLineEnd = trimmed.indexOf("\n");
|
|
138
|
+
if (firstLineEnd === -1) {
|
|
139
|
+
return { metadata: {}, content: source, lineOffset: 1 };
|
|
121
140
|
}
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
const headerLine = trimmed.slice(0, firstLineEnd).trim();
|
|
142
|
+
if (headerLine !== "---") {
|
|
143
|
+
return { metadata: {}, content: source, lineOffset: 1 };
|
|
144
|
+
}
|
|
145
|
+
const afterHeader = trimmed.slice(firstLineEnd + 1);
|
|
146
|
+
const lines = afterHeader.split("\n");
|
|
147
|
+
let closingIndex = -1;
|
|
148
|
+
for (let i = 0; i < lines.length; i++) {
|
|
149
|
+
if (lines[i]?.trim() === "---") {
|
|
150
|
+
closingIndex = i;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (closingIndex === -1) {
|
|
155
|
+
this.diagnostics.push({
|
|
156
|
+
code: "UNCLOSED_FRONTMATTER",
|
|
157
|
+
message: 'Frontmatter block was opened with "---" but never closed.',
|
|
158
|
+
severity: "warning",
|
|
159
|
+
loc: {
|
|
160
|
+
start: { line: 1, column: 1 },
|
|
161
|
+
end: { line: 1, column: 4 }
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return { metadata: {}, content: source, lineOffset: 1 };
|
|
165
|
+
}
|
|
166
|
+
const frontmatterLines = lines.slice(0, closingIndex);
|
|
167
|
+
const bodyLines = lines.slice(closingIndex + 1);
|
|
168
|
+
const metadata = this.parseYamlMetadata(frontmatterLines.join("\n"));
|
|
169
|
+
const lineOffset = closingIndex + 2;
|
|
170
|
+
return {
|
|
171
|
+
metadata,
|
|
172
|
+
content: bodyLines.join("\n"),
|
|
173
|
+
lineOffset
|
|
174
|
+
};
|
|
126
175
|
}
|
|
127
176
|
parseYamlMetadata(block) {
|
|
128
177
|
const metadata = {};
|
|
@@ -134,8 +183,8 @@ var DefaultYumiaParser = class {
|
|
|
134
183
|
const colonIndex = trimmed.indexOf(":");
|
|
135
184
|
if (colonIndex > 0) {
|
|
136
185
|
const key = trimmed.slice(0, colonIndex).trim();
|
|
137
|
-
const
|
|
138
|
-
const unquoted =
|
|
186
|
+
const rawVal = trimmed.slice(colonIndex + 1).trim();
|
|
187
|
+
const unquoted = rawVal.replace(/^['"](.*)['"]$/, "$1");
|
|
139
188
|
metadata[key] = unquoted;
|
|
140
189
|
}
|
|
141
190
|
}
|
|
@@ -154,52 +203,79 @@ var DefaultYumiaParser = class {
|
|
|
154
203
|
result.aspectRatio = metadata["aspectRatio"];
|
|
155
204
|
return result;
|
|
156
205
|
}
|
|
157
|
-
splitSlides(content) {
|
|
206
|
+
splitSlides(content, startLineOffset) {
|
|
158
207
|
const lines = content.split("\n");
|
|
159
208
|
const slides = [];
|
|
160
209
|
let currentSlideLines = [];
|
|
161
|
-
|
|
210
|
+
let currentStartLine = startLineOffset;
|
|
211
|
+
for (let i = 0; i < lines.length; i++) {
|
|
212
|
+
const line = lines[i] ?? "";
|
|
162
213
|
if (line.trim() === "---") {
|
|
163
|
-
slides.push(
|
|
214
|
+
slides.push({
|
|
215
|
+
content: currentSlideLines.join("\n"),
|
|
216
|
+
startLine: currentStartLine
|
|
217
|
+
});
|
|
164
218
|
currentSlideLines = [];
|
|
219
|
+
currentStartLine = startLineOffset + i + 1;
|
|
165
220
|
} else {
|
|
166
221
|
currentSlideLines.push(line);
|
|
167
222
|
}
|
|
168
223
|
}
|
|
169
224
|
if (currentSlideLines.length > 0) {
|
|
170
|
-
slides.push(
|
|
225
|
+
slides.push({
|
|
226
|
+
content: currentSlideLines.join("\n"),
|
|
227
|
+
startLine: currentStartLine
|
|
228
|
+
});
|
|
171
229
|
}
|
|
172
230
|
return slides;
|
|
173
231
|
}
|
|
174
|
-
parseSlide(slideContent) {
|
|
232
|
+
parseSlide(slideContent, startLine) {
|
|
175
233
|
const lines = slideContent.split("\n");
|
|
176
|
-
const { elements, notes, layout } = this.parseLines(lines);
|
|
234
|
+
const { elements, notes, layout } = this.parseLines(lines, startLine);
|
|
177
235
|
const slideOptions = {};
|
|
178
236
|
if (notes)
|
|
179
237
|
slideOptions.notes = notes;
|
|
180
238
|
if (layout)
|
|
181
239
|
slideOptions.layout = layout;
|
|
240
|
+
slideOptions.loc = {
|
|
241
|
+
start: { line: startLine, column: 1 },
|
|
242
|
+
end: {
|
|
243
|
+
line: startLine + lines.length - 1,
|
|
244
|
+
column: (lines[lines.length - 1]?.length || 0) + 1
|
|
245
|
+
}
|
|
246
|
+
};
|
|
182
247
|
return createSlide(elements, slideOptions);
|
|
183
248
|
}
|
|
184
|
-
parseLines(lines) {
|
|
249
|
+
parseLines(lines, baseLine) {
|
|
185
250
|
const elements = [];
|
|
186
251
|
let notes;
|
|
187
252
|
let layout;
|
|
188
253
|
let i = 0;
|
|
189
254
|
let currentParagraph = [];
|
|
255
|
+
let paragraphStartLine = baseLine;
|
|
190
256
|
let currentList = null;
|
|
191
257
|
const flushParagraph = () => {
|
|
192
258
|
if (currentParagraph.length > 0) {
|
|
193
259
|
const text = currentParagraph.join(" ").trim();
|
|
194
260
|
if (text) {
|
|
195
|
-
|
|
261
|
+
const el = createParagraph(text);
|
|
262
|
+
el.loc = {
|
|
263
|
+
start: { line: paragraphStartLine, column: 1 },
|
|
264
|
+
end: { line: paragraphStartLine + currentParagraph.length - 1, column: 1 }
|
|
265
|
+
};
|
|
266
|
+
elements.push(el);
|
|
196
267
|
}
|
|
197
268
|
currentParagraph = [];
|
|
198
269
|
}
|
|
199
270
|
};
|
|
200
271
|
const flushList = () => {
|
|
201
272
|
if (currentList && currentList.items.length > 0) {
|
|
202
|
-
|
|
273
|
+
const el = createList(currentList.items, currentList.ordered);
|
|
274
|
+
el.loc = {
|
|
275
|
+
start: { line: currentList.startLine, column: 1 },
|
|
276
|
+
end: { line: currentList.startLine + currentList.items.length - 1, column: 1 }
|
|
277
|
+
};
|
|
278
|
+
elements.push(el);
|
|
203
279
|
currentList = null;
|
|
204
280
|
}
|
|
205
281
|
};
|
|
@@ -210,6 +286,7 @@ var DefaultYumiaParser = class {
|
|
|
210
286
|
while (i < lines.length) {
|
|
211
287
|
const rawLine = lines[i] ?? "";
|
|
212
288
|
const line = rawLine.trim();
|
|
289
|
+
const currentLineNum = baseLine + i;
|
|
213
290
|
if (!line) {
|
|
214
291
|
flushAll();
|
|
215
292
|
i++;
|
|
@@ -219,24 +296,53 @@ var DefaultYumiaParser = class {
|
|
|
219
296
|
flushAll();
|
|
220
297
|
const language = line.slice(3).trim() || void 0;
|
|
221
298
|
const codeLines = [];
|
|
299
|
+
const codeStartLine = currentLineNum;
|
|
222
300
|
i++;
|
|
223
|
-
|
|
301
|
+
let closed = false;
|
|
302
|
+
while (i < lines.length) {
|
|
303
|
+
const checkLine = lines[i]?.trim() ?? "";
|
|
304
|
+
if (checkLine.startsWith("```")) {
|
|
305
|
+
closed = true;
|
|
306
|
+
i++;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
224
309
|
codeLines.push(lines[i] ?? "");
|
|
225
310
|
i++;
|
|
226
311
|
}
|
|
227
|
-
|
|
228
|
-
|
|
312
|
+
if (!closed) {
|
|
313
|
+
this.diagnostics.push({
|
|
314
|
+
code: "UNCLOSED_CODE_BLOCK",
|
|
315
|
+
message: `Unclosed code block opened at line ${codeStartLine}`,
|
|
316
|
+
severity: "warning",
|
|
317
|
+
loc: {
|
|
318
|
+
start: { line: codeStartLine, column: 1 },
|
|
319
|
+
end: { line: baseLine + i - 1, column: 1 }
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
const el = createCode(codeLines.join("\n"), language);
|
|
324
|
+
el.loc = {
|
|
325
|
+
start: { line: codeStartLine, column: 1 },
|
|
326
|
+
end: { line: baseLine + i - 1, column: 1 }
|
|
327
|
+
};
|
|
328
|
+
elements.push(el);
|
|
229
329
|
continue;
|
|
230
330
|
}
|
|
231
331
|
if (line.startsWith(":::")) {
|
|
232
332
|
flushAll();
|
|
233
333
|
const directiveHeader = line.slice(3).trim();
|
|
334
|
+
const directiveStartLine = currentLineNum;
|
|
234
335
|
if (directiveHeader.startsWith("layout")) {
|
|
235
336
|
const layoutMatch = directiveHeader.match(/^layout\s*(.*)$/);
|
|
236
337
|
const rawArg = layoutMatch && layoutMatch[1] ? layoutMatch[1].trim() : "default";
|
|
237
338
|
const mode = rawArg.replace(/^['"](.*)['"]$/, "$1");
|
|
238
339
|
layout = mode;
|
|
239
|
-
|
|
340
|
+
const el = createLayoutDirective(mode);
|
|
341
|
+
el.loc = {
|
|
342
|
+
start: { line: currentLineNum, column: 1 },
|
|
343
|
+
end: { line: currentLineNum, column: rawLine.length + 1 }
|
|
344
|
+
};
|
|
345
|
+
elements.push(el);
|
|
240
346
|
i++;
|
|
241
347
|
continue;
|
|
242
348
|
}
|
|
@@ -245,6 +351,7 @@ var DefaultYumiaParser = class {
|
|
|
245
351
|
const blockLines = [];
|
|
246
352
|
i++;
|
|
247
353
|
let nestedCount = 1;
|
|
354
|
+
let closed = false;
|
|
248
355
|
while (i < lines.length) {
|
|
249
356
|
const innerLine = lines[i]?.trim() ?? "";
|
|
250
357
|
if (innerLine.startsWith(":::") && innerLine.length > 3) {
|
|
@@ -252,6 +359,7 @@ var DefaultYumiaParser = class {
|
|
|
252
359
|
} else if (innerLine === ":::") {
|
|
253
360
|
nestedCount--;
|
|
254
361
|
if (nestedCount === 0) {
|
|
362
|
+
closed = true;
|
|
255
363
|
i++;
|
|
256
364
|
break;
|
|
257
365
|
}
|
|
@@ -259,36 +367,99 @@ var DefaultYumiaParser = class {
|
|
|
259
367
|
blockLines.push(lines[i] ?? "");
|
|
260
368
|
i++;
|
|
261
369
|
}
|
|
370
|
+
if (!closed) {
|
|
371
|
+
this.diagnostics.push({
|
|
372
|
+
code: "UNCLOSED_DIRECTIVE",
|
|
373
|
+
message: `Directive ':::${directiveName}' at line ${directiveStartLine} was never closed with ':::'`,
|
|
374
|
+
severity: "warning",
|
|
375
|
+
loc: {
|
|
376
|
+
start: { line: directiveStartLine, column: 1 },
|
|
377
|
+
end: { line: baseLine + i - 1, column: 1 }
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
const blockBaseLine = directiveStartLine + 1;
|
|
262
382
|
if (directiveName === "notes") {
|
|
263
383
|
notes = blockLines.map((l) => l.trim()).filter(Boolean).join("\n");
|
|
264
384
|
} else if (directiveName === "card") {
|
|
265
385
|
const title = directiveArg.replace(/^['"](.*)['"]$/, "$1") || void 0;
|
|
266
|
-
const { elements: cardElements } = this.parseLines(blockLines);
|
|
267
|
-
|
|
386
|
+
const { elements: cardElements } = this.parseLines(blockLines, blockBaseLine);
|
|
387
|
+
const el = createCard(cardElements, title);
|
|
388
|
+
el.loc = {
|
|
389
|
+
start: { line: directiveStartLine, column: 1 },
|
|
390
|
+
end: { line: baseLine + i - 1, column: 1 }
|
|
391
|
+
};
|
|
392
|
+
elements.push(el);
|
|
268
393
|
} else if (directiveName === "columns") {
|
|
269
394
|
const ratios = directiveArg ? directiveArg.replace(/^ratios=['"]?(.*?)['"]?$/, "$1") : void 0;
|
|
270
|
-
const columns = this.parseColumnsBlock(blockLines);
|
|
271
|
-
|
|
395
|
+
const columns = this.parseColumnsBlock(blockLines, blockBaseLine);
|
|
396
|
+
const el = createColumns(columns, ratios);
|
|
397
|
+
el.loc = {
|
|
398
|
+
start: { line: directiveStartLine, column: 1 },
|
|
399
|
+
end: { line: baseLine + i - 1, column: 1 }
|
|
400
|
+
};
|
|
401
|
+
elements.push(el);
|
|
272
402
|
} else if (directiveName === "quote") {
|
|
273
403
|
const author = directiveArg.replace(/^['"](.*)['"]$/, "$1") || void 0;
|
|
274
404
|
const quoteText = blockLines.map((l) => l.trim()).join(" ").trim();
|
|
275
|
-
|
|
405
|
+
const el = createQuote(quoteText, author);
|
|
406
|
+
el.loc = {
|
|
407
|
+
start: { line: directiveStartLine, column: 1 },
|
|
408
|
+
end: { line: baseLine + i - 1, column: 1 }
|
|
409
|
+
};
|
|
410
|
+
elements.push(el);
|
|
276
411
|
}
|
|
277
412
|
continue;
|
|
278
413
|
}
|
|
414
|
+
if (line.startsWith("|") && line.endsWith("|")) {
|
|
415
|
+
const nextLine = lines[i + 1]?.trim() ?? "";
|
|
416
|
+
if (nextLine.startsWith("|") && nextLine.includes("---")) {
|
|
417
|
+
flushAll();
|
|
418
|
+
const tableStartLine = currentLineNum;
|
|
419
|
+
const headerCells = line.slice(1, -1).split("|").map((c) => c.trim());
|
|
420
|
+
const rows = [];
|
|
421
|
+
i += 2;
|
|
422
|
+
while (i < lines.length) {
|
|
423
|
+
const rowLine = lines[i]?.trim() ?? "";
|
|
424
|
+
if (!rowLine.startsWith("|") || !rowLine.endsWith("|")) {
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
const rowCells = rowLine.slice(1, -1).split("|").map((c) => c.trim());
|
|
428
|
+
rows.push(rowCells);
|
|
429
|
+
i++;
|
|
430
|
+
}
|
|
431
|
+
const tableEl = createTable(rows, headerCells);
|
|
432
|
+
tableEl.loc = {
|
|
433
|
+
start: { line: tableStartLine, column: 1 },
|
|
434
|
+
end: { line: baseLine + i - 1, column: 1 }
|
|
435
|
+
};
|
|
436
|
+
elements.push(tableEl);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
279
440
|
const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
|
|
280
441
|
if (headingMatch && headingMatch[1] && headingMatch[2]) {
|
|
281
442
|
flushAll();
|
|
282
443
|
const level = headingMatch[1].length;
|
|
283
444
|
const text = headingMatch[2].trim();
|
|
284
|
-
|
|
445
|
+
const el = createHeading(text, level);
|
|
446
|
+
el.loc = {
|
|
447
|
+
start: { line: currentLineNum, column: 1 },
|
|
448
|
+
end: { line: currentLineNum, column: rawLine.length + 1 }
|
|
449
|
+
};
|
|
450
|
+
elements.push(el);
|
|
285
451
|
i++;
|
|
286
452
|
continue;
|
|
287
453
|
}
|
|
288
454
|
if (line.startsWith(">")) {
|
|
289
455
|
flushAll();
|
|
290
456
|
const quoteText = line.replace(/^>\s*/, "").trim();
|
|
291
|
-
|
|
457
|
+
const el = createQuote(quoteText);
|
|
458
|
+
el.loc = {
|
|
459
|
+
start: { line: currentLineNum, column: 1 },
|
|
460
|
+
end: { line: currentLineNum, column: rawLine.length + 1 }
|
|
461
|
+
};
|
|
462
|
+
elements.push(el);
|
|
292
463
|
i++;
|
|
293
464
|
continue;
|
|
294
465
|
}
|
|
@@ -298,7 +469,12 @@ var DefaultYumiaParser = class {
|
|
|
298
469
|
const alt = imageMatch[1] || void 0;
|
|
299
470
|
const src = imageMatch[2].trim();
|
|
300
471
|
const caption = imageMatch[3] || void 0;
|
|
301
|
-
|
|
472
|
+
const el = createImage(src, alt, caption);
|
|
473
|
+
el.loc = {
|
|
474
|
+
start: { line: currentLineNum, column: 1 },
|
|
475
|
+
end: { line: currentLineNum, column: rawLine.length + 1 }
|
|
476
|
+
};
|
|
477
|
+
elements.push(el);
|
|
302
478
|
i++;
|
|
303
479
|
continue;
|
|
304
480
|
}
|
|
@@ -307,7 +483,7 @@ var DefaultYumiaParser = class {
|
|
|
307
483
|
flushParagraph();
|
|
308
484
|
if (!currentList || currentList.ordered) {
|
|
309
485
|
flushList();
|
|
310
|
-
currentList = { ordered: false, items: [] };
|
|
486
|
+
currentList = { ordered: false, items: [], startLine: currentLineNum };
|
|
311
487
|
}
|
|
312
488
|
currentList.items.push(unorderMatch[1].trim());
|
|
313
489
|
i++;
|
|
@@ -318,13 +494,16 @@ var DefaultYumiaParser = class {
|
|
|
318
494
|
flushParagraph();
|
|
319
495
|
if (!currentList || !currentList.ordered) {
|
|
320
496
|
flushList();
|
|
321
|
-
currentList = { ordered: true, items: [] };
|
|
497
|
+
currentList = { ordered: true, items: [], startLine: currentLineNum };
|
|
322
498
|
}
|
|
323
499
|
currentList.items.push(orderMatch[1].trim());
|
|
324
500
|
i++;
|
|
325
501
|
continue;
|
|
326
502
|
}
|
|
327
503
|
flushList();
|
|
504
|
+
if (currentParagraph.length === 0) {
|
|
505
|
+
paragraphStartLine = currentLineNum;
|
|
506
|
+
}
|
|
328
507
|
currentParagraph.push(line);
|
|
329
508
|
i++;
|
|
330
509
|
}
|
|
@@ -335,24 +514,41 @@ var DefaultYumiaParser = class {
|
|
|
335
514
|
...layout ? { layout } : {}
|
|
336
515
|
};
|
|
337
516
|
}
|
|
338
|
-
parseColumnsBlock(lines) {
|
|
517
|
+
parseColumnsBlock(lines, baseLine) {
|
|
339
518
|
const columns = [];
|
|
340
519
|
let i = 0;
|
|
341
520
|
while (i < lines.length) {
|
|
342
521
|
const line = lines[i]?.trim() ?? "";
|
|
522
|
+
const currentLineNum = baseLine + i;
|
|
343
523
|
if (line.startsWith(":::column")) {
|
|
344
524
|
const arg = line.slice(9).trim();
|
|
345
525
|
const widthMatch = arg.match(/width=['"]?(.*?)['"]?$/);
|
|
346
526
|
const width = widthMatch && widthMatch[1] ? widthMatch[1] : void 0;
|
|
347
527
|
const colLines = [];
|
|
528
|
+
const colStartLine = currentLineNum;
|
|
348
529
|
i++;
|
|
349
|
-
|
|
530
|
+
let nestedCount = 1;
|
|
531
|
+
while (i < lines.length) {
|
|
532
|
+
const innerLine = lines[i]?.trim() ?? "";
|
|
533
|
+
if (innerLine.startsWith(":::") && innerLine.length > 3) {
|
|
534
|
+
nestedCount++;
|
|
535
|
+
} else if (innerLine === ":::") {
|
|
536
|
+
nestedCount--;
|
|
537
|
+
if (nestedCount === 0) {
|
|
538
|
+
i++;
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
350
542
|
colLines.push(lines[i] ?? "");
|
|
351
543
|
i++;
|
|
352
544
|
}
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
545
|
+
const { elements } = this.parseLines(colLines, colStartLine + 1);
|
|
546
|
+
const col = createColumn(elements, width);
|
|
547
|
+
col.loc = {
|
|
548
|
+
start: { line: colStartLine, column: 1 },
|
|
549
|
+
end: { line: baseLine + i - 1, column: 1 }
|
|
550
|
+
};
|
|
551
|
+
columns.push(col);
|
|
356
552
|
} else {
|
|
357
553
|
i++;
|
|
358
554
|
}
|
|
@@ -370,12 +566,17 @@ var DEFAULT_VIEWPORT = {
|
|
|
370
566
|
width: 1920,
|
|
371
567
|
height: 1080
|
|
372
568
|
};
|
|
569
|
+
var VIEWPORT_PRESETS = {
|
|
570
|
+
"16:9": { width: 1920, height: 1080 },
|
|
571
|
+
"4:3": { width: 1440, height: 1080 },
|
|
572
|
+
"16:10": { width: 1920, height: 1200 }
|
|
573
|
+
};
|
|
373
574
|
var DefaultLayoutEngine = class {
|
|
374
575
|
computeSlide(slide, viewport = DEFAULT_VIEWPORT, options = {}) {
|
|
375
576
|
const padding = options.padding ?? 64;
|
|
376
577
|
const gap = options.gap ?? 24;
|
|
377
|
-
const availableWidth = viewport.width - padding * 2;
|
|
378
|
-
const availableHeight = viewport.height - padding * 2;
|
|
578
|
+
const availableWidth = Math.max(100, viewport.width - padding * 2);
|
|
579
|
+
const availableHeight = Math.max(100, viewport.height - padding * 2);
|
|
379
580
|
if (slide.elements.length === 0) {
|
|
380
581
|
return {
|
|
381
582
|
...slide.id ? { slideId: slide.id } : {},
|
|
@@ -396,10 +597,11 @@ var DefaultLayoutEngine = class {
|
|
|
396
597
|
overflowAmount
|
|
397
598
|
};
|
|
398
599
|
}
|
|
399
|
-
computePresentation(presentation, viewport
|
|
600
|
+
computePresentation(presentation, viewport, options = {}) {
|
|
601
|
+
const effectiveViewport = viewport || presentation.metadata.aspectRatio && VIEWPORT_PRESETS[presentation.metadata.aspectRatio] || DEFAULT_VIEWPORT;
|
|
400
602
|
return {
|
|
401
|
-
viewport,
|
|
402
|
-
slides: presentation.slides.map((slide) => this.computeSlide(slide,
|
|
603
|
+
viewport: effectiveViewport,
|
|
604
|
+
slides: presentation.slides.map((slide) => this.computeSlide(slide, effectiveViewport, options))
|
|
403
605
|
};
|
|
404
606
|
}
|
|
405
607
|
layoutElementList(elements, startX, startY, availableWidth, gap) {
|
|
@@ -435,6 +637,14 @@ var DefaultLayoutEngine = class {
|
|
|
435
637
|
const height = this.estimateQuoteHeight(element, width);
|
|
436
638
|
return { element, bounds: { x, y, width, height } };
|
|
437
639
|
}
|
|
640
|
+
case "table": {
|
|
641
|
+
const height = this.estimateTableHeight(element);
|
|
642
|
+
return { element, bounds: { x, y, width, height } };
|
|
643
|
+
}
|
|
644
|
+
case "image": {
|
|
645
|
+
const height = this.estimateImageHeight(element);
|
|
646
|
+
return { element, bounds: { x, y, width, height } };
|
|
647
|
+
}
|
|
438
648
|
case "card": {
|
|
439
649
|
return this.layoutCard(element, x, y, width, gap);
|
|
440
650
|
}
|
|
@@ -449,7 +659,7 @@ var DefaultLayoutEngine = class {
|
|
|
449
659
|
}
|
|
450
660
|
layoutCard(element, x, y, width, gap) {
|
|
451
661
|
const cardPadding = 28;
|
|
452
|
-
const innerWidth = width - cardPadding * 2;
|
|
662
|
+
const innerWidth = Math.max(10, width - cardPadding * 2);
|
|
453
663
|
const titleHeight = element.title ? 56 : 0;
|
|
454
664
|
const innerStartY = y + cardPadding + titleHeight;
|
|
455
665
|
const { nodes: children, totalHeight: innerHeight } = this.layoutElementList(element.elements, x + cardPadding, innerStartY, innerWidth, gap / 1.5);
|
|
@@ -468,7 +678,7 @@ var DefaultLayoutEngine = class {
|
|
|
468
678
|
}
|
|
469
679
|
const ratios = this.parseRatios(element.ratios, columnCount);
|
|
470
680
|
const totalGap = gap * (columnCount - 1);
|
|
471
|
-
const usableWidth = width - totalGap;
|
|
681
|
+
const usableWidth = Math.max(10, width - totalGap);
|
|
472
682
|
let currentX = x;
|
|
473
683
|
const columnNodes = [];
|
|
474
684
|
let maxColumnHeight = 0;
|
|
@@ -531,6 +741,17 @@ var DefaultLayoutEngine = class {
|
|
|
531
741
|
const lines = Math.ceil(quote.text.length / charsPerLine) || 1;
|
|
532
742
|
return lines * 32 + 24;
|
|
533
743
|
}
|
|
744
|
+
estimateTableHeight(table) {
|
|
745
|
+
const headerHeight = table.headers && table.headers.length > 0 ? 50 : 0;
|
|
746
|
+
const rowsHeight = (table.rows ? table.rows.length : 0) * 42;
|
|
747
|
+
return Math.max(60, headerHeight + rowsHeight + 16);
|
|
748
|
+
}
|
|
749
|
+
estimateImageHeight(image) {
|
|
750
|
+
if (typeof image.height === "number") {
|
|
751
|
+
return image.height;
|
|
752
|
+
}
|
|
753
|
+
return 320;
|
|
754
|
+
}
|
|
534
755
|
};
|
|
535
756
|
|
|
536
757
|
// ../theme/dist/default-theme.js
|
|
@@ -632,6 +853,18 @@ var YumiaCompiler = class {
|
|
|
632
853
|
parse(source, options) {
|
|
633
854
|
return this.parser.parse(source, options);
|
|
634
855
|
}
|
|
856
|
+
validate(source) {
|
|
857
|
+
const presentation = this.parse(source);
|
|
858
|
+
const diagnostics = presentation.diagnostics || [];
|
|
859
|
+
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
860
|
+
const warnings = diagnostics.filter((d) => d.severity === "warning");
|
|
861
|
+
return {
|
|
862
|
+
valid: errors.length === 0,
|
|
863
|
+
slideCount: presentation.slides.length,
|
|
864
|
+
errors,
|
|
865
|
+
warnings
|
|
866
|
+
};
|
|
867
|
+
}
|
|
635
868
|
layout(presentation, viewport) {
|
|
636
869
|
return this.layoutEngine.computePresentation(presentation, viewport ?? this.viewport);
|
|
637
870
|
}
|
|
@@ -649,10 +882,68 @@ var YumiaCompiler = class {
|
|
|
649
882
|
const presentation = this.parse(source, options?.parserOptions);
|
|
650
883
|
return this.render(presentation, renderer, options?.renderContext);
|
|
651
884
|
}
|
|
885
|
+
getSchema() {
|
|
886
|
+
return {
|
|
887
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
888
|
+
title: "YumiaMDPresentation",
|
|
889
|
+
type: "object",
|
|
890
|
+
description: "YumiaMD semantic presentation structure and directive definitions",
|
|
891
|
+
properties: {
|
|
892
|
+
frontmatter: {
|
|
893
|
+
type: "object",
|
|
894
|
+
properties: {
|
|
895
|
+
title: { type: "string" },
|
|
896
|
+
subtitle: { type: "string" },
|
|
897
|
+
author: { type: "string" },
|
|
898
|
+
date: { type: "string" },
|
|
899
|
+
theme: { type: "string" },
|
|
900
|
+
aspectRatio: { enum: ["16:9", "4:3", "16:10"] }
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
directives: {
|
|
904
|
+
type: "object",
|
|
905
|
+
properties: {
|
|
906
|
+
columns: {
|
|
907
|
+
syntax: ':::columns [ratios="50:50"]\\n:::column\\n...\\n:::\\n:::',
|
|
908
|
+
description: "Multi-column grid layout"
|
|
909
|
+
},
|
|
910
|
+
card: {
|
|
911
|
+
syntax: ":::card [Title]\\n...\\n:::",
|
|
912
|
+
description: "Visual container card with theme border and background"
|
|
913
|
+
},
|
|
914
|
+
notes: {
|
|
915
|
+
syntax: ":::notes\\nSpeaker notes text\\n:::",
|
|
916
|
+
description: "Speaker notes attached to slide metadata"
|
|
917
|
+
},
|
|
918
|
+
quote: {
|
|
919
|
+
syntax: ":::quote [Author]\\nQuote text\\n:::",
|
|
920
|
+
description: "Highlighted quotation with accent bar"
|
|
921
|
+
},
|
|
922
|
+
layout: {
|
|
923
|
+
syntax: ":::layout [mode]",
|
|
924
|
+
description: "Slide layout mode directive"
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
}
|
|
652
931
|
};
|
|
653
932
|
|
|
654
933
|
// ../renderer-pptx/dist/renderer.js
|
|
655
934
|
import pptxgen from "pptxgenjs";
|
|
935
|
+
var CSS_NAMED_COLORS = {
|
|
936
|
+
white: "ffffff",
|
|
937
|
+
black: "000000",
|
|
938
|
+
red: "ef4444",
|
|
939
|
+
blue: "3b82f6",
|
|
940
|
+
green: "22c55e",
|
|
941
|
+
yellow: "eab308",
|
|
942
|
+
gray: "64748b",
|
|
943
|
+
grey: "64748b",
|
|
944
|
+
slate: "0f172a",
|
|
945
|
+
transparent: "ffffff"
|
|
946
|
+
};
|
|
656
947
|
var PptxRenderer = class {
|
|
657
948
|
name = "PptxRenderer";
|
|
658
949
|
targetFormat = "pptx";
|
|
@@ -668,16 +959,18 @@ var PptxRenderer = class {
|
|
|
668
959
|
const author = presentation.metadata.author || "YumiaMD";
|
|
669
960
|
pptx.title = title;
|
|
670
961
|
pptx.author = author;
|
|
671
|
-
const
|
|
962
|
+
const is43 = presentation.metadata.aspectRatio === "4:3";
|
|
963
|
+
const slideWidthInches = is43 ? 10 : 13.333;
|
|
672
964
|
const slideHeightInches = 7.5;
|
|
673
|
-
|
|
674
|
-
pptx.
|
|
675
|
-
|
|
965
|
+
const layoutName = is43 ? "YUMIA_4_3" : "YUMIA_16_9";
|
|
966
|
+
pptx.defineLayout({ name: layoutName, width: slideWidthInches, height: slideHeightInches });
|
|
967
|
+
pptx.layout = layoutName;
|
|
968
|
+
const pixelViewport = is43 ? { width: 1440, height: 1080 } : { width: 1920, height: 1080 };
|
|
676
969
|
const scaleX = slideWidthInches / pixelViewport.width;
|
|
677
970
|
const scaleY = slideHeightInches / pixelViewport.height;
|
|
678
971
|
for (const slide of presentation.slides) {
|
|
679
972
|
const pptxSlide = pptx.addSlide();
|
|
680
|
-
const bgColor = this.cleanHexColor(theme.colors.background);
|
|
973
|
+
const bgColor = this.cleanHexColor(slide.background?.value || theme.colors.background);
|
|
681
974
|
pptxSlide.background = { color: bgColor };
|
|
682
975
|
const slideLayout = this.layoutEngine.computeSlide(slide, pixelViewport);
|
|
683
976
|
for (const node of slideLayout.nodes) {
|
|
@@ -693,7 +986,7 @@ var PptxRenderer = class {
|
|
|
693
986
|
format: "pptx",
|
|
694
987
|
data: uint8Array,
|
|
695
988
|
slideCount: presentation.slides.length,
|
|
696
|
-
fileName: `${title.toLowerCase().replace(
|
|
989
|
+
fileName: `${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}.pptx`
|
|
697
990
|
};
|
|
698
991
|
}
|
|
699
992
|
renderNode(pptxSlide, pptx, node, scaleX, scaleY, theme) {
|
|
@@ -715,6 +1008,12 @@ var PptxRenderer = class {
|
|
|
715
1008
|
case "quote":
|
|
716
1009
|
this.renderQuote(pptxSlide, pptx, element, rect, theme);
|
|
717
1010
|
break;
|
|
1011
|
+
case "table":
|
|
1012
|
+
this.renderTable(pptxSlide, element, rect, theme);
|
|
1013
|
+
break;
|
|
1014
|
+
case "image":
|
|
1015
|
+
this.renderImage(pptxSlide, element, rect);
|
|
1016
|
+
break;
|
|
718
1017
|
case "card":
|
|
719
1018
|
this.renderCard(pptxSlide, pptx, node, scaleX, scaleY, theme);
|
|
720
1019
|
break;
|
|
@@ -781,6 +1080,75 @@ var PptxRenderer = class {
|
|
|
781
1080
|
margin: 0
|
|
782
1081
|
});
|
|
783
1082
|
}
|
|
1083
|
+
renderTable(pptxSlide, table, rect, theme) {
|
|
1084
|
+
const tableRows = [];
|
|
1085
|
+
const headerBg = this.cleanHexColor(theme.colors.primary);
|
|
1086
|
+
const borderColor = this.cleanHexColor(theme.colors.border || "#cbd5e1");
|
|
1087
|
+
if (table.headers && table.headers.length > 0) {
|
|
1088
|
+
tableRows.push(table.headers.map((h) => ({
|
|
1089
|
+
text: h,
|
|
1090
|
+
options: {
|
|
1091
|
+
bold: true,
|
|
1092
|
+
color: "ffffff",
|
|
1093
|
+
fill: { color: headerBg },
|
|
1094
|
+
fontSize: 14,
|
|
1095
|
+
align: "center"
|
|
1096
|
+
}
|
|
1097
|
+
})));
|
|
1098
|
+
}
|
|
1099
|
+
if (table.rows) {
|
|
1100
|
+
table.rows.forEach((row, rowIndex) => {
|
|
1101
|
+
const rowBg = rowIndex % 2 === 1 ? "f8fafc" : "ffffff";
|
|
1102
|
+
tableRows.push(row.map((cell) => ({
|
|
1103
|
+
text: cell,
|
|
1104
|
+
options: {
|
|
1105
|
+
color: this.cleanHexColor(theme.colors.text),
|
|
1106
|
+
fill: { color: rowBg },
|
|
1107
|
+
fontSize: 13
|
|
1108
|
+
}
|
|
1109
|
+
})));
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
if (tableRows.length > 0) {
|
|
1113
|
+
pptxSlide.addTable(tableRows, {
|
|
1114
|
+
x: rect.x,
|
|
1115
|
+
y: rect.y,
|
|
1116
|
+
w: rect.w,
|
|
1117
|
+
border: { type: "solid", pt: 1, color: borderColor },
|
|
1118
|
+
margin: [4, 8, 4, 8]
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
renderImage(pptxSlide, image, rect) {
|
|
1123
|
+
try {
|
|
1124
|
+
pptxSlide.addImage({
|
|
1125
|
+
path: image.src,
|
|
1126
|
+
x: rect.x,
|
|
1127
|
+
y: rect.y,
|
|
1128
|
+
w: rect.w,
|
|
1129
|
+
h: rect.h
|
|
1130
|
+
});
|
|
1131
|
+
} catch {
|
|
1132
|
+
pptxSlide.addShape("rect", {
|
|
1133
|
+
x: rect.x,
|
|
1134
|
+
y: rect.y,
|
|
1135
|
+
w: rect.w,
|
|
1136
|
+
h: rect.h,
|
|
1137
|
+
fill: { color: "e2e8f0" },
|
|
1138
|
+
line: { color: "94a3b8", width: 1 }
|
|
1139
|
+
});
|
|
1140
|
+
pptxSlide.addText(`[Image: ${image.alt || image.src}]`, {
|
|
1141
|
+
x: rect.x,
|
|
1142
|
+
y: rect.y,
|
|
1143
|
+
w: rect.w,
|
|
1144
|
+
h: rect.h,
|
|
1145
|
+
fontSize: 12,
|
|
1146
|
+
color: "64748b",
|
|
1147
|
+
align: "center",
|
|
1148
|
+
valign: "middle"
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
784
1152
|
renderCard(pptxSlide, pptx, node, scaleX, scaleY, theme) {
|
|
785
1153
|
const card = node.element;
|
|
786
1154
|
const rect = this.toInches(node.bounds, scaleX, scaleY);
|
|
@@ -892,15 +1260,29 @@ var PptxRenderer = class {
|
|
|
892
1260
|
return 18;
|
|
893
1261
|
}
|
|
894
1262
|
}
|
|
895
|
-
cleanHexColor(
|
|
896
|
-
|
|
1263
|
+
cleanHexColor(raw) {
|
|
1264
|
+
if (!raw)
|
|
1265
|
+
return "000000";
|
|
1266
|
+
const trimmed = raw.trim().toLowerCase();
|
|
1267
|
+
if (CSS_NAMED_COLORS[trimmed]) {
|
|
1268
|
+
return CSS_NAMED_COLORS[trimmed];
|
|
1269
|
+
}
|
|
1270
|
+
const hexMatch = trimmed.match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
1271
|
+
if (hexMatch && hexMatch[1]) {
|
|
1272
|
+
const hex = hexMatch[1];
|
|
1273
|
+
if (hex.length === 3) {
|
|
1274
|
+
return hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
1275
|
+
}
|
|
1276
|
+
return hex;
|
|
1277
|
+
}
|
|
1278
|
+
return "000000";
|
|
897
1279
|
}
|
|
898
1280
|
};
|
|
899
1281
|
|
|
900
1282
|
// src/cli.ts
|
|
901
1283
|
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
902
1284
|
import { basename, dirname, extname, join, resolve } from "path";
|
|
903
|
-
var VERSION = "0.1.
|
|
1285
|
+
var VERSION = "0.1.6";
|
|
904
1286
|
function printHelp() {
|
|
905
1287
|
return `
|
|
906
1288
|
YumiaMD \u2014 Markdown-based presentation compiler (v${VERSION})
|
|
@@ -913,11 +1295,13 @@ Commands:
|
|
|
913
1295
|
validate <file> Validate a .yumia.md presentation syntax and structure
|
|
914
1296
|
lint <file> Analyze presentation for layout overflows and accessibility
|
|
915
1297
|
inspect <file> Inspect the AST and geometric layout tree
|
|
1298
|
+
schema Output machine-readable JSON schema for AI agents
|
|
916
1299
|
build <file> Compile a presentation to editable PowerPoint (.pptx)
|
|
917
1300
|
|
|
918
1301
|
Options:
|
|
919
1302
|
--out, -o <file> Specify output file path (default: dist/<name>.pptx)
|
|
920
1303
|
--format, -f <fmt> Target output format: pptx (default)
|
|
1304
|
+
--json Output results formatted as JSON for CI/CD and AI tools
|
|
921
1305
|
--layout Show computed geometric bounding boxes in 'inspect'
|
|
922
1306
|
-h, --help Show this help message
|
|
923
1307
|
-v, --version Show CLI version
|
|
@@ -931,8 +1315,17 @@ async function runCli(argv) {
|
|
|
931
1315
|
if (args.includes("-v") || args.includes("--version")) {
|
|
932
1316
|
return { exitCode: 0, output: `yumia v${VERSION}` };
|
|
933
1317
|
}
|
|
934
|
-
const
|
|
935
|
-
const
|
|
1318
|
+
const isJson = args.includes("--json");
|
|
1319
|
+
const nonFlagArgs = args.filter((a) => !a.startsWith("-"));
|
|
1320
|
+
const command = nonFlagArgs[0];
|
|
1321
|
+
const target = nonFlagArgs[1];
|
|
1322
|
+
if (command === "schema") {
|
|
1323
|
+
const compiler = new YumiaCompiler();
|
|
1324
|
+
return {
|
|
1325
|
+
exitCode: 0,
|
|
1326
|
+
output: JSON.stringify(compiler.getSchema(), null, 2)
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
936
1329
|
if (command === "init") {
|
|
937
1330
|
const projectName = target || "yumia-deck";
|
|
938
1331
|
const projectDir = join(process.cwd(), projectName);
|
|
@@ -943,38 +1336,56 @@ async function runCli(argv) {
|
|
|
943
1336
|
const sampleContent = `---
|
|
944
1337
|
title: ${projectName}
|
|
945
1338
|
theme: default
|
|
1339
|
+
aspectRatio: "16:9"
|
|
946
1340
|
---
|
|
947
1341
|
|
|
948
1342
|
# ${projectName}
|
|
949
|
-
Presentation authoring
|
|
1343
|
+
Presentation authoring designed for humans and AI.
|
|
950
1344
|
|
|
951
1345
|
:::notes
|
|
952
|
-
Opening slide introducing the
|
|
1346
|
+
Opening slide introducing the presentation deck.
|
|
953
1347
|
:::
|
|
954
1348
|
|
|
955
1349
|
---
|
|
956
1350
|
|
|
957
|
-
# Architecture
|
|
1351
|
+
# Architecture & Modularity
|
|
958
1352
|
|
|
959
1353
|
:::columns ratios="50:50"
|
|
960
1354
|
|
|
961
1355
|
:::column
|
|
962
|
-
:::card
|
|
963
|
-
-
|
|
964
|
-
-
|
|
1356
|
+
:::card Core Pipeline
|
|
1357
|
+
- Semantic AST model
|
|
1358
|
+
- Markdown + Directive parser
|
|
1359
|
+
- 100% Deterministic layout
|
|
965
1360
|
:::
|
|
966
1361
|
:::
|
|
967
1362
|
|
|
968
1363
|
:::column
|
|
969
|
-
:::card Output
|
|
970
|
-
-
|
|
971
|
-
-
|
|
1364
|
+
:::card Native Output
|
|
1365
|
+
- Fully editable PowerPoint objects
|
|
1366
|
+
- Vector PDF documents
|
|
1367
|
+
- Interactive HTML decks
|
|
972
1368
|
:::
|
|
973
1369
|
:::
|
|
974
1370
|
|
|
975
1371
|
:::
|
|
976
1372
|
`.trim();
|
|
977
1373
|
writeFileSync(join(projectDir, "presentation.yumia.md"), sampleContent, "utf-8");
|
|
1374
|
+
if (isJson) {
|
|
1375
|
+
return {
|
|
1376
|
+
exitCode: 0,
|
|
1377
|
+
output: JSON.stringify(
|
|
1378
|
+
{
|
|
1379
|
+
success: true,
|
|
1380
|
+
project: projectName,
|
|
1381
|
+
path: projectDir,
|
|
1382
|
+
entry: join(projectDir, "presentation.yumia.md")
|
|
1383
|
+
},
|
|
1384
|
+
null,
|
|
1385
|
+
2
|
|
1386
|
+
)
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
978
1389
|
return {
|
|
979
1390
|
exitCode: 0,
|
|
980
1391
|
output: `\u2713 Created presentation project '${projectName}' at ./${projectName}
|
|
@@ -982,33 +1393,62 @@ Opening slide introducing the project.
|
|
|
982
1393
|
Then compile with: yumia build ./${projectName}/presentation.yumia.md`
|
|
983
1394
|
};
|
|
984
1395
|
} catch (err) {
|
|
1396
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1397
|
+
if (isJson) {
|
|
1398
|
+
return { exitCode: 1, output: JSON.stringify({ success: false, error: msg }, null, 2) };
|
|
1399
|
+
}
|
|
985
1400
|
return {
|
|
986
1401
|
exitCode: 1,
|
|
987
|
-
output: `\u2717 Initialization failed: ${
|
|
1402
|
+
output: `\u2717 Initialization failed: ${msg}`
|
|
988
1403
|
};
|
|
989
1404
|
}
|
|
990
1405
|
}
|
|
991
1406
|
if (command === "validate") {
|
|
992
1407
|
if (!target) {
|
|
993
|
-
|
|
1408
|
+
const msg = "Error: Please specify a file to validate.";
|
|
1409
|
+
return { exitCode: 1, output: isJson ? JSON.stringify({ error: msg }) : msg };
|
|
994
1410
|
}
|
|
995
1411
|
try {
|
|
996
1412
|
const source = readFileSync(target, "utf-8");
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1413
|
+
const compiler = new YumiaCompiler();
|
|
1414
|
+
const validation = compiler.validate(source);
|
|
1415
|
+
if (isJson) {
|
|
1416
|
+
return {
|
|
1417
|
+
exitCode: validation.valid ? 0 : 1,
|
|
1418
|
+
output: JSON.stringify(validation, null, 2)
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
if (validation.valid) {
|
|
1422
|
+
const warnInfo = validation.warnings.length > 0 ? ` (${validation.warnings.length} warning(s))` : "";
|
|
1423
|
+
return {
|
|
1424
|
+
exitCode: 0,
|
|
1425
|
+
output: `\u2713 Presentation '${target}' is valid. Detected ${validation.slideCount} slide(s)${warnInfo}.`
|
|
1426
|
+
};
|
|
1427
|
+
} else {
|
|
1428
|
+
const errorLines = validation.errors.map(
|
|
1429
|
+
(e) => ` - [Line ${e.loc?.start.line || "?"}] ${e.message}`
|
|
1430
|
+
);
|
|
1431
|
+
return {
|
|
1432
|
+
exitCode: 1,
|
|
1433
|
+
output: `\u2717 Validation failed with ${validation.errors.length} error(s):
|
|
1434
|
+
${errorLines.join("\n")}`
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1002
1437
|
} catch (err) {
|
|
1438
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1439
|
+
if (isJson) {
|
|
1440
|
+
return { exitCode: 1, output: JSON.stringify({ valid: false, error: msg }) };
|
|
1441
|
+
}
|
|
1003
1442
|
return {
|
|
1004
1443
|
exitCode: 1,
|
|
1005
|
-
output: `\u2717 Validation failed: ${
|
|
1444
|
+
output: `\u2717 Validation failed: ${msg}`
|
|
1006
1445
|
};
|
|
1007
1446
|
}
|
|
1008
1447
|
}
|
|
1009
1448
|
if (command === "lint") {
|
|
1010
1449
|
if (!target) {
|
|
1011
|
-
|
|
1450
|
+
const msg = "Error: Please specify a file to lint.";
|
|
1451
|
+
return { exitCode: 1, output: isJson ? JSON.stringify({ error: msg }) : msg };
|
|
1012
1452
|
}
|
|
1013
1453
|
try {
|
|
1014
1454
|
const source = readFileSync(target, "utf-8");
|
|
@@ -1019,40 +1459,67 @@ Opening slide introducing the project.
|
|
|
1019
1459
|
layout.slides.forEach((slideResult, index) => {
|
|
1020
1460
|
const slideNum = index + 1;
|
|
1021
1461
|
if (slideResult.overflow) {
|
|
1022
|
-
issues.push(
|
|
1023
|
-
|
|
1024
|
-
|
|
1462
|
+
issues.push({
|
|
1463
|
+
slide: slideNum,
|
|
1464
|
+
type: "overflow",
|
|
1465
|
+
message: `Content exceeds slide height by ~${Math.round(slideResult.overflowAmount || 0)}px`
|
|
1466
|
+
});
|
|
1025
1467
|
}
|
|
1026
1468
|
const slide = presentation.slides[index];
|
|
1027
1469
|
if (slide.elements.length === 0) {
|
|
1028
|
-
issues.push(
|
|
1470
|
+
issues.push({
|
|
1471
|
+
slide: slideNum,
|
|
1472
|
+
type: "empty",
|
|
1473
|
+
message: "Slide has no content elements."
|
|
1474
|
+
});
|
|
1029
1475
|
}
|
|
1030
1476
|
});
|
|
1477
|
+
if (isJson) {
|
|
1478
|
+
return {
|
|
1479
|
+
exitCode: 0,
|
|
1480
|
+
output: JSON.stringify(
|
|
1481
|
+
{
|
|
1482
|
+
target,
|
|
1483
|
+
passed: issues.length === 0,
|
|
1484
|
+
slideCount: presentation.slides.length,
|
|
1485
|
+
issues
|
|
1486
|
+
},
|
|
1487
|
+
null,
|
|
1488
|
+
2
|
|
1489
|
+
)
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1031
1492
|
if (issues.length === 0) {
|
|
1032
1493
|
return {
|
|
1033
1494
|
exitCode: 0,
|
|
1034
1495
|
output: `\u2713 Yumia Lint: All ${presentation.slides.length} slide(s) passed with 0 issues.`
|
|
1035
1496
|
};
|
|
1036
1497
|
} else {
|
|
1498
|
+
const formatted = issues.map((i) => `\u26A0 Slide ${i.slide}: ${i.message}`).join("\n");
|
|
1037
1499
|
return {
|
|
1038
1500
|
exitCode: 0,
|
|
1039
1501
|
output: `Yumia Lint Report for '${target}':
|
|
1040
1502
|
|
|
1041
|
-
${
|
|
1503
|
+
${formatted}
|
|
1042
1504
|
|
|
1043
1505
|
Found ${issues.length} warning(s).`
|
|
1044
1506
|
};
|
|
1045
1507
|
}
|
|
1046
1508
|
} catch (err) {
|
|
1509
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1510
|
+
if (isJson) {
|
|
1511
|
+
return { exitCode: 1, output: JSON.stringify({ error: msg }) };
|
|
1512
|
+
}
|
|
1047
1513
|
return {
|
|
1048
1514
|
exitCode: 1,
|
|
1049
|
-
output: `\u2717 Linting failed: ${
|
|
1515
|
+
output: `\u2717 Linting failed: ${msg}`
|
|
1050
1516
|
};
|
|
1051
1517
|
}
|
|
1052
1518
|
}
|
|
1053
1519
|
if (command === "inspect") {
|
|
1054
1520
|
if (!target) {
|
|
1055
|
-
|
|
1521
|
+
const msg = "Error: Please specify a file to inspect.";
|
|
1522
|
+
return { exitCode: 1, output: isJson ? JSON.stringify({ error: msg }) : msg };
|
|
1056
1523
|
}
|
|
1057
1524
|
try {
|
|
1058
1525
|
const source = readFileSync(target, "utf-8");
|
|
@@ -1070,15 +1537,20 @@ Found ${issues.length} warning(s).`
|
|
|
1070
1537
|
output: JSON.stringify(presentation, null, 2)
|
|
1071
1538
|
};
|
|
1072
1539
|
} catch (err) {
|
|
1540
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1541
|
+
if (isJson) {
|
|
1542
|
+
return { exitCode: 1, output: JSON.stringify({ error: msg }) };
|
|
1543
|
+
}
|
|
1073
1544
|
return {
|
|
1074
1545
|
exitCode: 1,
|
|
1075
|
-
output: `\u2717 Inspection failed: ${
|
|
1546
|
+
output: `\u2717 Inspection failed: ${msg}`
|
|
1076
1547
|
};
|
|
1077
1548
|
}
|
|
1078
1549
|
}
|
|
1079
1550
|
if (command === "build") {
|
|
1080
1551
|
if (!target) {
|
|
1081
|
-
|
|
1552
|
+
const msg = "Error: Please specify a presentation file to build.";
|
|
1553
|
+
return { exitCode: 1, output: isJson ? JSON.stringify({ error: msg }) : msg };
|
|
1082
1554
|
}
|
|
1083
1555
|
try {
|
|
1084
1556
|
const resolvedInput = resolve(process.cwd(), target);
|
|
@@ -1097,14 +1569,34 @@ Found ${issues.length} warning(s).`
|
|
|
1097
1569
|
const result = await compiler.compile(source, renderer);
|
|
1098
1570
|
const buffer = result.data instanceof Uint8Array ? Buffer.from(result.data) : Buffer.from(new Uint8Array(result.data));
|
|
1099
1571
|
writeFileSync(outputPath, buffer);
|
|
1572
|
+
if (isJson) {
|
|
1573
|
+
return {
|
|
1574
|
+
exitCode: 0,
|
|
1575
|
+
output: JSON.stringify(
|
|
1576
|
+
{
|
|
1577
|
+
success: true,
|
|
1578
|
+
source: target,
|
|
1579
|
+
output: outputPath,
|
|
1580
|
+
slideCount: result.slideCount,
|
|
1581
|
+
format: result.format
|
|
1582
|
+
},
|
|
1583
|
+
null,
|
|
1584
|
+
2
|
|
1585
|
+
)
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1100
1588
|
return {
|
|
1101
1589
|
exitCode: 0,
|
|
1102
1590
|
output: `\u2713 Successfully compiled '${target}' \u2794 '${outputPath}' (${result.slideCount} native editable slides)`
|
|
1103
1591
|
};
|
|
1104
1592
|
} catch (err) {
|
|
1593
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1594
|
+
if (isJson) {
|
|
1595
|
+
return { exitCode: 1, output: JSON.stringify({ success: false, error: msg }) };
|
|
1596
|
+
}
|
|
1105
1597
|
return {
|
|
1106
1598
|
exitCode: 1,
|
|
1107
|
-
output: `\u2717 Build failed: ${
|
|
1599
|
+
output: `\u2717 Build failed: ${msg}`
|
|
1108
1600
|
};
|
|
1109
1601
|
}
|
|
1110
1602
|
}
|
|
@@ -1132,6 +1624,7 @@ export {
|
|
|
1132
1624
|
DefaultYumiaParser,
|
|
1133
1625
|
parseYumia,
|
|
1134
1626
|
DEFAULT_VIEWPORT,
|
|
1627
|
+
VIEWPORT_PRESETS,
|
|
1135
1628
|
DefaultLayoutEngine,
|
|
1136
1629
|
defaultTheme,
|
|
1137
1630
|
createTheme,
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export * from '@yumiamd/layout';
|
|
|
6
6
|
export * from '@yumiamd/renderer';
|
|
7
7
|
export * from '@yumiamd/renderer-pptx';
|
|
8
8
|
|
|
9
|
-
declare const VERSION = "0.1.
|
|
9
|
+
declare const VERSION = "0.1.6";
|
|
10
10
|
declare function printHelp(): string;
|
|
11
11
|
declare function runCli(argv: string[]): Promise<{
|
|
12
12
|
exitCode: number;
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
DefaultYumiaParser,
|
|
5
5
|
PptxRenderer,
|
|
6
6
|
VERSION,
|
|
7
|
+
VIEWPORT_PRESETS,
|
|
7
8
|
YumiaCompiler,
|
|
8
9
|
createCard,
|
|
9
10
|
createCode,
|
|
@@ -24,13 +25,14 @@ import {
|
|
|
24
25
|
parseYumia,
|
|
25
26
|
printHelp,
|
|
26
27
|
runCli
|
|
27
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-FHM6V4JH.js";
|
|
28
29
|
export {
|
|
29
30
|
DEFAULT_VIEWPORT,
|
|
30
31
|
DefaultLayoutEngine,
|
|
31
32
|
DefaultYumiaParser,
|
|
32
33
|
PptxRenderer,
|
|
33
34
|
VERSION,
|
|
35
|
+
VIEWPORT_PRESETS,
|
|
34
36
|
YumiaCompiler,
|
|
35
37
|
createCard,
|
|
36
38
|
createCode,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yumiamd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Command line interface and compiler for YumiaMD presentations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"tsup": "^8.5.1",
|
|
23
|
-
"@yumiamd/ast": "0.1.
|
|
24
|
-
"@yumiamd/
|
|
25
|
-
"@yumiamd/core": "0.1.
|
|
26
|
-
"@yumiamd/
|
|
27
|
-
"@yumiamd/renderer": "0.1.
|
|
28
|
-
"@yumiamd/renderer-pptx": "0.1.
|
|
29
|
-
"@yumiamd/theme": "0.1.
|
|
23
|
+
"@yumiamd/ast": "0.1.6",
|
|
24
|
+
"@yumiamd/parser": "0.1.6",
|
|
25
|
+
"@yumiamd/core": "0.1.6",
|
|
26
|
+
"@yumiamd/layout": "0.1.6",
|
|
27
|
+
"@yumiamd/renderer": "0.1.6",
|
|
28
|
+
"@yumiamd/renderer-pptx": "0.1.6",
|
|
29
|
+
"@yumiamd/theme": "0.1.6"
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
32
|
"yumia",
|