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 +21 -0
- package/README.md +160 -0
- package/dist/index.cjs +10 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +54 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
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"}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|