Skip to content

Commit 6e61e8c

Browse files
committed
fix input output
1 parent e2b3f85 commit 6e61e8c

4 files changed

Lines changed: 284 additions & 9 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@node-projects/layout2vector",
3-
"version": "5.25.0",
3+
"version": "5.26.0",
44
"description": "DOM → Geometry → DXF/PDF/PNG library",
55
"repository": {
66
"type": "git",

src/extractors/form-controls.ts

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1084,11 +1084,181 @@ function getInputDisplayValue(input: HTMLInputElement): ControlDisplayText {
10841084
switch (type) {
10851085
case "password":
10861086
return { text: "*".repeat(value.length), isPlaceholder: false };
1087+
case "date":
1088+
case "time":
10871089
case "datetime-local":
1088-
return { text: value.replace("T", " "), isPlaceholder: false };
1090+
case "month":
1091+
case "week": {
1092+
const localized = formatTemporalInputDisplayValue(input, type, value);
1093+
if (localized) {
1094+
return { text: localized, isPlaceholder: false };
1095+
}
1096+
break;
1097+
}
10891098
default:
10901099
return { text: value, isPlaceholder: false };
10911100
}
1101+
1102+
return { text: type === "datetime-local" ? value.replace("T", " ") : value, isPlaceholder: false };
1103+
}
1104+
1105+
function formatTemporalInputDisplayValue(
1106+
input: HTMLInputElement,
1107+
type: string,
1108+
value: string
1109+
): string | null {
1110+
try {
1111+
switch (type) {
1112+
case "date": {
1113+
const date = parseDateValue(value);
1114+
if (!date) return null;
1115+
return new Intl.DateTimeFormat(undefined, {
1116+
year: "numeric",
1117+
month: "2-digit",
1118+
day: "2-digit",
1119+
}).format(date);
1120+
}
1121+
case "time": {
1122+
const parts = parseTimeValue(value);
1123+
if (!parts) return null;
1124+
const date = new Date(2000, 0, 1, parts.hour, parts.minute, parts.second, parts.millisecond);
1125+
return new Intl.DateTimeFormat(undefined, {
1126+
hour: "numeric",
1127+
minute: "2-digit",
1128+
second: parts.hasSecond ? "2-digit" : undefined,
1129+
}).format(date);
1130+
}
1131+
case "datetime-local": {
1132+
const date = parseDateTimeLocalValue(value);
1133+
if (!date) return null;
1134+
return new Intl.DateTimeFormat(undefined, {
1135+
dateStyle: "short",
1136+
timeStyle: "short",
1137+
}).format(date);
1138+
}
1139+
case "month": {
1140+
const date = parseMonthValue(value);
1141+
if (!date) return null;
1142+
return new Intl.DateTimeFormat(undefined, {
1143+
year: "numeric",
1144+
month: "long",
1145+
}).format(date);
1146+
}
1147+
case "week": {
1148+
const parsed = parseWeekValue(value);
1149+
if (!parsed) return null;
1150+
const rangeFormatter = new Intl.DateTimeFormat(undefined, {
1151+
month: "short",
1152+
day: "numeric",
1153+
});
1154+
const startLabel = rangeFormatter.format(parsed.start);
1155+
const endLabel = rangeFormatter.format(parsed.end);
1156+
const yearLabel = new Intl.DateTimeFormat(undefined, {
1157+
year: "numeric",
1158+
}).format(parsed.start);
1159+
return `${startLabel} - ${endLabel}, ${yearLabel}`;
1160+
}
1161+
default:
1162+
return null;
1163+
}
1164+
} catch {
1165+
return null;
1166+
}
1167+
}
1168+
1169+
function parseDateValue(value: string): Date | null {
1170+
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
1171+
if (!match) return null;
1172+
1173+
const year = Number.parseInt(match[1], 10);
1174+
const month = Number.parseInt(match[2], 10);
1175+
const day = Number.parseInt(match[3], 10);
1176+
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) return null;
1177+
1178+
return new Date(year, month - 1, day, 12, 0, 0, 0);
1179+
}
1180+
1181+
function parseTimeValue(value: string): {
1182+
hour: number;
1183+
minute: number;
1184+
second: number;
1185+
millisecond: number;
1186+
hasSecond: boolean;
1187+
} | null {
1188+
const match = value.match(/^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?$/);
1189+
if (!match) return null;
1190+
1191+
const hour = Number.parseInt(match[1], 10);
1192+
const minute = Number.parseInt(match[2], 10);
1193+
const second = match[3] ? Number.parseInt(match[3], 10) : 0;
1194+
const millisecond = match[4] ? Number.parseInt(match[4].padEnd(3, "0"), 10) : 0;
1195+
if (!Number.isFinite(hour) || !Number.isFinite(minute) || !Number.isFinite(second) || !Number.isFinite(millisecond)) {
1196+
return null;
1197+
}
1198+
1199+
return {
1200+
hour,
1201+
minute,
1202+
second,
1203+
millisecond,
1204+
hasSecond: !!match[3],
1205+
};
1206+
}
1207+
1208+
function parseDateTimeLocalValue(value: string): Date | null {
1209+
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?$/);
1210+
if (!match) return null;
1211+
1212+
const year = Number.parseInt(match[1], 10);
1213+
const month = Number.parseInt(match[2], 10);
1214+
const day = Number.parseInt(match[3], 10);
1215+
const hour = Number.parseInt(match[4], 10);
1216+
const minute = Number.parseInt(match[5], 10);
1217+
const second = match[6] ? Number.parseInt(match[6], 10) : 0;
1218+
const millisecond = match[7] ? Number.parseInt(match[7].padEnd(3, "0"), 10) : 0;
1219+
if (!Number.isFinite(year)
1220+
|| !Number.isFinite(month)
1221+
|| !Number.isFinite(day)
1222+
|| !Number.isFinite(hour)
1223+
|| !Number.isFinite(minute)
1224+
|| !Number.isFinite(second)
1225+
|| !Number.isFinite(millisecond)) {
1226+
return null;
1227+
}
1228+
1229+
return new Date(year, month - 1, day, hour, minute, second, millisecond);
1230+
}
1231+
1232+
function parseMonthValue(value: string): Date | null {
1233+
const match = value.match(/^(\d{4})-(\d{2})$/);
1234+
if (!match) return null;
1235+
1236+
const year = Number.parseInt(match[1], 10);
1237+
const month = Number.parseInt(match[2], 10);
1238+
if (!Number.isFinite(year) || !Number.isFinite(month)) return null;
1239+
1240+
return new Date(year, month - 1, 1, 12, 0, 0, 0);
1241+
}
1242+
1243+
function parseWeekValue(value: string): { start: Date; end: Date } | null {
1244+
const match = value.match(/^(\d{4})-W(\d{2})$/);
1245+
if (!match) return null;
1246+
1247+
const isoYear = Number.parseInt(match[1], 10);
1248+
const isoWeek = Number.parseInt(match[2], 10);
1249+
if (!Number.isFinite(isoYear) || !Number.isFinite(isoWeek)) return null;
1250+
1251+
// ISO week 1 is the week containing Jan 4. Monday is day 1.
1252+
const jan4 = new Date(isoYear, 0, 4, 12, 0, 0, 0);
1253+
const day = jan4.getDay();
1254+
const mondayOffset = day === 0 ? -6 : 1 - day;
1255+
const firstWeekMonday = new Date(isoYear, 0, 4 + mondayOffset, 12, 0, 0, 0);
1256+
const start = new Date(firstWeekMonday);
1257+
start.setDate(firstWeekMonday.getDate() + (isoWeek - 1) * 7);
1258+
const end = new Date(start);
1259+
end.setDate(start.getDate() + 6);
1260+
1261+
return { start, end };
10921262
}
10931263

10941264
function defaultButtonLabel(type: string): string {

tests/unit/form-controls.test.ts

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,104 @@ test.describe("Form control conversion", () => {
5757
};
5858
}
5959

60+
function parseDateValue(value: string): Date | null {
61+
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
62+
if (!match) return null;
63+
return new Date(Number.parseInt(match[1], 10), Number.parseInt(match[2], 10) - 1, Number.parseInt(match[3], 10), 12, 0, 0, 0);
64+
}
65+
66+
function parseTimeValue(value: string): { hour: number; minute: number; second: number; hasSecond: boolean } | null {
67+
const match = value.match(/^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?$/);
68+
if (!match) return null;
69+
return {
70+
hour: Number.parseInt(match[1], 10),
71+
minute: Number.parseInt(match[2], 10),
72+
second: match[3] ? Number.parseInt(match[3], 10) : 0,
73+
hasSecond: !!match[3],
74+
};
75+
}
76+
77+
function parseDateTimeLocalValue(value: string): Date | null {
78+
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?$/);
79+
if (!match) return null;
80+
return new Date(
81+
Number.parseInt(match[1], 10),
82+
Number.parseInt(match[2], 10) - 1,
83+
Number.parseInt(match[3], 10),
84+
Number.parseInt(match[4], 10),
85+
Number.parseInt(match[5], 10),
86+
match[6] ? Number.parseInt(match[6], 10) : 0,
87+
match[7] ? Number.parseInt(match[7].padEnd(3, "0"), 10) : 0
88+
);
89+
}
90+
91+
function parseMonthValue(value: string): Date | null {
92+
const match = value.match(/^(\d{4})-(\d{2})$/);
93+
if (!match) return null;
94+
return new Date(Number.parseInt(match[1], 10), Number.parseInt(match[2], 10) - 1, 1, 12, 0, 0, 0);
95+
}
96+
97+
function parseWeekValue(value: string): { start: Date; end: Date } | null {
98+
const match = value.match(/^(\d{4})-W(\d{2})$/);
99+
if (!match) return null;
100+
const isoYear = Number.parseInt(match[1], 10);
101+
const isoWeek = Number.parseInt(match[2], 10);
102+
const jan4 = new Date(isoYear, 0, 4, 12, 0, 0, 0);
103+
const day = jan4.getDay();
104+
const mondayOffset = day === 0 ? -6 : 1 - day;
105+
const firstWeekMonday = new Date(isoYear, 0, 4 + mondayOffset, 12, 0, 0, 0);
106+
const start = new Date(firstWeekMonday);
107+
start.setDate(firstWeekMonday.getDate() + (isoWeek - 1) * 7);
108+
const end = new Date(start);
109+
end.setDate(start.getDate() + 6);
110+
return { start, end };
111+
}
112+
113+
function temporalDisplay(type: string, value: string): string {
114+
if (!value) return "";
115+
switch (type) {
116+
case "date": {
117+
const date = parseDateValue(value);
118+
if (!date) return value;
119+
return new Intl.DateTimeFormat(undefined, { year: "numeric", month: "2-digit", day: "2-digit" }).format(date);
120+
}
121+
case "time": {
122+
const parts = parseTimeValue(value);
123+
if (!parts) return value;
124+
const date = new Date(2000, 0, 1, parts.hour, parts.minute, parts.second, 0);
125+
return new Intl.DateTimeFormat(undefined, {
126+
hour: "numeric",
127+
minute: "2-digit",
128+
second: parts.hasSecond ? "2-digit" : undefined,
129+
}).format(date);
130+
}
131+
case "datetime-local": {
132+
const date = parseDateTimeLocalValue(value);
133+
if (!date) return value.replace("T", " ");
134+
return new Intl.DateTimeFormat(undefined, { dateStyle: "short", timeStyle: "short" }).format(date);
135+
}
136+
case "month": {
137+
const date = parseMonthValue(value);
138+
if (!date) return value;
139+
return new Intl.DateTimeFormat(undefined, { year: "numeric", month: "long" }).format(date);
140+
}
141+
case "week": {
142+
const parsed = parseWeekValue(value);
143+
if (!parsed) return value;
144+
const startLabel = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric" }).format(parsed.start);
145+
const endLabel = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric" }).format(parsed.end);
146+
const yearLabel = new Intl.DateTimeFormat(undefined, { year: "numeric" }).format(parsed.start);
147+
return `${startLabel} - ${endLabel}, ${yearLabel}`;
148+
}
149+
default:
150+
return value;
151+
}
152+
}
153+
154+
function controlValue(id: string): string {
155+
return (document.getElementById(id) as HTMLInputElement).value;
156+
}
157+
60158
return {
61159
gated: await summarize("text-input", false),
62160
textInput: await summarize("text-input"),
@@ -78,6 +176,13 @@ test.describe("Form control conversion", () => {
78176
datetime: await summarize("datetime"),
79177
month: await summarize("month"),
80178
week: await summarize("week"),
179+
expectedTemporal: {
180+
date: temporalDisplay("date", controlValue("date")),
181+
time: temporalDisplay("time", controlValue("time")),
182+
datetime: temporalDisplay("datetime-local", controlValue("datetime")),
183+
month: temporalDisplay("month", controlValue("month")),
184+
week: temporalDisplay("week", controlValue("week")),
185+
},
81186
};
82187
});
83188

@@ -129,11 +234,11 @@ test.describe("Form control conversion", () => {
129234
expect(summary.file.texts).toContain("Choose File");
130235
expect(summary.file.texts.join(" ")).toContain("notes.txt");
131236

132-
expect(summary.date.texts).toContain("2026-04-13");
133-
expect(summary.time.texts).toContain("14:35");
134-
expect(summary.datetime.texts).toContain("2026-04-13 14:35");
135-
expect(summary.month.texts).toContain("2026-04");
136-
expect(summary.week.texts).toContain("2026-W16");
237+
expect(summary.date.texts).toContain(summary.expectedTemporal.date);
238+
expect(summary.time.texts).toContain(summary.expectedTemporal.time);
239+
expect(summary.datetime.texts).toContain(summary.expectedTemporal.datetime);
240+
expect(summary.month.texts).toContain(summary.expectedTemporal.month);
241+
expect(summary.week.texts).toContain(summary.expectedTemporal.week);
137242
});
138243

139244
test("uses placeholder pseudo styles and avoids inventing boxes for transparent proxy textareas", async ({ page }) => {

0 commit comments

Comments
 (0)