kennzeichen 0.1.0

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 ADDED
@@ -0,0 +1,234 @@
1
+ # kennzeichen
2
+
3
+ German license plate parsing and validation. Framework-agnostic core with React adapter.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install kennzeichen
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - Parse German license plates with ambiguity detection
14
+ - 726 valid location codes (Unterscheidungszeichen)
15
+ - Handles E (electric) and H (historic) suffixes
16
+ - Framework-agnostic core
17
+ - React hook + headless component
18
+
19
+ ## Usage
20
+
21
+ ### Core (any environment)
22
+
23
+ The core module works in any JavaScript environment (Node.js, browser, Deno, etc.).
24
+
25
+ ```typescript
26
+ import {
27
+ parseLicensePlate,
28
+ formatParsedPlate,
29
+ isValidLicensePlate,
30
+ sanitizeLicensePlate,
31
+ LOCATION,
32
+ } from "kennzeichen";
33
+
34
+ // Parse a license plate
35
+ const result = parseLicensePlate("M-AB 1234");
36
+ // { type: 'unambiguous', plate: { part1: 'M', part2: 'AB', part3: '1234' } }
37
+
38
+ // Handle ambiguous input (no separator)
39
+ const ambiguous = parseLicensePlate("CEE1234");
40
+ // { type: 'ambiguous', options: [
41
+ // { part1: 'C', part2: 'EE', part3: '1234' },
42
+ // { part1: 'CE', part2: 'E', part3: '1234' }
43
+ // ] }
44
+
45
+ // Format a parsed plate
46
+ formatParsedPlate({ part1: "M", part2: "AB", part3: "1234" });
47
+ // 'M-AB 1234'
48
+
49
+ // Validate
50
+ isValidLicensePlate("M-AB 1234"); // true
51
+ isValidLicensePlate("XX-AB 1234"); // false (invalid location)
52
+
53
+ // Sanitize (remove separators)
54
+ sanitizeLicensePlate("M-AB 1234"); // 'MAB1234'
55
+
56
+ // Check if a location code exists
57
+ LOCATION.includes("M"); // true
58
+ LOCATION.includes("XX"); // false
59
+ ```
60
+
61
+ ### Parse Result Types
62
+
63
+ ```typescript
64
+ type ParseResult =
65
+ | { type: "unambiguous"; plate: ParsedPlate } // Single valid interpretation
66
+ | { type: "ambiguous"; options: ParsedPlate[] } // Multiple valid interpretations
67
+ | { type: "partial"; plate: Partial<ParsedPlate> } // Incomplete but valid so far
68
+ | { type: "invalid" }; // Cannot be parsed
69
+
70
+ type ParsedPlate = {
71
+ part1: string; // Location code (e.g., 'M', 'HH', 'BGL')
72
+ part2: string; // Letters (e.g., 'AB', 'X')
73
+ part3: string; // Numbers + optional suffix (e.g., '1234', '99E', '42H')
74
+ };
75
+ ```
76
+
77
+ ### React Hook
78
+
79
+ The hook provides full control over the input state and disambiguation logic.
80
+
81
+ ```tsx
82
+ import { useLicensePlate, formatParsedPlate } from "kennzeichen/react";
83
+
84
+ function MyLicensePlateInput({ value, onChange }) {
85
+ const lp = useLicensePlate({ value, onChange });
86
+
87
+ return (
88
+ <div className="relative">
89
+ <input
90
+ value={lp.inputValue}
91
+ onChange={lp.handleChange}
92
+ onKeyDown={lp.handleKeyDown}
93
+ onBlur={lp.handleBlur}
94
+ placeholder="z.B. M-AB 1234"
95
+ />
96
+ {lp.isDropdownOpen && lp.options && (
97
+ <ul className="absolute mt-1 border rounded bg-white shadow-lg">
98
+ {lp.options.map((opt, i) => (
99
+ <li
100
+ key={i}
101
+ className={`px-3 py-2 cursor-pointer ${
102
+ i === lp.activeIndex ? "bg-blue-100" : "hover:bg-gray-100"
103
+ }`}
104
+ onMouseEnter={() => lp.setActiveIndex(i)}
105
+ onMouseDown={(e) => e.preventDefault()}
106
+ onClick={() => lp.selectOption(opt)}
107
+ >
108
+ {formatParsedPlate(opt)}
109
+ </li>
110
+ ))}
111
+ </ul>
112
+ )}
113
+ </div>
114
+ );
115
+ }
116
+ ```
117
+
118
+ ### React Headless Component
119
+
120
+ The headless component uses render props to reduce boilerplate while still giving you full control over the UI.
121
+
122
+ ```tsx
123
+ import { LicensePlateInput } from "kennzeichen/react";
124
+
125
+ function MyLicensePlateInput({ value, onChange }) {
126
+ return (
127
+ <LicensePlateInput value={value} onChange={onChange}>
128
+ {({ inputProps, isOpen, options }) => (
129
+ <div className="relative">
130
+ <input
131
+ {...inputProps}
132
+ className="border rounded px-3 py-2 w-full"
133
+ placeholder="z.B. M-AB 1234"
134
+ />
135
+ {isOpen && options && (
136
+ <ul className="absolute mt-1 border rounded bg-white shadow-lg w-full">
137
+ {options.map((opt) => (
138
+ <li
139
+ key={opt.formatted}
140
+ className={`px-3 py-2 cursor-pointer ${
141
+ opt.isActive ? "bg-blue-100" : "hover:bg-gray-100"
142
+ }`}
143
+ {...opt.getProps()}
144
+ >
145
+ {opt.formatted}
146
+ </li>
147
+ ))}
148
+ </ul>
149
+ )}
150
+ </div>
151
+ )}
152
+ </LicensePlateInput>
153
+ );
154
+ }
155
+ ```
156
+
157
+ ## API Reference
158
+
159
+ ### Core
160
+
161
+ #### `parseLicensePlate(input: string): ParseResult`
162
+
163
+ Parses a license plate input string into its component parts. Handles various input formats:
164
+
165
+ - With hyphen: `"M-AB 1234"`
166
+ - With spaces: `"M AB 1234"`
167
+ - Without separators: `"MAB1234"`
168
+
169
+ #### `formatParsedPlate(plate: Partial<ParsedPlate>): string`
170
+
171
+ Formats a parsed plate into the standard display format: `"M-AB 1234"`.
172
+
173
+ #### `isValidLicensePlate(input: string): boolean`
174
+
175
+ Returns `true` if the input can be unambiguously parsed as a valid German license plate.
176
+
177
+ #### `sanitizeLicensePlate(plate: string): string`
178
+
179
+ Removes all spaces and hyphens from a license plate string.
180
+
181
+ #### `LOCATION: string[]`
182
+
183
+ Array of all 726 valid German location codes.
184
+
185
+ #### `LOCATION_SET: Set<string>`
186
+
187
+ Set of all valid location codes for O(1) lookup.
188
+
189
+ ### React
190
+
191
+ #### `useLicensePlate(options): UseLicensePlateReturn`
192
+
193
+ React hook for managing license plate input state.
194
+
195
+ **Options:**
196
+
197
+ - `value?: string` - Controlled value
198
+ - `onChange?: (value: string) => void` - Called when value changes
199
+ - `onBlur?: () => void` - Called when input loses focus
200
+
201
+ **Returns:**
202
+
203
+ - `inputValue: string` - Current input value
204
+ - `parseResult: ParseResult` - Current parse result
205
+ - `handleChange` - Input onChange handler
206
+ - `handleKeyDown` - Input onKeyDown handler (for dropdown navigation)
207
+ - `handleBlur` - Input onBlur handler
208
+ - `isDropdownOpen: boolean` - Whether dropdown should be visible
209
+ - `options: ParsedPlate[] | null` - Disambiguation options
210
+ - `activeIndex: number` - Currently highlighted option
211
+ - `selectOption(option)` - Select a disambiguation option
212
+ - `setActiveIndex(index)` - Set highlighted option
213
+
214
+ #### `<LicensePlateInput>` (Headless Component)
215
+
216
+ Render props component for license plate input.
217
+
218
+ **Props:**
219
+
220
+ - `value?: string` - Controlled value
221
+ - `onChange?: (value: string) => void` - Called when value changes
222
+ - `onBlur?: () => void` - Called when input loses focus
223
+ - `children: (props: RenderProps) => ReactNode` - Render function
224
+
225
+ **RenderProps:**
226
+
227
+ - `inputProps` - Props to spread on input element
228
+ - `isOpen: boolean` - Whether dropdown should be visible
229
+ - `options: OptionItem[] | null` - Disambiguation options with helper methods
230
+ - `parseResult: ParseResult` - Current parse result
231
+
232
+ ## License
233
+
234
+ MIT