form-char-count 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # form-char-count
2
+
3
+ A lightweight JavaScript/TypeScript library that accurately calculates how form input values (`<textarea>`, `<input>`) will be counted after HTTP submission and in backend/database storage.
4
+
5
+ ## Why?
6
+
7
+ ### Problem 1: Surrogate Pairs
8
+
9
+ ```javascript
10
+ 'πŸŽ‰'.length // β†’ 2 (JavaScript)
11
+ // MySQL CHAR_LENGTH("πŸŽ‰") β†’ 1
12
+ ```
13
+
14
+ ### Problem 2: Newline Normalization
15
+
16
+ ```javascript
17
+ textarea.value // "a\nb" β†’ 3 chars (JavaScript)
18
+ // After HTTP submission β†’ "a\r\nb" β†’ 4 chars (Backend)
19
+ ```
20
+
21
+ ### Problem 3: Lone Surrogates
22
+
23
+ Corrupted data from copy-paste. MySQL treats these as invalid UTF-8.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install form-char-count
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ### Basic
34
+
35
+ ```typescript
36
+ import { countChars, isValidLength } from 'form-char-count'
37
+
38
+ const text = 'Hello\nWorldπŸŽ‰'
39
+
40
+ const result = countChars(text)
41
+ // {
42
+ // length: 13, // "Hello\r\nWorldπŸŽ‰" = 13 code points
43
+ // byteLength: 16, // UTF-8 bytes
44
+ // hasLoneSurrogate: false,
45
+ // newlineCount: 1
46
+ // }
47
+
48
+ // VARCHAR(255) validation
49
+ if (isValidLength(text, 255)) {
50
+ // OK
51
+ }
52
+ ```
53
+
54
+ ### React Example
55
+
56
+ ```tsx
57
+ import { countChars } from 'form-char-count'
58
+
59
+ function TextArea({ maxLength }: { maxLength: number }) {
60
+ const [value, setValue] = useState('')
61
+ const { length } = countChars(value)
62
+ const isOver = length > maxLength
63
+
64
+ return (
65
+ <div>
66
+ <textarea value={value} onChange={(e) => setValue(e.target.value)} />
67
+ <span style={{ color: isOver ? 'red' : 'inherit' }}>
68
+ {length} / {maxLength}
69
+ </span>
70
+ </div>
71
+ )
72
+ }
73
+ ```
74
+
75
+ ### Vue.js Example
76
+
77
+ ```vue
78
+ <script setup lang="ts">
79
+ import { ref, computed } from 'vue'
80
+ import { countChars } from 'form-char-count'
81
+
82
+ const text = ref('')
83
+ const maxLength = 255
84
+
85
+ const charCount = computed(() => countChars(text.value))
86
+ const isOver = computed(() => charCount.value.length > maxLength)
87
+ </script>
88
+
89
+ <template>
90
+ <div>
91
+ <textarea v-model="text" />
92
+ <span :class="{ error: isOver }">
93
+ {{ charCount.length }} / {{ maxLength }}
94
+ </span>
95
+ </div>
96
+ </template>
97
+ ```
98
+
99
+ ### Options
100
+
101
+ ```typescript
102
+ // LF normalization (if backend converts \r\n β†’ \n)
103
+ const result = countChars(text, { newline: 'lf' })
104
+
105
+ // No normalization
106
+ const result = countChars(text, { newline: 'none' })
107
+ ```
108
+
109
+ ## API
110
+
111
+ ### `countChars(str, options?)`
112
+
113
+ Returns character count information.
114
+
115
+ ```typescript
116
+ interface CountResult {
117
+ length: number // Code point count (MySQL CHAR_LENGTH compatible)
118
+ byteLength: number // UTF-8 byte length (MySQL LENGTH compatible)
119
+ hasLoneSurrogate: boolean // Contains invalid surrogate?
120
+ newlineCount: number // Number of newlines
121
+ }
122
+ ```
123
+
124
+ ### `isValidLength(str, maxLength, options?)`
125
+
126
+ Returns `true` if within the character limit. Useful for VARCHAR(N) validation.
127
+
128
+ ### `isValidByteLength(str, maxBytes, options?)`
129
+
130
+ Returns `true` if within the byte limit. Useful for index size restrictions.
131
+
132
+ ### Options
133
+
134
+ ```typescript
135
+ interface CountOptions {
136
+ newline?: 'crlf' | 'lf' | 'none' // Default: 'crlf'
137
+ }
138
+ ```
139
+
140
+ | Value | Description |
141
+ | -------- | ------------------------------------------------------------- |
142
+ | `'crlf'` | Convert `\n` β†’ `\r\n` (default, matches HTTP form submission) |
143
+ | `'lf'` | Convert `\r\n` β†’ `\n` |
144
+ | `'none'` | No normalization |
145
+
146
+ ## Features
147
+
148
+ - Zero dependencies
149
+ - ESM and CommonJS support
150
+ - TypeScript types included
151
+ - Tiny size (~1KB minified, ~500B gzipped)
152
+
153
+ ## Browser Support
154
+
155
+ - Modern browsers (ES2018+)
156
+ - Node.js >= 16
157
+
158
+ ## License
159
+
160
+ [MIT](./LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const u=new RegExp("[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]","g");function g(n){return u.test(n)}function l(n){return n.replace(u,"")}function h(n,e){switch(e){case"crlf":return n.replace(/\r\n/g,`
2
+ `).replace(/\r/g,`
3
+ `).replace(/\n/g,`\r
4
+ `);case"lf":return n.replace(/\r\n/g,`
5
+ `).replace(/\r/g,`
6
+ `);case"none":return n}}function f(n){const e=n.replace(/\r\n/g,`
7
+ `).replace(/\r/g,`
8
+ `);let t=0;for(const r of e)r===`
9
+ `&&t++;return t}function d(n){return[...l(n)].length}function L(n){const e=l(n);return new TextEncoder().encode(e).length}function c(n,e){const t=e?.newline??"crlf",r=g(n),a=f(n),o=h(n,t),i=d(o),s=L(o);return{length:i,byteLength:s,hasLoneSurrogate:r,newlineCount:a}}function F(n,e,t){return c(n,t).length<=e}function p(n,e,t){return c(n,t).byteLength<=e}exports.countChars=c;exports.isValidByteLength=p;exports.isValidLength=F;
10
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/index.ts"],"sourcesContent":["/**\n * Options for character counting\n */\nexport interface CountOptions {\n /**\n * How to normalize newlines\n * - 'crlf': Convert \\n to \\r\\n (default, matches HTTP form submission)\n * - 'lf': Convert \\r\\n to \\n\n * - 'none': No normalization\n */\n newline?: 'crlf' | 'lf' | 'none'\n}\n\n/**\n * Result of character counting\n */\nexport interface CountResult {\n /** Code point count (after CRLF normalization, excluding lone surrogates) */\n length: number\n /** UTF-8 byte length (after CRLF normalization, excluding lone surrogates) */\n byteLength: number\n /** Whether the string contains lone surrogates */\n hasLoneSurrogate: boolean\n /** Number of newlines */\n newlineCount: number\n}\n\n// Regex to match lone surrogates (unpaired high or low surrogates)\nconst LONE_SURROGATE_REGEX =\n /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/g\n\n/**\n * Check if a string contains lone surrogates\n */\nfunction hasLoneSurrogates(str: string): boolean {\n return LONE_SURROGATE_REGEX.test(str)\n}\n\n/**\n * Remove lone surrogates from a string\n */\nfunction removeLoneSurrogates(str: string): string {\n return str.replace(LONE_SURROGATE_REGEX, '')\n}\n\n/**\n * Normalize newlines according to the specified mode\n */\nfunction normalizeNewlines(str: string, mode: 'crlf' | 'lf' | 'none'): string {\n switch (mode) {\n case 'crlf':\n // First normalize all line endings to LF, then convert to CRLF\n return str\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\r/g, '\\n')\n .replace(/\\n/g, '\\r\\n')\n case 'lf':\n // Convert CRLF to LF and standalone CR to LF\n return str.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n')\n case 'none':\n return str\n }\n}\n\n/**\n * Count newlines in a string (counts \\r\\n as 1, \\n as 1, \\r as 1)\n */\nfunction countNewlines(str: string): number {\n // Normalize to LF first, then count\n const normalized = str.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n')\n let count = 0\n for (const char of normalized) {\n if (char === '\\n') count++\n }\n return count\n}\n\n/**\n * Count code points excluding lone surrogates\n */\nfunction countCodePoints(str: string): number {\n // Remove lone surrogates first\n const clean = removeLoneSurrogates(str)\n // Count code points using spread operator\n return [...clean].length\n}\n\n/**\n * Calculate UTF-8 byte length\n */\nfunction calcByteLength(str: string): number {\n // Remove lone surrogates first\n const clean = removeLoneSurrogates(str)\n // Use TextEncoder for accurate UTF-8 byte count\n return new TextEncoder().encode(clean).length\n}\n\n/**\n * Count characters in a string, accurately reflecting how the string\n * will be counted after HTTP form submission and in backend/database.\n *\n * @param str - The string to count\n * @param options - Counting options\n * @returns Counting result\n */\nexport function countChars(str: string, options?: CountOptions): CountResult {\n const newlineMode = options?.newline ?? 'crlf'\n const hasLoneSurrogate = hasLoneSurrogates(str)\n\n // Check newline count before normalization (logical newlines)\n const newlineCount = countNewlines(str)\n\n // Normalize newlines for counting\n const normalized = normalizeNewlines(str, newlineMode)\n\n // Count code points and bytes after normalization\n const length = countCodePoints(normalized)\n const byteLength = calcByteLength(normalized)\n\n return {\n length,\n byteLength,\n hasLoneSurrogate,\n newlineCount,\n }\n}\n\n/**\n * Check if a string is within the specified character length limit.\n * Useful for VARCHAR(N) validation.\n *\n * @param str - The string to check\n * @param maxLength - Maximum allowed code point count\n * @param options - Counting options\n * @returns true if within limit\n */\nexport function isValidLength(\n str: string,\n maxLength: number,\n options?: CountOptions\n): boolean {\n return countChars(str, options).length <= maxLength\n}\n\n/**\n * Check if a string is within the specified byte length limit.\n * Useful for byte-based column limits or index restrictions.\n *\n * @param str - The string to check\n * @param maxBytes - Maximum allowed byte count\n * @param options - Counting options\n * @returns true if within limit\n */\nexport function isValidByteLength(\n str: string,\n maxBytes: number,\n options?: CountOptions\n): boolean {\n return countChars(str, options).byteLength <= maxBytes\n}\n"],"names":["LONE_SURROGATE_REGEX","hasLoneSurrogates","str","removeLoneSurrogates","normalizeNewlines","mode","countNewlines","normalized","count","char","countCodePoints","calcByteLength","clean","countChars","options","newlineMode","hasLoneSurrogate","newlineCount","length","byteLength","isValidLength","maxLength","isValidByteLength","maxBytes"],"mappings":"gFA4BA,MAAMA,EACJ,WAAA,iFAAA,GAAA,EAKF,SAASC,EAAkBC,EAAsB,CAC/C,OAAOF,EAAqB,KAAKE,CAAG,CACtC,CAKA,SAASC,EAAqBD,EAAqB,CACjD,OAAOA,EAAI,QAAQF,EAAsB,EAAE,CAC7C,CAKA,SAASI,EAAkBF,EAAaG,EAAsC,CAC5E,OAAQA,EAAA,CACN,IAAK,OAEH,OAAOH,EACJ,QAAQ,QAAS;AAAA,CAAI,EACrB,QAAQ,MAAO;AAAA,CAAI,EACnB,QAAQ,MAAO;AAAA,CAAM,EAC1B,IAAK,KAEH,OAAOA,EAAI,QAAQ,QAAS;AAAA,CAAI,EAAE,QAAQ,MAAO;AAAA,CAAI,EACvD,IAAK,OACH,OAAOA,CAAA,CAEb,CAKA,SAASI,EAAcJ,EAAqB,CAE1C,MAAMK,EAAaL,EAAI,QAAQ,QAAS;AAAA,CAAI,EAAE,QAAQ,MAAO;AAAA,CAAI,EACjE,IAAIM,EAAQ,EACZ,UAAWC,KAAQF,EACbE,IAAS;AAAA,GAAMD,IAErB,OAAOA,CACT,CAKA,SAASE,EAAgBR,EAAqB,CAI5C,MAAO,CAAC,GAFMC,EAAqBD,CAAG,CAEtB,EAAE,MACpB,CAKA,SAASS,EAAeT,EAAqB,CAE3C,MAAMU,EAAQT,EAAqBD,CAAG,EAEtC,OAAO,IAAI,YAAA,EAAc,OAAOU,CAAK,EAAE,MACzC,CAUO,SAASC,EAAWX,EAAaY,EAAqC,CAC3E,MAAMC,EAAcD,GAAS,SAAW,OAClCE,EAAmBf,EAAkBC,CAAG,EAGxCe,EAAeX,EAAcJ,CAAG,EAGhCK,EAAaH,EAAkBF,EAAKa,CAAW,EAG/CG,EAASR,EAAgBH,CAAU,EACnCY,EAAaR,EAAeJ,CAAU,EAE5C,MAAO,CACL,OAAAW,EACA,WAAAC,EACA,iBAAAH,EACA,aAAAC,CAAA,CAEJ,CAWO,SAASG,EACdlB,EACAmB,EACAP,EACS,CACT,OAAOD,EAAWX,EAAKY,CAAO,EAAE,QAAUO,CAC5C,CAWO,SAASC,EACdpB,EACAqB,EACAT,EACS,CACT,OAAOD,EAAWX,EAAKY,CAAO,EAAE,YAAcS,CAChD"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Options for character counting
3
+ */
4
+ export interface CountOptions {
5
+ /**
6
+ * How to normalize newlines
7
+ * - 'crlf': Convert \n to \r\n (default, matches HTTP form submission)
8
+ * - 'lf': Convert \r\n to \n
9
+ * - 'none': No normalization
10
+ */
11
+ newline?: 'crlf' | 'lf' | 'none';
12
+ }
13
+ /**
14
+ * Result of character counting
15
+ */
16
+ export interface CountResult {
17
+ /** Code point count (after CRLF normalization, excluding lone surrogates) */
18
+ length: number;
19
+ /** UTF-8 byte length (after CRLF normalization, excluding lone surrogates) */
20
+ byteLength: number;
21
+ /** Whether the string contains lone surrogates */
22
+ hasLoneSurrogate: boolean;
23
+ /** Number of newlines */
24
+ newlineCount: number;
25
+ }
26
+ /**
27
+ * Count characters in a string, accurately reflecting how the string
28
+ * will be counted after HTTP form submission and in backend/database.
29
+ *
30
+ * @param str - The string to count
31
+ * @param options - Counting options
32
+ * @returns Counting result
33
+ */
34
+ export declare function countChars(str: string, options?: CountOptions): CountResult;
35
+ /**
36
+ * Check if a string is within the specified character length limit.
37
+ * Useful for VARCHAR(N) validation.
38
+ *
39
+ * @param str - The string to check
40
+ * @param maxLength - Maximum allowed code point count
41
+ * @param options - Counting options
42
+ * @returns true if within limit
43
+ */
44
+ export declare function isValidLength(str: string, maxLength: number, options?: CountOptions): boolean;
45
+ /**
46
+ * Check if a string is within the specified byte length limit.
47
+ * Useful for byte-based column limits or index restrictions.
48
+ *
49
+ * @param str - The string to check
50
+ * @param maxBytes - Maximum allowed byte count
51
+ * @param options - Counting options
52
+ * @returns true if within limit
53
+ */
54
+ export declare function isValidByteLength(str: string, maxBytes: number, options?: CountOptions): boolean;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Options for character counting
3
+ */
4
+ export interface CountOptions {
5
+ /**
6
+ * How to normalize newlines
7
+ * - 'crlf': Convert \n to \r\n (default, matches HTTP form submission)
8
+ * - 'lf': Convert \r\n to \n
9
+ * - 'none': No normalization
10
+ */
11
+ newline?: 'crlf' | 'lf' | 'none';
12
+ }
13
+ /**
14
+ * Result of character counting
15
+ */
16
+ export interface CountResult {
17
+ /** Code point count (after CRLF normalization, excluding lone surrogates) */
18
+ length: number;
19
+ /** UTF-8 byte length (after CRLF normalization, excluding lone surrogates) */
20
+ byteLength: number;
21
+ /** Whether the string contains lone surrogates */
22
+ hasLoneSurrogate: boolean;
23
+ /** Number of newlines */
24
+ newlineCount: number;
25
+ }
26
+ /**
27
+ * Count characters in a string, accurately reflecting how the string
28
+ * will be counted after HTTP form submission and in backend/database.
29
+ *
30
+ * @param str - The string to count
31
+ * @param options - Counting options
32
+ * @returns Counting result
33
+ */
34
+ export declare function countChars(str: string, options?: CountOptions): CountResult;
35
+ /**
36
+ * Check if a string is within the specified character length limit.
37
+ * Useful for VARCHAR(N) validation.
38
+ *
39
+ * @param str - The string to check
40
+ * @param maxLength - Maximum allowed code point count
41
+ * @param options - Counting options
42
+ * @returns true if within limit
43
+ */
44
+ export declare function isValidLength(str: string, maxLength: number, options?: CountOptions): boolean;
45
+ /**
46
+ * Check if a string is within the specified byte length limit.
47
+ * Useful for byte-based column limits or index restrictions.
48
+ *
49
+ * @param str - The string to check
50
+ * @param maxBytes - Maximum allowed byte count
51
+ * @param options - Counting options
52
+ * @returns true if within limit
53
+ */
54
+ export declare function isValidByteLength(str: string, maxBytes: number, options?: CountOptions): boolean;
package/dist/index.js ADDED
@@ -0,0 +1,60 @@
1
+ const o = new RegExp("[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]", "g");
2
+ function g(n) {
3
+ return o.test(n);
4
+ }
5
+ function u(n) {
6
+ return n.replace(o, "");
7
+ }
8
+ function f(n, e) {
9
+ switch (e) {
10
+ case "crlf":
11
+ return n.replace(/\r\n/g, `
12
+ `).replace(/\r/g, `
13
+ `).replace(/\n/g, `\r
14
+ `);
15
+ case "lf":
16
+ return n.replace(/\r\n/g, `
17
+ `).replace(/\r/g, `
18
+ `);
19
+ case "none":
20
+ return n;
21
+ }
22
+ }
23
+ function h(n) {
24
+ const e = n.replace(/\r\n/g, `
25
+ `).replace(/\r/g, `
26
+ `);
27
+ let t = 0;
28
+ for (const r of e)
29
+ r === `
30
+ ` && t++;
31
+ return t;
32
+ }
33
+ function F(n) {
34
+ return [...u(n)].length;
35
+ }
36
+ function p(n) {
37
+ const e = u(n);
38
+ return new TextEncoder().encode(e).length;
39
+ }
40
+ function l(n, e) {
41
+ const t = e?.newline ?? "crlf", r = g(n), a = h(n), c = f(n, t), i = F(c), s = p(c);
42
+ return {
43
+ length: i,
44
+ byteLength: s,
45
+ hasLoneSurrogate: r,
46
+ newlineCount: a
47
+ };
48
+ }
49
+ function L(n, e, t) {
50
+ return l(n, t).length <= e;
51
+ }
52
+ function d(n, e, t) {
53
+ return l(n, t).byteLength <= e;
54
+ }
55
+ export {
56
+ l as countChars,
57
+ d as isValidByteLength,
58
+ L as isValidLength
59
+ };
60
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * Options for character counting\n */\nexport interface CountOptions {\n /**\n * How to normalize newlines\n * - 'crlf': Convert \\n to \\r\\n (default, matches HTTP form submission)\n * - 'lf': Convert \\r\\n to \\n\n * - 'none': No normalization\n */\n newline?: 'crlf' | 'lf' | 'none'\n}\n\n/**\n * Result of character counting\n */\nexport interface CountResult {\n /** Code point count (after CRLF normalization, excluding lone surrogates) */\n length: number\n /** UTF-8 byte length (after CRLF normalization, excluding lone surrogates) */\n byteLength: number\n /** Whether the string contains lone surrogates */\n hasLoneSurrogate: boolean\n /** Number of newlines */\n newlineCount: number\n}\n\n// Regex to match lone surrogates (unpaired high or low surrogates)\nconst LONE_SURROGATE_REGEX =\n /[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/g\n\n/**\n * Check if a string contains lone surrogates\n */\nfunction hasLoneSurrogates(str: string): boolean {\n return LONE_SURROGATE_REGEX.test(str)\n}\n\n/**\n * Remove lone surrogates from a string\n */\nfunction removeLoneSurrogates(str: string): string {\n return str.replace(LONE_SURROGATE_REGEX, '')\n}\n\n/**\n * Normalize newlines according to the specified mode\n */\nfunction normalizeNewlines(str: string, mode: 'crlf' | 'lf' | 'none'): string {\n switch (mode) {\n case 'crlf':\n // First normalize all line endings to LF, then convert to CRLF\n return str\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\r/g, '\\n')\n .replace(/\\n/g, '\\r\\n')\n case 'lf':\n // Convert CRLF to LF and standalone CR to LF\n return str.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n')\n case 'none':\n return str\n }\n}\n\n/**\n * Count newlines in a string (counts \\r\\n as 1, \\n as 1, \\r as 1)\n */\nfunction countNewlines(str: string): number {\n // Normalize to LF first, then count\n const normalized = str.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n')\n let count = 0\n for (const char of normalized) {\n if (char === '\\n') count++\n }\n return count\n}\n\n/**\n * Count code points excluding lone surrogates\n */\nfunction countCodePoints(str: string): number {\n // Remove lone surrogates first\n const clean = removeLoneSurrogates(str)\n // Count code points using spread operator\n return [...clean].length\n}\n\n/**\n * Calculate UTF-8 byte length\n */\nfunction calcByteLength(str: string): number {\n // Remove lone surrogates first\n const clean = removeLoneSurrogates(str)\n // Use TextEncoder for accurate UTF-8 byte count\n return new TextEncoder().encode(clean).length\n}\n\n/**\n * Count characters in a string, accurately reflecting how the string\n * will be counted after HTTP form submission and in backend/database.\n *\n * @param str - The string to count\n * @param options - Counting options\n * @returns Counting result\n */\nexport function countChars(str: string, options?: CountOptions): CountResult {\n const newlineMode = options?.newline ?? 'crlf'\n const hasLoneSurrogate = hasLoneSurrogates(str)\n\n // Check newline count before normalization (logical newlines)\n const newlineCount = countNewlines(str)\n\n // Normalize newlines for counting\n const normalized = normalizeNewlines(str, newlineMode)\n\n // Count code points and bytes after normalization\n const length = countCodePoints(normalized)\n const byteLength = calcByteLength(normalized)\n\n return {\n length,\n byteLength,\n hasLoneSurrogate,\n newlineCount,\n }\n}\n\n/**\n * Check if a string is within the specified character length limit.\n * Useful for VARCHAR(N) validation.\n *\n * @param str - The string to check\n * @param maxLength - Maximum allowed code point count\n * @param options - Counting options\n * @returns true if within limit\n */\nexport function isValidLength(\n str: string,\n maxLength: number,\n options?: CountOptions\n): boolean {\n return countChars(str, options).length <= maxLength\n}\n\n/**\n * Check if a string is within the specified byte length limit.\n * Useful for byte-based column limits or index restrictions.\n *\n * @param str - The string to check\n * @param maxBytes - Maximum allowed byte count\n * @param options - Counting options\n * @returns true if within limit\n */\nexport function isValidByteLength(\n str: string,\n maxBytes: number,\n options?: CountOptions\n): boolean {\n return countChars(str, options).byteLength <= maxBytes\n}\n"],"names":["LONE_SURROGATE_REGEX","hasLoneSurrogates","str","removeLoneSurrogates","normalizeNewlines","mode","countNewlines","normalized","count","char","countCodePoints","calcByteLength","clean","countChars","options","newlineMode","hasLoneSurrogate","newlineCount","length","byteLength","isValidLength","maxLength","isValidByteLength","maxBytes"],"mappings":"AA4BA,MAAMA,IACJ,WAAA,kFAAA,GAAA;AAKF,SAASC,EAAkBC,GAAsB;AAC/C,SAAOF,EAAqB,KAAKE,CAAG;AACtC;AAKA,SAASC,EAAqBD,GAAqB;AACjD,SAAOA,EAAI,QAAQF,GAAsB,EAAE;AAC7C;AAKA,SAASI,EAAkBF,GAAaG,GAAsC;AAC5E,UAAQA,GAAA;AAAA,IACN,KAAK;AAEH,aAAOH,EACJ,QAAQ,SAAS;AAAA,CAAI,EACrB,QAAQ,OAAO;AAAA,CAAI,EACnB,QAAQ,OAAO;AAAA,CAAM;AAAA,IAC1B,KAAK;AAEH,aAAOA,EAAI,QAAQ,SAAS;AAAA,CAAI,EAAE,QAAQ,OAAO;AAAA,CAAI;AAAA,IACvD,KAAK;AACH,aAAOA;AAAA,EAAA;AAEb;AAKA,SAASI,EAAcJ,GAAqB;AAE1C,QAAMK,IAAaL,EAAI,QAAQ,SAAS;AAAA,CAAI,EAAE,QAAQ,OAAO;AAAA,CAAI;AACjE,MAAIM,IAAQ;AACZ,aAAWC,KAAQF;AACjB,IAAIE,MAAS;AAAA,KAAMD;AAErB,SAAOA;AACT;AAKA,SAASE,EAAgBR,GAAqB;AAI5C,SAAO,CAAC,GAFMC,EAAqBD,CAAG,CAEtB,EAAE;AACpB;AAKA,SAASS,EAAeT,GAAqB;AAE3C,QAAMU,IAAQT,EAAqBD,CAAG;AAEtC,SAAO,IAAI,YAAA,EAAc,OAAOU,CAAK,EAAE;AACzC;AAUO,SAASC,EAAWX,GAAaY,GAAqC;AAC3E,QAAMC,IAAcD,GAAS,WAAW,QAClCE,IAAmBf,EAAkBC,CAAG,GAGxCe,IAAeX,EAAcJ,CAAG,GAGhCK,IAAaH,EAAkBF,GAAKa,CAAW,GAG/CG,IAASR,EAAgBH,CAAU,GACnCY,IAAaR,EAAeJ,CAAU;AAE5C,SAAO;AAAA,IACL,QAAAW;AAAA,IACA,YAAAC;AAAA,IACA,kBAAAH;AAAA,IACA,cAAAC;AAAA,EAAA;AAEJ;AAWO,SAASG,EACdlB,GACAmB,GACAP,GACS;AACT,SAAOD,EAAWX,GAAKY,CAAO,EAAE,UAAUO;AAC5C;AAWO,SAASC,EACdpB,GACAqB,GACAT,GACS;AACT,SAAOD,EAAWX,GAAKY,CAAO,EAAE,cAAcS;AAChD;"}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "form-char-count",
3
+ "version": "0.0.1",
4
+ "description": "Calculate form input character count accurately for HTTP submission and backend/DB storage",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "vite build && tsc --emitDeclarationOnly && node -e \"require('fs').copyFileSync('dist/index.d.ts', 'dist/index.d.cts')\"",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "lint": "eslint src tests",
29
+ "format": "prettier --write .",
30
+ "format:check": "prettier --check .",
31
+ "spell": "cspell \"**/*.{ts,js,json,md}\""
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/usapopopooon/form-char-count.git"
36
+ },
37
+ "keywords": [
38
+ "form",
39
+ "character-count",
40
+ "textarea",
41
+ "input",
42
+ "validation",
43
+ "surrogate-pair",
44
+ "crlf",
45
+ "mysql",
46
+ "varchar"
47
+ ],
48
+ "author": "",
49
+ "license": "MIT",
50
+ "bugs": {
51
+ "url": "https://github.com/usapopopooon/form-char-count/issues"
52
+ },
53
+ "homepage": "https://github.com/usapopopooon/form-char-count#readme",
54
+ "volta": {
55
+ "node": "22.22.0",
56
+ "npm": "11.8.0"
57
+ },
58
+ "devDependencies": {
59
+ "@eslint/js": "^9.39.2",
60
+ "@types/node": "^25.0.10",
61
+ "cspell": "^9.6.0",
62
+ "eslint": "^9.39.2",
63
+ "prettier": "^3.8.1",
64
+ "typescript": "^5.9.3",
65
+ "typescript-eslint": "^8.53.1",
66
+ "vite": "^7.3.1",
67
+ "vitest": "^4.0.17"
68
+ }
69
+ }