Skip to content

Commit 08dcc85

Browse files
committed
rules.bytes のオプションに newline を追加
1 parent e87baad commit 08dcc85

7 files changed

Lines changed: 132 additions & 51 deletions

File tree

HISTORY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
### History
22

3-
#### 1.1.x 2026/x/xx
3+
#### 1.1.0 2026/x/xx
44

55
- `attach` の引数に `onChange` を追加
6+
- `rules.bytes` のオプションに `newline` を追加
67
- `textarea` に対して `attach` を実施した場合に改行できなかった不具合を修正
78
- 文字の制御が入らない場合はアンドゥ/リドゥを行える機能を追加
89

docs/api.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -599,24 +599,27 @@ rules.width({
599599
#### `bytes()`
600600

601601
入力値の最大サイズ(バイト数)を制御します。単位は `"utf-8" | "utf-16" | "utf-32" | "sjis" | "cp932"` から選べます。
602+
改行コードの扱いも指定でき、バイト数の計算に反映されます。
602603

603604
****
604605

605606
```js
606607
rules.bytes({
607608
max: 64,
608609
mode: "block",
609-
unit: "utf-8"
610+
unit: "utf-8",
611+
newline: "\r\n"
610612
});
611613
```
612614

613615
**オプション**
614616

615-
| option | type | default | 説明 |
616-
| ------ | ------------------------------------------------------ | --------- | --------------------------------------------------------------------------------------------- |
617-
| `max` | `number` | - | 最大サイズ(バイト数)。未指定なら制限なし |
618-
| `mode` | `"block" \| "error"` | `"block"` | 最大サイズを超えたときの挙動(`block`: 超過分を入力時にカット / `error`: 変更せずエラー扱い) |
619-
| `unit` | `"utf-8" \| "utf-16" \| "utf-32" \| "sjis" \| "cp932"` | `"utf-8"` | サイズの単位(バイト数の数え方) |
617+
| option | type | default | 説明 |
618+
| --------- | ------------------------------------------------------ | --------- | --------------------------------------------------------------------------------------------- |
619+
| `max` | `number` | - | 最大サイズ(バイト数)。未指定なら制限なし |
620+
| `mode` | `"block" \| "error"` | `"block"` | 最大サイズを超えたときの挙動(`block`: 超過分を入力時にカット / `error`: 変更せずエラー扱い) |
621+
| `unit` | `"utf-8" \| "utf-16" \| "utf-32" \| "sjis" \| "cp932"` | `"utf-8"` | サイズの単位(バイト数の数え方) |
622+
| `newline` | `"\n" \| "\r" \| "\r\n"` | `"\n"` | 改行コードの扱い。内部で改行は正規化され、指定した改行コードとしてバイト数が計算される |
620623

621624
**補足**
622625

@@ -625,8 +628,8 @@ rules.bytes({
625628
- `"utf-32"`: コードポイント数 × 4(例:`"é"` は 8 bytes)
626629
- `"sjis"` / `"cp932"`: Shift_JIS(Windows-31J 系)にエンコードしたときのバイト数
627630
- 「旧Windows互換」「Shift_JIS 前提の外部I/F」などに合わせる用途です。エンコード不能になるので、 `filter()``"sjis-only"` / `"cp932-only"` など)とセットで使用してください。
628-
629-
※ ライブラリ内部では Shift_JIS で算出します(sjis/cp932 の差異はフィルタ側の許可範囲で制御する想定)
631+
- ライブラリ内部では Shift_JIS で算出します(sjis/cp932 の差異はフィルタ側の許可範囲で制御する想定)
632+
- 入力中の改行コード(`\r\n`, `\r`, `\n`)は内部で一度 `\n` へ正規化後に計算を行います
630633

631634
**運用上の注意**
632635

examples/dev/textarea.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ const guard = attach(input, {
1818
max: 10,
1919
mode: "block",
2020
unit: "grapheme"
21+
}),
22+
rules.bytes({
23+
max: 5,
24+
mode: "block",
25+
unit: "sjis",
26+
newline: "\r\n"
2127
})
2228
],
2329
onChange: (guard) => {

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": "text-input-guard",
3-
"version": "1.0.2",
3+
"version": "1.1.0",
44
"description": "A JavaScript input guard library for Japanese web apps, handling full-width digits, numeric rules, digit limits, and formatted display.",
55
"keywords": ["input-validation",
66
"numeric-input",

src/rules/bytes.js

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import { parseDatasetNumber, parseDatasetEnum } from "./_dataset.js";
1414
/**
1515
* bytes ルールのオプション
1616
* @typedef {Object} BytesRuleOptions
17-
* @property {number} [max] - 最大長(グラフェム数)。未指定なら制限なし
17+
* @property {number} [max] - バイト数。未指定なら制限なし
1818
* @property {"block"|"error"} [mode="block"] - 入力中に最大長を超えたときの挙動
1919
* @property {"utf-8"|"utf-16"|"utf-32"|"sjis"|"cp932"} [unit="utf-8"] - サイズの単位(sjis系を使用する場合はfilterも必須)
20+
* @property {"\n"|"\r"|"\r\n"} [newline="\n"] - 改行の扱い(バイト数計算に影響あり)
2021
*/
2122

2223
/**
@@ -25,25 +26,28 @@ import { parseDatasetNumber, parseDatasetEnum } from "./_dataset.js";
2526
*/
2627

2728
/**
28-
* グラフェム/UTF-16コード単位/UTF-32コード単位の長さを調べる
29+
* テキストのバイト数を調べる
2930
* @param {string} text
3031
* @param {"utf-8"|"utf-16"|"utf-32"|"sjis"|"cp932"} unit
32+
* @param {"\n"|"\r"|"\r\n"} newline
3133
* @returns {number}
3234
*/
33-
const getTextBytesByUnit = function(text, unit) {
35+
const getTextBytesByUnit = function(text, unit, newline) {
3436
if (text.length === 0) {
3537
return 0;
3638
}
39+
40+
const normalizedText = text.replace(/\r?\n/g, newline);
41+
3742
if (unit === "utf-8") {
38-
return Mojix.toUTF8Array(text).length;
43+
return Mojix.toUTF8Array(normalizedText).length;
3944
} else if (unit === "utf-16") {
40-
return Mojix.toUTF16Array(text).length * 2;
45+
return Mojix.toUTF16Array(normalizedText).length * 2;
4146
} else if (unit === "utf-32") {
42-
return Mojix.toUTF32Array(text).length * 4;
47+
return Mojix.toUTF32Array(normalizedText).length * 4;
4348
} else if (unit === "sjis" || unit === "cp932") {
44-
return Mojix.encode(text, "Shift_JIS").length;
49+
return Mojix.encode(normalizedText, "Shift_JIS").length;
4550
} else {
46-
// ここには来ない
4751
throw new Error(`Invalid unit: ${unit}`);
4852
}
4953
};
@@ -53,9 +57,10 @@ const getTextBytesByUnit = function(text, unit) {
5357
* @param {string} text
5458
* @param {"utf-8"|"utf-16"|"utf-32"|"sjis"|"cp932"} unit
5559
* @param {number} max
60+
* @param {"\n"|"\r"|"\r\n"} newline
5661
* @returns {string}
5762
*/
58-
const cutTextByUnit = function(text, unit, max) {
63+
const cutTextByUnit = function(text, unit, max, newline) {
5964
/**
6065
* グラフェムの配列
6166
* @type {Grapheme[]}
@@ -74,19 +79,10 @@ const cutTextByUnit = function(text, unit, max) {
7479
const outputGraphemeArray = [];
7580

7681
for (let i = 0; i < graphemeArray.length; i++) {
77-
const g = graphemeArray[i];
78-
7982
// 1グラフェムあたりの長さ
80-
let byteCount = 0;
81-
if (unit === "utf-8") {
82-
byteCount = Mojix.toUTF8Array(Mojix.toStringFromMojiArray([g])).length;
83-
} else if (unit === "utf-16") {
84-
byteCount = Mojix.toUTF16Array(Mojix.toStringFromMojiArray([g])).length * 2;
85-
} else if (unit === "utf-32") {
86-
byteCount = Mojix.toUTF32Array(Mojix.toStringFromMojiArray([g])).length * 4;
87-
} else if (unit === "sjis" || unit === "cp932") {
88-
byteCount = Mojix.encode(Mojix.toStringFromMojiArray([g]), "Shift_JIS").length;
89-
}
83+
const g = graphemeArray[i];
84+
const gText = Mojix.toStringFromMojiArray([g]);
85+
const byteCount = getTextBytesByUnit(gText, unit, newline);
9086

9187
if (count + byteCount > max) {
9288
// 空配列を渡すとNUL文字を返すため、空配列のときは空文字を返す
@@ -111,15 +107,16 @@ const cutTextByUnit = function(text, unit, max) {
111107
* @param {string} insertedText 追加するテキスト
112108
* @param {"utf-8"|"utf-16"|"utf-32"|"sjis"|"cp932"} unit
113109
* @param {number} max
110+
* @param {"\n"|"\r"|"\r\n"} newline
114111
* @returns {string} 追加するテキストを切ったもの(切る必要がない場合は insertedText をそのまま返す)
115112
*/
116-
const cutBytes = function(beforeText, insertedText, unit, max) {
117-
const beforeTextLen = getTextBytesByUnit(beforeText, unit);
113+
const cutBytes = function(beforeText, insertedText, unit, max, newline) {
114+
const beforeTextLen = getTextBytesByUnit(beforeText, unit, newline);
118115

119116
// すでに最大長を超えている場合は追加のテキストを全て切る
120117
if (beforeTextLen >= max) { return ""; }
121118

122-
const insertedTextLen = getTextBytesByUnit(insertedText, unit);
119+
const insertedTextLen = getTextBytesByUnit(insertedText, unit, newline);
123120
const totalLen = beforeTextLen + insertedTextLen;
124121

125122
if (totalLen <= max) {
@@ -129,7 +126,7 @@ const cutBytes = function(beforeText, insertedText, unit, max) {
129126

130127
// 超える場合は追加のテキストを切る
131128
const allowedAddLen = max - beforeTextLen;
132-
return cutTextByUnit(insertedText, unit, allowedAddLen);
129+
return cutTextByUnit(insertedText, unit, allowedAddLen, newline);
133130
};
134131

135132
/**
@@ -138,48 +135,46 @@ const cutBytes = function(beforeText, insertedText, unit, max) {
138135
* @returns {import("../text-input-guard.js").Rule}
139136
*/
140137
export function bytes(options = {}) {
141-
/** @type {BytesRuleOptions} */
142-
const opt = {
143-
max: typeof options.max === "number" ? options.max : undefined,
144-
mode: options.mode ?? "block",
145-
unit: options.unit ?? "utf-8"
146-
};
138+
const max = typeof options.max === "number" ? options.max : undefined;
139+
const mode = options.mode ?? "block";
140+
const unit = options.unit ?? "utf-8";
141+
const newline = options.newline ?? "\n";
147142

148143
return {
149144
name: "bytes",
150145
targets: ["input", "textarea"],
151146

152147
normalizeChar(value, ctx) {
153148
// block 以外は何もしない
154-
if (opt.mode !== "block") {
149+
if (mode !== "block") {
155150
return value;
156151
}
157152
// max 未指定なら制限なし
158-
if (typeof opt.max !== "number") {
153+
if (typeof max !== "number") {
159154
return value;
160155
}
161156

162-
const cutText = cutBytes(ctx.beforeText, value, opt.unit, opt.max);
157+
const cutText = cutBytes(ctx.beforeText, value, unit, max, newline);
163158
return cutText;
164159
},
165160

166161
validate(value, ctx) {
167162
// error 以外は何もしない
168-
if (opt.mode !== "error") {
163+
if (mode !== "error") {
169164
return;
170165
}
171166
// max 未指定なら制限なし
172-
if (typeof opt.max !== "number") {
167+
if (typeof max !== "number") {
173168
return;
174169
}
175170

176-
const len = getTextBytesByUnit(value, opt.unit);
177-
if (len > opt.max) {
171+
const len = getTextBytesByUnit(value, unit, newline);
172+
if (len > max) {
178173
ctx.pushError({
179174
code: "bytes.max_overflow",
180175
rule: "bytes",
181176
phase: "validate",
182-
detail: { limit: opt.max, actual: len }
177+
detail: { limit: max, actual: len }
183178
});
184179
}
185180
}
@@ -196,6 +191,7 @@ export function bytes(options = {}) {
196191
* - data-tig-rules-bytes-max -> dataset.tigRulesBytesMax
197192
* - data-tig-rules-bytes-mode -> dataset.tigRulesBytesMode
198193
* - data-tig-rules-bytes-unit -> dataset.tigRulesBytesUnit
194+
* - data-tig-rules-bytes-newline -> dataset.tigRulesBytesNewline
199195
*
200196
* @param {DOMStringMap} dataset
201197
* @param {HTMLInputElement|HTMLTextAreaElement} _el
@@ -228,5 +224,13 @@ bytes.fromDataset = function fromDataset(dataset, _el) {
228224
options.unit = unit;
229225
}
230226

227+
const newline = parseDatasetEnum(
228+
dataset.tigRulesBytesNewline,
229+
["\n", "\r", "\r\n"]
230+
);
231+
if (newline != null) {
232+
options.newline = newline;
233+
}
234+
231235
return bytes(options);
232236
};

src/rules/bytes.test.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,70 @@ test("bytes - fromDataset: unit が不正(未対応値)なら default(utf-8)
237237
assert.equal(errs.length, 1);
238238
assert.deepEqual(errs[0].detail, { limit: 4, actual: 5 });
239239
});
240+
241+
test("bytes - newline: LF(\\n) を1バイトとして扱う(utf-8)", () => {
242+
const rule = bytes({ max: 3, mode: "block", unit: "utf-8", newline: "\n" });
243+
244+
const ctx = makeCtx({
245+
beforeText: "",
246+
insertedText: "a\nb" // a(1) + \n(1) + b(1) = 3
247+
});
248+
249+
const out = rule.normalizeChar("a\nb", ctx);
250+
251+
assert.equal(out, "a\nb");
252+
});
253+
254+
test("bytes - newline: CRLF(\\r\\n) を2バイトとして扱う(utf-8)", () => {
255+
const rule = bytes({ max: 3, mode: "block", unit: "utf-8", newline: "\r\n" });
256+
257+
const ctx = makeCtx({
258+
beforeText: "",
259+
insertedText: "a\nb" // a(1) + \r\n(2) + b(1) = 4 > 3
260+
});
261+
262+
const out = rule.normalizeChar("a\nb", ctx);
263+
264+
// "a\n" まで入る想定(= a + 改行)
265+
assert.equal(out, "a\n");
266+
});
267+
268+
test("bytes - newline: CR(\\r) を1バイトとして扱う(utf-8)", () => {
269+
const rule = bytes({ max: 2, mode: "block", unit: "utf-8", newline: "\r" });
270+
271+
const ctx = makeCtx({
272+
beforeText: "",
273+
insertedText: "\n\n" // 改行2つ → 1byte×2 = 2
274+
});
275+
276+
const out = rule.normalizeChar("\n\n", ctx);
277+
278+
assert.equal(out, "\n\n");
279+
});
280+
281+
test("bytes - newline: 入力に CRLF が混在しても正規化してカウントされる", () => {
282+
const rule = bytes({ max: 3, mode: "block", unit: "utf-8", newline: "\n" });
283+
284+
const ctx = makeCtx({
285+
beforeText: "",
286+
insertedText: "a\r\nb"
287+
});
288+
289+
const out = rule.normalizeChar("a\r\nb", ctx);
290+
291+
// a(1) + \n(1) + b(1) = 3
292+
assert.equal(out, "a\r\nb");
293+
});
294+
295+
test("bytes - newline: validate でも改行が指定コードで計算される", () => {
296+
const rule = bytes({ max: 3, mode: "error", unit: "utf-8", newline: "\r\n" });
297+
298+
const ctx = makeCtx();
299+
300+
// a(1) + \r\n(2) + b(1) = 4 > 3
301+
rule.validate("a\nb", ctx);
302+
303+
const errs = ctx._getErrors();
304+
assert.equal(errs.length, 1);
305+
assert.deepEqual(errs[0].detail, { limit: 3, actual: 4 });
306+
});

0 commit comments

Comments
 (0)