text-input-guard 0.1.5 → 0.1.6

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.
@@ -919,8 +919,8 @@ class InputGuard {
919
919
  * ルール実行に渡すコンテキストを作る(pushErrorで errors に積める)
920
920
  * @returns {GuardContext}
921
921
  */
922
- createCtx() {
923
- const snap = this.beforeInputSnapshot;
922
+ createCtx({ useSnapshot = true } = {}) {
923
+ const snap = useSnapshot ? this.beforeInputSnapshot : null;
924
924
  const inputType = snap?.inputType ?? "";
925
925
  const insertedText = snap?.insertedText ?? "";
926
926
 
@@ -1200,6 +1200,8 @@ class InputGuard {
1200
1200
  const current = display.value;
1201
1201
 
1202
1202
  const ctx = this.createCtx();
1203
+
1204
+ ctx.beforeText = "";
1203
1205
  ctx.afterText = current;
1204
1206
 
1205
1207
  let v = current;
@@ -1400,10 +1402,11 @@ class InputGuard {
1400
1402
  this.revertRequest = null;
1401
1403
 
1402
1404
  const display = /** @type {HTMLInputElement|HTMLTextAreaElement} */ (this.displayElement);
1403
- const ctx = this.createCtx();
1405
+ const ctx = this.createCtx({ useSnapshot: false });
1404
1406
 
1405
1407
  // 1) raw候補(displayから取得)
1406
1408
  let raw = display.value;
1409
+ ctx.beforeText = "";
1407
1410
  ctx.afterText = raw;
1408
1411
 
1409
1412
  // 2) 正規化(rawとして扱う形に揃える)
@@ -6384,7 +6387,7 @@ const getTextLengthByUnit = function(text, unit) {
6384
6387
  * @param {number} max
6385
6388
  * @returns {string}
6386
6389
  */
6387
- const cutTextByUnit = function(text, unit, max) {
6390
+ const cutTextByUnit$1 = function(text, unit, max) {
6388
6391
  /**
6389
6392
  * グラフェムの配列
6390
6393
  * @type {Grapheme[]}
@@ -6444,13 +6447,13 @@ const cutTextByUnit = function(text, unit, max) {
6444
6447
  * @returns {string} 追加するテキストを切ったもの(切る必要がない場合は insertedText をそのまま返す)
6445
6448
  */
6446
6449
  const cutLength = function(beforeText, insertedText, unit, max) {
6447
- const orgLen = getTextLengthByUnit(beforeText, unit);
6450
+ const beforeTextLen = getTextLengthByUnit(beforeText, unit);
6448
6451
 
6449
6452
  // すでに最大長を超えている場合は追加のテキストを全て切る
6450
- if (orgLen >= max) { return ""; }
6453
+ if (beforeTextLen >= max) { return ""; }
6451
6454
 
6452
- const addLen = getTextLengthByUnit(insertedText, unit);
6453
- const totalLen = orgLen + addLen;
6455
+ const insertedTextLen = getTextLengthByUnit(insertedText, unit);
6456
+ const totalLen = beforeTextLen + insertedTextLen;
6454
6457
 
6455
6458
  if (totalLen <= max) {
6456
6459
  // 今回の追加で範囲内に収まるなら何もしない
@@ -6458,8 +6461,8 @@ const cutLength = function(beforeText, insertedText, unit, max) {
6458
6461
  }
6459
6462
 
6460
6463
  // 超える場合は追加のテキストを切る
6461
- const allowedAddLen = max - orgLen;
6462
- return cutTextByUnit(insertedText, unit, allowedAddLen);
6464
+ const allowedAddLen = max - beforeTextLen;
6465
+ return cutTextByUnit$1(insertedText, unit, allowedAddLen);
6463
6466
  };
6464
6467
 
6465
6468
  /**
@@ -6489,13 +6492,7 @@ function length(options = {}) {
6489
6492
  return value;
6490
6493
  }
6491
6494
 
6492
- const beforeText = ctx.beforeText;
6493
- const insertedText = ctx.insertedText;
6494
- if (insertedText === "") {
6495
- return value;
6496
- }
6497
-
6498
- const cutText = cutLength(beforeText, insertedText, opt.unit, opt.max);
6495
+ const cutText = cutLength(ctx.beforeText, value, opt.unit, opt.max);
6499
6496
  return cutText;
6500
6497
  },
6501
6498
 
@@ -6570,6 +6567,376 @@ length.fromDataset = function fromDataset(dataset, _el) {
6570
6567
  return length(options);
6571
6568
  };
6572
6569
 
6570
+ /**
6571
+ * The script is part of TextInputGuard.
6572
+ *
6573
+ * AUTHOR:
6574
+ * natade-jp (https://github.com/natade-jp)
6575
+ *
6576
+ * LICENSE:
6577
+ * The MIT license https://opensource.org/licenses/MIT
6578
+ */
6579
+
6580
+
6581
+ /**
6582
+ * width ルールのオプション
6583
+ * @typedef {Object} WidthRuleOptions
6584
+ * @property {number} [max] - 最大長(全角は2, 半角は1)
6585
+ * @property {"block"|"error"} [overflowInput="block"] - 入力中に最大長を超えたときの挙動
6586
+ *
6587
+ * block : 最大長を超える部分を切る
6588
+ * error : エラーを積むだけ(値は変更しない)
6589
+ */
6590
+
6591
+ /**
6592
+ * width ルールを生成する
6593
+ * @param {WidthRuleOptions} [options]
6594
+ * @returns {Rule}
6595
+ */
6596
+ function width(options = {}) {
6597
+ /** @type {WidthRuleOptions} */
6598
+ const opt = {
6599
+ max: typeof options.max === "number" ? options.max : undefined,
6600
+ overflowInput: options.overflowInput ?? "block"
6601
+ };
6602
+
6603
+ return {
6604
+ name: "length",
6605
+ targets: ["input", "textarea"],
6606
+
6607
+ normalizeChar(value, ctx) {
6608
+ // block 以外は何もしない
6609
+ if (opt.overflowInput !== "block") {
6610
+ return value;
6611
+ }
6612
+ // max 未指定なら制限なし
6613
+ if (typeof opt.max !== "number") {
6614
+ return value;
6615
+ }
6616
+
6617
+ /*
6618
+ * 指定したテキストを切り出す
6619
+ * - 0幅 ... グラフェムを構成する要素
6620
+ * (結合文字, 異体字セレクタ, スキントーン修飾子,
6621
+ * Tag Sequence 構成文字, ZWSP, ZWNJ, ZWJ, WJ)
6622
+ * - 1幅 ... ASCII文字, 半角カタカナ, Regional Indicator(単体)
6623
+ * - 2幅 ... 上記以外
6624
+ * ※ Unicode が配布してる EastAsianWidth.txt は使用していません。
6625
+ * (目的としては Shift_JIS 時代の半角全角だと思われるため)
6626
+ */
6627
+ const cutText = Mojix.cutTextForWidth(value, 0, opt.max);
6628
+ return cutText;
6629
+ },
6630
+
6631
+ validate(value, ctx) {
6632
+ // error 以外は何もしない
6633
+ if (opt.overflowInput !== "error") {
6634
+ return value;
6635
+ }
6636
+ // max 未指定なら制限なし
6637
+ if (typeof opt.max !== "number") {
6638
+ return;
6639
+ }
6640
+
6641
+ /*
6642
+ * 指定したテキストの横幅を半角/全角でカウント
6643
+ * - 0幅 ... グラフェムを構成する要素
6644
+ * (結合文字, 異体字セレクタ, スキントーン修飾子,
6645
+ * Tag Sequence 構成文字, ZWSP, ZWNJ, ZWJ, WJ)
6646
+ * - 1幅 ... ASCII文字, 半角カタカナ, Regional Indicator(単体)
6647
+ * - 2幅 ... 上記以外
6648
+ * ※ Unicode が配布してる EastAsianWidth.txt は使用していません。
6649
+ * (目的としては Shift_JIS 時代の半角全角だと思われるため)
6650
+ */
6651
+ const len = Mojix.getWidth(value);
6652
+ if (len > opt.max) {
6653
+ ctx.pushError({
6654
+ code: "length.max_overflow",
6655
+ rule: "length",
6656
+ phase: "validate",
6657
+ detail: { max: opt.max, actual: len }
6658
+ });
6659
+ }
6660
+ }
6661
+ };
6662
+ }
6663
+
6664
+ /**
6665
+ * datasetから length ルールを生成する
6666
+ * - data-tig-rules-length が無ければ null
6667
+ * - オプションは data-tig-rules-length-xxx から読む
6668
+ *
6669
+ * 対応する data 属性(dataset 名)
6670
+ * - data-tig-rules-length -> dataset.tigRulesWidth
6671
+ * - data-tig-rules-length-max -> dataset.tigRulesWidthMax
6672
+ * - data-tig-rules-length-overflow-input -> dataset.tigRulesWidthOverflowInput
6673
+ *
6674
+ * @param {DOMStringMap} dataset
6675
+ * @param {HTMLInputElement|HTMLTextAreaElement} _el
6676
+ * @returns {Rule|null}
6677
+ */
6678
+ width.fromDataset = function fromDataset(dataset, _el) {
6679
+ // ON判定
6680
+ if (dataset.tigRulesWidth == null) {
6681
+ return null;
6682
+ }
6683
+
6684
+ /** @type {WidthRuleOptions} */
6685
+ const options = {};
6686
+
6687
+ const max = parseDatasetNumber(dataset.tigRulesWidthMax);
6688
+ if (max != null) {
6689
+ options.max = max;
6690
+ }
6691
+
6692
+ const overflowInput = parseDatasetEnum(
6693
+ dataset.tigRulesWidthOverflowInput,
6694
+ ["block", "error"]
6695
+ );
6696
+ if (overflowInput != null) {
6697
+ options.overflowInput = overflowInput;
6698
+ }
6699
+
6700
+ return width(options);
6701
+ };
6702
+
6703
+ /**
6704
+ * The script is part of TextInputGuard.
6705
+ *
6706
+ * AUTHOR:
6707
+ * natade-jp (https://github.com/natade-jp)
6708
+ *
6709
+ * LICENSE:
6710
+ * The MIT license https://opensource.org/licenses/MIT
6711
+ */
6712
+
6713
+
6714
+ /**
6715
+ * bytes ルールのオプション
6716
+ * @typedef {Object} BytesRuleOptions
6717
+ * @property {number} [max] - 最大長(グラフェム数)。未指定なら制限なし
6718
+ * @property {"block"|"error"} [overflowInput="block"] - 入力中に最大長を超えたときの挙動
6719
+ * @property {"utf-8"|"utf-16"|"utf-32"|"sjis"|"cp932"} [unit="utf-8"] - サイズの単位(sjis系を使用する場合はfilterも必須)
6720
+ *
6721
+ * block : 最大長を超える部分を切る
6722
+ * error : エラーを積むだけ(値は変更しない)
6723
+ */
6724
+
6725
+ /**
6726
+ * グラフェム(1グラフェムは、UTF-32の配列)
6727
+ * @typedef {number[]} Grapheme
6728
+ */
6729
+
6730
+ /**
6731
+ * グラフェム/UTF-16コード単位/UTF-32コード単位の長さを調べる
6732
+ * @param {string} text
6733
+ * @param {"utf-8"|"utf-16"|"utf-32"|"sjis"|"cp932"} unit
6734
+ * @returns {number}
6735
+ */
6736
+ const getTextBytesByUnit = function(text, unit) {
6737
+ if (text.length === 0) {
6738
+ return 0;
6739
+ }
6740
+ if (unit === "utf-8") {
6741
+ return Mojix.toUTF8Array(text).length;
6742
+ } else if (unit === "utf-16") {
6743
+ return Mojix.toUTF16Array(text).length * 2;
6744
+ } else if (unit === "utf-32") {
6745
+ return Mojix.toUTF32Array(text).length * 4;
6746
+ } else if (unit === "sjis" || unit === "cp932") {
6747
+ return Mojix.encode(text, "Shift_JIS").length;
6748
+ } else {
6749
+ // ここには来ない
6750
+ throw new Error(`Invalid unit: ${unit}`);
6751
+ }
6752
+ };
6753
+
6754
+ /**
6755
+ * グラフェム/UTF-16コード単位/UTF-32コード単位でテキストを切る
6756
+ * @param {string} text
6757
+ * @param {"utf-8"|"utf-16"|"utf-32"|"sjis"|"cp932"} unit
6758
+ * @param {number} max
6759
+ * @returns {string}
6760
+ */
6761
+ const cutTextByUnit = function(text, unit, max) {
6762
+ /**
6763
+ * グラフェムの配列
6764
+ * @type {Grapheme[]}
6765
+ */
6766
+ const graphemeArray = Mojix.toMojiArrayFromString(text);
6767
+
6768
+ /**
6769
+ * 現在の位置
6770
+ */
6771
+ let count = 0;
6772
+
6773
+ /**
6774
+ * グラフェムの配列(出力用)
6775
+ * @type {Grapheme[]}
6776
+ */
6777
+ const outputGraphemeArray = [];
6778
+
6779
+ for (let i = 0; i < graphemeArray.length; i++) {
6780
+ const g = graphemeArray[i];
6781
+
6782
+ // 1グラフェムあたりの長さ
6783
+ let byteCount = 0;
6784
+ if (unit === "utf-8") {
6785
+ byteCount = Mojix.toUTF8Array(Mojix.toStringFromMojiArray([g])).length;
6786
+ } else if (unit === "utf-16") {
6787
+ byteCount = Mojix.toUTF16Array(Mojix.toStringFromMojiArray([g])).length * 2;
6788
+ } else if (unit === "utf-32") {
6789
+ byteCount = Mojix.toUTF32Array(Mojix.toStringFromMojiArray([g])).length * 4;
6790
+ } else if (unit === "sjis" || unit === "cp932") {
6791
+ byteCount = Mojix.encode(Mojix.toStringFromMojiArray([g]), "Shift_JIS").length;
6792
+ }
6793
+
6794
+ if (count + byteCount > max) {
6795
+ // 空配列を渡すとNUL文字を返すため、空配列のときは空文字を返す
6796
+ if (outputGraphemeArray.length === 0) {
6797
+ return "";
6798
+ }
6799
+ // 超える前の位置で文字列化して返す
6800
+ return Mojix.toStringFromMojiArray(outputGraphemeArray);
6801
+ }
6802
+
6803
+ count += byteCount;
6804
+ outputGraphemeArray.push(g);
6805
+ }
6806
+
6807
+ // 全部入るなら元の text を返す
6808
+ return text;
6809
+ };
6810
+
6811
+ /**
6812
+ * 元のテキストと追加のテキストの合計が max を超える場合、追加のテキストを切って合計が max に収まるようにする
6813
+ * @param {string} beforeText 元のテキスト
6814
+ * @param {string} insertedText 追加するテキスト
6815
+ * @param {"utf-8"|"utf-16"|"utf-32"|"sjis"|"cp932"} unit
6816
+ * @param {number} max
6817
+ * @returns {string} 追加するテキストを切ったもの(切る必要がない場合は insertedText をそのまま返す)
6818
+ */
6819
+ const cutBytes = function(beforeText, insertedText, unit, max) {
6820
+ const beforeTextLen = getTextBytesByUnit(beforeText, unit);
6821
+
6822
+ // すでに最大長を超えている場合は追加のテキストを全て切る
6823
+ if (beforeTextLen >= max) { return ""; }
6824
+
6825
+ const insertedTextLen = getTextBytesByUnit(insertedText, unit);
6826
+ const totalLen = beforeTextLen + insertedTextLen;
6827
+
6828
+ if (totalLen <= max) {
6829
+ // 今回の追加で範囲内に収まるなら何もしない
6830
+ return insertedText;
6831
+ }
6832
+
6833
+ // 超える場合は追加のテキストを切る
6834
+ const allowedAddLen = max - beforeTextLen;
6835
+ return cutTextByUnit(insertedText, unit, allowedAddLen);
6836
+ };
6837
+
6838
+ /**
6839
+ * bytes ルールを生成する
6840
+ * @param {BytesRuleOptions} [options]
6841
+ * @returns {Rule}
6842
+ */
6843
+ function bytes(options = {}) {
6844
+ /** @type {BytesRuleOptions} */
6845
+ const opt = {
6846
+ max: typeof options.max === "number" ? options.max : undefined,
6847
+ overflowInput: options.overflowInput ?? "block",
6848
+ unit: options.unit ?? "utf-8"
6849
+ };
6850
+
6851
+ return {
6852
+ name: "bytes",
6853
+ targets: ["input", "textarea"],
6854
+
6855
+ normalizeChar(value, ctx) {
6856
+ // block 以外は何もしない
6857
+ if (opt.overflowInput !== "block") {
6858
+ return value;
6859
+ }
6860
+ // max 未指定なら制限なし
6861
+ if (typeof opt.max !== "number") {
6862
+ return value;
6863
+ }
6864
+
6865
+ const cutText = cutBytes(ctx.beforeText, value, opt.unit, opt.max);
6866
+ return cutText;
6867
+ },
6868
+
6869
+ validate(value, ctx) {
6870
+ // error 以外は何もしない
6871
+ if (opt.overflowInput !== "error") {
6872
+ return value;
6873
+ }
6874
+ // max 未指定なら制限なし
6875
+ if (typeof opt.max !== "number") {
6876
+ return;
6877
+ }
6878
+
6879
+ const len = getTextBytesByUnit(value, opt.unit);
6880
+ if (len > opt.max) {
6881
+ ctx.pushError({
6882
+ code: "bytes.max_overflow",
6883
+ rule: "bytes",
6884
+ phase: "validate",
6885
+ detail: { max: opt.max, actual: len }
6886
+ });
6887
+ }
6888
+ }
6889
+ };
6890
+ }
6891
+
6892
+ /**
6893
+ * datasetから bytes ルールを生成する
6894
+ * - data-tig-rules-bytes が無ければ null
6895
+ * - オプションは data-tig-rules-bytes-xxx から読む
6896
+ *
6897
+ * 対応する data 属性(dataset 名)
6898
+ * - data-tig-rules-bytes -> dataset.tigRulesBytes
6899
+ * - data-tig-rules-bytes-max -> dataset.tigRulesBytesMax
6900
+ * - data-tig-rules-bytes-overflow-input -> dataset.tigRulesBytesOverflowInput
6901
+ * - data-tig-rules-bytes-unit -> dataset.tigRulesBytesUnit
6902
+ *
6903
+ * @param {DOMStringMap} dataset
6904
+ * @param {HTMLInputElement|HTMLTextAreaElement} _el
6905
+ * @returns {Rule|null}
6906
+ */
6907
+ bytes.fromDataset = function fromDataset(dataset, _el) {
6908
+ // ON判定
6909
+ if (dataset.tigRulesBytes == null) {
6910
+ return null;
6911
+ }
6912
+
6913
+ /** @type {BytesRuleOptions} */
6914
+ const options = {};
6915
+
6916
+ const max = parseDatasetNumber(dataset.tigRulesBytesMax);
6917
+ if (max != null) {
6918
+ options.max = max;
6919
+ }
6920
+
6921
+ const overflowInput = parseDatasetEnum(
6922
+ dataset.tigRulesBytesOverflowInput,
6923
+ ["block", "error"]
6924
+ );
6925
+ if (overflowInput != null) {
6926
+ options.overflowInput = overflowInput;
6927
+ }
6928
+
6929
+ const unit = parseDatasetEnum(
6930
+ dataset.tigRulesBytesUnit,
6931
+ ["utf-8", "utf-16", "utf-32", "sjis", "cp932"]
6932
+ );
6933
+ if (unit != null) {
6934
+ options.unit = unit;
6935
+ }
6936
+
6937
+ return bytes(options);
6938
+ };
6939
+
6573
6940
  /**
6574
6941
  * The script is part of TextInputGuard.
6575
6942
  *
@@ -6829,6 +7196,8 @@ const auto = new InputGuardAutoAttach(attach, [
6829
7196
  { name: "ascii", fromDataset: ascii.fromDataset },
6830
7197
  { name: "filter", fromDataset: filter.fromDataset },
6831
7198
  { name: "length", fromDataset: length.fromDataset },
7199
+ { name: "width", fromDataset: width.fromDataset },
7200
+ { name: "bytes", fromDataset: bytes.fromDataset },
6832
7201
  { name: "prefix", fromDataset: prefix.fromDataset },
6833
7202
  { name: "suffix", fromDataset: suffix.fromDataset },
6834
7203
  { name: "trim", fromDataset: trim.fromDataset }
@@ -6851,6 +7220,8 @@ const rules = {
6851
7220
  ascii,
6852
7221
  filter,
6853
7222
  length,
7223
+ width,
7224
+ bytes,
6854
7225
  prefix,
6855
7226
  suffix,
6856
7227
  trim
@@ -6858,10 +7229,10 @@ const rules = {
6858
7229
 
6859
7230
  /**
6860
7231
  * バージョン(ビルド時に置換したいならここを差し替える)
6861
- * 例: rollup replace で ""0.1.5"" を package.json の version に置換
7232
+ * 例: rollup replace で ""0.1.6"" を package.json の version に置換
6862
7233
  */
6863
7234
  // @ts-ignore
6864
7235
  // eslint-disable-next-line no-undef
6865
- const version = "0.1.5" ;
7236
+ const version = "0.1.6" ;
6866
7237
 
6867
- export { ascii, attach, attachAll, autoAttach, comma, digits, filter, kana, length, numeric, prefix, rules, suffix, trim, version };
7238
+ export { ascii, attach, attachAll, autoAttach, bytes, comma, digits, filter, kana, length, numeric, prefix, rules, suffix, trim, version, width };