@typescript-calendar-lib/cli 0.2.2 → 0.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typescript-calendar-lib/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "typescript-calendar-lib": "src/bin.ts"
@@ -16,7 +16,7 @@
16
16
  "!src/*.test.ts"
17
17
  ],
18
18
  "dependencies": {
19
- "@typescript-calendar-lib/core": "0.2.2",
19
+ "@typescript-calendar-lib/core": "0.2.5",
20
20
  "cli-table3": "^0.6.5"
21
21
  },
22
22
  "peerDependencies": {
package/src/ansi.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  // ─── ANSI 色付け ─────────────────────────────────────────
2
2
 
3
+ import { displayWidth } from "./align.ts";
4
+
3
5
  const ANSI_PATTERN = new RegExp(`${"\u001b"}\\[[0-9;]*m`, "g");
4
6
 
5
7
  /** ANSI コードを付与する(code が undefined ならそのまま) */
@@ -12,7 +14,12 @@ export function colorize(
12
14
  return `\u001b[${code}m${text}\u001b[0m`;
13
15
  }
14
16
 
15
- /** ANSI エスケープシーケンスを除去した表示幅を返す */
17
+ /** ANSI エスケープシーケンスを除去した文字列を返す */
18
+ export function stripAnsi(text: string): string {
19
+ return text.replace(ANSI_PATTERN, "");
20
+ }
21
+
22
+ /** ANSI エスケープシーケンスを除去した表示幅を返す(全角文字は2列) */
16
23
  export function visibleWidth(text: string): number {
17
- return text.replace(ANSI_PATTERN, "").length;
24
+ return displayWidth(stripAnsi(text));
18
25
  }
package/src/bin.ts CHANGED
@@ -4,21 +4,10 @@ import type {
4
4
  Locale,
5
5
  WeekStart,
6
6
  } from "@typescript-calendar-lib/core";
7
+ import { createDate } from "@typescript-calendar-lib/core";
7
8
  import { calendar } from "./calendar.ts";
8
9
  import type { ColorSchemeName, ThemeName } from "./theme.ts";
9
10
 
10
- const THEMES: readonly ThemeName[] = ["default", "modern"];
11
- const COLOR_SCHEMES: readonly ColorSchemeName[] = [
12
- "default",
13
- "ocean",
14
- "forest",
15
- "sunset",
16
- "mono",
17
- ];
18
- const LOCALES: readonly Locale[] = ["en", "ja", "es", "de", "fr", "ko", "zh"];
19
- const WEEK_STARTS: readonly WeekStart[] = ["sunday", "monday"];
20
- const HIGHLIGHT_STYLES: readonly HighlightStyle[] = ["bracket", "reverse"];
21
-
22
11
  export interface CliArgs {
23
12
  year?: number;
24
13
  month?: number;
@@ -37,6 +26,27 @@ export interface ParseResult {
37
26
  help?: boolean;
38
27
  }
39
28
 
29
+ const THEMES: readonly ThemeName[] = ["default", "modern"];
30
+ const COLOR_SCHEMES: readonly ColorSchemeName[] = [
31
+ "default",
32
+ "ocean",
33
+ "forest",
34
+ "sunset",
35
+ "mono",
36
+ ];
37
+ const LOCALES: readonly Locale[] = ["en", "ja", "es", "de", "fr", "ko", "zh"];
38
+ const WEEK_STARTS: readonly WeekStart[] = ["sunday", "monday"];
39
+ const HIGHLIGHT_STYLES: readonly HighlightStyle[] = ["bracket", "reverse"];
40
+
41
+ /** 値を一つ取るオプションと、代入先のフィールド名 */
42
+ const VALUE_OPTIONS: Record<string, keyof CliArgs> = {
43
+ "--theme": "theme",
44
+ "--color-scheme": "colorScheme",
45
+ "--locale": "locale",
46
+ "--week-start": "weekStart",
47
+ "--highlight-style": "highlightStyle",
48
+ };
49
+
40
50
  /** YYYY-MM-DD 形式の日付文字列をパースする */
41
51
  export function parseDate(value: string): Date | null {
42
52
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
@@ -44,29 +54,18 @@ export function parseDate(value: string): Date | null {
44
54
  const year = Number(match[1]);
45
55
  const month = Number(match[2]);
46
56
  const day = Number(match[3]);
47
- if (month < 1 || month > 12) return null;
48
- if (day < 1 || day > 31) return null;
49
- const date = new Date(year, month - 1, day);
57
+ if (year < 1 || month < 1 || month > 12 || day < 1 || day > 31) {
58
+ return null;
59
+ }
60
+ const date = createDate(year, month - 1, day);
50
61
  if (
51
62
  date.getFullYear() !== year ||
52
63
  date.getMonth() !== month - 1 ||
53
64
  date.getDate() !== day
54
- )
65
+ ) {
55
66
  return null;
56
- return date;
57
- }
58
-
59
- /** 指定した候補リストに値が含まれるか検証する */
60
- function validateChoice<T extends string>(
61
- value: T | undefined,
62
- choices: readonly T[],
63
- name: string,
64
- ): string | undefined {
65
- if (value === undefined) return undefined;
66
- if (!choices.includes(value)) {
67
- return `Invalid ${name}: "${value}" (expected: ${choices.join(" | ")})`;
68
67
  }
69
- return undefined;
68
+ return date;
70
69
  }
71
70
 
72
71
  /**
@@ -74,109 +73,85 @@ function validateChoice<T extends string>(
74
73
  */
75
74
  export function parseArgs(args: readonly string[]): ParseResult {
76
75
  const result: CliArgs = {};
77
- const missing = (name: string): ParseResult => ({
76
+ const error = (message: string): ParseResult => ({
78
77
  args: result,
79
- error: `Missing value for option: ${name}`,
78
+ error: message,
80
79
  });
81
80
 
82
81
  for (let i = 0; i < args.length; i++) {
83
82
  const arg = args[i]!;
84
- const next = (): string | undefined => args[++i];
85
-
86
- switch (arg) {
87
- case "-h":
88
- case "--help":
89
- return { args: result, help: true };
90
- case "--theme": {
91
- const value = next();
92
- if (value === undefined) return missing("--theme");
93
- result.theme = value as ThemeName;
94
- break;
95
- }
96
- case "--color-scheme": {
97
- const value = next();
98
- if (value === undefined) return missing("--color-scheme");
99
- result.colorScheme = value as ColorSchemeName;
100
- break;
101
- }
102
- case "--color":
103
- result.color = true;
104
- break;
105
- case "--locale": {
106
- const value = next();
107
- if (value === undefined) return missing("--locale");
108
- result.locale = value as Locale;
109
- break;
110
- }
111
- case "--week-start": {
112
- const value = next();
113
- if (value === undefined) return missing("--week-start");
114
- result.weekStart = value as WeekStart;
115
- break;
116
- }
117
- case "--highlight": {
118
- const value = next();
119
- if (value === undefined) return missing("--highlight");
120
- const parsed = parseDate(value);
121
- if (parsed === null) {
122
- return {
123
- args: result,
124
- error: `Invalid --highlight date: "${value}" (expected YYYY-MM-DD, e.g. 2026-09-08)`,
125
- };
126
- }
127
- result.highlight = parsed;
128
- break;
83
+
84
+ if (arg === "-h" || arg === "--help") {
85
+ return { args: result, help: true };
86
+ }
87
+ if (arg === "--color") {
88
+ result.color = true;
89
+ continue;
90
+ }
91
+ if (arg === "--highlight") {
92
+ const value = args[++i];
93
+ if (value === undefined) {
94
+ return error("Missing value for option: --highlight");
129
95
  }
130
- case "--highlight-style": {
131
- const value = next();
132
- if (value === undefined) return missing("--highlight-style");
133
- result.highlightStyle = value as HighlightStyle;
134
- break;
96
+ const parsed = parseDate(value);
97
+ if (parsed === null) {
98
+ return error(
99
+ `Invalid --highlight date: "${value}" (expected YYYY-MM-DD, e.g. 2026-09-08)`,
100
+ );
135
101
  }
136
- default: {
137
- if (arg.startsWith("-")) {
138
- return { args: result, error: `Unknown option: ${arg}` };
139
- }
140
- if (!/^\d+$/.test(arg)) {
141
- return {
142
- args: result,
143
- error: `Invalid argument: "${arg}" (expected a year or month number)`,
144
- };
145
- }
146
- if (result.year === undefined) {
147
- result.year = Number(arg);
148
- } else if (result.month === undefined) {
149
- result.month = Number(arg);
150
- } else {
151
- return { args: result, error: `Too many arguments: "${arg}"` };
152
- }
102
+ result.highlight = parsed;
103
+ continue;
104
+ }
105
+
106
+ const key = VALUE_OPTIONS[arg];
107
+ if (key !== undefined) {
108
+ const value = args[++i];
109
+ if (value === undefined) {
110
+ return error(`Missing value for option: ${arg}`);
153
111
  }
112
+ (result as Record<string, unknown>)[key] = value;
113
+ continue;
114
+ }
115
+
116
+ const numeric = /^-?\d+$/.test(arg);
117
+ if (arg.startsWith("-") && !numeric) {
118
+ return error(`Unknown option: ${arg}`);
119
+ }
120
+ if (!numeric) {
121
+ return error(
122
+ `Invalid argument: "${arg}" (expected a year or month number)`,
123
+ );
124
+ }
125
+ if (result.year === undefined) {
126
+ result.year = Number(arg);
127
+ } else if (result.month === undefined) {
128
+ result.month = Number(arg);
129
+ } else {
130
+ return error(`Too many arguments: "${arg}"`);
154
131
  }
155
132
  }
156
133
 
157
134
  // 値のバリデーション
158
135
  if (result.year !== undefined && (result.year < 1 || result.year > 9999)) {
159
- return {
160
- args: result,
161
- error: `Invalid year: ${result.year} (expected 1–9999)`,
162
- };
136
+ return error(`Invalid year: ${result.year} (expected 1–9999)`);
163
137
  }
164
138
  if (result.month !== undefined && (result.month < 1 || result.month > 12)) {
165
- return {
166
- args: result,
167
- error: `Invalid month: ${result.month} (expected 1–12)`,
168
- };
139
+ return error(`Invalid month: ${result.month} (expected 1–12)`);
169
140
  }
170
141
 
171
- for (const [value, choices, name] of [
172
- [result.theme, THEMES, "theme"] as const,
173
- [result.colorScheme, COLOR_SCHEMES, "color-scheme"] as const,
174
- [result.locale, LOCALES, "locale"] as const,
175
- [result.weekStart, WEEK_STARTS, "week-start"] as const,
176
- [result.highlightStyle, HIGHLIGHT_STYLES, "highlight-style"] as const,
177
- ]) {
178
- const error = validateChoice(value, choices, name);
179
- if (error) return { args: result, error };
142
+ const choiceTable: Array<[string | undefined, readonly string[], string]> = [
143
+ [result.theme, THEMES, "theme"],
144
+ [result.colorScheme, COLOR_SCHEMES, "color-scheme"],
145
+ [result.locale, LOCALES, "locale"],
146
+ [result.weekStart, WEEK_STARTS, "week-start"],
147
+ [result.highlightStyle, HIGHLIGHT_STYLES, "highlight-style"],
148
+ ];
149
+ for (const [value, choices, name] of choiceTable) {
150
+ if (value !== undefined && !choices.includes(value)) {
151
+ return error(
152
+ `Invalid ${name}: "${value}" (expected: ${choices.join(" | ")})`,
153
+ );
154
+ }
180
155
  }
181
156
 
182
157
  return { args: result };
package/src/border.ts CHANGED
@@ -7,6 +7,19 @@ export function innerWidth(cellWidth: number, cols: number): number {
7
7
  return cols * cellWidth + (cols - 1);
8
8
  }
9
9
 
10
+ /** セル幅の水平線を cols 個つなげた区切り線を生成する */
11
+ function divider(
12
+ frame: FrameChars,
13
+ cellWidth: number,
14
+ cols: number,
15
+ left: string,
16
+ right: string,
17
+ join: string,
18
+ ): string {
19
+ const segments = Array<string>(cols).fill(frame.h.repeat(cellWidth));
20
+ return `${left}${segments.join(join)}${right}`;
21
+ }
22
+
10
23
  /** 上枠: ┌────┬────...────┐ */
11
24
  export function topBorder(
12
25
  frame: FrameChars,
@@ -22,8 +35,14 @@ export function bottomBorder(
22
35
  cellWidth: number,
23
36
  cols: number,
24
37
  ): string {
25
- const segments = Array<string>(cols).fill(frame.h.repeat(cellWidth));
26
- return `${frame.bottomLeft}${segments.join(frame.footJ)}${frame.bottomRight}`;
38
+ return divider(
39
+ frame,
40
+ cellWidth,
41
+ cols,
42
+ frame.bottomLeft,
43
+ frame.bottomRight,
44
+ frame.footJ,
45
+ );
27
46
  }
28
47
 
29
48
  /** 区切り行: ├────┬────...┬────┤ */
@@ -32,6 +51,5 @@ export function separatorRow(
32
51
  cellWidth: number,
33
52
  cols: number,
34
53
  ): string {
35
- const segments = Array<string>(cols).fill(frame.h.repeat(cellWidth));
36
- return `├${segments.join(frame.j)}┤`;
54
+ return divider(frame, cellWidth, cols, "├", "┤", frame.j);
37
55
  }
package/src/calendar.ts CHANGED
@@ -4,67 +4,28 @@ import type {
4
4
  CalendarOptions,
5
5
  CalendarRangeOptions,
6
6
  CalendarYearOptions,
7
- RenderMonthOptions,
8
7
  } from "./types.ts";
9
8
 
10
- // ─── Options 解決 ─────────────────────────────────────────
11
-
12
- /**
13
- * 各 API に共通のオプションをデフォルト値込みの描画オプションに正規化する。
14
- * core 由来のオプションと CLI 固有のオプション(theme 等)をまとめる。
15
- */
16
- function toRenderOptions(
17
- options: CalendarOptions | CalendarYearOptions | CalendarRangeOptions,
18
- ): RenderMonthOptions {
19
- const {
20
- locale = "en",
21
- weekStart = "sunday",
22
- highlight,
23
- highlightStyle = "bracket",
24
- range,
25
- color = false,
26
- theme,
27
- colorScheme,
28
- today,
29
- } = options;
30
-
31
- return {
32
- locale,
33
- weekStart,
34
- highlight,
35
- highlightStyle,
36
- range,
37
- color,
38
- theme,
39
- colorScheme,
40
- today,
41
- };
42
- }
43
-
44
- // ─── 公開API ─────────────────────────────────────────────
45
-
46
9
  /**
47
10
  * 月カレンダーをテキストで返す
48
11
  */
49
12
  export function calendar(options: CalendarOptions): string {
50
- return renderMonth(options.year, options.month, toRenderOptions(options));
13
+ return renderMonth(options.year, options.month, options);
51
14
  }
52
15
 
53
16
  /**
54
17
  * 年間カレンダーを4列×3行でテキストで返す
55
18
  */
56
19
  export function calendarYear(options: CalendarYearOptions): string {
57
- return renderYear(options.year, toRenderOptions(options));
20
+ return renderYear(options.year, options);
58
21
  }
59
22
 
60
23
  /**
61
24
  * 任意の日付範囲のカレンダーをテキストで返す
62
25
  */
63
26
  export function calendarRange(options: CalendarRangeOptions): string {
64
- const renderOptions = toRenderOptions(options);
65
27
  const months = getMonthRange(options.from, options.to);
66
-
67
28
  return months
68
- .map(({ year, month }) => renderMonth(year, month, renderOptions))
29
+ .map(({ year, month }) => renderMonth(year, month, options))
69
30
  .join("\n\n");
70
31
  }
package/src/month.ts CHANGED
@@ -1,19 +1,22 @@
1
1
  import {
2
2
  buildMonthGrid,
3
+ createDate,
3
4
  getMonthName,
4
5
  getWeekdayHeaders,
5
6
  isDateInRange,
6
7
  isSameDay,
7
8
  } from "@typescript-calendar-lib/core";
8
- import { centerText, centerTextFull, padStartWidth } from "./align.ts";
9
+ import {
10
+ centerText,
11
+ centerTextFull,
12
+ displayWidth,
13
+ padStartWidth,
14
+ } from "./align.ts";
9
15
  import { colorize } from "./ansi.ts";
10
16
  import { bottomBorder, innerWidth, separatorRow, topBorder } from "./border.ts";
11
- import type { CliPalette } from "./theme.ts";
12
17
  import { resolveColorScheme, resolveTheme } from "./theme.ts";
13
18
  import type { RenderMonthOptions } from "./types.ts";
14
19
 
15
- const CELL_WIDTH = 3;
16
-
17
20
  /**
18
21
  * 1ヶ月分のカレンダーテキストを描画する
19
22
  */
@@ -36,28 +39,61 @@ export function renderMonth(
36
39
 
37
40
  const theme = resolveTheme(themeOption);
38
41
  const palette = resolveColorScheme(schemeOption);
42
+ const frame = theme.frame;
39
43
 
40
44
  const title = `${getMonthName(locale, month)} ${year}`;
41
45
  const weekdays = getWeekdayHeaders(locale, weekStart);
42
46
  const grid = buildMonthGrid(year, month, weekStart);
47
+ const cols = weekdays.length;
48
+
49
+ // セル幅はテーマ指定を基本としつつ、以下を満たすように広げる:
50
+ // - 曜日ヘッダーの表示幅(fr の "dim." 等がセル幅を超えると列が崩れる)
51
+ // - bracket ハイライトは2桁の日付で `[10]` の4文字になるため
52
+ const cellWidth = Math.max(
53
+ theme.cellWidth,
54
+ ...weekdays.map(displayWidth),
55
+ highlight !== undefined && highlightStyle === "bracket" ? 4 : 2,
56
+ );
57
+
58
+ // 枠なしテーマは separator、枠ありテーマは縦線でセルを繋ぐ
59
+ const sep = frame === null ? theme.separator : frame.v;
60
+
61
+ /** 1セルを描画する(day は日数または null=空欄) */
62
+ const renderCell = (day: number | null): string => {
63
+ if (day === null) return " ".repeat(cellWidth);
64
+
65
+ const date = createDate(year, month - 1, day);
66
+ const isHighlight = highlight !== undefined && isSameDay(date, highlight);
67
+ const isInRange = isDateInRange(date, range);
68
+ const isToday = isSameDay(date, today);
69
+ const isWeekend = date.getDay() === 0 || date.getDay() === 6;
70
+
71
+ const text = (
72
+ isHighlight && highlightStyle === "bracket" ? `[${day}]` : String(day)
73
+ ).padStart(cellWidth);
74
+
75
+ let code: number | undefined;
76
+ if (isHighlight && highlightStyle === "reverse") {
77
+ code = palette.highlight ?? 7;
78
+ } else if (isInRange && !isHighlight) {
79
+ code = palette.range ?? 33;
80
+ } else if (isToday && palette.today !== undefined) {
81
+ code = palette.today;
82
+ } else if (isWeekend && palette.weekend !== undefined) {
83
+ code = palette.weekend;
84
+ } else if (palette.day !== undefined) {
85
+ code = palette.day;
86
+ }
43
87
 
44
- // bracket ハイライトは2桁の日付で `[10]` の4文字になるため、
45
- // ハイライト中は全セルを4列に揃えて列ずれを防ぐ
46
- const cellWidth =
47
- highlight !== undefined && highlightStyle === "bracket"
48
- ? CELL_WIDTH + 1
49
- : CELL_WIDTH;
88
+ return colorize(text, code, color);
89
+ };
50
90
 
51
91
  const lines: string[] = [];
52
92
 
53
- if (theme.frame === null) {
93
+ if (frame === null) {
54
94
  // ── 枠なし(default) ──
55
- const sep = theme.separator;
56
- const totalWidth =
57
- weekdays.length * cellWidth + (weekdays.length - 1) * sep.length;
58
-
95
+ const totalWidth = cols * cellWidth + (cols - 1) * sep.length;
59
96
  lines.push(centerText(title, totalWidth));
60
-
61
97
  lines.push(
62
98
  weekdays
63
99
  .map((d) =>
@@ -65,51 +101,20 @@ export function renderMonth(
65
101
  )
66
102
  .join(sep),
67
103
  );
68
-
69
- for (const row of grid) {
70
- if (row.every((d) => d === null)) continue;
71
-
72
- const cells = row.map((day) =>
73
- renderCell(
74
- year,
75
- month,
76
- day,
77
- highlight,
78
- highlightStyle,
79
- range,
80
- today,
81
- color,
82
- palette,
83
- cellWidth,
84
- ),
85
- );
86
-
87
- lines.push(cells.join(sep));
88
- }
89
104
  } else {
90
105
  // ── 枠あり(modern) ──
91
- const frame = theme.frame;
92
-
93
106
  lines.push(
94
- colorize(
95
- topBorder(frame, cellWidth, weekdays.length),
96
- palette.frame,
97
- color,
98
- ),
107
+ colorize(topBorder(frame, cellWidth, cols), palette.frame, color),
99
108
  );
100
109
  lines.push(
101
110
  colorize(
102
- `${frame.v}${centerTextFull(title, innerWidth(cellWidth, weekdays.length))}${frame.v}`,
111
+ `${frame.v}${centerTextFull(title, innerWidth(cellWidth, cols))}${frame.v}`,
103
112
  palette.title,
104
113
  color,
105
114
  ),
106
115
  );
107
116
  lines.push(
108
- colorize(
109
- separatorRow(frame, cellWidth, weekdays.length),
110
- palette.frame,
111
- color,
112
- ),
117
+ colorize(separatorRow(frame, cellWidth, cols), palette.frame, color),
113
118
  );
114
119
  lines.push(
115
120
  colorize(
@@ -119,89 +124,21 @@ export function renderMonth(
119
124
  ),
120
125
  );
121
126
  lines.push(
122
- colorize(
123
- separatorRow(frame, cellWidth, weekdays.length),
124
- palette.frame,
125
- color,
126
- ),
127
+ colorize(separatorRow(frame, cellWidth, cols), palette.frame, color),
127
128
  );
129
+ }
128
130
 
129
- for (const row of grid) {
130
- if (row.every((d) => d === null)) continue;
131
-
132
- const cells = row.map((day) =>
133
- renderCell(
134
- year,
135
- month,
136
- day,
137
- highlight,
138
- highlightStyle,
139
- range,
140
- today,
141
- color,
142
- palette,
143
- cellWidth,
144
- ),
145
- );
146
-
147
- lines.push(`${frame.v}${cells.join(frame.v)}${frame.v}`);
148
- }
131
+ for (const row of grid) {
132
+ if (row.every((d) => d === null)) continue;
133
+ const cells = row.map(renderCell).join(sep);
134
+ lines.push(frame === null ? cells : `${frame.v}${cells}${frame.v}`);
135
+ }
149
136
 
137
+ if (frame !== null) {
150
138
  lines.push(
151
- colorize(
152
- bottomBorder(frame, cellWidth, weekdays.length),
153
- palette.frame,
154
- color,
155
- ),
139
+ colorize(bottomBorder(frame, cellWidth, cols), palette.frame, color),
156
140
  );
157
141
  }
158
142
 
159
143
  return lines.join("\n");
160
144
  }
161
-
162
- // ─── セル ─────────────────────────────────────────────────
163
-
164
- function renderCell(
165
- year: number,
166
- month: number,
167
- day: number | null,
168
- highlight: Date | undefined,
169
- highlightStyle: "bracket" | "reverse",
170
- range: { from: Date; to: Date } | undefined,
171
- today: Date,
172
- color: boolean,
173
- palette: CliPalette,
174
- cellWidth: number,
175
- ): string {
176
- if (day === null) {
177
- return " ".repeat(cellWidth);
178
- }
179
-
180
- const date = new Date(year, month - 1, day);
181
- const isHighlight = highlight !== undefined && isSameDay(date, highlight);
182
- const isInRange = isDateInRange(date, range);
183
- const isToday = isSameDay(date, today);
184
- const isWeekend = date.getDay() === 0 || date.getDay() === 6;
185
-
186
- let text: string;
187
- if (isHighlight && highlightStyle === "bracket") {
188
- text = `[${day}]`.padStart(cellWidth);
189
- } else {
190
- text = String(day).padStart(cellWidth);
191
- }
192
-
193
- let code: number | undefined;
194
- if (isHighlight && highlightStyle === "reverse") {
195
- code = palette.highlight ?? 7;
196
- } else if (isInRange && !isHighlight) {
197
- code = palette.range ?? 33;
198
- } else if (isToday && palette.today !== undefined) {
199
- code = palette.today;
200
- } else if (isWeekend && palette.weekend !== undefined) {
201
- code = palette.weekend;
202
- } else if (palette.day !== undefined) {
203
- code = palette.day;
204
- }
205
-
206
- return colorize(text, code, color);
207
- }
package/src/theme.ts CHANGED
@@ -52,9 +52,9 @@ export const THEMES: Record<ThemeName, CliTheme> = {
52
52
  };
53
53
 
54
54
  export function resolveTheme(theme?: ThemeName | CliTheme): CliTheme {
55
- if (theme === undefined) return THEMES.default;
56
- if (typeof theme === "string") return THEMES[theme] ?? THEMES.default;
57
- return theme;
55
+ return typeof theme === "string"
56
+ ? (THEMES[theme] ?? THEMES.default)
57
+ : (theme ?? THEMES.default);
58
58
  }
59
59
 
60
60
  // ─── カラースキーム ───────────────────────────────────────
@@ -140,9 +140,7 @@ export const COLOR_SCHEMES: Record<ColorSchemeName, CliPalette> = {
140
140
  export function resolveColorScheme(
141
141
  scheme?: ColorSchemeName | CliPalette,
142
142
  ): CliPalette {
143
- if (scheme === undefined) return COLOR_SCHEMES.default;
144
- if (typeof scheme === "string") {
145
- return COLOR_SCHEMES[scheme] ?? COLOR_SCHEMES.default;
146
- }
147
- return scheme;
143
+ return typeof scheme === "string"
144
+ ? (COLOR_SCHEMES[scheme] ?? COLOR_SCHEMES.default)
145
+ : (scheme ?? COLOR_SCHEMES.default);
148
146
  }
package/src/year.ts CHANGED
@@ -1,50 +1,48 @@
1
- import { visibleWidth } from "./ansi.ts";
1
+ import { displayWidth } from "./align.ts";
2
+ import { stripAnsi } from "./ansi.ts";
2
3
  import { renderMonth } from "./month.ts";
3
4
  import type { RenderMonthOptions } from "./types.ts";
4
5
 
5
6
  /**
6
7
  * 年間カレンダーを4列×3行でテキストで返す
8
+ *
9
+ * 列幅・パディングは「ANSI 除去後のターミナル表示幅」で計算する。
10
+ * 全角文字(日本語・韓国語・中国語の月名や曜日)も正しく揃う。
7
11
  */
8
12
  export function renderYear(
9
13
  year: number,
10
14
  options: RenderMonthOptions = {},
11
15
  ): string {
12
- const months: string[] = [];
13
- for (let m = 1; m <= 12; m++) {
14
- months.push(renderMonth(year, m, options));
15
- }
16
-
17
- const monthLines = months.map((m) => m.split("\n"));
18
- const maxLines = Math.max(...monthLines.map((l) => l.length));
16
+ const widthOf = (line: string): number => displayWidth(stripAnsi(line));
19
17
 
20
- const colWidths = monthLines.map((lines) =>
21
- Math.max(...lines.map((l) => visibleWidth(l))),
18
+ const months = Array.from({ length: 12 }, (_, i) =>
19
+ renderMonth(year, i + 1, options).split("\n"),
22
20
  );
21
+ const maxLines = Math.max(...months.map((lines) => lines.length));
22
+ const colWidths = months.map((lines) => Math.max(...lines.map(widthOf)));
23
23
 
24
- const result: string[] = [];
25
- for (let row = 0; row < 3; row++) {
26
- const rowLines: string[] = [];
27
- for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
28
- const parts: string[] = [];
29
- for (let col = 0; col < 4; col++) {
30
- const monthIdx = row * 4 + col;
31
- if (monthIdx >= 12) {
32
- parts.push("");
33
- continue;
34
- }
35
- const lines = monthLines[monthIdx]!;
36
- const line = lines[lineIdx] ?? "";
37
- const pad = Math.max(0, colWidths[monthIdx]! - visibleWidth(line));
38
- parts.push(line + " ".repeat(pad));
39
- }
40
- rowLines.push(parts.join(" "));
41
- }
42
- result.push(rowLines.join("\n"));
24
+ /** 月の各行をその列幅まで右パディングする(行が足りなければ空行として埋める) */
25
+ const padMonth = (monthIdx: number): string[] =>
26
+ Array.from({ length: maxLines }, (_, li) => {
27
+ const line = months[monthIdx]![li] ?? "";
28
+ return (
29
+ line + " ".repeat(Math.max(0, colWidths[monthIdx]! - widthOf(line)))
30
+ );
31
+ });
43
32
 
44
- if (row < 2) {
45
- result.push("");
46
- }
33
+ const padded = months.map((_, mi) => padMonth(mi));
34
+
35
+ const rows: string[] = [];
36
+ for (let row = 0; row < 3; row++) {
37
+ rows.push(
38
+ Array.from({ length: maxLines }, (_, li) =>
39
+ Array.from({ length: 4 }, (_, col) => {
40
+ const monthIdx = row * 4 + col;
41
+ return monthIdx < 12 ? padded[monthIdx]![li]! : "";
42
+ }).join(" "),
43
+ ).join("\n"),
44
+ );
47
45
  }
48
46
 
49
- return result.join("\n");
47
+ return rows.join("\n\n");
50
48
  }