yumiamd 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-GMIVPFBA.js";
4
+ } from "./chunk-WUJKHJJU.js";
5
5
 
6
6
  // src/bin.ts
7
7
  var result = await runCli(process.argv);
@@ -47,6 +47,17 @@ function createCard(elements, title, variant) {
47
47
  ...variant ? { variant } : {}
48
48
  };
49
49
  }
50
+ function createMetric(value, label, variant, description, unit, change) {
51
+ return {
52
+ type: "metric",
53
+ value,
54
+ label,
55
+ ...variant ? { variant } : {},
56
+ ...description ? { description } : {},
57
+ ...unit ? { unit } : {},
58
+ ...change ? { change } : {}
59
+ };
60
+ }
50
61
  function createCode(code, language, highlightLines) {
51
62
  return {
52
63
  type: "code",
@@ -111,13 +122,14 @@ var DefaultYumiaParser = class {
111
122
  }
112
123
  const normalized = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
113
124
  const { metadata, content, lineOffset } = this.extractFrontmatter(normalized);
125
+ if (!content.trim()) {
126
+ return createPresentation(metadata, []);
127
+ }
114
128
  const slideChunks = this.splitSlides(content, lineOffset);
115
129
  const slides = [];
116
130
  for (const chunk of slideChunks) {
117
131
  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
- }
132
+ slides.push(slide);
121
133
  }
122
134
  const presentation = createPresentation(metadata, slides);
123
135
  if (this.diagnostics.length > 0) {
@@ -346,6 +358,28 @@ var DefaultYumiaParser = class {
346
358
  i++;
347
359
  continue;
348
360
  }
361
+ if (directiveHeader.startsWith("metric")) {
362
+ const valueMatch = directiveHeader.match(/value=['"](.*?)['"]/);
363
+ const labelMatch = directiveHeader.match(/label=['"](.*?)['"]/);
364
+ const variantMatch = directiveHeader.match(/variant=['"](.*?)['"]/);
365
+ const descMatch = directiveHeader.match(/description=['"](.*?)['"]/);
366
+ const unitMatch = directiveHeader.match(/unit=['"](.*?)['"]/);
367
+ const changeMatch = directiveHeader.match(/change=['"](.*?)['"]/);
368
+ const value = valueMatch ? valueMatch[1] : "0";
369
+ const label = labelMatch ? labelMatch[1] : "";
370
+ const variant = variantMatch ? variantMatch[1] : void 0;
371
+ const desc = descMatch ? descMatch[1] : void 0;
372
+ const unit = unitMatch ? unitMatch[1] : void 0;
373
+ const change = changeMatch ? changeMatch[1] : void 0;
374
+ const el = createMetric(value, label, variant, desc, unit, change);
375
+ el.loc = {
376
+ start: { line: currentLineNum, column: 1 },
377
+ end: { line: currentLineNum, column: rawLine.length + 1 }
378
+ };
379
+ elements.push(el);
380
+ i++;
381
+ continue;
382
+ }
349
383
  const [directiveName, ...args] = directiveHeader.split(" ");
350
384
  const directiveArg = args.join(" ").trim();
351
385
  const blockLines = [];
@@ -354,7 +388,8 @@ var DefaultYumiaParser = class {
354
388
  let closed = false;
355
389
  while (i < lines.length) {
356
390
  const innerLine = lines[i]?.trim() ?? "";
357
- if (innerLine.startsWith(":::") && innerLine.length > 3) {
391
+ const isSingleLine = innerLine.startsWith(":::metric") || innerLine.startsWith(":::layout");
392
+ if (innerLine.startsWith(":::") && innerLine.length > 3 && !isSingleLine) {
358
393
  nestedCount++;
359
394
  } else if (innerLine === ":::") {
360
395
  nestedCount--;
@@ -382,9 +417,17 @@ var DefaultYumiaParser = class {
382
417
  if (directiveName === "notes") {
383
418
  notes = blockLines.map((l) => l.trim()).filter(Boolean).join("\n");
384
419
  } else if (directiveName === "card") {
385
- const title = directiveArg.replace(/^['"](.*)['"]$/, "$1") || void 0;
420
+ let cardTitle;
421
+ let cardVariant;
422
+ const variantMatch = directiveArg.match(/variant=['"](.*?)['"]/);
423
+ if (variantMatch) {
424
+ cardVariant = variantMatch[1];
425
+ cardTitle = directiveArg.replace(/variant=['"].*?['"]/, "").trim().replace(/^['"](.*)['"]$/, "$1") || void 0;
426
+ } else {
427
+ cardTitle = directiveArg.replace(/^['"](.*)['"]$/, "$1") || void 0;
428
+ }
386
429
  const { elements: cardElements } = this.parseLines(blockLines, blockBaseLine);
387
- const el = createCard(cardElements, title);
430
+ const el = createCard(cardElements, cardTitle, cardVariant);
388
431
  el.loc = {
389
432
  start: { line: directiveStartLine, column: 1 },
390
433
  end: { line: baseLine + i - 1, column: 1 }
@@ -530,7 +573,8 @@ var DefaultYumiaParser = class {
530
573
  let nestedCount = 1;
531
574
  while (i < lines.length) {
532
575
  const innerLine = lines[i]?.trim() ?? "";
533
- if (innerLine.startsWith(":::") && innerLine.length > 3) {
576
+ const isSingleLine = innerLine.startsWith(":::metric") || innerLine.startsWith(":::layout");
577
+ if (innerLine.startsWith(":::") && innerLine.length > 3 && !isSingleLine) {
534
578
  nestedCount++;
535
579
  } else if (innerLine === ":::") {
536
580
  nestedCount--;
@@ -645,6 +689,10 @@ var DefaultLayoutEngine = class {
645
689
  const height = this.estimateImageHeight(element);
646
690
  return { element, bounds: { x, y, width, height } };
647
691
  }
692
+ case "metric": {
693
+ const height = this.estimateMetricHeight(element);
694
+ return { element, bounds: { x, y, width, height } };
695
+ }
648
696
  case "card": {
649
697
  return this.layoutCard(element, x, y, width, gap);
650
698
  }
@@ -759,6 +807,9 @@ var DefaultLayoutEngine = class {
759
807
  }
760
808
  return 320;
761
809
  }
810
+ estimateMetricHeight(metric) {
811
+ return metric.change ? 140 : 120;
812
+ }
762
813
  };
763
814
 
764
815
  // ../theme/dist/default-theme.js
@@ -773,7 +824,11 @@ var defaultTheme = {
773
824
  text: "#0f172a",
774
825
  muted: "#64748b",
775
826
  accent: "#06b6d4",
776
- border: "#e2e8f0"
827
+ border: "#e2e8f0",
828
+ success: "#10b981",
829
+ warning: "#f59e0b",
830
+ danger: "#ef4444",
831
+ info: "#3b82f6"
777
832
  },
778
833
  typography: {
779
834
  headingFont: "Inter, system-ui, -apple-system, sans-serif",
@@ -830,29 +885,523 @@ var defaultTheme = {
830
885
  }
831
886
  }
832
887
  };
833
- function createTheme(overrides) {
888
+ var cyberpunkTheme = {
889
+ name: "cyberpunk",
890
+ description: "High-energy dark cyberpunk theme with neon pink and cyan accents",
891
+ colors: {
892
+ primary: "#FF2E88",
893
+ secondary: "#00F0FF",
894
+ background: "#0B0B12",
895
+ surface: "#151522",
896
+ text: "#FFFFFF",
897
+ muted: "#A7A7B5",
898
+ accent: "#7B61FF",
899
+ border: "#2E2E48",
900
+ success: "#00FF9F",
901
+ warning: "#FFE600",
902
+ danger: "#FF0055",
903
+ info: "#00F0FF"
904
+ },
905
+ typography: {
906
+ headingFont: "Outfit, Inter, sans-serif",
907
+ bodyFont: "Inter, sans-serif",
908
+ codeFont: "JetBrains Mono, monospace",
909
+ sizes: {
910
+ h1: 46,
911
+ h2: 38,
912
+ h3: 28,
913
+ h4: 22,
914
+ body: 18,
915
+ small: 14,
916
+ code: 16
917
+ }
918
+ },
919
+ spacing: {
920
+ unit: 8,
921
+ slidePadding: 48,
922
+ elementGap: 28
923
+ },
924
+ radius: {
925
+ default: 12,
926
+ sm: 6,
927
+ md: 12,
928
+ lg: 20,
929
+ full: 9999
930
+ },
931
+ components: {
932
+ card: {
933
+ background: "#151522",
934
+ borderColor: "#FF2E88",
935
+ borderRadius: 16,
936
+ padding: 28
937
+ },
938
+ code: {
939
+ background: "#07070D",
940
+ textColor: "#00F0FF",
941
+ borderRadius: 8
942
+ }
943
+ }
944
+ };
945
+ var minimalTheme = {
946
+ name: "minimal",
947
+ description: "Monochrome Swiss-style design with maximum clarity and contrast",
948
+ colors: {
949
+ primary: "#111111",
950
+ secondary: "#333333",
951
+ background: "#FFFFFF",
952
+ surface: "#F5F5F7",
953
+ text: "#111111",
954
+ muted: "#777777",
955
+ accent: "#000000",
956
+ border: "#E5E5E5",
957
+ success: "#10B981",
958
+ warning: "#F59E0B",
959
+ danger: "#EF4444",
960
+ info: "#3B82F6"
961
+ },
962
+ typography: {
963
+ headingFont: "Helvetica Neue, Arial, sans-serif",
964
+ bodyFont: "Helvetica Neue, Arial, sans-serif",
965
+ codeFont: "Menlo, Monaco, monospace",
966
+ sizes: {
967
+ h1: 44,
968
+ h2: 34,
969
+ h3: 26,
970
+ h4: 20,
971
+ body: 18,
972
+ small: 14,
973
+ code: 15
974
+ }
975
+ },
976
+ spacing: {
977
+ unit: 8,
978
+ slidePadding: 56,
979
+ elementGap: 24
980
+ },
981
+ radius: {
982
+ default: 4,
983
+ sm: 2,
984
+ md: 4,
985
+ lg: 8,
986
+ full: 9999
987
+ },
988
+ components: {
989
+ card: {
990
+ background: "#F5F5F7",
991
+ borderColor: "#E5E5E5",
992
+ borderRadius: 6,
993
+ padding: 24
994
+ }
995
+ }
996
+ };
997
+ var corporateTheme = {
998
+ name: "corporate",
999
+ description: "Executive navy blue palette tailored for business and enterprise decks",
1000
+ colors: {
1001
+ primary: "#0A2540",
1002
+ secondary: "#635BFF",
1003
+ background: "#FFFFFF",
1004
+ surface: "#F8FAFC",
1005
+ text: "#1A1F36",
1006
+ muted: "#4F566B",
1007
+ accent: "#00D4B2",
1008
+ border: "#E3E8EE",
1009
+ success: "#00D4B2",
1010
+ warning: "#FFC043",
1011
+ danger: "#DF1B41",
1012
+ info: "#635BFF"
1013
+ },
1014
+ typography: {
1015
+ headingFont: "Inter, Segoe UI, sans-serif",
1016
+ bodyFont: "Inter, Segoe UI, sans-serif",
1017
+ codeFont: "Consolas, monospace",
1018
+ sizes: {
1019
+ h1: 42,
1020
+ h2: 34,
1021
+ h3: 26,
1022
+ h4: 20,
1023
+ body: 18,
1024
+ small: 14,
1025
+ code: 15
1026
+ }
1027
+ },
1028
+ spacing: {
1029
+ unit: 8,
1030
+ slidePadding: 48,
1031
+ elementGap: 24
1032
+ },
1033
+ radius: {
1034
+ default: 8,
1035
+ sm: 4,
1036
+ md: 8,
1037
+ lg: 12,
1038
+ full: 9999
1039
+ },
1040
+ components: {
1041
+ card: {
1042
+ background: "#F8FAFC",
1043
+ borderColor: "#0A2540",
1044
+ borderRadius: 8,
1045
+ padding: 24
1046
+ }
1047
+ }
1048
+ };
1049
+ var terminalTheme = {
1050
+ name: "terminal",
1051
+ description: "Retro hacker green-on-dark theme with pure monospace aesthetic",
1052
+ colors: {
1053
+ primary: "#00FF66",
1054
+ secondary: "#33FF88",
1055
+ background: "#0C100C",
1056
+ surface: "#141A14",
1057
+ text: "#E0FFE0",
1058
+ muted: "#4E7A4E",
1059
+ accent: "#FFB000",
1060
+ border: "#1F331F",
1061
+ success: "#00FF66",
1062
+ warning: "#FFB000",
1063
+ danger: "#FF3333",
1064
+ info: "#00E5FF"
1065
+ },
1066
+ typography: {
1067
+ headingFont: "JetBrains Mono, Courier New, monospace",
1068
+ bodyFont: "JetBrains Mono, Courier New, monospace",
1069
+ codeFont: "JetBrains Mono, Courier New, monospace",
1070
+ sizes: {
1071
+ h1: 40,
1072
+ h2: 32,
1073
+ h3: 26,
1074
+ h4: 20,
1075
+ body: 17,
1076
+ small: 13,
1077
+ code: 15
1078
+ }
1079
+ },
1080
+ spacing: {
1081
+ unit: 8,
1082
+ slidePadding: 48,
1083
+ elementGap: 24
1084
+ },
1085
+ radius: {
1086
+ default: 0,
1087
+ sm: 0,
1088
+ md: 0,
1089
+ lg: 0,
1090
+ full: 0
1091
+ },
1092
+ components: {
1093
+ card: {
1094
+ background: "#141A14",
1095
+ borderColor: "#00FF66",
1096
+ borderRadius: 0,
1097
+ padding: 24
1098
+ }
1099
+ }
1100
+ };
1101
+ var academicTheme = {
1102
+ name: "academic",
1103
+ description: "Scholarly paper theme with serif typography and crimson accents",
1104
+ colors: {
1105
+ primary: "#8B0000",
1106
+ secondary: "#1C3F60",
1107
+ background: "#FCFBF7",
1108
+ surface: "#F5F2EB",
1109
+ text: "#2C2B29",
1110
+ muted: "#6B6862",
1111
+ accent: "#C29B38",
1112
+ border: "#E0DACD",
1113
+ success: "#2E7D32",
1114
+ warning: "#E65100",
1115
+ danger: "#C62828",
1116
+ info: "#1565C0"
1117
+ },
1118
+ typography: {
1119
+ headingFont: "Georgia, Times New Roman, serif",
1120
+ bodyFont: "Georgia, Times New Roman, serif",
1121
+ codeFont: "Courier New, monospace",
1122
+ sizes: {
1123
+ h1: 42,
1124
+ h2: 34,
1125
+ h3: 26,
1126
+ h4: 20,
1127
+ body: 18,
1128
+ small: 14,
1129
+ code: 15
1130
+ }
1131
+ },
1132
+ spacing: {
1133
+ unit: 8,
1134
+ slidePadding: 52,
1135
+ elementGap: 24
1136
+ },
1137
+ radius: {
1138
+ default: 4,
1139
+ sm: 2,
1140
+ md: 4,
1141
+ lg: 6,
1142
+ full: 9999
1143
+ },
1144
+ components: {
1145
+ card: {
1146
+ background: "#F5F2EB",
1147
+ borderColor: "#8B0000",
1148
+ borderRadius: 4,
1149
+ padding: 24
1150
+ }
1151
+ }
1152
+ };
1153
+ var THEME_REGISTRY = {
1154
+ default: defaultTheme,
1155
+ cyberpunk: cyberpunkTheme,
1156
+ minimal: minimalTheme,
1157
+ corporate: corporateTheme,
1158
+ terminal: terminalTheme,
1159
+ academic: academicTheme
1160
+ };
1161
+ function resolveTheme(themeNameOrRef, explicitOverrides) {
1162
+ const name = typeof themeNameOrRef === "string" ? themeNameOrRef : themeNameOrRef?.name || "default";
1163
+ const baseTheme = THEME_REGISTRY[name.toLowerCase()] || defaultTheme;
1164
+ const refOverrides = typeof themeNameOrRef === "object" ? themeNameOrRef.overrides : void 0;
1165
+ const mergedOverrides = {
1166
+ ...refOverrides,
1167
+ ...explicitOverrides,
1168
+ colors: { ...baseTheme.colors, ...refOverrides?.colors, ...explicitOverrides?.colors },
1169
+ typography: {
1170
+ ...baseTheme.typography,
1171
+ ...refOverrides?.typography,
1172
+ ...explicitOverrides?.typography
1173
+ },
1174
+ components: {
1175
+ ...baseTheme.components,
1176
+ ...refOverrides?.components,
1177
+ ...explicitOverrides?.components
1178
+ }
1179
+ };
834
1180
  return {
835
- ...defaultTheme,
836
- ...overrides,
837
- colors: { ...defaultTheme.colors, ...overrides.colors },
838
- typography: { ...defaultTheme.typography, ...overrides.typography },
839
- spacing: { ...defaultTheme.spacing, ...overrides.spacing },
840
- radius: { ...defaultTheme.radius, ...overrides.radius },
841
- shadows: { ...defaultTheme.shadows, ...overrides.shadows },
842
- components: { ...defaultTheme.components, ...overrides.components }
1181
+ ...baseTheme,
1182
+ ...mergedOverrides
843
1183
  };
844
1184
  }
1185
+ function createTheme(overrides) {
1186
+ return resolveTheme(overrides.name, overrides);
1187
+ }
1188
+
1189
+ // ../core/dist/linter.js
1190
+ var YumiaLinter = class {
1191
+ layoutEngine;
1192
+ constructor(layoutEngine) {
1193
+ this.layoutEngine = layoutEngine ?? new DefaultLayoutEngine();
1194
+ }
1195
+ lint(presentation, options = {}) {
1196
+ const issues = [];
1197
+ const layout = this.layoutEngine.computePresentation(presentation, options.viewport);
1198
+ if (presentation.diagnostics) {
1199
+ for (const diag of presentation.diagnostics) {
1200
+ issues.push({
1201
+ code: diag.code || "YUM002",
1202
+ slide: 1,
1203
+ message: diag.message,
1204
+ severity: diag.severity === "error" ? "error" : "warning",
1205
+ loc: diag.loc
1206
+ });
1207
+ }
1208
+ }
1209
+ presentation.slides.forEach((slide, index) => {
1210
+ const slideNum = index + 1;
1211
+ const slideLayout = layout.slides[index];
1212
+ if (slide.elements.length === 0) {
1213
+ issues.push({
1214
+ code: "YUM006",
1215
+ slide: slideNum,
1216
+ message: "Slide contains no content elements.",
1217
+ severity: "warning",
1218
+ loc: slide.loc
1219
+ });
1220
+ return;
1221
+ }
1222
+ if (slideLayout && slideLayout.overflow) {
1223
+ const overflowPx = Math.round(slideLayout.overflowAmount || 0);
1224
+ issues.push({
1225
+ code: "YUM001",
1226
+ slide: slideNum,
1227
+ message: `Content exceeds slide viewport bounds by ~${overflowPx}px. Consider reducing font size or splitting into multiple slides.`,
1228
+ severity: options.strict ? "error" : "warning",
1229
+ loc: slide.loc
1230
+ });
1231
+ }
1232
+ const hasHeading = this.slideHasHeading(slide.elements);
1233
+ if (!hasHeading) {
1234
+ issues.push({
1235
+ code: "YUM003",
1236
+ slide: slideNum,
1237
+ message: "Slide has no heading (h1-h4). Consider adding a descriptive title for structure.",
1238
+ severity: "warning",
1239
+ loc: slide.loc
1240
+ });
1241
+ }
1242
+ const { listCount, wordCount } = this.calculateSlideDensity(slide.elements);
1243
+ if (listCount > 7) {
1244
+ issues.push({
1245
+ code: "YUM004",
1246
+ slide: slideNum,
1247
+ message: `High information density: slide contains ${listCount} list items (recommended max: 7).`,
1248
+ severity: "warning",
1249
+ loc: slide.loc
1250
+ });
1251
+ } else if (wordCount > 120) {
1252
+ issues.push({
1253
+ code: "YUM004",
1254
+ slide: slideNum,
1255
+ message: `High information density: slide contains ~${wordCount} words (recommended max: 120 words).`,
1256
+ severity: "warning",
1257
+ loc: slide.loc
1258
+ });
1259
+ }
1260
+ this.checkImages(slide.elements, slideNum, issues);
1261
+ const maxDepth = this.getMaxNestingDepth(slide.elements, 0);
1262
+ if (maxDepth > 2) {
1263
+ issues.push({
1264
+ code: "YUM008",
1265
+ slide: slideNum,
1266
+ message: `Excessive directive nesting depth (${maxDepth} levels). Maximum recommended depth is 2.`,
1267
+ severity: "warning",
1268
+ loc: slide.loc
1269
+ });
1270
+ }
1271
+ if (options.includeOptionalNotesRule && (!slide.notes || slide.notes.trim() === "")) {
1272
+ issues.push({
1273
+ code: "YUM009",
1274
+ slide: slideNum,
1275
+ message: "Slide has no speaker notes (:::notes directive).",
1276
+ severity: "info",
1277
+ loc: slide.loc
1278
+ });
1279
+ }
1280
+ });
1281
+ const errors = issues.filter((i) => i.severity === "error");
1282
+ const warnings = issues.filter((i) => i.severity === "warning");
1283
+ const infos = issues.filter((i) => i.severity === "info");
1284
+ return {
1285
+ passed: options.strict ? errors.length === 0 && warnings.length === 0 : errors.length === 0,
1286
+ totalSlides: presentation.slides.length,
1287
+ issueCount: issues.length,
1288
+ errors,
1289
+ warnings,
1290
+ infos
1291
+ };
1292
+ }
1293
+ slideHasHeading(elements) {
1294
+ for (const el of elements) {
1295
+ if (el.type === "heading")
1296
+ return true;
1297
+ if (el.type === "card" && el.title)
1298
+ return true;
1299
+ if (el.type === "columns") {
1300
+ const cols = el.columns;
1301
+ for (const col of cols) {
1302
+ if (this.slideHasHeading(col.elements))
1303
+ return true;
1304
+ }
1305
+ }
1306
+ }
1307
+ return false;
1308
+ }
1309
+ calculateSlideDensity(elements) {
1310
+ let listCount = 0;
1311
+ let wordCount = 0;
1312
+ const traverse = (els) => {
1313
+ for (const el of els) {
1314
+ if (el.type === "list") {
1315
+ const list = el;
1316
+ listCount += list.items.length;
1317
+ for (const item of list.items) {
1318
+ wordCount += item.text.split(/\s+/).filter(Boolean).length;
1319
+ }
1320
+ } else if (el.type === "paragraph") {
1321
+ const p = el;
1322
+ wordCount += p.text.split(/\s+/).filter(Boolean).length;
1323
+ } else if (el.type === "card") {
1324
+ const card = el;
1325
+ if (card.title)
1326
+ wordCount += card.title.split(/\s+/).filter(Boolean).length;
1327
+ if (card.elements)
1328
+ traverse(card.elements);
1329
+ } else if (el.type === "columns") {
1330
+ const cols = el;
1331
+ for (const col of cols.columns) {
1332
+ traverse(col.elements);
1333
+ }
1334
+ }
1335
+ }
1336
+ };
1337
+ traverse(elements);
1338
+ return { listCount, wordCount };
1339
+ }
1340
+ checkImages(elements, slideNum, issues) {
1341
+ const traverse = (els) => {
1342
+ for (const el of els) {
1343
+ if (el.type === "image") {
1344
+ const img = el;
1345
+ if (!img.alt || img.alt.trim() === "") {
1346
+ issues.push({
1347
+ code: "YUM007",
1348
+ slide: slideNum,
1349
+ message: `Image '${img.src}' is missing descriptive alt text for accessibility.`,
1350
+ severity: "warning",
1351
+ loc: img.loc
1352
+ });
1353
+ }
1354
+ if (!img.src || img.src.trim() === "") {
1355
+ issues.push({
1356
+ code: "YUM005",
1357
+ slide: slideNum,
1358
+ message: "Image element has empty src attribute.",
1359
+ severity: "error",
1360
+ loc: img.loc
1361
+ });
1362
+ }
1363
+ } else if (el.type === "card" && el.elements) {
1364
+ traverse(el.elements);
1365
+ } else if (el.type === "columns") {
1366
+ for (const col of el.columns) {
1367
+ traverse(col.elements);
1368
+ }
1369
+ }
1370
+ }
1371
+ };
1372
+ traverse(elements);
1373
+ }
1374
+ getMaxNestingDepth(elements, currentDepth) {
1375
+ let max = currentDepth;
1376
+ for (const el of elements) {
1377
+ if (el.type === "card" && el.elements) {
1378
+ const d = this.getMaxNestingDepth(el.elements, currentDepth + 1);
1379
+ if (d > max)
1380
+ max = d;
1381
+ } else if (el.type === "columns") {
1382
+ for (const col of el.columns) {
1383
+ const d = this.getMaxNestingDepth(col.elements, currentDepth + 1);
1384
+ if (d > max)
1385
+ max = d;
1386
+ }
1387
+ }
1388
+ }
1389
+ return max;
1390
+ }
1391
+ };
845
1392
 
846
1393
  // ../core/dist/compiler.js
847
1394
  var YumiaCompiler = class {
848
1395
  parser;
849
1396
  layoutEngine;
850
1397
  defaultTheme;
1398
+ linter;
851
1399
  viewport;
852
1400
  constructor(config = {}) {
853
1401
  this.parser = config.parser ?? new DefaultYumiaParser();
854
1402
  this.layoutEngine = config.layoutEngine ?? new DefaultLayoutEngine();
855
1403
  this.defaultTheme = config.defaultTheme ?? defaultTheme;
1404
+ this.linter = new YumiaLinter(this.layoutEngine);
856
1405
  if (config.viewport) {
857
1406
  this.viewport = config.viewport;
858
1407
  }
@@ -872,12 +1421,20 @@ var YumiaCompiler = class {
872
1421
  warnings
873
1422
  };
874
1423
  }
1424
+ lint(sourceOrPresentation, options) {
1425
+ const presentation = typeof sourceOrPresentation === "string" ? this.parse(sourceOrPresentation) : sourceOrPresentation;
1426
+ return this.linter.lint(presentation, {
1427
+ ...this.viewport ? { viewport: this.viewport } : {},
1428
+ ...options
1429
+ });
1430
+ }
875
1431
  layout(presentation, viewport) {
876
1432
  return this.layoutEngine.computePresentation(presentation, viewport ?? this.viewport);
877
1433
  }
878
1434
  async render(presentation, renderer, contextOverrides = {}) {
879
1435
  const layout = contextOverrides.layout ?? this.layout(presentation);
880
- const theme = contextOverrides.theme ?? this.defaultTheme;
1436
+ const presentationTheme = presentation.metadata.theme ? resolveTheme(presentation.metadata.theme) : this.defaultTheme;
1437
+ const theme = contextOverrides.theme ?? presentationTheme;
881
1438
  const context = {
882
1439
  theme,
883
1440
  layout,
@@ -903,7 +1460,10 @@ var YumiaCompiler = class {
903
1460
  subtitle: { type: "string" },
904
1461
  author: { type: "string" },
905
1462
  date: { type: "string" },
906
- theme: { type: "string" },
1463
+ theme: {
1464
+ type: "string",
1465
+ enum: ["default", "cyberpunk", "minimal", "corporate", "terminal", "academic"]
1466
+ },
907
1467
  aspectRatio: { enum: ["16:9", "4:3", "16:10"] }
908
1468
  }
909
1469
  },
@@ -915,9 +1475,13 @@ var YumiaCompiler = class {
915
1475
  description: "Multi-column grid layout"
916
1476
  },
917
1477
  card: {
918
- syntax: ":::card [Title]\\n...\\n:::",
1478
+ syntax: ':::card [Title] [variant="primary|success|warning|danger|info"]\\n...\\n:::',
919
1479
  description: "Visual container card with theme border and background"
920
1480
  },
1481
+ metric: {
1482
+ syntax: ':::metric value="99.9%" label="Uptime" change="+0.4%" variant="success"',
1483
+ description: "Stat callout box with prominent value, label, and trend"
1484
+ },
921
1485
  notes: {
922
1486
  syntax: ":::notes\\nSpeaker notes text\\n:::",
923
1487
  description: "Speaker notes attached to slide metadata"
@@ -992,7 +1556,8 @@ var PptxRenderer = class {
992
1556
  async render(presentation, context = {}) {
993
1557
  const PptxConstructor = pptxgen.default || pptxgen;
994
1558
  const pptx = new PptxConstructor();
995
- const theme = context.theme || defaultTheme;
1559
+ const resolvedTheme = presentation.metadata.theme ? resolveTheme(presentation.metadata.theme) : defaultTheme;
1560
+ const theme = context.theme || resolvedTheme;
996
1561
  const title = presentation.metadata.title || "Yumia Presentation";
997
1562
  const author = presentation.metadata.author || "YumiaMD";
998
1563
  pptx.title = title;
@@ -1052,6 +1617,9 @@ var PptxRenderer = class {
1052
1617
  case "image":
1053
1618
  this.renderImage(pptxSlide, element, rect);
1054
1619
  break;
1620
+ case "metric":
1621
+ this.renderMetric(pptxSlide, pptx, node, scaleX, scaleY, theme);
1622
+ break;
1055
1623
  case "card":
1056
1624
  this.renderCard(pptxSlide, pptx, node, scaleX, scaleY, theme);
1057
1625
  break;
@@ -1197,12 +1765,79 @@ var PptxRenderer = class {
1197
1765
  });
1198
1766
  }
1199
1767
  }
1768
+ renderMetric(pptxSlide, pptx, node, scaleX, scaleY, theme) {
1769
+ const metric = node.element;
1770
+ const rect = this.toInches(node.bounds, scaleX, scaleY);
1771
+ const cardTheme = theme.components?.card;
1772
+ let accentColor = theme.colors.primary;
1773
+ if (metric.variant && theme.colors[metric.variant]) {
1774
+ accentColor = theme.colors[metric.variant];
1775
+ }
1776
+ const fillColor = this.cleanHexColor(cardTheme?.background || theme.colors.surface);
1777
+ const borderColor = this.cleanHexColor(metric.variant && theme.colors[metric.variant] ? theme.colors[metric.variant] : cardTheme?.borderColor || theme.colors.border || "#cbd5e1");
1778
+ pptxSlide.addShape(pptx.ShapeType.roundRect, {
1779
+ x: rect.x,
1780
+ y: rect.y,
1781
+ w: rect.w,
1782
+ h: rect.h,
1783
+ fill: { color: fillColor },
1784
+ line: { color: borderColor, width: 1.5 },
1785
+ rectRadius: 0.08
1786
+ });
1787
+ pptxSlide.addText(metric.label.toUpperCase(), {
1788
+ x: rect.x + 0.15,
1789
+ y: rect.y + 0.15,
1790
+ w: rect.w - 0.3,
1791
+ h: 0.25,
1792
+ fontSize: 11,
1793
+ bold: true,
1794
+ color: this.cleanHexColor(theme.colors.muted || "#64748b"),
1795
+ fontFace: theme.typography.headingFont.split(",")[0]?.trim() || "Arial",
1796
+ align: "center",
1797
+ valign: "middle"
1798
+ });
1799
+ const displayValue = metric.unit ? `${metric.value} ${metric.unit}` : metric.value;
1800
+ pptxSlide.addText(displayValue, {
1801
+ x: rect.x + 0.1,
1802
+ y: rect.y + 0.4,
1803
+ w: rect.w - 0.2,
1804
+ h: 0.55,
1805
+ fontSize: 30,
1806
+ bold: true,
1807
+ color: this.cleanHexColor(accentColor),
1808
+ fontFace: theme.typography.headingFont.split(",")[0]?.trim() || "Arial",
1809
+ align: "center",
1810
+ valign: "middle"
1811
+ });
1812
+ if (metric.change) {
1813
+ const isPositive = metric.change.startsWith("+");
1814
+ const changeColor = isPositive ? this.cleanHexColor(theme.colors.success || "#10b981") : this.cleanHexColor(theme.colors.danger || "#ef4444");
1815
+ pptxSlide.addText(metric.change, {
1816
+ x: rect.x + 0.1,
1817
+ y: rect.y + rect.h - 0.32,
1818
+ w: rect.w - 0.2,
1819
+ h: 0.22,
1820
+ fontSize: 12,
1821
+ bold: true,
1822
+ color: changeColor,
1823
+ fontFace: theme.typography.bodyFont.split(",")[0]?.trim() || "Arial",
1824
+ align: "center",
1825
+ valign: "middle"
1826
+ });
1827
+ }
1828
+ }
1200
1829
  renderCard(pptxSlide, pptx, node, scaleX, scaleY, theme) {
1201
1830
  const card = node.element;
1202
1831
  const rect = this.toInches(node.bounds, scaleX, scaleY);
1203
1832
  const cardTheme = theme.components?.card;
1204
1833
  const fillColor = this.cleanHexColor(cardTheme?.background || theme.colors.surface);
1205
- const borderColor = this.cleanHexColor(cardTheme?.borderColor || theme.colors.border || "#cbd5e1");
1834
+ let borderColor = this.cleanHexColor(cardTheme?.borderColor || theme.colors.border || "#cbd5e1");
1835
+ let titleColor = this.cleanHexColor(theme.colors.primary);
1836
+ if (card.variant && theme.colors[card.variant]) {
1837
+ const variantHex = this.cleanHexColor(theme.colors[card.variant]);
1838
+ borderColor = variantHex;
1839
+ titleColor = variantHex;
1840
+ }
1206
1841
  pptxSlide.addShape(pptx.ShapeType.roundRect, {
1207
1842
  x: rect.x,
1208
1843
  y: rect.y,
@@ -1220,7 +1855,7 @@ var PptxRenderer = class {
1220
1855
  h: 0.35,
1221
1856
  fontSize: 20,
1222
1857
  bold: true,
1223
- color: this.cleanHexColor(theme.colors.primary),
1858
+ color: titleColor,
1224
1859
  fontFace: theme.typography.headingFont.split(",")[0]?.trim() || "Arial"
1225
1860
  });
1226
1861
  }
@@ -1331,7 +1966,7 @@ var PptxRenderer = class {
1331
1966
  // src/cli.ts
1332
1967
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
1333
1968
  import { basename, dirname, extname, join, resolve } from "path";
1334
- var VERSION = "0.1.7";
1969
+ var VERSION = "0.1.8";
1335
1970
  function printHelp() {
1336
1971
  return `
1337
1972
  YumiaMD \u2014 Markdown-based presentation compiler (v${VERSION})
@@ -1350,6 +1985,7 @@ Commands:
1350
1985
  Options:
1351
1986
  --out, -o <file> Specify output file path (default: dist/<name>.pptx)
1352
1987
  --format, -f <fmt> Target output format: pptx (default)
1988
+ --strict Enforce zero warnings in 'lint' (exits with code 1 on warning)
1353
1989
  --json Output results formatted as JSON for CI/CD and AI tools
1354
1990
  --layout Show computed geometric bounding boxes in 'inspect'
1355
1991
  -h, --help Show this help message
@@ -1365,6 +2001,7 @@ async function runCli(argv) {
1365
2001
  return { exitCode: 0, output: `yumia v${VERSION}` };
1366
2002
  }
1367
2003
  const isJson = args.includes("--json");
2004
+ const isStrict = args.includes("--strict");
1368
2005
  const nonFlagArgs = args.filter((a) => !a.startsWith("-"));
1369
2006
  const command = nonFlagArgs[0];
1370
2007
  const target = nonFlagArgs[1];
@@ -1501,57 +2138,46 @@ ${errorLines.join("\n")}`
1501
2138
  }
1502
2139
  try {
1503
2140
  const source = readFileSync(target, "utf-8");
1504
- const presentation = parseYumia(source);
1505
- const engine = new DefaultLayoutEngine();
1506
- const layout = engine.computePresentation(presentation);
1507
- const issues = [];
1508
- layout.slides.forEach((slideResult, index) => {
1509
- const slideNum = index + 1;
1510
- if (slideResult.overflow) {
1511
- issues.push({
1512
- slide: slideNum,
1513
- type: "overflow",
1514
- message: `Content exceeds slide height by ~${Math.round(slideResult.overflowAmount || 0)}px`
1515
- });
1516
- }
1517
- const slide = presentation.slides[index];
1518
- if (slide.elements.length === 0) {
1519
- issues.push({
1520
- slide: slideNum,
1521
- type: "empty",
1522
- message: "Slide has no content elements."
1523
- });
1524
- }
1525
- });
2141
+ const compiler = new YumiaCompiler();
2142
+ const report = compiler.lint(source, { strict: isStrict });
1526
2143
  if (isJson) {
1527
2144
  return {
1528
- exitCode: 0,
2145
+ exitCode: report.passed ? 0 : 1,
1529
2146
  output: JSON.stringify(
1530
2147
  {
1531
2148
  target,
1532
- passed: issues.length === 0,
1533
- slideCount: presentation.slides.length,
1534
- issues
2149
+ ...report
1535
2150
  },
1536
2151
  null,
1537
2152
  2
1538
2153
  )
1539
2154
  };
1540
2155
  }
1541
- if (issues.length === 0) {
2156
+ if (report.issueCount === 0) {
1542
2157
  return {
1543
2158
  exitCode: 0,
1544
- output: `\u2713 Yumia Lint: All ${presentation.slides.length} slide(s) passed with 0 issues.`
2159
+ output: `\u2713 Yumia Lint: All ${report.totalSlides} slide(s) passed with 0 issues.`
1545
2160
  };
1546
2161
  } else {
1547
- const formatted = issues.map((i) => `\u26A0 Slide ${i.slide}: ${i.message}`).join("\n");
2162
+ const issueLines = [];
2163
+ for (const e of report.errors) {
2164
+ issueLines.push(` \u2717 [${e.code}] Slide ${e.slide}: ${e.message}`);
2165
+ }
2166
+ for (const w of report.warnings) {
2167
+ issueLines.push(` \u26A0 [${w.code}] Slide ${w.slide}: ${w.message}`);
2168
+ }
2169
+ for (const info of report.infos) {
2170
+ issueLines.push(` \u2139 [${info.code}] Slide ${info.slide}: ${info.message}`);
2171
+ }
2172
+ const summary = `Found ${report.errors.length} error(s), ${report.warnings.length} warning(s).`;
2173
+ const exitCode = report.passed ? 0 : 1;
1548
2174
  return {
1549
- exitCode: 0,
2175
+ exitCode,
1550
2176
  output: `Yumia Lint Report for '${target}':
1551
2177
 
1552
- ${formatted}
2178
+ ${issueLines.join("\n")}
1553
2179
 
1554
- Found ${issues.length} warning(s).`
2180
+ ${summary}${isStrict && report.warnings.length > 0 ? " (failed due to --strict)" : ""}`
1555
2181
  };
1556
2182
  }
1557
2183
  } catch (err) {
@@ -1663,6 +2289,7 @@ export {
1663
2289
  createList,
1664
2290
  createImage,
1665
2291
  createCard,
2292
+ createMetric,
1666
2293
  createCode,
1667
2294
  createQuote,
1668
2295
  createTable,
@@ -1676,7 +2303,15 @@ export {
1676
2303
  VIEWPORT_PRESETS,
1677
2304
  DefaultLayoutEngine,
1678
2305
  defaultTheme,
2306
+ cyberpunkTheme,
2307
+ minimalTheme,
2308
+ corporateTheme,
2309
+ terminalTheme,
2310
+ academicTheme,
2311
+ THEME_REGISTRY,
2312
+ resolveTheme,
1679
2313
  createTheme,
2314
+ YumiaLinter,
1680
2315
  YumiaCompiler,
1681
2316
  parseInlineMarkdown,
1682
2317
  PptxRenderer,
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.7";
9
+ declare const VERSION = "0.1.8";
10
10
  declare function printHelp(): string;
11
11
  declare function runCli(argv: string[]): Promise<{
12
12
  exitCode: number;
package/dist/index.js CHANGED
@@ -3,9 +3,13 @@ import {
3
3
  DefaultLayoutEngine,
4
4
  DefaultYumiaParser,
5
5
  PptxRenderer,
6
+ THEME_REGISTRY,
6
7
  VERSION,
7
8
  VIEWPORT_PRESETS,
8
9
  YumiaCompiler,
10
+ YumiaLinter,
11
+ academicTheme,
12
+ corporateTheme,
9
13
  createCard,
10
14
  createCode,
11
15
  createColumn,
@@ -15,26 +19,35 @@ import {
15
19
  createImage,
16
20
  createLayoutDirective,
17
21
  createList,
22
+ createMetric,
18
23
  createParagraph,
19
24
  createPresentation,
20
25
  createQuote,
21
26
  createSlide,
22
27
  createTable,
23
28
  createTheme,
29
+ cyberpunkTheme,
24
30
  defaultTheme,
31
+ minimalTheme,
25
32
  parseInlineMarkdown,
26
33
  parseYumia,
27
34
  printHelp,
28
- runCli
29
- } from "./chunk-GMIVPFBA.js";
35
+ resolveTheme,
36
+ runCli,
37
+ terminalTheme
38
+ } from "./chunk-WUJKHJJU.js";
30
39
  export {
31
40
  DEFAULT_VIEWPORT,
32
41
  DefaultLayoutEngine,
33
42
  DefaultYumiaParser,
34
43
  PptxRenderer,
44
+ THEME_REGISTRY,
35
45
  VERSION,
36
46
  VIEWPORT_PRESETS,
37
47
  YumiaCompiler,
48
+ YumiaLinter,
49
+ academicTheme,
50
+ corporateTheme,
38
51
  createCard,
39
52
  createCode,
40
53
  createColumn,
@@ -44,15 +57,20 @@ export {
44
57
  createImage,
45
58
  createLayoutDirective,
46
59
  createList,
60
+ createMetric,
47
61
  createParagraph,
48
62
  createPresentation,
49
63
  createQuote,
50
64
  createSlide,
51
65
  createTable,
52
66
  createTheme,
67
+ cyberpunkTheme,
53
68
  defaultTheme,
69
+ minimalTheme,
54
70
  parseInlineMarkdown,
55
71
  parseYumia,
56
72
  printHelp,
57
- runCli
73
+ resolveTheme,
74
+ runCli,
75
+ terminalTheme
58
76
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yumiamd",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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.7",
24
- "@yumiamd/parser": "0.1.7",
25
- "@yumiamd/renderer": "0.1.7",
26
- "@yumiamd/core": "0.1.7",
27
- "@yumiamd/renderer-pptx": "0.1.7",
28
- "@yumiamd/layout": "0.1.7",
29
- "@yumiamd/theme": "0.1.7"
23
+ "@yumiamd/ast": "0.1.8",
24
+ "@yumiamd/parser": "0.1.8",
25
+ "@yumiamd/core": "0.1.8",
26
+ "@yumiamd/renderer": "0.1.8",
27
+ "@yumiamd/layout": "0.1.8",
28
+ "@yumiamd/renderer-pptx": "0.1.8",
29
+ "@yumiamd/theme": "0.1.8"
30
30
  },
31
31
  "keywords": [
32
32
  "yumia",