yumiamd 0.1.9 → 0.1.10

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-F3CX7UG6.js";
4
+ } from "./chunk-PQYPVPV2.js";
5
5
 
6
6
  // src/bin.ts
7
7
  var result = await runCli(process.argv);
@@ -2017,10 +2017,756 @@ var PptxRenderer = class {
2017
2017
  }
2018
2018
  };
2019
2019
 
2020
+ // ../renderer-html/dist/renderer.js
2021
+ var HtmlRenderer = class {
2022
+ name = "HtmlRenderer";
2023
+ targetFormat = "html";
2024
+ async render(presentation, context = {}) {
2025
+ const colorOverrides = presentation.metadata.colors ? { colors: presentation.metadata.colors } : void 0;
2026
+ const resolvedTheme = resolveTheme(presentation.metadata.theme, colorOverrides);
2027
+ const theme = context.theme || resolvedTheme || defaultTheme;
2028
+ const title = presentation.metadata.title || "YumiaMD Presentation";
2029
+ const aspectRatio = presentation.metadata.aspectRatio || "16:9";
2030
+ const is43 = aspectRatio === "4:3";
2031
+ const ratioAspect = is43 ? "4 / 3" : "16 / 9";
2032
+ const options = context.options || {};
2033
+ const liveReloadScript = options.liveReload ? `
2034
+ <!-- YumiaMD Live Reload -->
2035
+ <script>
2036
+ (function() {
2037
+ const port = ${options.liveReloadPort || 3e3};
2038
+ const evtSource = new EventSource('/__yumia_live_reload');
2039
+ evtSource.onmessage = function(event) {
2040
+ if (event.data === 'reload') {
2041
+ const currentHash = window.location.hash;
2042
+ fetch(window.location.href)
2043
+ .then(res => res.text())
2044
+ .then(html => {
2045
+ const parser = new DOMParser();
2046
+ const doc = parser.parseFromString(html, 'text/html');
2047
+ const newDeck = doc.getElementById('yumia-deck');
2048
+ const oldDeck = document.getElementById('yumia-deck');
2049
+ if (newDeck && oldDeck) {
2050
+ oldDeck.innerHTML = newDeck.innerHTML;
2051
+ window.deckController.init();
2052
+ if (currentHash) window.location.hash = currentHash;
2053
+ } else {
2054
+ window.location.reload();
2055
+ }
2056
+ })
2057
+ .catch(() => window.location.reload());
2058
+ }
2059
+ };
2060
+ })();
2061
+ </script>
2062
+ ` : "";
2063
+ const slidesHtml = presentation.slides.map((slide, idx) => this.renderSlide(slide, idx + 1, presentation.slides.length, theme)).join("\n");
2064
+ const html = `<!DOCTYPE html>
2065
+ <html lang="en">
2066
+ <head>
2067
+ <meta charset="UTF-8">
2068
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2069
+ <title>${this.escapeHtml(title)}</title>
2070
+ <link rel="preconnect" href="https://fonts.googleapis.com">
2071
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
2072
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&family=Outfit:wght@600;700;800&display=swap" rel="stylesheet">
2073
+ <style>
2074
+ :root {
2075
+ --yumia-bg: ${theme.colors.background};
2076
+ --yumia-surface: ${theme.colors.surface};
2077
+ --yumia-text: ${theme.colors.text};
2078
+ --yumia-muted: ${theme.colors.muted || "#94a3b8"};
2079
+ --yumia-primary: ${theme.colors.primary};
2080
+ --yumia-secondary: ${theme.colors.secondary || theme.colors.primary};
2081
+ --yumia-accent: ${theme.colors.accent || theme.colors.primary};
2082
+ --yumia-border: ${theme.colors.border || "rgba(255,255,255,0.1)"};
2083
+ --yumia-success: ${theme.colors.success || "#10b981"};
2084
+ --yumia-warning: ${theme.colors.warning || "#f59e0b"};
2085
+ --yumia-danger: ${theme.colors.danger || "#ef4444"};
2086
+ --yumia-info: ${theme.colors.info || "#3b82f6"};
2087
+ --yumia-font-heading: ${theme.typography.headingFont};
2088
+ --yumia-font-body: ${theme.typography.bodyFont};
2089
+ --yumia-font-code: ${theme.typography.codeFont || "monospace"};
2090
+ --yumia-ratio: ${ratioAspect};
2091
+ }
2092
+
2093
+ * {
2094
+ box-sizing: border-box;
2095
+ margin: 0;
2096
+ padding: 0;
2097
+ }
2098
+
2099
+ body {
2100
+ background-color: #050508;
2101
+ color: var(--yumia-text);
2102
+ font-family: var(--yumia-font-body);
2103
+ overflow: hidden;
2104
+ width: 100vw;
2105
+ height: 100vh;
2106
+ display: flex;
2107
+ align-items: center;
2108
+ justify-content: center;
2109
+ user-select: none;
2110
+ }
2111
+
2112
+ #yumia-deck {
2113
+ position: relative;
2114
+ width: 100%;
2115
+ height: 100%;
2116
+ display: flex;
2117
+ align-items: center;
2118
+ justify-content: center;
2119
+ }
2120
+
2121
+ .yumia-slide-wrapper {
2122
+ position: relative;
2123
+ width: min(94vw, calc(94vh * (${is43 ? "4 / 3" : "16 / 9"})));
2124
+ height: min(calc(94vw / (${is43 ? "4 / 3" : "16 / 9"})), 94vh);
2125
+ aspect-ratio: var(--yumia-ratio);
2126
+ background-color: var(--yumia-bg);
2127
+ border-radius: 12px;
2128
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7), 0 0 0 1px var(--yumia-border);
2129
+ overflow: hidden;
2130
+ display: none;
2131
+ flex-direction: column;
2132
+ padding: 4.5% 5.5%;
2133
+ animation: fadeIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
2134
+ }
2135
+
2136
+ .yumia-slide-wrapper.active {
2137
+ display: flex;
2138
+ }
2139
+
2140
+ @keyframes fadeIn {
2141
+ from { opacity: 0; transform: scale(0.985); }
2142
+ to { opacity: 1; transform: scale(1); }
2143
+ }
2144
+
2145
+ /* Headings */
2146
+ h1, h2, h3, h4 {
2147
+ font-family: var(--yumia-font-heading);
2148
+ font-weight: 700;
2149
+ line-height: 1.15;
2150
+ margin-bottom: 0.7em;
2151
+ letter-spacing: -0.02em;
2152
+ }
2153
+
2154
+ h1 {
2155
+ font-size: clamp(2rem, 3.8vw, 3.4rem);
2156
+ color: var(--yumia-primary);
2157
+ }
2158
+
2159
+ h2 {
2160
+ font-size: clamp(1.6rem, 2.8vw, 2.5rem);
2161
+ color: var(--yumia-text);
2162
+ }
2163
+
2164
+ h3 {
2165
+ font-size: clamp(1.3rem, 2.2vw, 1.9rem);
2166
+ color: var(--yumia-text);
2167
+ }
2168
+
2169
+ h4 {
2170
+ font-size: clamp(1.1rem, 1.7vw, 1.4rem);
2171
+ color: var(--yumia-muted);
2172
+ }
2173
+
2174
+ /* Paragraphs */
2175
+ p {
2176
+ font-size: clamp(1rem, 1.4vw, 1.25rem);
2177
+ line-height: 1.6;
2178
+ color: var(--yumia-text);
2179
+ margin-bottom: 0.8em;
2180
+ }
2181
+
2182
+ p strong, li strong {
2183
+ color: var(--yumia-primary);
2184
+ font-weight: 700;
2185
+ }
2186
+
2187
+ p em, li em {
2188
+ font-style: italic;
2189
+ color: var(--yumia-secondary);
2190
+ }
2191
+
2192
+ p code, li code {
2193
+ font-family: var(--yumia-font-code);
2194
+ background: rgba(255, 255, 255, 0.08);
2195
+ padding: 0.15em 0.4em;
2196
+ border-radius: 4px;
2197
+ font-size: 0.9em;
2198
+ color: var(--yumia-accent);
2199
+ }
2200
+
2201
+ /* Lists */
2202
+ ul, ol {
2203
+ font-size: clamp(1rem, 1.35vw, 1.2rem);
2204
+ line-height: 1.65;
2205
+ margin-bottom: 1em;
2206
+ padding-left: 1.5em;
2207
+ }
2208
+
2209
+ li {
2210
+ margin-bottom: 0.5em;
2211
+ color: var(--yumia-text);
2212
+ }
2213
+
2214
+ li::marker {
2215
+ color: var(--yumia-primary);
2216
+ }
2217
+
2218
+ /* Columns */
2219
+ .yumia-columns {
2220
+ display: grid;
2221
+ gap: 1.5rem;
2222
+ width: 100%;
2223
+ margin: 0.8rem 0;
2224
+ align-items: stretch;
2225
+ }
2226
+
2227
+ .yumia-column {
2228
+ display: flex;
2229
+ flex-direction: column;
2230
+ gap: 0.8rem;
2231
+ }
2232
+
2233
+ /* Cards */
2234
+ .yumia-card {
2235
+ background: var(--yumia-surface);
2236
+ border: 1.5px solid var(--yumia-border);
2237
+ border-radius: 12px;
2238
+ padding: 1.25rem 1.5rem;
2239
+ display: flex;
2240
+ flex-direction: column;
2241
+ gap: 0.6rem;
2242
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
2243
+ }
2244
+
2245
+ .yumia-card[data-variant="primary"] {
2246
+ border-color: var(--yumia-primary);
2247
+ }
2248
+ .yumia-card[data-variant="primary"] .yumia-card-title {
2249
+ color: var(--yumia-primary);
2250
+ }
2251
+
2252
+ .yumia-card[data-variant="warning"] {
2253
+ border-color: var(--yumia-warning);
2254
+ }
2255
+ .yumia-card[data-variant="warning"] .yumia-card-title {
2256
+ color: var(--yumia-warning);
2257
+ }
2258
+
2259
+ .yumia-card[data-variant="success"] {
2260
+ border-color: var(--yumia-success);
2261
+ }
2262
+ .yumia-card[data-variant="success"] .yumia-card-title {
2263
+ color: var(--yumia-success);
2264
+ }
2265
+
2266
+ .yumia-card[data-variant="info"] {
2267
+ border-color: var(--yumia-info);
2268
+ }
2269
+ .yumia-card[data-variant="info"] .yumia-card-title {
2270
+ color: var(--yumia-info);
2271
+ }
2272
+
2273
+ .yumia-card-title {
2274
+ font-family: var(--yumia-font-heading);
2275
+ font-size: 1.25rem;
2276
+ font-weight: 700;
2277
+ color: var(--yumia-primary);
2278
+ margin-bottom: 0.3rem;
2279
+ }
2280
+
2281
+ /* Metrics */
2282
+ .yumia-metric {
2283
+ background: var(--yumia-surface);
2284
+ border: 1.5px solid var(--yumia-border);
2285
+ border-radius: 12px;
2286
+ padding: 1rem 1.2rem;
2287
+ display: flex;
2288
+ flex-direction: column;
2289
+ align-items: center;
2290
+ justify-content: center;
2291
+ text-align: center;
2292
+ gap: 0.25rem;
2293
+ }
2294
+
2295
+ .yumia-metric[data-variant="primary"] { border-color: var(--yumia-primary); }
2296
+ .yumia-metric[data-variant="primary"] .yumia-metric-value { color: var(--yumia-primary); }
2297
+ .yumia-metric[data-variant="success"] { border-color: var(--yumia-success); }
2298
+ .yumia-metric[data-variant="success"] .yumia-metric-value { color: var(--yumia-success); }
2299
+ .yumia-metric[data-variant="info"] { border-color: var(--yumia-info); }
2300
+ .yumia-metric[data-variant="info"] .yumia-metric-value { color: var(--yumia-info); }
2301
+ .yumia-metric[data-variant="warning"] { border-color: var(--yumia-warning); }
2302
+ .yumia-metric[data-variant="warning"] .yumia-metric-value { color: var(--yumia-warning); }
2303
+ .yumia-metric[data-variant="danger"] { border-color: var(--yumia-danger); }
2304
+ .yumia-metric[data-variant="danger"] .yumia-metric-value { color: var(--yumia-danger); }
2305
+
2306
+ .yumia-metric-label {
2307
+ font-size: 0.75rem;
2308
+ font-weight: 700;
2309
+ text-transform: uppercase;
2310
+ letter-spacing: 0.08em;
2311
+ color: var(--yumia-muted);
2312
+ }
2313
+
2314
+ .yumia-metric-value {
2315
+ font-family: var(--yumia-font-heading);
2316
+ font-size: clamp(1.8rem, 3.2vw, 2.6rem);
2317
+ font-weight: 800;
2318
+ line-height: 1.1;
2319
+ color: var(--yumia-primary);
2320
+ }
2321
+
2322
+ .yumia-metric-change {
2323
+ font-size: 0.85rem;
2324
+ font-weight: 600;
2325
+ }
2326
+ .yumia-metric-change.positive { color: var(--yumia-success); }
2327
+ .yumia-metric-change.negative { color: var(--yumia-danger); }
2328
+
2329
+ /* Tables */
2330
+ table {
2331
+ width: 100%;
2332
+ border-collapse: collapse;
2333
+ margin: 1rem 0;
2334
+ font-size: clamp(0.9rem, 1.15vw, 1.05rem);
2335
+ border: 1px solid var(--yumia-border);
2336
+ border-radius: 8px;
2337
+ overflow: hidden;
2338
+ }
2339
+
2340
+ th {
2341
+ background: var(--yumia-primary);
2342
+ color: #ffffff;
2343
+ font-weight: 700;
2344
+ padding: 0.75rem 1rem;
2345
+ text-align: left;
2346
+ }
2347
+
2348
+ td {
2349
+ padding: 0.7rem 1rem;
2350
+ border-top: 1px solid var(--yumia-border);
2351
+ color: var(--yumia-text);
2352
+ background: var(--yumia-surface);
2353
+ }
2354
+
2355
+ tr:nth-child(even) td {
2356
+ background: rgba(255, 255, 255, 0.03);
2357
+ }
2358
+
2359
+ /* Code Blocks */
2360
+ pre {
2361
+ background: #0a0a10;
2362
+ border: 1px solid var(--yumia-border);
2363
+ border-radius: 8px;
2364
+ padding: 1rem 1.25rem;
2365
+ overflow-x: auto;
2366
+ margin: 0.8rem 0;
2367
+ }
2368
+
2369
+ pre code {
2370
+ font-family: var(--yumia-font-code);
2371
+ font-size: 0.95rem;
2372
+ color: #00F0FF;
2373
+ background: none;
2374
+ padding: 0;
2375
+ }
2376
+
2377
+ /* Blockquotes */
2378
+ blockquote {
2379
+ border-left: 4px solid var(--yumia-accent);
2380
+ padding: 0.6rem 1.2rem;
2381
+ margin: 1rem 0;
2382
+ font-style: italic;
2383
+ color: var(--yumia-muted);
2384
+ font-size: 1.1rem;
2385
+ background: rgba(255, 255, 255, 0.02);
2386
+ border-radius: 0 8px 8px 0;
2387
+ }
2388
+
2389
+ /* Controls Overlay */
2390
+ .yumia-controls {
2391
+ position: fixed;
2392
+ bottom: 20px;
2393
+ right: 24px;
2394
+ display: flex;
2395
+ align-items: center;
2396
+ gap: 12px;
2397
+ background: rgba(15, 23, 42, 0.85);
2398
+ backdrop-filter: blur(12px);
2399
+ padding: 8px 16px;
2400
+ border-radius: 30px;
2401
+ border: 1px solid rgba(255, 255, 255, 0.15);
2402
+ z-index: 1000;
2403
+ font-size: 13px;
2404
+ color: #e2e8f0;
2405
+ }
2406
+
2407
+ .yumia-btn {
2408
+ background: none;
2409
+ border: none;
2410
+ color: #f8fafc;
2411
+ cursor: pointer;
2412
+ padding: 4px 8px;
2413
+ border-radius: 6px;
2414
+ font-size: 14px;
2415
+ display: flex;
2416
+ align-items: center;
2417
+ justify-content: center;
2418
+ transition: background 0.15s ease;
2419
+ }
2420
+
2421
+ .yumia-btn:hover {
2422
+ background: rgba(255, 255, 255, 0.15);
2423
+ }
2424
+
2425
+ .yumia-progress-bar {
2426
+ position: absolute;
2427
+ bottom: 0;
2428
+ left: 0;
2429
+ height: 4px;
2430
+ background: var(--yumia-primary);
2431
+ transition: width 0.25s ease;
2432
+ }
2433
+
2434
+ /* Notes Drawer */
2435
+ .yumia-notes-drawer {
2436
+ position: fixed;
2437
+ bottom: 0;
2438
+ left: 0;
2439
+ right: 0;
2440
+ max-height: 220px;
2441
+ background: rgba(10, 10, 18, 0.95);
2442
+ backdrop-filter: blur(16px);
2443
+ border-top: 1px solid var(--yumia-primary);
2444
+ padding: 16px 24px;
2445
+ overflow-y: auto;
2446
+ display: none;
2447
+ z-index: 2000;
2448
+ font-size: 14px;
2449
+ line-height: 1.6;
2450
+ color: #cbd5e1;
2451
+ }
2452
+
2453
+ .yumia-notes-drawer.open {
2454
+ display: block;
2455
+ }
2456
+ </style>
2457
+ </head>
2458
+ <body>
2459
+ <div id="yumia-deck">
2460
+ ${slidesHtml}
2461
+ </div>
2462
+
2463
+ <div class="yumia-controls">
2464
+ <button class="yumia-btn" id="btn-prev" title="Previous Slide (\u2190)">\u25C0</button>
2465
+ <span id="slide-indicator">1 / ${presentation.slides.length}</span>
2466
+ <button class="yumia-btn" id="btn-next" title="Next Slide (\u2192)">\u25B6</button>
2467
+ <button class="yumia-btn" id="btn-notes" title="Toggle Notes (N)">\u{1F4DD}</button>
2468
+ <button class="yumia-btn" id="btn-fs" title="Fullscreen (F)">\u26F6</button>
2469
+ </div>
2470
+
2471
+ <div id="notes-drawer" class="yumia-notes-drawer"></div>
2472
+
2473
+ <script>
2474
+ (function() {
2475
+ const slides = document.querySelectorAll('.yumia-slide-wrapper');
2476
+ const indicator = document.getElementById('slide-indicator');
2477
+ const notesDrawer = document.getElementById('notes-drawer');
2478
+ let currentIdx = 0;
2479
+
2480
+ function updateSlide(newIdx) {
2481
+ if (newIdx < 0 || newIdx >= slides.length) return;
2482
+ slides[currentIdx]?.classList.remove('active');
2483
+ currentIdx = newIdx;
2484
+ slides[currentIdx]?.classList.add('active');
2485
+
2486
+ if (indicator) {
2487
+ indicator.textContent = (currentIdx + 1) + ' / ' + slides.length;
2488
+ }
2489
+
2490
+ const currentSlide = slides[currentIdx];
2491
+ const notes = currentSlide ? currentSlide.getAttribute('data-notes') : '';
2492
+ if (notesDrawer) {
2493
+ notesDrawer.innerHTML = notes ? '<strong>Speaker Notes:</strong><br>' + notes : '<em>No notes for this slide.</em>';
2494
+ }
2495
+
2496
+ window.location.hash = '#' + (currentIdx + 1);
2497
+ }
2498
+
2499
+ function init() {
2500
+ const hash = window.location.hash.replace('#', '');
2501
+ const initialIdx = parseInt(hash, 10) - 1;
2502
+ updateSlide(isNaN(initialIdx) ? 0 : initialIdx);
2503
+ }
2504
+
2505
+ window.deckController = {
2506
+ init: init,
2507
+ next: function() { updateSlide(currentIdx + 1); },
2508
+ prev: function() { updateSlide(currentIdx - 1); },
2509
+ toggleNotes: function() { notesDrawer?.classList.toggle('open'); },
2510
+ toggleFs: function() {
2511
+ if (!document.fullscreenElement) {
2512
+ document.documentElement.requestFullscreen().catch(() => {});
2513
+ } else {
2514
+ document.exitFullscreen().catch(() => {});
2515
+ }
2516
+ }
2517
+ };
2518
+
2519
+ document.getElementById('btn-next')?.addEventListener('click', window.deckController.next);
2520
+ document.getElementById('btn-prev')?.addEventListener('click', window.deckController.prev);
2521
+ document.getElementById('btn-notes')?.addEventListener('click', window.deckController.toggleNotes);
2522
+ document.getElementById('btn-fs')?.addEventListener('click', window.deckController.toggleFs);
2523
+
2524
+ window.addEventListener('keydown', function(e) {
2525
+ if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown') {
2526
+ e.preventDefault();
2527
+ window.deckController.next();
2528
+ } else if (e.key === 'ArrowLeft' || e.key === 'Backspace' || e.key === 'PageUp') {
2529
+ e.preventDefault();
2530
+ window.deckController.prev();
2531
+ } else if (e.key.toLowerCase() === 'f') {
2532
+ window.deckController.toggleFs();
2533
+ } else if (e.key.toLowerCase() === 'n') {
2534
+ window.deckController.toggleNotes();
2535
+ }
2536
+ });
2537
+
2538
+ // Touch swipe support
2539
+ let touchStartX = 0;
2540
+ window.addEventListener('touchstart', function(e) {
2541
+ touchStartX = e.changedTouches[0]?.screenX || 0;
2542
+ });
2543
+ window.addEventListener('touchend', function(e) {
2544
+ const touchEndX = e.changedTouches[0]?.screenX || 0;
2545
+ if (touchStartX - touchEndX > 50) window.deckController.next();
2546
+ if (touchEndX - touchStartX > 50) window.deckController.prev();
2547
+ });
2548
+
2549
+ init();
2550
+ })();
2551
+ </script>
2552
+ ${liveReloadScript}
2553
+ </body>
2554
+ </html>`;
2555
+ return {
2556
+ format: "html",
2557
+ html,
2558
+ slideCount: presentation.slides.length
2559
+ };
2560
+ }
2561
+ renderSlide(slide, slideNum, totalSlides, theme) {
2562
+ const activeClass = slideNum === 1 ? "active" : "";
2563
+ const notesAttr = slide.notes ? this.escapeHtml(slide.notes.replace(/\n/g, "<br>")) : "";
2564
+ const progressPercent = Math.round(slideNum / totalSlides * 100);
2565
+ const elementsHtml = slide.elements.map((el) => this.renderElement(el, theme)).join("\n");
2566
+ return `
2567
+ <div class="yumia-slide-wrapper ${activeClass}" id="slide-${slideNum}" data-notes="${notesAttr}">
2568
+ ${elementsHtml}
2569
+ <div class="yumia-progress-bar" style="width: ${progressPercent}%;"></div>
2570
+ </div>`;
2571
+ }
2572
+ renderElement(element, theme) {
2573
+ switch (element.type) {
2574
+ case "heading": {
2575
+ const h = element;
2576
+ const tag = `h${Math.min(4, Math.max(1, h.level))}`;
2577
+ return `<${tag}>${this.formatInline(h.text)}</${tag}>`;
2578
+ }
2579
+ case "paragraph": {
2580
+ const p = element;
2581
+ return `<p>${this.formatInline(p.text)}</p>`;
2582
+ }
2583
+ case "list": {
2584
+ const l = element;
2585
+ const tag = l.ordered ? "ol" : "ul";
2586
+ const items = l.items.map((i) => `<li>${this.formatInline(i.text)}</li>`).join("\n");
2587
+ return `<${tag}>${items}</${tag}>`;
2588
+ }
2589
+ case "code": {
2590
+ const c = element;
2591
+ const langClass = c.language ? `class="language-${c.language}"` : "";
2592
+ return `<pre><code ${langClass}>${this.escapeHtml(c.code)}</code></pre>`;
2593
+ }
2594
+ case "quote": {
2595
+ const q = element;
2596
+ const author = q.author ? `<br><small>\u2014 ${this.escapeHtml(q.author)}</small>` : "";
2597
+ return `<blockquote>\u201C${this.formatInline(q.text)}\u201D${author}</blockquote>`;
2598
+ }
2599
+ case "table": {
2600
+ const t = element;
2601
+ let html = "<table>";
2602
+ if (t.headers && t.headers.length > 0) {
2603
+ html += "<thead><tr>";
2604
+ html += t.headers.map((h) => `<th>${this.formatInline(h.replace(/\*\*/g, ""))}</th>`).join("");
2605
+ html += "</tr></thead>";
2606
+ }
2607
+ if (t.rows) {
2608
+ html += "<tbody>";
2609
+ for (const row of t.rows) {
2610
+ html += "<tr>";
2611
+ html += row.map((c) => `<td>${this.formatInline(c.replace(/\*\*/g, ""))}</td>`).join("");
2612
+ html += "</tr>";
2613
+ }
2614
+ html += "</tbody>";
2615
+ }
2616
+ html += "</table>";
2617
+ return html;
2618
+ }
2619
+ case "image": {
2620
+ const img = element;
2621
+ const alt = img.alt ? `alt="${this.escapeHtml(img.alt)}"` : "";
2622
+ return `<img src="${this.escapeHtml(img.src)}" ${alt} style="max-width: 100%; border-radius: 8px;">`;
2623
+ }
2624
+ case "metric": {
2625
+ const m = element;
2626
+ const variant = m.variant || "primary";
2627
+ const displayVal = m.unit ? `${m.value} ${m.unit}` : m.value;
2628
+ const changeHtml = m.change ? `<span class="yumia-metric-change ${m.change.startsWith("+") ? "positive" : "negative"}">${this.escapeHtml(m.change)}</span>` : "";
2629
+ return `
2630
+ <div class="yumia-metric" data-variant="${variant}">
2631
+ <span class="yumia-metric-label">${this.escapeHtml(m.label)}</span>
2632
+ <span class="yumia-metric-value">${this.escapeHtml(displayVal)}</span>
2633
+ ${changeHtml}
2634
+ </div>`;
2635
+ }
2636
+ case "card": {
2637
+ const card = element;
2638
+ const variant = card.variant || "default";
2639
+ const titleHtml = card.title ? `<div class="yumia-card-title">${this.escapeHtml(card.title)}</div>` : "";
2640
+ const innerHtml = card.elements ? card.elements.map((child) => this.renderElement(child, theme)).join("\n") : "";
2641
+ return `
2642
+ <div class="yumia-card" data-variant="${variant}">
2643
+ ${titleHtml}
2644
+ ${innerHtml}
2645
+ </div>`;
2646
+ }
2647
+ case "columns": {
2648
+ const cols = element;
2649
+ const colCount = cols.columns.length;
2650
+ const ratioTemplate = cols.ratios ? cols.ratios.split(":").map((r) => `${r}fr`).join(" ") : `repeat(${colCount}, 1fr)`;
2651
+ const colsHtml = cols.columns.map((col) => {
2652
+ const inner = col.elements.map((child) => this.renderElement(child, theme)).join("\n");
2653
+ return `<div class="yumia-column">${inner}</div>`;
2654
+ }).join("\n");
2655
+ return `<div class="yumia-columns" style="grid-template-columns: ${ratioTemplate};">${colsHtml}</div>`;
2656
+ }
2657
+ default:
2658
+ return "";
2659
+ }
2660
+ }
2661
+ formatInline(text) {
2662
+ const escaped = this.escapeHtml(text);
2663
+ return escaped.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>").replace(/\*(.*?)\*/g, "<em>$1</em>").replace(/`(.*?)`/g, "<code>$1</code>");
2664
+ }
2665
+ escapeHtml(str) {
2666
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2667
+ }
2668
+ };
2669
+
2670
+ // src/dev-server.ts
2671
+ import { createServer } from "http";
2672
+ import { readFileSync, watch } from "fs";
2673
+ import { resolve } from "path";
2674
+ function startDevServer(filePath, options = {}) {
2675
+ return new Promise((resolvePromise, rejectPromise) => {
2676
+ const port = options.port || 3e3;
2677
+ const resolvedPath = resolve(process.cwd(), filePath);
2678
+ const compiler = new YumiaCompiler();
2679
+ const renderer = new HtmlRenderer();
2680
+ const sseClients = /* @__PURE__ */ new Set();
2681
+ let watcher = null;
2682
+ let debounceTimer = null;
2683
+ async function getCompiledHtml() {
2684
+ try {
2685
+ const source = readFileSync(resolvedPath, "utf-8");
2686
+ const output = await compiler.compile(source, renderer, {
2687
+ renderContext: {
2688
+ options: {
2689
+ liveReload: true,
2690
+ liveReloadPort: port
2691
+ }
2692
+ }
2693
+ });
2694
+ return output.html;
2695
+ } catch (err) {
2696
+ const errMsg = err instanceof Error ? err.message : String(err);
2697
+ return `<!DOCTYPE html><html><head><title>YumiaMD Compile Error</title><style>body{background:#0b0b12;color:#ef4444;font-family:monospace;padding:40px;}</style></head><body><h1>Compile Error</h1><pre>${errMsg}</pre></body></html>`;
2698
+ }
2699
+ }
2700
+ const server = createServer(async (req, res) => {
2701
+ const url = req.url || "/";
2702
+ if (url === "/__yumia_live_reload") {
2703
+ res.writeHead(200, {
2704
+ "Content-Type": "text/event-stream",
2705
+ "Cache-Control": "no-cache",
2706
+ Connection: "keep-alive",
2707
+ "Access-Control-Allow-Origin": "*"
2708
+ });
2709
+ res.write("data: connected\n\n");
2710
+ sseClients.add(res);
2711
+ req.on("close", () => {
2712
+ sseClients.delete(res);
2713
+ });
2714
+ return;
2715
+ }
2716
+ if (url === "/" || url.startsWith("/#")) {
2717
+ const html = await getCompiledHtml();
2718
+ res.writeHead(200, {
2719
+ "Content-Type": "text/html; charset=utf-8",
2720
+ "Cache-Control": "no-store"
2721
+ });
2722
+ res.end(html);
2723
+ return;
2724
+ }
2725
+ res.writeHead(404, { "Content-Type": "text/plain" });
2726
+ res.end("Not Found");
2727
+ });
2728
+ server.on("error", (err) => {
2729
+ rejectPromise(err);
2730
+ });
2731
+ server.listen(port, () => {
2732
+ const url = `http://localhost:${port}`;
2733
+ try {
2734
+ watcher = watch(resolvedPath, () => {
2735
+ if (debounceTimer) clearTimeout(debounceTimer);
2736
+ debounceTimer = setTimeout(() => {
2737
+ for (const client of sseClients) {
2738
+ try {
2739
+ client.write("data: reload\n\n");
2740
+ } catch {
2741
+ sseClients.delete(client);
2742
+ }
2743
+ }
2744
+ }, 40);
2745
+ });
2746
+ } catch (watchErr) {
2747
+ console.warn(`[Yumia Dev] Warning: could not watch file: ${watchErr}`);
2748
+ }
2749
+ resolvePromise({
2750
+ server,
2751
+ port,
2752
+ url,
2753
+ close: () => new Promise((resClose) => {
2754
+ if (watcher) watcher.close();
2755
+ for (const client of sseClients) {
2756
+ client.end();
2757
+ }
2758
+ sseClients.clear();
2759
+ server.close(() => resClose());
2760
+ })
2761
+ });
2762
+ });
2763
+ });
2764
+ }
2765
+
2020
2766
  // src/cli.ts
2021
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
2022
- import { basename, dirname, extname, join, resolve } from "path";
2023
- var VERSION = "0.1.9";
2767
+ import { mkdirSync, readFileSync as readFileSync2, watch as fsWatch, writeFileSync } from "fs";
2768
+ import { basename, dirname, extname, join, resolve as resolve2 } from "path";
2769
+ var VERSION = "0.1.10";
2024
2770
  function printHelp() {
2025
2771
  return `
2026
2772
  YumiaMD \u2014 Markdown-based presentation compiler (v${VERSION})
@@ -2030,11 +2776,13 @@ Usage:
2030
2776
 
2031
2777
  Commands:
2032
2778
  init [name] Scaffold a new YumiaMD presentation project
2779
+ dev <file> Start live-reload dev server with instant HTML preview
2780
+ watch <file> Watch presentation file and recompile automatically on save
2033
2781
  validate <file> Validate a .yumia.md presentation syntax and structure
2034
2782
  lint <file> Analyze presentation for layout overflows and accessibility
2035
2783
  inspect <file> Inspect the AST and geometric layout tree
2036
2784
  schema Output machine-readable JSON schema for AI agents
2037
- build <file> Compile a presentation to editable PowerPoint (.pptx)
2785
+ build <file> Compile a presentation to PowerPoint (.pptx) or HTML (.html)
2038
2786
 
2039
2787
  Theming & Color Options:
2040
2788
  --theme, -t <name> Base theme: default | cyberpunk | minimal | corporate | terminal | academic
@@ -2044,9 +2792,12 @@ Theming & Color Options:
2044
2792
  --text Hex text color (e.g. "#FFFFFF" or "#0F172A")
2045
2793
  --accent Hex accent bar & highlight color
2046
2794
 
2047
- Compiler & Output Options:
2048
- --out, -o <file> Specify output file path (default: dist/<name>.pptx)
2049
- --format, -f <fmt> Target output format: pptx (default)
2795
+ Server & Compiler Options:
2796
+ --port <num> Port for live dev server (default: 3000)
2797
+ --open Open default browser automatically in dev mode
2798
+ --watch, -w Watch for file changes during compilation
2799
+ --out, -o <file> Specify output file path (default: dist/<name>.<format>)
2800
+ --format, -f <fmt> Target output format: pptx (default) | html
2050
2801
  --strict Enforce zero warnings in 'lint' (exits with code 1 on warning)
2051
2802
  --json Output results formatted as JSON for CI/CD and AI tools
2052
2803
  --layout Show computed geometric bounding boxes in 'inspect'
@@ -2079,6 +2830,7 @@ async function runCli(argv) {
2079
2830
  }
2080
2831
  const isJson = args.includes("--json");
2081
2832
  const isStrict = args.includes("--strict");
2833
+ const isWatch = args.includes("--watch") || args.includes("-w");
2082
2834
  const nonFlagArgs = args.filter((a) => !a.startsWith("-"));
2083
2835
  const command = nonFlagArgs[0];
2084
2836
  const target = nonFlagArgs[1];
@@ -2088,6 +2840,7 @@ async function runCli(argv) {
2088
2840
  const cliSecondary = getFlagValue(args, ["--secondary"]);
2089
2841
  const cliText = getFlagValue(args, ["--text"]);
2090
2842
  const cliAccent = getFlagValue(args, ["--accent"]);
2843
+ const cliFormat = (getFlagValue(args, ["--format", "-f"]) || "pptx").toLowerCase();
2091
2844
  if (command === "schema") {
2092
2845
  const compiler = new YumiaCompiler();
2093
2846
  return {
@@ -2175,7 +2928,7 @@ Opening slide introducing the presentation deck.
2175
2928
  } catch (err) {
2176
2929
  const msg = err instanceof Error ? err.message : String(err);
2177
2930
  if (isJson) {
2178
- return { exitCode: 1, output: JSON.stringify({ success: false, error: msg }, null, 2) };
2931
+ return { exitCode: 1, output: JSON.stringify({ success: false, error: msg }) };
2179
2932
  }
2180
2933
  return {
2181
2934
  exitCode: 1,
@@ -2183,13 +2936,56 @@ Opening slide introducing the presentation deck.
2183
2936
  };
2184
2937
  }
2185
2938
  }
2939
+ if (command === "dev") {
2940
+ if (!target) {
2941
+ const msg = "Error: Please specify a presentation file to preview in dev server.";
2942
+ return { exitCode: 1, output: isJson ? JSON.stringify({ error: msg }) : msg };
2943
+ }
2944
+ try {
2945
+ const resolvedInput = resolve2(process.cwd(), target);
2946
+ const portStr = getFlagValue(args, ["--port"]);
2947
+ const port = portStr ? parseInt(portStr, 10) : 3e3;
2948
+ const open = args.includes("--open");
2949
+ const devInstance = await startDevServer(resolvedInput, { port, open });
2950
+ if (isJson) {
2951
+ return {
2952
+ exitCode: 0,
2953
+ output: JSON.stringify(
2954
+ {
2955
+ success: true,
2956
+ url: devInstance.url,
2957
+ port: devInstance.port,
2958
+ file: resolvedInput
2959
+ },
2960
+ null,
2961
+ 2
2962
+ )
2963
+ };
2964
+ }
2965
+ return {
2966
+ exitCode: 0,
2967
+ output: `\u{1F680} YumiaMD Live Dev Server running at: ${devInstance.url}
2968
+ Watching: ${target}
2969
+ Hot-reloading active via SSE. Press Ctrl+C to stop.`
2970
+ };
2971
+ } catch (err) {
2972
+ const msg = err instanceof Error ? err.message : String(err);
2973
+ if (isJson) {
2974
+ return { exitCode: 1, output: JSON.stringify({ success: false, error: msg }) };
2975
+ }
2976
+ return {
2977
+ exitCode: 1,
2978
+ output: `\u2717 Dev server failed: ${msg}`
2979
+ };
2980
+ }
2981
+ }
2186
2982
  if (command === "validate") {
2187
2983
  if (!target) {
2188
2984
  const msg = "Error: Please specify a file to validate.";
2189
2985
  return { exitCode: 1, output: isJson ? JSON.stringify({ error: msg }) : msg };
2190
2986
  }
2191
2987
  try {
2192
- const source = readFileSync(target, "utf-8");
2988
+ const source = readFileSync2(target, "utf-8");
2193
2989
  const compiler = new YumiaCompiler();
2194
2990
  const validation = compiler.validate(source);
2195
2991
  if (isJson) {
@@ -2231,7 +3027,7 @@ ${errorLines.join("\n")}`
2231
3027
  return { exitCode: 1, output: isJson ? JSON.stringify({ error: msg }) : msg };
2232
3028
  }
2233
3029
  try {
2234
- const source = readFileSync(target, "utf-8");
3030
+ const source = readFileSync2(target, "utf-8");
2235
3031
  const compiler = new YumiaCompiler();
2236
3032
  const report = compiler.lint(source, { strict: isStrict });
2237
3033
  if (isJson) {
@@ -2291,7 +3087,7 @@ ${summary}${isStrict && report.warnings.length > 0 ? " (failed due to --strict)"
2291
3087
  return { exitCode: 1, output: isJson ? JSON.stringify({ error: msg }) : msg };
2292
3088
  }
2293
3089
  try {
2294
- const source = readFileSync(target, "utf-8");
3090
+ const source = readFileSync2(target, "utf-8");
2295
3091
  const presentation = parseYumia(source);
2296
3092
  if (args.includes("--layout")) {
2297
3093
  const engine = new DefaultLayoutEngine();
@@ -2316,25 +3112,25 @@ ${summary}${isStrict && report.warnings.length > 0 ? " (failed due to --strict)"
2316
3112
  };
2317
3113
  }
2318
3114
  }
2319
- if (command === "build") {
3115
+ if (command === "build" || command === "watch") {
2320
3116
  if (!target) {
2321
- const msg = "Error: Please specify a presentation file to build.";
3117
+ const msg = `Error: Please specify a presentation file to ${command}.`;
2322
3118
  return { exitCode: 1, output: isJson ? JSON.stringify({ error: msg }) : msg };
2323
3119
  }
2324
3120
  try {
2325
- const resolvedInput = resolve(process.cwd(), target);
2326
- const source = readFileSync(resolvedInput, "utf-8");
3121
+ const resolvedInput = resolve2(process.cwd(), target);
3122
+ const isHtml = cliFormat === "html";
3123
+ const targetExtension = isHtml ? ".html" : ".pptx";
2327
3124
  let outputPath = "";
2328
3125
  const outFlagIndex = args.findIndex((a) => a === "--out" || a === "-o");
2329
3126
  if (outFlagIndex !== -1 && args[outFlagIndex + 1]) {
2330
- outputPath = resolve(process.cwd(), args[outFlagIndex + 1]);
3127
+ outputPath = resolve2(process.cwd(), args[outFlagIndex + 1]);
2331
3128
  } else {
2332
3129
  const fileBase = basename(resolvedInput, extname(resolvedInput)).replace(/\.yumia$/, "");
2333
- outputPath = join(dirname(resolvedInput), "dist", `${fileBase}.pptx`);
3130
+ outputPath = join(dirname(resolvedInput), "dist", `${fileBase}${targetExtension}`);
2334
3131
  }
2335
3132
  mkdirSync(dirname(outputPath), { recursive: true });
2336
3133
  const compiler = new YumiaCompiler();
2337
- const renderer = new PptxRenderer();
2338
3134
  const cliColorOverrides = {};
2339
3135
  if (cliBg) cliColorOverrides.background = cliBg;
2340
3136
  if (cliPrimary) cliColorOverrides.primary = cliPrimary;
@@ -2344,11 +3140,34 @@ ${summary}${isStrict && report.warnings.length > 0 ? " (failed due to --strict)"
2344
3140
  const renderTheme = cliTheme || Object.keys(cliColorOverrides).length > 0 ? resolveTheme(cliTheme || "default", {
2345
3141
  ...Object.keys(cliColorOverrides).length > 0 ? { colors: cliColorOverrides } : {}
2346
3142
  }) : void 0;
2347
- const result = await compiler.compile(source, renderer, {
2348
- ...renderTheme ? { renderContext: { theme: renderTheme } } : {}
2349
- });
2350
- const buffer = result.data instanceof Uint8Array ? Buffer.from(result.data) : Buffer.from(new Uint8Array(result.data));
2351
- writeFileSync(outputPath, buffer);
3143
+ const compileFile = async () => {
3144
+ const source = readFileSync2(resolvedInput, "utf-8");
3145
+ if (isHtml) {
3146
+ const htmlRenderer = new HtmlRenderer();
3147
+ const result2 = await compiler.compile(source, htmlRenderer, {
3148
+ ...renderTheme ? { renderContext: { theme: renderTheme } } : {}
3149
+ });
3150
+ writeFileSync(outputPath, result2.html, "utf-8");
3151
+ return { slideCount: result2.slideCount, format: result2.format };
3152
+ } else {
3153
+ const pptxRenderer = new PptxRenderer();
3154
+ const result2 = await compiler.compile(source, pptxRenderer, {
3155
+ ...renderTheme ? { renderContext: { theme: renderTheme } } : {}
3156
+ });
3157
+ const buffer = result2.data instanceof Uint8Array ? Buffer.from(result2.data) : Buffer.from(new Uint8Array(result2.data));
3158
+ writeFileSync(outputPath, buffer);
3159
+ return { slideCount: result2.slideCount, format: result2.format };
3160
+ }
3161
+ };
3162
+ const result = await compileFile();
3163
+ if (command === "watch" || isWatch) {
3164
+ fsWatch(resolvedInput, async () => {
3165
+ try {
3166
+ await compileFile();
3167
+ } catch {
3168
+ }
3169
+ });
3170
+ }
2352
3171
  if (isJson) {
2353
3172
  return {
2354
3173
  exitCode: 0,
@@ -2358,16 +3177,18 @@ ${summary}${isStrict && report.warnings.length > 0 ? " (failed due to --strict)"
2358
3177
  source: target,
2359
3178
  output: outputPath,
2360
3179
  slideCount: result.slideCount,
2361
- format: result.format
3180
+ format: result.format,
3181
+ watching: command === "watch" || isWatch
2362
3182
  },
2363
3183
  null,
2364
3184
  2
2365
3185
  )
2366
3186
  };
2367
3187
  }
3188
+ const watchMsg = command === "watch" || isWatch ? " [Watching for changes...]" : "";
2368
3189
  return {
2369
3190
  exitCode: 0,
2370
- output: `\u2713 Successfully compiled '${target}' \u2794 '${outputPath}' (${result.slideCount} native editable slides)`
3191
+ output: `\u2713 Successfully compiled '${target}' \u2794 '${outputPath}' (${result.slideCount} ${isHtml ? "interactive HTML slides" : "native editable slides"})${watchMsg}`
2371
3192
  };
2372
3193
  } catch (err) {
2373
3194
  const msg = err instanceof Error ? err.message : String(err);
@@ -2421,6 +3242,8 @@ export {
2421
3242
  cleanFontFace,
2422
3243
  parseInlineMarkdown,
2423
3244
  PptxRenderer,
3245
+ HtmlRenderer,
3246
+ startDevServer,
2424
3247
  VERSION,
2425
3248
  printHelp,
2426
3249
  runCli
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Server } from 'node:http';
1
2
  export * from '@yumiamd/core';
2
3
  export * from '@yumiamd/ast';
3
4
  export * from '@yumiamd/parser';
@@ -5,12 +6,25 @@ export * from '@yumiamd/theme';
5
6
  export * from '@yumiamd/layout';
6
7
  export * from '@yumiamd/renderer';
7
8
  export * from '@yumiamd/renderer-pptx';
9
+ export * from '@yumiamd/renderer-html';
8
10
 
9
- declare const VERSION = "0.1.9";
11
+ declare const VERSION = "0.1.10";
10
12
  declare function printHelp(): string;
11
13
  declare function runCli(argv: string[]): Promise<{
12
14
  exitCode: number;
13
15
  output: string;
14
16
  }>;
15
17
 
16
- export { VERSION, printHelp, runCli };
18
+ interface DevServerOptions {
19
+ port?: number;
20
+ open?: boolean;
21
+ }
22
+ interface DevServerInstance {
23
+ server: Server;
24
+ port: number;
25
+ url: string;
26
+ close: () => Promise<void>;
27
+ }
28
+ declare function startDevServer(filePath: string, options?: DevServerOptions): Promise<DevServerInstance>;
29
+
30
+ export { type DevServerInstance, type DevServerOptions, VERSION, printHelp, runCli, startDevServer };
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  DEFAULT_VIEWPORT,
3
3
  DefaultLayoutEngine,
4
4
  DefaultYumiaParser,
5
+ HtmlRenderer,
5
6
  PptxRenderer,
6
7
  THEME_REGISTRY,
7
8
  VERSION,
@@ -35,12 +36,14 @@ import {
35
36
  printHelp,
36
37
  resolveTheme,
37
38
  runCli,
39
+ startDevServer,
38
40
  terminalTheme
39
- } from "./chunk-F3CX7UG6.js";
41
+ } from "./chunk-PQYPVPV2.js";
40
42
  export {
41
43
  DEFAULT_VIEWPORT,
42
44
  DefaultLayoutEngine,
43
45
  DefaultYumiaParser,
46
+ HtmlRenderer,
44
47
  PptxRenderer,
45
48
  THEME_REGISTRY,
46
49
  VERSION,
@@ -74,5 +77,6 @@ export {
74
77
  printHelp,
75
78
  resolveTheme,
76
79
  runCli,
80
+ startDevServer,
77
81
  terminalTheme
78
82
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yumiamd",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Command line interface and compiler for YumiaMD presentations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,13 +20,14 @@
20
20
  },
21
21
  "devDependencies": {
22
22
  "tsup": "^8.5.1",
23
- "@yumiamd/core": "0.1.9",
24
- "@yumiamd/ast": "0.1.9",
25
- "@yumiamd/parser": "0.1.9",
26
- "@yumiamd/renderer": "0.1.9",
27
- "@yumiamd/layout": "0.1.9",
28
- "@yumiamd/renderer-pptx": "0.1.9",
29
- "@yumiamd/theme": "0.1.9"
23
+ "@yumiamd/ast": "0.1.10",
24
+ "@yumiamd/layout": "0.1.10",
25
+ "@yumiamd/core": "0.1.10",
26
+ "@yumiamd/parser": "0.1.10",
27
+ "@yumiamd/renderer": "0.1.10",
28
+ "@yumiamd/renderer-pptx": "0.1.10",
29
+ "@yumiamd/renderer-html": "0.1.10",
30
+ "@yumiamd/theme": "0.1.10"
30
31
  },
31
32
  "keywords": [
32
33
  "yumia",