ht-skills 0.2.0 → 0.2.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.
Files changed (2) hide show
  1. package/lib/cli.js +109 -35
  2. package/package.json +1 -1
package/lib/cli.js CHANGED
@@ -431,16 +431,89 @@ function normalizeInlineText(value) {
431
431
  return String(value || "").replace(/\s+/g, " ").trim();
432
432
  }
433
433
 
434
+ const ANSI_PATTERN = /\u001B\[[0-?]*[ -/]*[@-~]/g;
435
+
436
+ function stripAnsi(value) {
437
+ return String(value || "").replace(ANSI_PATTERN, "");
438
+ }
439
+
440
+ function isFullwidthCodePoint(codePoint) {
441
+ if (!Number.isFinite(codePoint)) {
442
+ return false;
443
+ }
444
+
445
+ return codePoint >= 0x1100 && (
446
+ codePoint <= 0x115f ||
447
+ codePoint === 0x2329 ||
448
+ codePoint === 0x232a ||
449
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
450
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
451
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
452
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
453
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
454
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
455
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
456
+ (codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
457
+ (codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
458
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd)
459
+ );
460
+ }
461
+
462
+ function getCharacterWidth(character) {
463
+ const codePoint = character.codePointAt(0);
464
+ if (!Number.isFinite(codePoint)) {
465
+ return 0;
466
+ }
467
+ if (
468
+ codePoint === 0 ||
469
+ (codePoint >= 0x0000 && codePoint <= 0x001f) ||
470
+ (codePoint >= 0x007f && codePoint <= 0x009f)
471
+ ) {
472
+ return 0;
473
+ }
474
+ return isFullwidthCodePoint(codePoint) ? 2 : 1;
475
+ }
476
+
477
+ function getDisplayWidth(value) {
478
+ let width = 0;
479
+ for (const character of stripAnsi(value)) {
480
+ width += getCharacterWidth(character);
481
+ }
482
+ return width;
483
+ }
484
+
485
+ function sliceTextByDisplayWidth(value, maxWidth) {
486
+ const safeWidth = Math.max(1, Number(maxWidth || 0));
487
+ const characters = [...String(value || "")];
488
+ let width = 0;
489
+ let index = 0;
490
+
491
+ while (index < characters.length) {
492
+ const characterWidth = getCharacterWidth(characters[index]);
493
+ if (width + characterWidth > safeWidth) {
494
+ break;
495
+ }
496
+ width += characterWidth;
497
+ index += 1;
498
+ }
499
+
500
+ return {
501
+ slice: characters.slice(0, index).join(""),
502
+ rest: characters.slice(index).join(""),
503
+ width,
504
+ };
505
+ }
506
+
434
507
  function truncateText(value, maxLength) {
435
508
  const text = normalizeInlineText(value);
436
509
  const safeMaxLength = Math.max(1, Number(maxLength || 0));
437
- if (text.length <= safeMaxLength) {
510
+ if (getDisplayWidth(text) <= safeMaxLength) {
438
511
  return text;
439
512
  }
440
513
  if (safeMaxLength <= 1) {
441
514
  return "…";
442
515
  }
443
- return `${text.slice(0, safeMaxLength - 1).trimEnd()}…`;
516
+ return `${sliceTextByDisplayWidth(text, safeMaxLength - 1).slice.trimEnd()}…`;
444
517
  }
445
518
 
446
519
  function wrapText(value, maxWidth) {
@@ -448,37 +521,37 @@ function wrapText(value, maxWidth) {
448
521
  const safeWidth = Math.max(1, Number(maxWidth || 0));
449
522
  if (!text) return [""];
450
523
 
451
- const words = text.split(" ");
452
524
  const lines = [];
453
- let current = "";
525
+ let remaining = text;
454
526
 
455
- for (const word of words) {
456
- if (!current) {
457
- if (word.length <= safeWidth) {
458
- current = word;
459
- continue;
460
- }
461
- lines.push(truncateText(word, safeWidth));
462
- continue;
527
+ while (remaining) {
528
+ if (getDisplayWidth(remaining) <= safeWidth) {
529
+ lines.push(remaining);
530
+ break;
463
531
  }
464
532
 
465
- const candidate = `${current} ${word}`;
466
- if (candidate.length <= safeWidth) {
467
- current = candidate;
468
- continue;
469
- }
533
+ const characters = [...remaining];
534
+ let width = 0;
535
+ let index = 0;
536
+ let lastBreakIndex = -1;
470
537
 
471
- lines.push(current);
472
- if (word.length <= safeWidth) {
473
- current = word;
474
- } else {
475
- lines.push(truncateText(word, safeWidth));
476
- current = "";
538
+ while (index < characters.length) {
539
+ const character = characters[index];
540
+ const characterWidth = getCharacterWidth(character);
541
+ if (width + characterWidth > safeWidth) {
542
+ break;
543
+ }
544
+ width += characterWidth;
545
+ index += 1;
546
+ if (character === " ") {
547
+ lastBreakIndex = index;
548
+ }
477
549
  }
478
- }
479
550
 
480
- if (current) {
481
- lines.push(current);
551
+ const breakIndex = lastBreakIndex > 0 ? lastBreakIndex : Math.max(1, index);
552
+ const line = characters.slice(0, breakIndex).join("").trimEnd();
553
+ lines.push(line);
554
+ remaining = characters.slice(breakIndex).join("").trimStart();
482
555
  }
483
556
 
484
557
  return lines.length ? lines : [""];
@@ -563,7 +636,7 @@ function formatSearchResult(item, { index = 0, colorize = (text) => text, width
563
636
  }
564
637
 
565
638
  const separator = " ";
566
- const availableDescriptionWidth = width - plainPrefix.length - separator.length;
639
+ const availableDescriptionWidth = width - getDisplayWidth(plainPrefix) - getDisplayWidth(separator);
567
640
  if (availableDescriptionWidth < minDescriptionWidth) {
568
641
  return truncateText(`${plainPrefix}${separator}${descriptionPart}`, width);
569
642
  }
@@ -578,25 +651,26 @@ function formatSearchCard(item, { index = 0, colorize = (text) => text, width =
578
651
  const slugPart = `${item.slug}@${item.latestVersion}`;
579
652
  const namePart = normalizeInlineText(item.name || "");
580
653
  const descriptionPart = normalizeInlineText(item.description || "");
581
- const maxInnerWidth = Math.max(42, width - 6);
654
+ const maxInnerWidth = Math.max(16, width - 6);
582
655
  const titleText = `${slugPart}${namePart ? ` (${namePart})` : ""}`;
583
656
  const detailLines = wrapText(descriptionPart || "-", maxInnerWidth);
584
657
  const contentWidth = Math.max(
585
- Math.min(maxInnerWidth, titleText.length),
586
- ...detailLines.map((line) => Math.min(maxInnerWidth, line.length)),
587
- 42,
658
+ 16,
659
+ getDisplayWidth(ordinal) + 1,
660
+ Math.min(maxInnerWidth, getDisplayWidth(titleText)),
661
+ ...detailLines.map((line) => Math.min(maxInnerWidth, getDisplayWidth(line))),
588
662
  );
589
- const titleSuffix = "─".repeat(Math.max(0, contentWidth - ordinal.length - 1));
663
+ const titleSuffix = "─".repeat(Math.max(0, contentWidth - getDisplayWidth(ordinal) - 1));
590
664
  const title = `${colorize("◇", "success")} ${colorize(ordinal, "accent")} ${colorize(titleSuffix, "muted")}╮`;
591
665
  const blank = `│ ${" ".repeat(contentWidth)} │`;
592
666
  const firstLineText = truncateText(titleText, contentWidth);
593
- const firstLine = `│ ${colorize(firstLineText, "accent")}${" ".repeat(Math.max(0, contentWidth - firstLineText.length))} │`;
667
+ const firstLine = `│ ${colorize(firstLineText, "accent")}${" ".repeat(Math.max(0, contentWidth - getDisplayWidth(firstLineText)))} │`;
594
668
  const detailRendered = detailLines.map((line) => {
595
669
  const text = truncateText(line, contentWidth);
596
- return `│ ${text}${" ".repeat(Math.max(0, contentWidth - text.length))} │`;
670
+ return `│ ${text}${" ".repeat(Math.max(0, contentWidth - getDisplayWidth(text)))} │`;
597
671
  });
598
672
  const bottom = `╰${"─".repeat(contentWidth + 2)}╯`;
599
- return [title, blank, firstLine, ...detailRendered, bottom].join("\n");
673
+ return [title, blank, firstLine, ...detailRendered, blank, bottom].join("\n");
600
674
  }
601
675
 
602
676
  function printFallbackIntro({ registry, slug, version, skillName, skillDescription, installTargets }, log = console.log) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ht-skills",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "CLI for installing and submitting skills from HT Skills Marketplace.",
5
5
  "type": "commonjs",
6
6
  "bin": {