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 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
  [![npm version](https://img.shields.io/npm/v/yumiamd.svg?color=blue)](https://www.npmjs.com/package/yumiamd)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
- # Compile to PowerPoint (.pptx)
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 & Workflow
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 / tools:
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, layoutEngine } from 'yumiamd';
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. Compile directly to PPTX buffer / file
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
- - 📐 **Deterministic Layout Engine**: Semantic multi-column grids, flex cards, and auto-spacing.
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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-MKQASLAK.js";
4
+ } from "./chunk-FHM6V4JH.js";
5
5
 
6
6
  // src/bin.ts
7
7
  var result = await runCli(process.argv);
@@ -103,26 +103,75 @@ function createLayoutDirective(mode, attributes) {
103
103
 
104
104
  // ../parser/dist/parser.js
105
105
  var DefaultYumiaParser = class {
106
- parse(source, _options) {
107
- const normalized = source.replace(/\r\n/g, "\n");
108
- const { metadata, content } = this.extractFrontmatter(normalized);
109
- const slideChunks = this.splitSlides(content);
110
- const slides = slideChunks.map((chunk) => this.parseSlide(chunk)).filter((slide) => slide.elements.length > 0 || slide.notes !== void 0);
111
- return createPresentation(metadata, slides);
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 endIndex = trimmed.indexOf("\n---", 3);
119
- if (endIndex === -1) {
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 frontmatterBlock = trimmed.slice(3, endIndex).trim();
123
- const afterFrontmatter = trimmed.slice(endIndex + 4);
124
- const metadata = this.parseYamlMetadata(frontmatterBlock);
125
- return { metadata, content: afterFrontmatter };
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 value = trimmed.slice(colonIndex + 1).trim();
138
- const unquoted = value.replace(/^['"](.*)['"]$/, "$1");
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
- for (const line of lines) {
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(currentSlideLines.join("\n"));
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(currentSlideLines.join("\n"));
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
- elements.push(createParagraph(text));
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
- elements.push(createList(currentList.items, currentList.ordered));
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
- while (i < lines.length && !lines[i]?.trim().startsWith("```")) {
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
- i++;
228
- elements.push(createCode(codeLines.join("\n"), language));
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
- elements.push(createLayoutDirective(mode));
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
- elements.push(createCard(cardElements, title));
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
- elements.push(createColumns(columns, ratios));
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
- elements.push(createQuote(quoteText, author));
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
- elements.push(createHeading(text, level));
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
- elements.push(createQuote(quoteText));
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
- elements.push(createImage(src, alt, caption));
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
- while (i < lines.length && lines[i]?.trim() !== ":::") {
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
- i++;
354
- const { elements } = this.parseLines(colLines);
355
- columns.push(createColumn(elements, width));
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 = DEFAULT_VIEWPORT, options = {}) {
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, viewport, options))
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 slideWidthInches = 13.333;
962
+ const is43 = presentation.metadata.aspectRatio === "4:3";
963
+ const slideWidthInches = is43 ? 10 : 13.333;
672
964
  const slideHeightInches = 7.5;
673
- pptx.defineLayout({ name: "YUMIA_16_9", width: slideWidthInches, height: slideHeightInches });
674
- pptx.layout = "YUMIA_16_9";
675
- const pixelViewport = { width: 1920, height: 1080 };
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(/\s+/g, "-")}.pptx`
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(hex) {
896
- return hex.replace("#", "").trim();
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.5";
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 command = args[0];
935
- const target = args[1];
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 with YumiaMD.
1343
+ Presentation authoring designed for humans and AI.
950
1344
 
951
1345
  :::notes
952
- Opening slide introducing the project.
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 Philosophy
963
- - Plain Markdown source
964
- - Semantic directives
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
- - Native editable PPTX
971
- - Clean vector PDF
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: ${err instanceof Error ? err.message : String(err)}`
1402
+ output: `\u2717 Initialization failed: ${msg}`
988
1403
  };
989
1404
  }
990
1405
  }
991
1406
  if (command === "validate") {
992
1407
  if (!target) {
993
- return { exitCode: 1, output: "Error: Please specify a file to validate." };
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 presentation = parseYumia(source);
998
- return {
999
- exitCode: 0,
1000
- output: `\u2713 Presentation '${target}' is valid. Detected ${presentation.slides.length} slide(s).`
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: ${err instanceof Error ? err.message : String(err)}`
1444
+ output: `\u2717 Validation failed: ${msg}`
1006
1445
  };
1007
1446
  }
1008
1447
  }
1009
1448
  if (command === "lint") {
1010
1449
  if (!target) {
1011
- return { exitCode: 1, output: "Error: Please specify a file to lint." };
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
- `\u26A0 Slide ${slideNum}: Content exceeds slide height by ~${Math.round(slideResult.overflowAmount || 0)}px`
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(`\u26A0 Slide ${slideNum}: Slide has no content elements.`);
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
- ${issues.join("\n")}
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: ${err instanceof Error ? err.message : String(err)}`
1515
+ output: `\u2717 Linting failed: ${msg}`
1050
1516
  };
1051
1517
  }
1052
1518
  }
1053
1519
  if (command === "inspect") {
1054
1520
  if (!target) {
1055
- return { exitCode: 1, output: "Error: Please specify a file to inspect." };
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: ${err instanceof Error ? err.message : String(err)}`
1546
+ output: `\u2717 Inspection failed: ${msg}`
1076
1547
  };
1077
1548
  }
1078
1549
  }
1079
1550
  if (command === "build") {
1080
1551
  if (!target) {
1081
- return { exitCode: 1, output: "Error: Please specify a presentation file to build." };
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: ${err instanceof Error ? err.message : String(err)}`
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.5";
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-MKQASLAK.js";
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.5",
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.5",
24
- "@yumiamd/layout": "0.1.5",
25
- "@yumiamd/core": "0.1.5",
26
- "@yumiamd/parser": "0.1.5",
27
- "@yumiamd/renderer": "0.1.5",
28
- "@yumiamd/renderer-pptx": "0.1.5",
29
- "@yumiamd/theme": "0.1.5"
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",