@salve-software/react-native-nitro-input-mask 1.0.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.
Files changed (204) hide show
  1. package/LICENSE +21 -0
  2. package/NitroInputMask.podspec +31 -0
  3. package/README.md +181 -0
  4. package/android/CMakeLists.txt +32 -0
  5. package/android/build.gradle +148 -0
  6. package/android/fix-prefab.gradle +51 -0
  7. package/android/gradle.properties +5 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +9 -0
  10. package/android/src/main/java/com/nitroinputmask/NitroInputMaskContext.kt +7 -0
  11. package/android/src/main/java/com/nitroinputmask/NitroInputMaskModule.kt +108 -0
  12. package/android/src/main/java/com/nitroinputmask/NitroInputMaskPackage.kt +29 -0
  13. package/android/src/main/java/com/nitroinputmask/NitroInputMaskServiceModule.kt +17 -0
  14. package/android/src/main/java/com/nitroinputmask/NitroInputMaskTextWatcher.kt +127 -0
  15. package/android/src/main/java/com/nitroinputmask/engine/CursorEngine.kt +30 -0
  16. package/android/src/main/java/com/nitroinputmask/engine/MaskEngine.kt +261 -0
  17. package/android/src/main/java/com/nitroinputmask/engine/MaskEngineFactory.kt +18 -0
  18. package/android/src/main/java/com/nitroinputmask/engine/MaskEngineProtocol.kt +8 -0
  19. package/android/src/main/java/com/nitroinputmask/masks/CreditCardMaskEngine.kt +47 -0
  20. package/android/src/main/java/com/nitroinputmask/masks/CustomMaskEngine.kt +16 -0
  21. package/android/src/main/java/com/nitroinputmask/masks/DatetimeMaskEngine.kt +38 -0
  22. package/android/src/main/java/com/nitroinputmask/masks/MoneyMaskEngine.kt +66 -0
  23. package/ios/Bridge.h +8 -0
  24. package/ios/NitroInputMaskDelegateProxy.swift +194 -0
  25. package/ios/NitroInputMaskModule.swift +114 -0
  26. package/ios/NitroInputMaskServiceModule.swift +11 -0
  27. package/ios/engine/CursorEngine.swift +52 -0
  28. package/ios/engine/MaskEngine.swift +221 -0
  29. package/ios/engine/MaskEngineFactory.swift +16 -0
  30. package/ios/engine/MaskEngineProtocol.swift +7 -0
  31. package/ios/masks/CreditCardMaskEngine.swift +57 -0
  32. package/ios/masks/CustomMaskEngine.swift +16 -0
  33. package/ios/masks/DatetimeMaskEngine.swift +46 -0
  34. package/ios/masks/MoneyMaskEngine.swift +66 -0
  35. package/lib/commonjs/classes/NitroInputMaskService/NitroInputMaskService.class.js +74 -0
  36. package/lib/commonjs/classes/NitroInputMaskService/NitroInputMaskService.class.js.map +1 -0
  37. package/lib/commonjs/classes/NitroInputMaskService/index.js +13 -0
  38. package/lib/commonjs/classes/NitroInputMaskService/index.js.map +1 -0
  39. package/lib/commonjs/classes/NitroInputMaskService/types/IApplyMaskProps.js +6 -0
  40. package/lib/commonjs/classes/NitroInputMaskService/types/IApplyMaskProps.js.map +1 -0
  41. package/lib/commonjs/classes/NitroInputMaskService/types/index.js +2 -0
  42. package/lib/commonjs/classes/NitroInputMaskService/types/index.js.map +1 -0
  43. package/lib/commonjs/classes/index.js +17 -0
  44. package/lib/commonjs/classes/index.js.map +1 -0
  45. package/lib/commonjs/components/NitroInputMask/index.js +41 -0
  46. package/lib/commonjs/components/NitroInputMask/index.js.map +1 -0
  47. package/lib/commonjs/components/NitroInputMask/types/NitroInputMaskProps.js +6 -0
  48. package/lib/commonjs/components/NitroInputMask/types/NitroInputMaskProps.js.map +1 -0
  49. package/lib/commonjs/components/NitroInputMask/types/index.js +2 -0
  50. package/lib/commonjs/components/NitroInputMask/types/index.js.map +1 -0
  51. package/lib/commonjs/components/index.js +17 -0
  52. package/lib/commonjs/components/index.js.map +1 -0
  53. package/lib/commonjs/index.js +39 -0
  54. package/lib/commonjs/index.js.map +1 -0
  55. package/lib/commonjs/nitro-module.js +17 -0
  56. package/lib/commonjs/nitro-module.js.map +1 -0
  57. package/lib/commonjs/package.json +1 -0
  58. package/lib/commonjs/specs/nitro-input-mask-service.nitro.js +6 -0
  59. package/lib/commonjs/specs/nitro-input-mask-service.nitro.js.map +1 -0
  60. package/lib/commonjs/specs/nitro-input-mask.nitro.js +6 -0
  61. package/lib/commonjs/specs/nitro-input-mask.nitro.js.map +1 -0
  62. package/lib/commonjs/types/CreditCardMaskOptions.js +2 -0
  63. package/lib/commonjs/types/CreditCardMaskOptions.js.map +1 -0
  64. package/lib/commonjs/types/CustomMaskOptions.js +2 -0
  65. package/lib/commonjs/types/CustomMaskOptions.js.map +1 -0
  66. package/lib/commonjs/types/DatetimeMaskOptions.js +2 -0
  67. package/lib/commonjs/types/DatetimeMaskOptions.js.map +1 -0
  68. package/lib/commonjs/types/MaskConfig.js +6 -0
  69. package/lib/commonjs/types/MaskConfig.js.map +1 -0
  70. package/lib/commonjs/types/MoneyMaskOptions.js +2 -0
  71. package/lib/commonjs/types/MoneyMaskOptions.js.map +1 -0
  72. package/lib/commonjs/types/index.js +2 -0
  73. package/lib/commonjs/types/index.js.map +1 -0
  74. package/lib/module/classes/NitroInputMaskService/NitroInputMaskService.class.js +70 -0
  75. package/lib/module/classes/NitroInputMaskService/NitroInputMaskService.class.js.map +1 -0
  76. package/lib/module/classes/NitroInputMaskService/index.js +4 -0
  77. package/lib/module/classes/NitroInputMaskService/index.js.map +1 -0
  78. package/lib/module/classes/NitroInputMaskService/types/IApplyMaskProps.js +4 -0
  79. package/lib/module/classes/NitroInputMaskService/types/IApplyMaskProps.js.map +1 -0
  80. package/lib/module/classes/NitroInputMaskService/types/index.js +2 -0
  81. package/lib/module/classes/NitroInputMaskService/types/index.js.map +1 -0
  82. package/lib/module/classes/index.js +4 -0
  83. package/lib/module/classes/index.js.map +1 -0
  84. package/lib/module/components/NitroInputMask/index.js +35 -0
  85. package/lib/module/components/NitroInputMask/index.js.map +1 -0
  86. package/lib/module/components/NitroInputMask/types/NitroInputMaskProps.js +4 -0
  87. package/lib/module/components/NitroInputMask/types/NitroInputMaskProps.js.map +1 -0
  88. package/lib/module/components/NitroInputMask/types/index.js +2 -0
  89. package/lib/module/components/NitroInputMask/types/index.js.map +1 -0
  90. package/lib/module/components/index.js +4 -0
  91. package/lib/module/components/index.js.map +1 -0
  92. package/lib/module/index.js +6 -0
  93. package/lib/module/index.js.map +1 -0
  94. package/lib/module/nitro-module.js +12 -0
  95. package/lib/module/nitro-module.js.map +1 -0
  96. package/lib/module/specs/nitro-input-mask-service.nitro.js +4 -0
  97. package/lib/module/specs/nitro-input-mask-service.nitro.js.map +1 -0
  98. package/lib/module/specs/nitro-input-mask.nitro.js +4 -0
  99. package/lib/module/specs/nitro-input-mask.nitro.js.map +1 -0
  100. package/lib/module/types/CreditCardMaskOptions.js +2 -0
  101. package/lib/module/types/CreditCardMaskOptions.js.map +1 -0
  102. package/lib/module/types/CustomMaskOptions.js +2 -0
  103. package/lib/module/types/CustomMaskOptions.js.map +1 -0
  104. package/lib/module/types/DatetimeMaskOptions.js +2 -0
  105. package/lib/module/types/DatetimeMaskOptions.js.map +1 -0
  106. package/lib/module/types/MaskConfig.js +4 -0
  107. package/lib/module/types/MaskConfig.js.map +1 -0
  108. package/lib/module/types/MoneyMaskOptions.js +2 -0
  109. package/lib/module/types/MoneyMaskOptions.js.map +1 -0
  110. package/lib/module/types/index.js +2 -0
  111. package/lib/module/types/index.js.map +1 -0
  112. package/lib/typescript/src/classes/NitroInputMaskService/NitroInputMaskService.class.d.ts +50 -0
  113. package/lib/typescript/src/classes/NitroInputMaskService/NitroInputMaskService.class.d.ts.map +1 -0
  114. package/lib/typescript/src/classes/NitroInputMaskService/index.d.ts +3 -0
  115. package/lib/typescript/src/classes/NitroInputMaskService/index.d.ts.map +1 -0
  116. package/lib/typescript/src/classes/NitroInputMaskService/types/IApplyMaskProps.d.ts +5 -0
  117. package/lib/typescript/src/classes/NitroInputMaskService/types/IApplyMaskProps.d.ts.map +1 -0
  118. package/lib/typescript/src/classes/NitroInputMaskService/types/index.d.ts +2 -0
  119. package/lib/typescript/src/classes/NitroInputMaskService/types/index.d.ts.map +1 -0
  120. package/lib/typescript/src/classes/index.d.ts +2 -0
  121. package/lib/typescript/src/classes/index.d.ts.map +1 -0
  122. package/lib/typescript/src/components/NitroInputMask/index.d.ts +5 -0
  123. package/lib/typescript/src/components/NitroInputMask/index.d.ts.map +1 -0
  124. package/lib/typescript/src/components/NitroInputMask/types/NitroInputMaskProps.d.ts +4 -0
  125. package/lib/typescript/src/components/NitroInputMask/types/NitroInputMaskProps.d.ts.map +1 -0
  126. package/lib/typescript/src/components/NitroInputMask/types/index.d.ts +2 -0
  127. package/lib/typescript/src/components/NitroInputMask/types/index.d.ts.map +1 -0
  128. package/lib/typescript/src/components/index.d.ts +2 -0
  129. package/lib/typescript/src/components/index.d.ts.map +1 -0
  130. package/lib/typescript/src/index.d.ts +4 -0
  131. package/lib/typescript/src/index.d.ts.map +1 -0
  132. package/lib/typescript/src/nitro-module.d.ts +5 -0
  133. package/lib/typescript/src/nitro-module.d.ts.map +1 -0
  134. package/lib/typescript/src/specs/nitro-input-mask-service.nitro.d.ts +9 -0
  135. package/lib/typescript/src/specs/nitro-input-mask-service.nitro.d.ts.map +1 -0
  136. package/lib/typescript/src/specs/nitro-input-mask.nitro.d.ts +23 -0
  137. package/lib/typescript/src/specs/nitro-input-mask.nitro.d.ts.map +1 -0
  138. package/lib/typescript/src/types/CreditCardMaskOptions.d.ts +5 -0
  139. package/lib/typescript/src/types/CreditCardMaskOptions.d.ts.map +1 -0
  140. package/lib/typescript/src/types/CustomMaskOptions.d.ts +4 -0
  141. package/lib/typescript/src/types/CustomMaskOptions.d.ts.map +1 -0
  142. package/lib/typescript/src/types/DatetimeMaskOptions.d.ts +4 -0
  143. package/lib/typescript/src/types/DatetimeMaskOptions.d.ts.map +1 -0
  144. package/lib/typescript/src/types/MaskConfig.d.ts +18 -0
  145. package/lib/typescript/src/types/MaskConfig.d.ts.map +1 -0
  146. package/lib/typescript/src/types/MoneyMaskOptions.d.ts +9 -0
  147. package/lib/typescript/src/types/MoneyMaskOptions.d.ts.map +1 -0
  148. package/lib/typescript/src/types/index.d.ts +6 -0
  149. package/lib/typescript/src/types/index.d.ts.map +1 -0
  150. package/nitro.json +40 -0
  151. package/nitrogen/generated/.gitattributes +1 -0
  152. package/nitrogen/generated/android/NitroInputMask+autolinking.cmake +83 -0
  153. package/nitrogen/generated/android/NitroInputMask+autolinking.gradle +27 -0
  154. package/nitrogen/generated/android/NitroInputMaskOnLoad.cpp +70 -0
  155. package/nitrogen/generated/android/NitroInputMaskOnLoad.hpp +34 -0
  156. package/nitrogen/generated/android/c++/JHybridNitroInputMaskServiceSpec.cpp +57 -0
  157. package/nitrogen/generated/android/c++/JHybridNitroInputMaskServiceSpec.hpp +63 -0
  158. package/nitrogen/generated/android/c++/JHybridNitroInputMaskSpec.cpp +68 -0
  159. package/nitrogen/generated/android/c++/JHybridNitroInputMaskSpec.hpp +66 -0
  160. package/nitrogen/generated/android/c++/JNitroMaskOptions.hpp +94 -0
  161. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinputmask/HybridNitroInputMaskServiceSpec.kt +54 -0
  162. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinputmask/HybridNitroInputMaskSpec.kt +66 -0
  163. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinputmask/NitroInputMaskOnLoad.kt +35 -0
  164. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinputmask/NitroMaskOptions.kt +96 -0
  165. package/nitrogen/generated/ios/NitroInputMask+autolinking.rb +62 -0
  166. package/nitrogen/generated/ios/NitroInputMask-Swift-Cxx-Bridge.cpp +50 -0
  167. package/nitrogen/generated/ios/NitroInputMask-Swift-Cxx-Bridge.hpp +124 -0
  168. package/nitrogen/generated/ios/NitroInputMask-Swift-Cxx-Umbrella.hpp +53 -0
  169. package/nitrogen/generated/ios/NitroInputMaskAutolinking.mm +41 -0
  170. package/nitrogen/generated/ios/NitroInputMaskAutolinking.swift +38 -0
  171. package/nitrogen/generated/ios/c++/HybridNitroInputMaskServiceSpecSwift.cpp +11 -0
  172. package/nitrogen/generated/ios/c++/HybridNitroInputMaskServiceSpecSwift.hpp +85 -0
  173. package/nitrogen/generated/ios/c++/HybridNitroInputMaskSpecSwift.cpp +11 -0
  174. package/nitrogen/generated/ios/c++/HybridNitroInputMaskSpecSwift.hpp +101 -0
  175. package/nitrogen/generated/ios/swift/HybridNitroInputMaskServiceSpec.swift +55 -0
  176. package/nitrogen/generated/ios/swift/HybridNitroInputMaskServiceSpec_cxx.swift +138 -0
  177. package/nitrogen/generated/ios/swift/HybridNitroInputMaskSpec.swift +58 -0
  178. package/nitrogen/generated/ios/swift/HybridNitroInputMaskSpec_cxx.swift +170 -0
  179. package/nitrogen/generated/ios/swift/NitroMaskOptions.swift +204 -0
  180. package/nitrogen/generated/shared/c++/HybridNitroInputMaskServiceSpec.cpp +21 -0
  181. package/nitrogen/generated/shared/c++/HybridNitroInputMaskServiceSpec.hpp +64 -0
  182. package/nitrogen/generated/shared/c++/HybridNitroInputMaskSpec.cpp +24 -0
  183. package/nitrogen/generated/shared/c++/HybridNitroInputMaskSpec.hpp +67 -0
  184. package/nitrogen/generated/shared/c++/NitroMaskOptions.hpp +120 -0
  185. package/package.json +136 -0
  186. package/src/classes/NitroInputMaskService/NitroInputMaskService.class.ts +70 -0
  187. package/src/classes/NitroInputMaskService/index.ts +2 -0
  188. package/src/classes/NitroInputMaskService/types/IApplyMaskProps.ts +3 -0
  189. package/src/classes/NitroInputMaskService/types/index.ts +1 -0
  190. package/src/classes/index.ts +1 -0
  191. package/src/components/NitroInputMask/index.tsx +41 -0
  192. package/src/components/NitroInputMask/types/NitroInputMaskProps.ts +4 -0
  193. package/src/components/NitroInputMask/types/index.ts +1 -0
  194. package/src/components/index.ts +1 -0
  195. package/src/index.ts +3 -0
  196. package/src/nitro-module.ts +14 -0
  197. package/src/specs/nitro-input-mask-service.nitro.ts +6 -0
  198. package/src/specs/nitro-input-mask.nitro.ts +25 -0
  199. package/src/types/CreditCardMaskOptions.ts +4 -0
  200. package/src/types/CustomMaskOptions.ts +3 -0
  201. package/src/types/DatetimeMaskOptions.ts +3 -0
  202. package/src/types/MaskConfig.ts +10 -0
  203. package/src/types/MoneyMaskOptions.ts +8 -0
  204. package/src/types/index.ts +5 -0
@@ -0,0 +1,127 @@
1
+ package com.nitroinputmask
2
+
3
+ import android.text.Editable
4
+ import android.text.TextWatcher
5
+ import android.widget.EditText
6
+ import com.nitroinputmask.engine.CursorEngine
7
+ import com.nitroinputmask.engine.MaskEngineProtocol
8
+ import java.lang.ref.WeakReference
9
+
10
+ // Weak reference avoids EditText → TextWatcher → EditText retain cycle
11
+ internal class NitroInputMaskTextWatcher(var engine: MaskEngineProtocol, editText: EditText) : TextWatcher {
12
+ private val editTextRef = WeakReference(editText)
13
+ var isProgrammatic = false
14
+ private var prevLength = 0
15
+ private var isDeletion = false
16
+ private var changeStart = 0
17
+ private var prevMasked = ""
18
+ private var prevCursorEnd = 0
19
+
20
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
21
+ prevLength = s?.length ?: 0
22
+ isDeletion = after < count
23
+ prevMasked = s?.toString() ?: ""
24
+ prevCursorEnd = start + count
25
+ }
26
+
27
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
28
+ changeStart = start
29
+ }
30
+
31
+ override fun afterTextChanged(s: Editable?) {
32
+ if (isProgrammatic) return
33
+ val editText = editTextRef.get() ?: return
34
+ val input = s?.toString()?.replace("\n", "") ?: ""
35
+
36
+ var (masked, _) = engine.apply(input)
37
+
38
+ // Separator deletion: masking re-produces the same text because the separator
39
+ // is always re-inserted. If there is data, block the deletion (text unchanged,
40
+ // cursor moves to just before the separator). If there is no data, clear the field.
41
+ var isSeparatorDeletion = false
42
+ if (isDeletion && masked == prevMasked) {
43
+ val (_, raw) = engine.apply(input)
44
+ if (raw.isEmpty()) {
45
+ masked = ""
46
+ } else {
47
+ isSeparatorDeletion = true
48
+ // masked stays == prevMasked; cursor will move to changeStart below
49
+ }
50
+ }
51
+
52
+ val cursorPos = when {
53
+ engine.wantsTrailingCursor -> when {
54
+ isSeparatorDeletion -> changeStart
55
+ isDeletion -> {
56
+ val digitsAfterDeleted = prevMasked.drop(prevCursorEnd).count { it.isDigit() }
57
+ if (digitsAfterDeleted == 0) {
58
+ masked.length
59
+ } else {
60
+ positionAfterDigits(prevMasked.take(changeStart).count { it.isDigit() }, masked)
61
+ }
62
+ }
63
+ else -> {
64
+ val digitsAfter = countDigits(prevMasked, prevCursorEnd)
65
+ cursorPositionWithDigitsAfter(digitsAfter, masked)
66
+ }
67
+ }
68
+ isDeletion -> minOf(changeStart, masked.length)
69
+ else -> {
70
+ val expMask = engine.expandedMask
71
+ if (expMask != null) {
72
+ val raw = CursorEngine.offsetAfterInsertion(prevMasked, masked, expMask, changeStart)
73
+ skipLeadingLiterals(raw, masked, expMask)
74
+ } else {
75
+ masked.length
76
+ }
77
+ }
78
+ }
79
+
80
+ if (masked == s?.toString()) {
81
+ editText.setSelection(cursorPos)
82
+ } else {
83
+ isProgrammatic = true
84
+ s?.replace(0, s.length, masked)
85
+ isProgrammatic = false
86
+ editText.setSelection(minOf(cursorPos, editText.text.length))
87
+ }
88
+ }
89
+
90
+ private fun positionAfterDigits(count: Int, text: String): Int {
91
+ if (count == 0) return text.indexOfFirst { it.isDigit() }.takeIf { it >= 0 } ?: 0
92
+ var found = 0
93
+ for (i in text.indices) {
94
+ if (text[i].isDigit()) {
95
+ found++
96
+ if (found == count) return i + 1
97
+ }
98
+ }
99
+ return text.length
100
+ }
101
+
102
+ private fun skipLeadingLiterals(offset: Int, masked: String, mask: String): Int {
103
+ val dataTokens = setOf('9', 'A', '*')
104
+ var idx = offset
105
+ while (idx < mask.length && mask[idx] !in dataTokens) idx++
106
+ return minOf(idx, masked.length)
107
+ }
108
+
109
+ private fun countDigits(text: String, from: Int): Int {
110
+ if (from >= text.length) return 0
111
+ return text.substring(from).count { it.isDigit() }
112
+ }
113
+
114
+ private fun cursorPositionWithDigitsAfter(count: Int, text: String): Int {
115
+ if (count == 0) return text.length
116
+ var digitsFromEnd = 0
117
+ var pos = text.length
118
+ while (pos > 0) {
119
+ pos--
120
+ if (text[pos].isDigit()) {
121
+ digitsFromEnd++
122
+ if (digitsFromEnd == count) return pos
123
+ }
124
+ }
125
+ return 0
126
+ }
127
+ }
@@ -0,0 +1,30 @@
1
+ package com.nitroinputmask.engine
2
+
3
+ object CursorEngine {
4
+ fun offsetAtEnd(masked: String, mask: String): Int {
5
+ for (i in masked.length - 1 downTo 0) {
6
+ if (i < mask.length && (mask[i] == '9' || mask[i] == 'A' || mask[i] == '*')) {
7
+ return i + 1
8
+ }
9
+ }
10
+ return masked.length
11
+ }
12
+
13
+ fun offsetAfterInsertion(oldMasked: String, newMasked: String, mask: String, at: Int): Int {
14
+ var dataRankBefore = 0
15
+ for (i in 0 until minOf(at, mask.length)) {
16
+ val m = mask[i]
17
+ if (m == '9' || m == 'A' || m == '*') dataRankBefore++
18
+ }
19
+ val targetRank = dataRankBefore + 1
20
+ var dataCount = 0
21
+ for (i in 0 until minOf(newMasked.length, mask.length)) {
22
+ val m = mask[i]
23
+ if (m == '9' || m == 'A' || m == '*') {
24
+ dataCount++
25
+ if (dataCount == targetRank) return i + 1
26
+ }
27
+ }
28
+ return newMasked.length
29
+ }
30
+ }
@@ -0,0 +1,261 @@
1
+ package com.nitroinputmask.engine
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Slot — private internal representation of each compiled mask position
5
+ // ---------------------------------------------------------------------------
6
+
7
+ internal sealed class Slot {
8
+ object Digit : Slot()
9
+ object Letter : Slot()
10
+ object Any : Slot()
11
+ data class Literal(val char: Char) : Slot()
12
+
13
+ /**
14
+ * A slot that belongs to an inline `[from-to]` range.
15
+ *
16
+ * @param blockName the range content string, e.g. "1-12" (used as the
17
+ * partial-tracking key; two ranges with identical bounds
18
+ * will share partial state — acceptable trade-off)
19
+ * @param position 0-based index of this slot within the range block
20
+ * @param length total character width (= max(from.digits, to.digits))
21
+ * @param from inclusive lower bound of the valid range
22
+ * @param to inclusive upper bound of the valid range
23
+ */
24
+ data class RangeSlot(
25
+ val blockName: String,
26
+ val length: Int,
27
+ val from: Int,
28
+ val to: Int
29
+ ) : Slot()
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // CompiledMask — result of compile(); shared between MaskEngine and callers
34
+ // ---------------------------------------------------------------------------
35
+
36
+ internal data class CompiledMask(
37
+ internal val slots: List<Slot>,
38
+ /** Expanded mask string used by CursorEngine (range positions become '9'). */
39
+ val expandedMask: String
40
+ ) {
41
+ val isEmpty: Boolean get() = slots.isEmpty()
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // MaskEngine
46
+ // ---------------------------------------------------------------------------
47
+
48
+ object MaskEngine {
49
+
50
+ // -------------------------------------------------------------------------
51
+ // compile
52
+ // -------------------------------------------------------------------------
53
+
54
+ /**
55
+ * Compiles a mask string into a [CompiledMask].
56
+ *
57
+ * Supported tokens:
58
+ * - `9` — any digit
59
+ * - `A` — any letter
60
+ * - `*` — any alphanumeric
61
+ * - `[from-to]` — integer range, e.g. `[1-12]`, `[1-31]`, `[0-23]`
62
+ * - any other character — literal
63
+ *
64
+ * Note: two `[from-to]` blocks with identical bounds share the same
65
+ * `blockName` key and therefore share partial-tracking state during
66
+ * [apply]. This is an acceptable trade-off; prefer distinct bounds when
67
+ * designing date masks (e.g. `[1-12]/[1-31]/9999`).
68
+ */
69
+ internal fun compile(mask: String): CompiledMask {
70
+ val slots = mutableListOf<Slot>()
71
+ val expanded = StringBuilder()
72
+ var i = 0
73
+
74
+ while (i < mask.length) {
75
+ if (mask[i] == '[') {
76
+ val closeIdx = mask.indexOf(']', i)
77
+ if (closeIdx == -1) {
78
+ // Malformed — treat '[' as a literal and advance past it.
79
+ slots.add(Slot.Literal('['))
80
+ expanded.append('[')
81
+ i++
82
+ continue
83
+ }
84
+
85
+ val content = mask.substring(i + 1, closeIdx) // e.g. "1-12"
86
+ val dashIdx = content.indexOf('-')
87
+ val from = if (dashIdx > 0) content.substring(0, dashIdx).toIntOrNull() else null
88
+ val to = if (dashIdx > 0) content.substring(dashIdx + 1).toIntOrNull() else null
89
+ if (from != null && to != null) {
90
+ val length = maxOf(from.toString().length, to.toString().length)
91
+ repeat(length) {
92
+ slots.add(Slot.RangeSlot(content, length, from, to))
93
+ expanded.append('9')
94
+ }
95
+ }
96
+ else {
97
+ // Malformed range — emit the entire "[...]" as individual literals.
98
+ val literal = "[$content]"
99
+ for (c in literal) { slots.add(Slot.Literal(c)); expanded.append(c) }
100
+ }
101
+ i = closeIdx + 1
102
+ }
103
+ else {
104
+ when (val ch = mask[i]) {
105
+ '9' -> { slots.add(Slot.Digit); expanded.append('9') }
106
+ 'A' -> { slots.add(Slot.Letter); expanded.append('A') }
107
+ '*' -> { slots.add(Slot.Any); expanded.append('*') }
108
+ else -> { slots.add(Slot.Literal(ch)); expanded.append(ch) }
109
+ }
110
+ i++
111
+ }
112
+ }
113
+
114
+ return CompiledMask(slots, expanded.toString())
115
+ }
116
+
117
+ // -------------------------------------------------------------------------
118
+ // apply
119
+ // -------------------------------------------------------------------------
120
+
121
+ internal fun apply(input: String, compiled: CompiledMask): Pair<String, String> {
122
+ if (compiled.slots.isEmpty()) return Pair(input, input)
123
+
124
+ val masked = StringBuilder()
125
+ val raw = StringBuilder()
126
+ val slots = compiled.slots
127
+
128
+ // Accumulate per-block digit strings so we can validate range prefixes
129
+ val blockPartials = mutableMapOf<String, StringBuilder>()
130
+
131
+ var ii = 0 // index into input characters
132
+ var si = 0 // index into slots
133
+
134
+ while (si < slots.size && ii < input.length) {
135
+ val slot = slots[si]
136
+ val c = input[ii]
137
+
138
+ when (slot) {
139
+ is Slot.Digit -> {
140
+ if (c.isDigit()) {
141
+ masked.append(c); raw.append(c); ii++; si++
142
+ }
143
+ else {
144
+ ii++
145
+ }
146
+ }
147
+
148
+ is Slot.Letter -> {
149
+ if (c.isLetter()) {
150
+ masked.append(c); raw.append(c); ii++; si++
151
+ }
152
+ else {
153
+ ii++
154
+ }
155
+ }
156
+
157
+ is Slot.Any -> {
158
+ if (c.isLetterOrDigit()) {
159
+ masked.append(c); raw.append(c); ii++; si++
160
+ }
161
+ else {
162
+ ii++
163
+ }
164
+ }
165
+
166
+ is Slot.Literal -> {
167
+ masked.append(slot.char)
168
+ if (c == slot.char) ii++
169
+ si++
170
+ }
171
+
172
+ is Slot.RangeSlot -> {
173
+ if (c.isDigit()) {
174
+ val partial = blockPartials.getOrPut(slot.blockName) { StringBuilder() }
175
+ val candidate = partial.toString() + c
176
+
177
+ if (isValidPrefix(candidate, slot.from, slot.to, slot.length)) {
178
+ partial.append(c)
179
+ masked.append(c)
180
+ raw.append(c)
181
+ ii++
182
+ si++
183
+ }
184
+ else {
185
+ // Invalid digit for this range position — skip the input char
186
+ ii++
187
+ }
188
+ }
189
+ else {
190
+ // Non-digit where a range digit is expected — skip
191
+ ii++
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ // Only emit trailing literals if some input was actually consumed.
198
+ if (ii > 0) while (si < slots.size) {
199
+ val slot = slots[si]
200
+
201
+ if (slot is Slot.Literal) {
202
+ masked.append(slot.char)
203
+ si++
204
+ }
205
+ else {
206
+ break
207
+ }
208
+ }
209
+
210
+ return Pair(masked.toString(), raw.toString())
211
+ }
212
+
213
+ // -------------------------------------------------------------------------
214
+ // extractRaw
215
+ // -------------------------------------------------------------------------
216
+
217
+ internal fun extractRaw(text: String, compiled: CompiledMask): String {
218
+ return extractRaw(text, compiled.expandedMask)
219
+ }
220
+
221
+ private fun extractRaw(text: String, expandedMask: String): String {
222
+ val raw = StringBuilder()
223
+ val textChars = text.toList()
224
+ val maskChars = expandedMask.toList()
225
+ var ti = 0
226
+ var mi = 0
227
+
228
+ while (ti < textChars.size && mi < maskChars.size) {
229
+ val m = maskChars[mi]
230
+ if (m == '9' || m == 'A' || m == '*') {
231
+ raw.append(textChars[ti])
232
+ ti++
233
+ mi++
234
+ }
235
+ else {
236
+ if (textChars[ti] == m) ti++
237
+ mi++
238
+ }
239
+ }
240
+
241
+ return raw.toString()
242
+ }
243
+
244
+ // -------------------------------------------------------------------------
245
+ // isValidPrefix
246
+ // -------------------------------------------------------------------------
247
+
248
+ /**
249
+ * Returns true if [partial] (the digits entered so far for a range block) is
250
+ * still consistent with producing a final value inside [from]..[to].
251
+ *
252
+ * After [partial] the remaining positions can be filled with '0'..'9', so we
253
+ * test whether the optimistic interval [min, max] overlaps [from, to].
254
+ */
255
+ private fun isValidPrefix(partial: String, from: Int, to: Int, length: Int): Boolean {
256
+ val remaining = length - partial.length
257
+ val partialMin = (partial + "0".repeat(remaining)).toIntOrNull() ?: return false
258
+ val partialMax = (partial + "9".repeat(remaining)).toIntOrNull() ?: return false
259
+ return partialMin <= to && partialMax >= from
260
+ }
261
+ }
@@ -0,0 +1,18 @@
1
+ package com.nitroinputmask.engine
2
+
3
+ import com.margelo.nitro.nitroinputmask.NitroMaskOptions
4
+ import com.nitroinputmask.masks.CreditCardMaskEngine
5
+ import com.nitroinputmask.masks.CustomMaskEngine
6
+ import com.nitroinputmask.masks.DatetimeMaskEngine
7
+ import com.nitroinputmask.masks.MoneyMaskEngine
8
+
9
+ internal object MaskEngineFactory {
10
+ fun build(maskType: String, options: NitroMaskOptions): MaskEngineProtocol {
11
+ return when (maskType) {
12
+ "money" -> MoneyMaskEngine(options)
13
+ "datetime" -> DatetimeMaskEngine(options.format ?: "DD/MM/YYYY")
14
+ "credit-card" -> CreditCardMaskEngine(options)
15
+ else -> CustomMaskEngine(options.mask ?: "") // "custom"
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,8 @@
1
+ package com.nitroinputmask.engine
2
+
3
+ interface MaskEngineProtocol {
4
+ fun apply(input: String): Pair<String, String>
5
+ val wantsTrailingCursor: Boolean
6
+ /** Expanded mask string for cursor positioning. Null means put cursor at end. */
7
+ val expandedMask: String?
8
+ }
@@ -0,0 +1,47 @@
1
+ package com.nitroinputmask.masks
2
+
3
+ import com.margelo.nitro.nitroinputmask.NitroMaskOptions
4
+ import com.nitroinputmask.engine.CompiledMask
5
+ import com.nitroinputmask.engine.MaskEngine
6
+ import com.nitroinputmask.engine.MaskEngineProtocol
7
+
8
+ internal class CreditCardMaskEngine(options: NitroMaskOptions) : MaskEngineProtocol {
9
+ private val compiled: CompiledMask
10
+ private val obfuscated: Boolean = options.obfuscated ?: false
11
+
12
+ init {
13
+ val mask = when (options.issuer) {
14
+ "amex" -> "9999 999999 99999"
15
+ "diners" -> "9999 999999 9999"
16
+ else -> "9999 9999 9999 9999" // visa-or-mastercard (default)
17
+ }
18
+ compiled = MaskEngine.compile(mask)
19
+ }
20
+
21
+ override fun apply(input: String): Pair<String, String> {
22
+ val (masked, raw) = MaskEngine.apply(input, compiled)
23
+
24
+ if (obfuscated && masked.isNotEmpty()) {
25
+ return Pair(obfuscate(masked), raw)
26
+ }
27
+
28
+ return Pair(masked, raw)
29
+ }
30
+
31
+ override val wantsTrailingCursor: Boolean = false
32
+ override val expandedMask: String? = compiled.expandedMask
33
+
34
+ private fun obfuscate(masked: String): String {
35
+ val lastSpaceIdx = masked.lastIndexOf(' ')
36
+ if (lastSpaceIdx == -1) return masked // single group — don't obfuscate
37
+
38
+ val prefix = masked.substring(0, lastSpaceIdx + 1)
39
+ val lastGroup = masked.substring(lastSpaceIdx + 1)
40
+
41
+ val obfuscatedPrefix = prefix.map { c ->
42
+ if (c.isDigit()) '*' else c
43
+ }.joinToString("")
44
+
45
+ return obfuscatedPrefix + lastGroup
46
+ }
47
+ }
@@ -0,0 +1,16 @@
1
+ package com.nitroinputmask.masks
2
+
3
+ import com.nitroinputmask.engine.CompiledMask
4
+ import com.nitroinputmask.engine.MaskEngine
5
+ import com.nitroinputmask.engine.MaskEngineProtocol
6
+
7
+ internal class CustomMaskEngine(mask: String) : MaskEngineProtocol {
8
+ private val compiled: CompiledMask = MaskEngine.compile(mask)
9
+
10
+ override fun apply(input: String): Pair<String, String> {
11
+ return MaskEngine.apply(input, compiled)
12
+ }
13
+
14
+ override val wantsTrailingCursor: Boolean = false
15
+ override val expandedMask: String? = compiled.expandedMask
16
+ }
@@ -0,0 +1,38 @@
1
+ package com.nitroinputmask.masks
2
+
3
+ import com.nitroinputmask.engine.CompiledMask
4
+ import com.nitroinputmask.engine.MaskEngine
5
+ import com.nitroinputmask.engine.MaskEngineProtocol
6
+
7
+ internal class DatetimeMaskEngine(format: String) : MaskEngineProtocol {
8
+ private val compiled: CompiledMask = MaskEngine.compile(formatToPattern(format))
9
+
10
+ override fun apply(input: String): Pair<String, String> {
11
+ return MaskEngine.apply(input, compiled)
12
+ }
13
+
14
+ override val wantsTrailingCursor: Boolean = false
15
+ override val expandedMask: String? = compiled.expandedMask
16
+
17
+ companion object {
18
+ private fun formatToPattern(format: String): String {
19
+ val sb = StringBuilder()
20
+ var i = 0
21
+
22
+ while (i < format.length) {
23
+ when {
24
+ format.startsWith("YYYY", i) -> { sb.append("9999"); i += 4 }
25
+ format.startsWith("MM", i) -> { sb.append("[1-12]"); i += 2 }
26
+ format.startsWith("DD", i) -> { sb.append("[1-31]"); i += 2 }
27
+ format.startsWith("HH", i) -> { sb.append("[0-23]"); i += 2 }
28
+ format.startsWith("hh", i) -> { sb.append("[1-12]"); i += 2 }
29
+ format.startsWith("mm", i) -> { sb.append("[0-59]"); i += 2 }
30
+ format.startsWith("ss", i) -> { sb.append("[0-59]"); i += 2 }
31
+ else -> { sb.append(format[i]); i++ }
32
+ }
33
+ }
34
+
35
+ return sb.toString()
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,66 @@
1
+ package com.nitroinputmask.masks
2
+
3
+ import com.margelo.nitro.nitroinputmask.NitroMaskOptions
4
+ import com.nitroinputmask.engine.MaskEngineProtocol
5
+
6
+ internal class MoneyMaskEngine(options: NitroMaskOptions) : MaskEngineProtocol {
7
+ private val precision: Int = (options.precision?.toInt()) ?: 2
8
+ private val separator: String = options.separator ?: ","
9
+ private val delimiter: String = options.delimiter ?: "."
10
+ private val unit: String = options.unit ?: ""
11
+ private val suffixUnit: String = options.suffixUnit ?: ""
12
+ private val zeroCents: Boolean = options.zeroCents ?: false
13
+
14
+ override fun apply(input: String): Pair<String, String> {
15
+ val digits = input.filter { it.isDigit() }
16
+
17
+ if (digits.isEmpty()) {
18
+ return Pair("", "")
19
+ }
20
+
21
+ val effectivePrecision = if (zeroCents) 0 else precision
22
+
23
+ // Pad on the left so we have at least (precision+1) digits
24
+ val minLength = effectivePrecision + 1
25
+ val padded = digits.padStart(maxOf(minLength, digits.length), '0')
26
+
27
+ // Split integer and decimal parts
28
+ var intPart: String
29
+ val centPart: String
30
+ if (effectivePrecision > 0) {
31
+ intPart = padded.dropLast(effectivePrecision)
32
+ centPart = padded.takeLast(effectivePrecision)
33
+ } else {
34
+ intPart = padded
35
+ centPart = ""
36
+ }
37
+
38
+ // Remove leading zeros from intPart, keep at least "0"
39
+ while (intPart.length > 1 && intPart.startsWith('0')) {
40
+ intPart = intPart.drop(1)
41
+ }
42
+
43
+ // Insert delimiter every 3 digits from right
44
+ val sb = StringBuilder()
45
+ val total = intPart.length
46
+ for (i in intPart.indices) {
47
+ sb.append(intPart[i])
48
+ val remaining = total - i - 1
49
+ if (remaining > 0 && remaining % 3 == 0) {
50
+ sb.append(delimiter)
51
+ }
52
+ }
53
+
54
+ // Build result
55
+ var masked = unit + sb.toString()
56
+ if (effectivePrecision > 0) {
57
+ masked += separator + centPart
58
+ }
59
+ masked += suffixUnit
60
+
61
+ return Pair(masked, digits)
62
+ }
63
+
64
+ override val wantsTrailingCursor: Boolean = true
65
+ override val expandedMask: String? = null
66
+ }
package/ios/Bridge.h ADDED
@@ -0,0 +1,8 @@
1
+ //
2
+ // Bridge.h
3
+ // nitro-input-mask
4
+ //
5
+ // Created by salvesoftware on 5/8/2026
6
+ //
7
+
8
+ #pragma once