svg-terminal 1.0.0 → 1.1.1
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 +7 -6
- package/dist/{chunk-IVINEQLU.js → chunk-24NH6UUG.js} +183 -31
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +15 -5
- package/dist/index.js +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Generate animated SVG terminals from a declarative YAML config. The output is a
|
|
|
18
18
|
```bash
|
|
19
19
|
npx svg-terminal init # writes terminal.yml
|
|
20
20
|
npx svg-terminal generate # writes terminal.svg
|
|
21
|
-
npx svg-terminal blocks # lists all
|
|
21
|
+
npx svg-terminal blocks # lists all 48 blocks
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
Or as a GitHub Action — refresh your profile README on a schedule:
|
|
@@ -31,14 +31,14 @@ Or as a GitHub Action — refresh your profile README on a schedule:
|
|
|
31
31
|
commit: true
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
See the full [GitHub Action](#github-action) section below, the [block catalog](./examples/blocks/) (
|
|
34
|
+
See the full [GitHub Action](#github-action) section below, the [block catalog](./examples/blocks/) (48 blocks, one preview each), and the [12-theme gallery](#themes).
|
|
35
35
|
|
|
36
36
|
### What's in the box
|
|
37
37
|
|
|
38
38
|
- **Declarative YAML config** — write blocks, pick a theme, run the CLI
|
|
39
|
-
- **
|
|
39
|
+
- **48 built-in blocks** — across identity, retro / fake-system, status, ASCII art, single- and multi-line animation, and humor categories. Browse the [block catalog](./examples/blocks/) for previews of each
|
|
40
40
|
- **12 built-in themes** — dracula, nord, monokai, amber, green-phosphor, cyberpunk, solarized-dark, win95, catppuccin, tokyo-night, gruvbox, high-contrast (with chrome to match)
|
|
41
|
-
- **
|
|
41
|
+
- **Frame animation** — `BlockResult.animation = { frames, fps, loop }` powers the 10 animated blocks (spinners, clock, dice, progress bar, etc.). Frames may be single- **or multi-line** as of #69 (`jumping-jack` is the reference multi-line block)
|
|
42
42
|
- **Dynamic-block cache** — the 5 cacheable blocks (weather, github-stats, github-languages, quote, fun-fact) write to `.svg-terminal-cache.json`. Pair with `--frozen-cache` for offline CI builds
|
|
43
43
|
- **Reduced-motion respected** — `@media (prefers-reduced-motion)` clamps the CSS fade-ins AND (since v0.17) the frame cycle. SMIL-driven typing reveal, cursor walk, and scroll-on-overflow remain animated; pair with `--static` for full stillness
|
|
44
44
|
- **Schema-validated, XSS-safe** — strict zod schema on every config field; user-controllable values are escaped at SVG emit sites. See [SECURITY.md](./SECURITY.md)
|
|
@@ -125,7 +125,7 @@ Each is the same 2-block config (motd + neofetch) rendered against the named the
|
|
|
125
125
|
|
|
126
126
|
## Blocks
|
|
127
127
|
|
|
128
|
-
Run `svg-terminal blocks` to list all
|
|
128
|
+
Run `svg-terminal blocks` to list all 48 (cacheable ones marked `*`), or `svg-terminal blocks <name>` to print one block's config schema directly without grepping the source.
|
|
129
129
|
|
|
130
130
|
| Block | Description |
|
|
131
131
|
|-------|-------------|
|
|
@@ -172,6 +172,7 @@ Run `svg-terminal blocks` to list all 47 (cacheable ones marked `*`), or `svg-te
|
|
|
172
172
|
| `progress-bar` | Fake build progress bar that fills 0% → 100% |
|
|
173
173
|
| `bouncing-dot` | Single glyph bouncing left ↔ right |
|
|
174
174
|
| `dice-roll` | N d6 dice that tumble and land on a result |
|
|
175
|
+
| `jumping-jack` | Multi-line stick figure doing jumping jacks (reference multi-line animation) |
|
|
175
176
|
| `palette-swatch` | One-line render of all 16 theme palette colors |
|
|
176
177
|
| `semver-bump` | Current semver + bump preview (major/minor/patch) |
|
|
177
178
|
| `ascii-calendar` | Current-month calendar grid with today highlighted |
|
|
@@ -230,7 +231,7 @@ accessibility:
|
|
|
230
231
|
describe: false # default true — emit <desc> with full content
|
|
231
232
|
```
|
|
232
233
|
|
|
233
|
-
**Reduced-motion caveat.** The SVG emits an inline `@media (prefers-reduced-motion: reduce)` rule,
|
|
234
|
+
**Reduced-motion caveat.** The SVG emits an inline `@media (prefers-reduced-motion: reduce)` rule, which applies to CSS animations — the fade-ins and the frame cycle (single- and multi-line) honor it (migrated SMIL → CSS in v0.17). The remaining SMIL holdouts — typing reveal, cursor walk, and scroll-on-overflow — don't read the same CSS media query, so users who set the OS-level reduced-motion preference still see those animate. If that's a problem for your audience, generate with `--static` — same content, no motion at all.
|
|
234
235
|
|
|
235
236
|
### Caching API responses
|
|
236
237
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { z as
|
|
2
|
+
import { z as z51 } from "zod";
|
|
3
3
|
|
|
4
4
|
// src/core/config.ts
|
|
5
5
|
import { readFileSync } from "fs";
|
|
@@ -1284,22 +1284,39 @@ function buildRevealClip(clipId, startX, charWidth, charCount, fontSize, startTi
|
|
|
1284
1284
|
const finalWidth = roundCoord(charCount * charWidth);
|
|
1285
1285
|
return `<defs><clipPath id="${clipId}"><rect x="${startX}" y="${-fontSize}" width="${finalWidth}" height="${fontSize * 2}">${setHold("width", 0, startTime)}<animate attributeName="width" values="${values.join(";")}" keyTimes="${keyTimes.join(";")}" calcMode="discrete" begin="${startTime}ms" dur="${typingDuration}ms" fill="freeze"/></rect></clipPath></defs>`;
|
|
1286
1286
|
}
|
|
1287
|
-
function generateAnimatedOutputLine(y, frames, color, startTime, colorMap, chrome, fps, loop) {
|
|
1287
|
+
function generateAnimatedOutputLine(y, frames, color, startTime, colorMap, chrome, fps, loop, lineHeight) {
|
|
1288
1288
|
const n = frames.length;
|
|
1289
1289
|
const frameDurMs = 1e3 / fps;
|
|
1290
1290
|
const cycleMs = Math.round(n * frameDurMs);
|
|
1291
1291
|
const iter = loop ? "infinite" : "1";
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1292
|
+
const height = Math.max(1, ...frames.map((f) => f.length));
|
|
1293
|
+
const animFor = (i) => `animation: frame-cycle-${n} ${cycleMs}ms linear ${Math.round(i * frameDurMs)}ms ${iter}`;
|
|
1294
|
+
const renderRow = (row) => {
|
|
1295
|
+
const styled = hasMarkup(row);
|
|
1296
|
+
return {
|
|
1297
|
+
fill: styled ? "" : ` fill="${escapeXml(color)}"`,
|
|
1298
|
+
content: styled ? generateStyledText(parseMarkup(row, colorMap, color), color, chrome.dimOpacity) : escapeXml(row)
|
|
1299
|
+
};
|
|
1300
|
+
};
|
|
1301
|
+
let body;
|
|
1302
|
+
if (height === 1) {
|
|
1303
|
+
body = frames.map((frame, i) => {
|
|
1304
|
+
const { fill, content } = renderRow(frame[0] ?? "");
|
|
1305
|
+
return `<text class="tt frame-cycle-${n}"${fill} opacity="${i === 0 ? "1" : "0"}" style="${animFor(i)}">${content}</text>`;
|
|
1306
|
+
}).join("");
|
|
1307
|
+
} else {
|
|
1308
|
+
body = frames.map((frame, i) => {
|
|
1309
|
+
const rows = [];
|
|
1310
|
+
for (let r = 0; r < height; r++) {
|
|
1311
|
+
const { fill, content } = renderRow(frame[r] ?? "");
|
|
1312
|
+
rows.push(`<text y="${roundCoord(r * lineHeight)}"${fill}>${content}</text>`);
|
|
1313
|
+
}
|
|
1314
|
+
return `<g class="tt frame-cycle-${n}" opacity="${i === 0 ? "1" : "0"}" style="${animFor(i)}">${rows.join("")}</g>`;
|
|
1315
|
+
}).join("");
|
|
1316
|
+
}
|
|
1300
1317
|
return `
|
|
1301
1318
|
<g transform="translate(0, ${y})"${fadeInStyle(startTime)}>
|
|
1302
|
-
${
|
|
1319
|
+
${body}
|
|
1303
1320
|
</g>`;
|
|
1304
1321
|
}
|
|
1305
1322
|
function generateOutputLine(y, content, color, startTime, colorMap, chrome, pinWidth, fontSize) {
|
|
@@ -1351,7 +1368,8 @@ function generateAllLines(frames, terminal, lineHeight, colors, chrome, animatio
|
|
|
1351
1368
|
colorMap,
|
|
1352
1369
|
chromeConfig,
|
|
1353
1370
|
frame.framesFps ?? 4,
|
|
1354
|
-
frame.framesLoop ?? true
|
|
1371
|
+
frame.framesLoop ?? true,
|
|
1372
|
+
lineHeight
|
|
1355
1373
|
)
|
|
1356
1374
|
);
|
|
1357
1375
|
} else {
|
|
@@ -1374,12 +1392,26 @@ function generateAllLines(frames, terminal, lineHeight, colors, chrome, animatio
|
|
|
1374
1392
|
return Array.from(processedLines.entries()).sort((a, b) => a[0] - b[0]).map(([, content]) => content).join("\n");
|
|
1375
1393
|
}
|
|
1376
1394
|
|
|
1395
|
+
// src/core/strict-mode.ts
|
|
1396
|
+
var STRICT = false;
|
|
1397
|
+
function setStrict(enabled) {
|
|
1398
|
+
STRICT = enabled;
|
|
1399
|
+
}
|
|
1400
|
+
function isStrict() {
|
|
1401
|
+
return STRICT;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1377
1404
|
// src/core/svg-generator.ts
|
|
1405
|
+
function animationHeight(frames) {
|
|
1406
|
+
return Math.max(1, ...frames.map((f) => f.length));
|
|
1407
|
+
}
|
|
1378
1408
|
function countTotalLines(sequences) {
|
|
1379
1409
|
let total = 0;
|
|
1380
1410
|
for (const seq of sequences) {
|
|
1381
1411
|
if (seq.type === "command") {
|
|
1382
1412
|
total += 1;
|
|
1413
|
+
} else if (seq.frames && seq.frames.length > 0) {
|
|
1414
|
+
total += animationHeight(seq.frames);
|
|
1383
1415
|
} else {
|
|
1384
1416
|
total += seq.content.split("\n").length;
|
|
1385
1417
|
}
|
|
@@ -1625,12 +1657,19 @@ function createAnimationFrames(sequences, terminal, maxVisibleLines, scrollDurat
|
|
|
1625
1657
|
continue;
|
|
1626
1658
|
}
|
|
1627
1659
|
if (seq.frames && seq.frames.length > 0) {
|
|
1628
|
-
|
|
1660
|
+
const height = animationHeight(seq.frames);
|
|
1661
|
+
if (height > maxVisibleLines) {
|
|
1662
|
+
const msg = `[svg-terminal] An animated block is ${height} rows tall but only ${maxVisibleLines} row(s) fit the terminal \u2014 the overflow is clipped. Use window.autoHeight (default) or a taller window.height / maxHeight. (#124)`;
|
|
1663
|
+
if (isStrict()) throw new Error(msg);
|
|
1664
|
+
console.warn(msg);
|
|
1665
|
+
}
|
|
1666
|
+
for (let r = 0; r < height; r++) buffer.push({ type: "output" });
|
|
1629
1667
|
frames.push({
|
|
1630
1668
|
time: currentTime,
|
|
1631
1669
|
type: "add-output",
|
|
1632
|
-
lineIndex: buffer.length -
|
|
1633
|
-
|
|
1670
|
+
lineIndex: buffer.length - height,
|
|
1671
|
+
// top row of the reserved band
|
|
1672
|
+
content: seq.frames[0].join("\n"),
|
|
1634
1673
|
// frame 0 acts as the static fallback
|
|
1635
1674
|
color: seq.color,
|
|
1636
1675
|
frames: seq.frames,
|
|
@@ -2170,7 +2209,63 @@ import { z as z7 } from "zod";
|
|
|
2170
2209
|
import { z as z6 } from "zod";
|
|
2171
2210
|
|
|
2172
2211
|
// src/core/http.ts
|
|
2212
|
+
import { isIP } from "net";
|
|
2173
2213
|
var DEFAULT_FETCH_TIMEOUT = 1e4;
|
|
2214
|
+
var MAX_REDIRECTS = 5;
|
|
2215
|
+
function ipv4Blocked(ip) {
|
|
2216
|
+
const parts = ip.split(".").map(Number);
|
|
2217
|
+
if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) {
|
|
2218
|
+
return false;
|
|
2219
|
+
}
|
|
2220
|
+
const [a, b] = parts;
|
|
2221
|
+
if (a === 0) return true;
|
|
2222
|
+
if (a === 10) return true;
|
|
2223
|
+
if (a === 127) return true;
|
|
2224
|
+
if (a === 169 && b === 254) return true;
|
|
2225
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
2226
|
+
if (a === 192 && b === 168) return true;
|
|
2227
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
2228
|
+
if (a === 255 && b === 255 && parts[2] === 255 && parts[3] === 255) return true;
|
|
2229
|
+
return false;
|
|
2230
|
+
}
|
|
2231
|
+
function ipv6Blocked(ip) {
|
|
2232
|
+
const v = ip.toLowerCase();
|
|
2233
|
+
if (v === "::1" || v === "::") return true;
|
|
2234
|
+
if (/^fe[89ab]/.test(v)) return true;
|
|
2235
|
+
if (/^f[cd]/.test(v)) return true;
|
|
2236
|
+
const dotted = v.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
2237
|
+
if (dotted) return ipv4Blocked(dotted[1]);
|
|
2238
|
+
const hex = v.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
2239
|
+
if (hex) {
|
|
2240
|
+
const hi = parseInt(hex[1], 16);
|
|
2241
|
+
const lo = parseInt(hex[2], 16);
|
|
2242
|
+
return ipv4Blocked(`${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`);
|
|
2243
|
+
}
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
function isBlockedHost(hostname) {
|
|
2247
|
+
const host = hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
|
|
2248
|
+
if (host === "localhost" || host.endsWith(".localhost")) return true;
|
|
2249
|
+
const kind = isIP(host);
|
|
2250
|
+
if (kind === 4) return ipv4Blocked(host);
|
|
2251
|
+
if (kind === 6) return ipv6Blocked(host);
|
|
2252
|
+
return false;
|
|
2253
|
+
}
|
|
2254
|
+
function fetchBlockReason(url) {
|
|
2255
|
+
let parsed;
|
|
2256
|
+
try {
|
|
2257
|
+
parsed = new URL(url);
|
|
2258
|
+
} catch {
|
|
2259
|
+
return "unparseable URL";
|
|
2260
|
+
}
|
|
2261
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
2262
|
+
return `unsupported scheme "${parsed.protocol}"`;
|
|
2263
|
+
}
|
|
2264
|
+
if (isBlockedHost(parsed.hostname)) {
|
|
2265
|
+
return "private / loopback / link-local address";
|
|
2266
|
+
}
|
|
2267
|
+
return null;
|
|
2268
|
+
}
|
|
2174
2269
|
function safeUrlForLog(url) {
|
|
2175
2270
|
try {
|
|
2176
2271
|
const u = new URL(url);
|
|
@@ -2207,20 +2302,47 @@ async function readCappedText(response, url) {
|
|
|
2207
2302
|
}
|
|
2208
2303
|
return new TextDecoder().decode(Buffer.concat(chunks));
|
|
2209
2304
|
}
|
|
2210
|
-
var USER_AGENT = `svg-terminal/${true ? "1.
|
|
2305
|
+
var USER_AGENT = `svg-terminal/${true ? "1.1.1" : "0.0.0-dev"}`;
|
|
2211
2306
|
async function fetchWithTimeout(url, timeoutMs = DEFAULT_FETCH_TIMEOUT) {
|
|
2307
|
+
const blocked = fetchBlockReason(url);
|
|
2308
|
+
if (blocked) {
|
|
2309
|
+
console.warn(`[svg-terminal] Refused to fetch ${safeUrlForLog(url)}: ${blocked}`);
|
|
2310
|
+
return null;
|
|
2311
|
+
}
|
|
2212
2312
|
const controller = new AbortController();
|
|
2213
2313
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2214
2314
|
try {
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2315
|
+
let currentUrl = url;
|
|
2316
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
2317
|
+
const response = await fetch(currentUrl, {
|
|
2318
|
+
signal: controller.signal,
|
|
2319
|
+
headers: { "User-Agent": USER_AGENT },
|
|
2320
|
+
redirect: "manual"
|
|
2321
|
+
});
|
|
2322
|
+
if (response.status >= 300 && response.status < 400 && response.headers.has("location")) {
|
|
2323
|
+
let next;
|
|
2324
|
+
try {
|
|
2325
|
+
next = new URL(response.headers.get("location"), currentUrl).toString();
|
|
2326
|
+
} catch {
|
|
2327
|
+
console.warn(`[svg-terminal] Bad redirect Location from ${safeUrlForLog(currentUrl)}`);
|
|
2328
|
+
return null;
|
|
2329
|
+
}
|
|
2330
|
+
const blockedHop = fetchBlockReason(next);
|
|
2331
|
+
if (blockedHop) {
|
|
2332
|
+
console.warn(`[svg-terminal] Refused redirect to ${safeUrlForLog(next)}: ${blockedHop}`);
|
|
2333
|
+
return null;
|
|
2334
|
+
}
|
|
2335
|
+
currentUrl = next;
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2338
|
+
if (!response.ok) {
|
|
2339
|
+
console.warn(`[svg-terminal] HTTP ${response.status} from ${safeUrlForLog(currentUrl)}`);
|
|
2340
|
+
return null;
|
|
2341
|
+
}
|
|
2342
|
+
return response;
|
|
2222
2343
|
}
|
|
2223
|
-
|
|
2344
|
+
console.warn(`[svg-terminal] Too many redirects (>${MAX_REDIRECTS}) fetching ${safeUrlForLog(url)}`);
|
|
2345
|
+
return null;
|
|
2224
2346
|
} catch (error) {
|
|
2225
2347
|
const message = error instanceof Error ? error.message : String(error);
|
|
2226
2348
|
if (message.includes("abort")) {
|
|
@@ -4339,6 +4461,34 @@ var tocBlock = {
|
|
|
4339
4461
|
}
|
|
4340
4462
|
};
|
|
4341
4463
|
|
|
4464
|
+
// src/blocks/jumping-jack.ts
|
|
4465
|
+
import { z as z50 } from "zod";
|
|
4466
|
+
var POSE_OPEN = ["\\o/", " | ", "/ \\"];
|
|
4467
|
+
var POSE_SHUT = [" o ", "/|\\", " | "];
|
|
4468
|
+
var jumpingJackSchema = z50.object({
|
|
4469
|
+
fps: z50.number().int().min(1).max(30).optional(),
|
|
4470
|
+
command: z50.string().optional(),
|
|
4471
|
+
color: z50.string().optional()
|
|
4472
|
+
}).strict();
|
|
4473
|
+
var jumpingJackBlock = {
|
|
4474
|
+
name: "jumping-jack",
|
|
4475
|
+
description: "A multi-line stick figure doing jumping jacks",
|
|
4476
|
+
configSchema: jumpingJackSchema,
|
|
4477
|
+
render(_context, config) {
|
|
4478
|
+
const fps = config["fps"] ?? 2;
|
|
4479
|
+
const command = config["command"] ?? "exercise";
|
|
4480
|
+
const color = config["color"] ?? "yellow";
|
|
4481
|
+
const paint = (rows) => rows.map((r) => `[[fg:${color}]]${r}[[/fg]]`);
|
|
4482
|
+
const frames = [paint(POSE_OPEN), paint(POSE_SHUT)];
|
|
4483
|
+
return {
|
|
4484
|
+
command,
|
|
4485
|
+
lines: frames[0],
|
|
4486
|
+
// static fallback (first pose, all 3 rows)
|
|
4487
|
+
animation: { frames, fps }
|
|
4488
|
+
};
|
|
4489
|
+
}
|
|
4490
|
+
};
|
|
4491
|
+
|
|
4342
4492
|
// src/blocks/index.ts
|
|
4343
4493
|
function registerBuiltinBlocks() {
|
|
4344
4494
|
registerBlocks([
|
|
@@ -4388,15 +4538,15 @@ function registerBuiltinBlocks() {
|
|
|
4388
4538
|
paletteSwatchBlock,
|
|
4389
4539
|
semverBumpBlock,
|
|
4390
4540
|
asciiCalendarBlock,
|
|
4391
|
-
tocBlock
|
|
4541
|
+
tocBlock,
|
|
4542
|
+
jumpingJackBlock
|
|
4392
4543
|
]);
|
|
4393
4544
|
}
|
|
4394
4545
|
|
|
4395
4546
|
// src/index.ts
|
|
4396
4547
|
registerBuiltinBlocks();
|
|
4397
|
-
var STRICT_BLOCK_CONFIG = false;
|
|
4398
4548
|
function setStrictBlockConfig(enabled) {
|
|
4399
|
-
|
|
4549
|
+
setStrict(enabled);
|
|
4400
4550
|
}
|
|
4401
4551
|
function validateBlockEntry(block, entry, index) {
|
|
4402
4552
|
const cfg = entry.config ?? {};
|
|
@@ -4404,7 +4554,7 @@ function validateBlockEntry(block, entry, index) {
|
|
|
4404
4554
|
try {
|
|
4405
4555
|
block.configSchema.parse(cfg);
|
|
4406
4556
|
} catch (err) {
|
|
4407
|
-
if (err instanceof
|
|
4557
|
+
if (err instanceof z51.ZodError) {
|
|
4408
4558
|
const issues = err.issues.map((i) => {
|
|
4409
4559
|
const path = i.path.length ? i.path.join(".") : "<root>";
|
|
4410
4560
|
return ` ${path}: ${i.message}`;
|
|
@@ -4435,7 +4585,7 @@ ${issues}`
|
|
|
4435
4585
|
const known = block.allowedKeys.join(", ");
|
|
4436
4586
|
const msg = `Unknown config key(s) [${list}] for block "${block.name}" at blocks[${index}]
|
|
4437
4587
|
Known keys: ${known}`;
|
|
4438
|
-
if (
|
|
4588
|
+
if (isStrict()) {
|
|
4439
4589
|
throw new BlockConfigError(block.name, index, msg);
|
|
4440
4590
|
}
|
|
4441
4591
|
console.warn(`[svg-terminal] warning: ${msg}`);
|
|
@@ -4470,7 +4620,9 @@ async function generate(userConfig, options = {}) {
|
|
|
4470
4620
|
pause: resolvePause(entry.pause ?? result.pause),
|
|
4471
4621
|
pinWidth: result.pinWidth,
|
|
4472
4622
|
...result.animation ? {
|
|
4473
|
-
frames
|
|
4623
|
+
// Carry frames as string[][] (rows preserved) straight through the
|
|
4624
|
+
// timeline — no lossy join/split round-trip (#69, nexus B-PIPELINE).
|
|
4625
|
+
frames: result.animation.frames,
|
|
4474
4626
|
framesFps: Math.min(30, Math.max(1, result.animation.fps ?? 4)),
|
|
4475
4627
|
framesLoop: result.animation.loop ?? true
|
|
4476
4628
|
} : {}
|
|
@@ -4600,4 +4752,4 @@ export {
|
|
|
4600
4752
|
inspectCache,
|
|
4601
4753
|
generateStatic
|
|
4602
4754
|
};
|
|
4603
|
-
//# sourceMappingURL=chunk-
|
|
4755
|
+
//# sourceMappingURL=chunk-24NH6UUG.js.map
|
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
mergeConfig,
|
|
12
12
|
setStrictBlockConfig,
|
|
13
13
|
themes
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-24NH6UUG.js";
|
|
15
15
|
|
|
16
16
|
// src/cli.ts
|
|
17
17
|
import { writeFileSync, watch as fsWatch } from "fs";
|
|
@@ -127,7 +127,7 @@ function isZodOptional(t) {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// src/cli.ts
|
|
130
|
-
var VERSION = true ? "1.
|
|
130
|
+
var VERSION = true ? "1.1.1" : "0.0.0-dev";
|
|
131
131
|
var args = process.argv.slice(2);
|
|
132
132
|
var command = args[0];
|
|
133
133
|
function getFlag(name) {
|
package/dist/index.d.ts
CHANGED
|
@@ -211,8 +211,11 @@ interface Sequence {
|
|
|
211
211
|
pause?: number;
|
|
212
212
|
/** Delay before this sequence starts in ms */
|
|
213
213
|
delay?: number;
|
|
214
|
-
/** Optional multi-frame payload — output sequences only. Each entry is one
|
|
215
|
-
|
|
214
|
+
/** Optional multi-frame payload — output sequences only. Each entry is one
|
|
215
|
+
* frame; each frame is an array of rows. Multi-line frames are supported
|
|
216
|
+
* (#69) — single-row frames (the common case) are `[oneLine]`. Triggers
|
|
217
|
+
* frame-cycle rendering. */
|
|
218
|
+
frames?: string[][];
|
|
216
219
|
/** Frames per second when `frames` is set (default 4, capped at 30). */
|
|
217
220
|
framesFps?: number;
|
|
218
221
|
/** Loop frames forever when set (default true). */
|
|
@@ -233,8 +236,9 @@ interface AnimationFrame {
|
|
|
233
236
|
typingDuration?: number;
|
|
234
237
|
scrollLines?: number;
|
|
235
238
|
bufferStart?: number;
|
|
236
|
-
/** Multi-frame payload — present only on add-output frames spawned from
|
|
237
|
-
|
|
239
|
+
/** Multi-frame payload — present only on add-output frames spawned from
|
|
240
|
+
* animated blocks. Each frame is an array of rows (#69 multi-line). */
|
|
241
|
+
frames?: string[][];
|
|
238
242
|
framesFps?: number;
|
|
239
243
|
framesLoop?: boolean;
|
|
240
244
|
/** Width-pinning opt-in from BlockResult.pinWidth. */
|
|
@@ -712,7 +716,13 @@ interface GenerateOptions {
|
|
|
712
716
|
*/
|
|
713
717
|
onCacheEvent?: (event: CacheEventType, key: string) => void;
|
|
714
718
|
}
|
|
715
|
-
/**
|
|
719
|
+
/**
|
|
720
|
+
* Enable strict mode globally — soft warnings (unknown block-config keys for
|
|
721
|
+
* schemaless blocks; an over-tall animated band, #124) become hard errors.
|
|
722
|
+
* The flag lives in `./core/strict-mode.js` so `svg-generator.ts` can read it
|
|
723
|
+
* without importing this module (which would be circular). Re-exported here as
|
|
724
|
+
* `setStrictBlockConfig` for back-compat with the CLI + library consumers.
|
|
725
|
+
*/
|
|
716
726
|
declare function setStrictBlockConfig(enabled: boolean): void;
|
|
717
727
|
/**
|
|
718
728
|
* Generate an animated SVG terminal from a declarative config.
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svg-terminal",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Generate animated SVG terminals for GitHub READMEs from a declarative YAML config. 47 built-in blocks, 12 themes, zero runtime deps in the output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -59,13 +59,14 @@
|
|
|
59
59
|
"node": ">=22.0.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
+
"@eslint/js": "^10.0.1",
|
|
62
63
|
"@types/js-yaml": "^4.0.9",
|
|
63
64
|
"@types/node": "^22.19.19",
|
|
64
65
|
"@vitest/coverage-v8": "^4.1.7",
|
|
65
|
-
"eslint": "^
|
|
66
|
+
"eslint": "^10.4.1",
|
|
66
67
|
"tsup": "^8.0.0",
|
|
67
68
|
"tsx": "^4.22.3",
|
|
68
|
-
"typescript": "^
|
|
69
|
+
"typescript": "^6.0.3",
|
|
69
70
|
"typescript-eslint": "^8.60.0",
|
|
70
71
|
"vitest": "^4.1.7"
|
|
71
72
|
},
|