censorship-id 1.0.0 → 1.1.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.
package/README.md CHANGED
@@ -7,7 +7,8 @@ A robust and simple npm package for detecting and masking Indonesian profanity,
7
7
 
8
8
  ## Features
9
9
 
10
- - **Comprehensive Dictionary**: Pre-loaded with hundreds of common Indonesian bad words.
10
+ - **Smart Detection**: Optional `smartMode` to handle leetspeak (e.g., `4njing`), repeated characters (e.g., `baaaaaabiii`), and punctuation insertion (e.g., `a.n.j.i.n.g`).
11
+ - **Comprehensive Dictionary**: Pre-loaded with common Indonesian bad words.
11
12
  - **Regex Powered**: Uses word boundaries to avoid false positives (e.g., masking "tai" but not "pantai").
12
13
  - **Customizable**: Add your own words to the list and choose your mask character.
13
14
  - **Keep First and Last**: Option to keep the first and last letters visible for better context.
@@ -54,6 +55,15 @@ console.log(visible);
54
55
  const customWords = censorId("Kamu sangat malas!", { customWords: ['malas'] });
55
56
  console.log(customWords);
56
57
  // Output: Kamu sangat *****!
58
+
59
+ // 4. Smart Mode (handles leetspeak & repeats)
60
+ const smart = censorId("D4s4r 4njing!", { smartMode: true });
61
+ console.log(smart);
62
+ // Output: D4s4r ******!
63
+
64
+ const repeats = censorId("baaaaaaaabiiiiii", { smartMode: true });
65
+ console.log(repeats);
66
+ // Output: ****************
57
67
  ```
58
68
 
59
69
  ## API Reference
@@ -65,6 +75,7 @@ console.log(customWords);
65
75
  - **`mask`** (string): The character used to mask words (default: `*`).
66
76
  - **`customWords`** (string[]): Additional words to censor.
67
77
  - **`keepFirstAndLast`** (boolean): If `true`, the first and last letters of the word remain visible (default: `false`).
78
+ - **`smartMode`** (boolean): If `true`, detects leetspeak and repeated characters (default: `false`).
68
79
 
69
80
  ## License
70
81
 
package/dist/index.d.mts CHANGED
@@ -18,6 +18,12 @@ interface CensorIdOptions {
18
18
  * @default false
19
19
  */
20
20
  keepFirstAndLast?: boolean;
21
+
22
+ /**
23
+ * If true, handles leetspeak (e.g., "4njing") and repeated characters (e.g., "baaaaaaabiii").
24
+ * @default false
25
+ */
26
+ smartMode?: boolean;
21
27
  }
22
28
 
23
29
  /**
package/dist/index.d.ts CHANGED
@@ -18,6 +18,12 @@ interface CensorIdOptions {
18
18
  * @default false
19
19
  */
20
20
  keepFirstAndLast?: boolean;
21
+
22
+ /**
23
+ * If true, handles leetspeak (e.g., "4njing") and repeated characters (e.g., "baaaaaaabiii").
24
+ * @default false
25
+ */
26
+ smartMode?: boolean;
21
27
  }
22
28
 
23
29
  /**
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- var f=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var i=f((m,a)=>{var j=["anjing","asu","babi","bangsat","bajing","bajingan","bedebah","berak","bejat","brengsek","buto","cuk","dancuk","entot","goblok","itil","jancuk","jembut","kontol","kunyuk","lonte","memek","monyet","ngentot","pantat","perek","puki","setan","siak","sinting","tai","tolol"];a.exports=j});var c=i();function y(t,e={}){if(typeof t!="string")return t;let{mask:r="*",customWords:g=[],keepFirstAndLast:l=!1}=e,u=[...new Set([...c,...g])].map(s=>s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")),o=t;return u.forEach(s=>{let d=new RegExp(`(?<=\\s|^|[^a-zA-Z0-9])${s}(?=\\s|$|[^a-zA-Z0-9])`,"gi");o=o.replace(d,n=>{if(l&&n.length>2){let k=n[0],p=n[n.length-1],b=r.repeat(n.length-2);return k+b+p}return r.repeat(n.length)})}),o}module.exports={censorId:y,dictionary:c};
1
+ var x=(n,t)=>()=>(t||n((t={exports:{}}).exports,t),t.exports);var k=x((P,u)=>{var C=["anjing","asu","babi","bangsat","bajing","bajingan","bedebah","berak","bejat","brengsek","buto","cuk","dancuk","entot","goblok","itil","jancuk","jembut","kontol","kunyuk","lonte","memek","monyet","ngentot","pantat","perek","puki","setan","siak","sinting","tai","tolol"];u.exports=C});var b=k(),p={4:"a",3:"e",1:"i",0:"o",5:"s",7:"t",8:"b",9:"g","@":"a",$:"s","!":"i","(":"c"};function w(n,t={}){if(typeof n!="string")return n;let{mask:g="*",customWords:f=[],keepFirstAndLast:d=!1,smartMode:h=!1}=t,j=[...new Set([...b,...f])].sort((s,r)=>r.length-s.length),o=n;return j.forEach(s=>{let r;h?r=Array.from(s).map((i,c)=>{let a=i.toLowerCase(),y=Object.keys(p).filter(l=>p[l]===a),m=[a,...y].map(l=>l.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")),A=c===s.length-1?"+":"+[^a-zA-Z0-9]*";return`[${m.join("")}]${A}`}).join(""):r=s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");let $=new RegExp(`(?<=\\s|^|[^a-zA-Z0-9])${r}(?=\\s|$|[^a-zA-Z0-9])`,"gi");o=o.replace($,e=>{if(d&&e.length>2){let i=e[0],c=e[e.length-1],a=g.repeat(e.length-2);return i+a+c}return g.repeat(e.length)})}),o}module.exports={censorId:w,dictionary:b};
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../dictionary.js","../index.js"],"sourcesContent":["/**\r\n * A comprehensive list of common Indonesian profanity, slang, and inappropriate words.\r\n */\r\nconst dictionary = [\r\n \"anjing\",\r\n \"asu\",\r\n \"babi\",\r\n \"bangsat\",\r\n \"bajing\",\r\n \"bajingan\",\r\n \"bedebah\",\r\n \"berak\",\r\n \"bejat\",\r\n \"brengsek\",\r\n \"buto\",\r\n \"cuk\",\r\n \"dancuk\",\r\n \"entot\",\r\n \"goblok\",\r\n \"itil\",\r\n \"jancuk\",\r\n \"jembut\",\r\n \"kontol\",\r\n \"kunyuk\",\r\n \"lonte\",\r\n \"memek\",\r\n \"monyet\",\r\n \"ngentot\",\r\n \"pantat\",\r\n \"perek\",\r\n \"puki\",\r\n \"setan\",\r\n \"siak\",\r\n \"sinting\",\r\n \"tai\",\r\n \"tolol\"\r\n];\r\n\r\nmodule.exports = dictionary;\r\n","const defaultDictionary = require(\"./dictionary\");\r\n\r\n/**\r\n * Censor Indonesian profanity and inappropriate words.\r\n *\r\n * @param {string} text - The input text to be filtered.\r\n * @param {object} options - Filtering options.\r\n * @param {string} options.mask - The character used to mask the words (default is '*').\r\n * @param {string[]} options.customWords - An array of additional words to censor.\r\n * @param {boolean} options.keepFirstAndLast - If true, keeps the first and last letters visible (default is false).\r\n * @returns {string} - The censored text.\r\n */\r\nfunction censorId(text, options = {}) {\r\n if (typeof text !== \"string\") {\r\n return text;\r\n }\r\n\r\n const {\r\n mask = \"*\",\r\n customWords = [],\r\n keepFirstAndLast = false\r\n } = options;\r\n\r\n // Merge default dictionary with custom words\r\n const wordsToCensor = [...new Set([...defaultDictionary, ...customWords])];\r\n\r\n // Escape special characters in words for safety in Regex\r\n const escapedWords = wordsToCensor.map(word =>\r\n word.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\r\n );\r\n\r\n let censoredText = text;\r\n\r\n // Iterate over each word and apply masking\r\n escapedWords.forEach(word => {\r\n // Regex with word boundaries (\\b) and case-insensitive flag (i)\r\n // Note: \\b doesn't work well with non-ASCII characters, but for Indonesian it's generally fine.\r\n // However, for more robustness with various symbols, we use a pattern that matches word start/end.\r\n const regex = new RegExp(`(?<=\\\\s|^|[^a-zA-Z0-9])${word}(?=\\\\s|$|[^a-zA-Z0-9])`, \"gi\");\r\n\r\n censoredText = censoredText.replace(regex, (match) => {\r\n if (keepFirstAndLast && match.length > 2) {\r\n const first = match[0];\r\n const last = match[match.length - 1];\r\n const middle = mask.repeat(match.length - 2);\r\n return first + middle + last;\r\n }\r\n return mask.repeat(match.length);\r\n });\r\n });\r\n\r\n return censoredText;\r\n}\r\n\r\nmodule.exports = {\r\n censorId,\r\n dictionary: defaultDictionary\r\n};\r\n"],"mappings":"8DAAA,IAAAA,EAAAC,EAAA,CAAAC,EAAAC,IAAA,CAGA,IAAMC,EAAa,CACjB,SACA,MACA,OACA,UACA,SACA,WACA,UACA,QACA,QACA,WACA,OACA,MACA,SACA,QACA,SACA,OACA,SACA,SACA,SACA,SACA,QACA,QACA,SACA,UACA,SACA,QACA,OACA,QACA,OACA,UACA,MACA,OACF,EAEAD,EAAO,QAAUC,ICtCjB,IAAMC,EAAoB,IAY1B,SAASC,EAASC,EAAMC,EAAU,CAAC,EAAG,CAClC,GAAI,OAAOD,GAAS,SAChB,OAAOA,EAGX,GAAM,CACF,KAAAE,EAAO,IACP,YAAAC,EAAc,CAAC,EACf,iBAAAC,EAAmB,EACvB,EAAIH,EAMEI,EAHgB,CAAC,GAAG,IAAI,IAAI,CAAC,GAAGP,EAAmB,GAAGK,CAAW,CAAC,CAAC,EAGtC,IAAIG,GACnCA,EAAK,QAAQ,sBAAuB,MAAM,CAC9C,EAEIC,EAAeP,EAGnB,OAAAK,EAAa,QAAQC,GAAQ,CAIzB,IAAME,EAAQ,IAAI,OAAO,0BAA0BF,CAAI,yBAA0B,IAAI,EAErFC,EAAeA,EAAa,QAAQC,EAAQC,GAAU,CAClD,GAAIL,GAAoBK,EAAM,OAAS,EAAG,CACtC,IAAMC,EAAQD,EAAM,CAAC,EACfE,EAAOF,EAAMA,EAAM,OAAS,CAAC,EAC7BG,EAASV,EAAK,OAAOO,EAAM,OAAS,CAAC,EAC3C,OAAOC,EAAQE,EAASD,CAC5B,CACA,OAAOT,EAAK,OAAOO,EAAM,MAAM,CACnC,CAAC,CACL,CAAC,EAEMF,CACX,CAEA,OAAO,QAAU,CACb,SAAAR,EACA,WAAYD,CAChB","names":["require_dictionary","__commonJSMin","exports","module","dictionary","defaultDictionary","censorId","text","options","mask","customWords","keepFirstAndLast","escapedWords","word","censoredText","regex","match","first","last","middle"]}
1
+ {"version":3,"sources":["../dictionary.js","../index.js"],"sourcesContent":["/**\r\n * A comprehensive list of common Indonesian profanity, slang, and inappropriate words.\r\n */\r\nconst dictionary = [\r\n \"anjing\",\r\n \"asu\",\r\n \"babi\",\r\n \"bangsat\",\r\n \"bajing\",\r\n \"bajingan\",\r\n \"bedebah\",\r\n \"berak\",\r\n \"bejat\",\r\n \"brengsek\",\r\n \"buto\",\r\n \"cuk\",\r\n \"dancuk\",\r\n \"entot\",\r\n \"goblok\",\r\n \"itil\",\r\n \"jancuk\",\r\n \"jembut\",\r\n \"kontol\",\r\n \"kunyuk\",\r\n \"lonte\",\r\n \"memek\",\r\n \"monyet\",\r\n \"ngentot\",\r\n \"pantat\",\r\n \"perek\",\r\n \"puki\",\r\n \"setan\",\r\n \"siak\",\r\n \"sinting\",\r\n \"tai\",\r\n \"tolol\"\r\n];\r\n\r\nmodule.exports = dictionary;\r\n","const defaultDictionary = require(\"./dictionary\");\r\n\r\n// Leetspeak mapping\r\nconst leetMap = {\r\n \"4\": \"a\",\r\n \"3\": \"e\",\r\n \"1\": \"i\",\r\n \"0\": \"o\",\r\n \"5\": \"s\",\r\n \"7\": \"t\",\r\n \"8\": \"b\",\r\n \"9\": \"g\",\r\n \"@\": \"a\",\r\n \"$\": \"s\",\r\n \"!\": \"i\",\r\n \"(\": \"c\"\r\n};\r\n\r\n/**\r\n * Censor Indonesian profanity and inappropriate words.\r\n *\r\n * @param {string} text - The input text to be filtered.\r\n * @param {object} options - Filtering options.\r\n * @param {string} options.mask - The character used to mask the words (default is '*').\r\n * @param {string[]} options.customWords - An array of additional words to censor.\r\n * @param {boolean} options.keepFirstAndLast - If true, keeps the first and last letters visible (default is false).\r\n * @param {boolean} options.smartMode - If true, handles leetspeak and repeated characters (default is false).\r\n * @returns {string} - The censored text.\r\n */\r\nfunction censorId(text, options = {}) {\r\n if (typeof text !== \"string\") {\r\n return text;\r\n }\r\n\r\n const {\r\n mask = \"*\",\r\n customWords = [],\r\n keepFirstAndLast = false,\r\n smartMode = false\r\n } = options;\r\n\r\n // Merge default dictionary with custom words and sort by length descending\r\n const wordsToCensor = [...new Set([...defaultDictionary, ...customWords])].sort((a, b) => b.length - a.length);\r\n\r\n let censoredText = text;\r\n\r\n // Iterate over each word and apply masking\r\n wordsToCensor.forEach(word => {\r\n let searchPattern;\r\n if (smartMode) {\r\n // Create a pattern that allows:\r\n // 1. Any number of repeats for each character (e.g., a+n+j+i+n+g+)\r\n // 2. Optional non-alphanumeric characters between letters (e.g., a[...]*n[...]*j)\r\n // 3. Mapping of leetspeak characters\r\n\r\n const charPatterns = Array.from(word).map((char, index) => {\r\n const lowerChar = char.toLowerCase();\r\n const leetChars = Object.keys(leetMap).filter(key => leetMap[key] === lowerChar);\r\n const variants = [lowerChar, ...leetChars].map(c => c.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"));\r\n\r\n // Only allow non-alphanumeric between characters, not after the last one\r\n const suffix = index === word.length - 1 ? \"+\" : \"+[^a-zA-Z0-9]*\";\r\n return `[${variants.join(\"\")}]${suffix}`;\r\n });\r\n\r\n searchPattern = charPatterns.join(\"\");\r\n } else {\r\n // Standard escaping for regex\r\n searchPattern = word.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\r\n }\r\n\r\n const regex = new RegExp(`(?<=\\\\s|^|[^a-zA-Z0-9])${searchPattern}(?=\\\\s|$|[^a-zA-Z0-9])`, \"gi\");\r\n\r\n censoredText = censoredText.replace(regex, (match) => {\r\n if (keepFirstAndLast && match.length > 2) {\r\n const first = match[0];\r\n const last = match[match.length - 1];\r\n const middle = mask.repeat(match.length - 2);\r\n return first + middle + last;\r\n }\r\n return mask.repeat(match.length);\r\n });\r\n });\r\n\r\n return censoredText;\r\n}\r\n\r\nmodule.exports = {\r\n censorId,\r\n dictionary: defaultDictionary\r\n};\r\n"],"mappings":"8DAAA,IAAAA,EAAAC,EAAA,CAAAC,EAAAC,IAAA,CAGA,IAAMC,EAAa,CACjB,SACA,MACA,OACA,UACA,SACA,WACA,UACA,QACA,QACA,WACA,OACA,MACA,SACA,QACA,SACA,OACA,SACA,SACA,SACA,SACA,QACA,QACA,SACA,UACA,SACA,QACA,OACA,QACA,OACA,UACA,MACA,OACF,EAEAD,EAAO,QAAUC,ICtCjB,IAAMC,EAAoB,IAGpBC,EAAU,CACZ,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,IAAK,IACL,EAAK,IACL,IAAK,IACL,IAAK,GACT,EAaA,SAASC,EAASC,EAAMC,EAAU,CAAC,EAAG,CAClC,GAAI,OAAOD,GAAS,SAChB,OAAOA,EAGX,GAAM,CACF,KAAAE,EAAO,IACP,YAAAC,EAAc,CAAC,EACf,iBAAAC,EAAmB,GACnB,UAAAC,EAAY,EAChB,EAAIJ,EAGEK,EAAgB,CAAC,GAAG,IAAI,IAAI,CAAC,GAAGT,EAAmB,GAAGM,CAAW,CAAC,CAAC,EAAE,KAAK,CAACI,EAAGC,IAAMA,EAAE,OAASD,EAAE,MAAM,EAEzGE,EAAeT,EAGnB,OAAAM,EAAc,QAAQI,GAAQ,CAC1B,IAAIC,EACAN,EAgBAM,EAVqB,MAAM,KAAKD,CAAI,EAAE,IAAI,CAACE,EAAMC,IAAU,CACvD,IAAMC,EAAYF,EAAK,YAAY,EAC7BG,EAAY,OAAO,KAAKjB,CAAO,EAAE,OAAOkB,GAAOlB,EAAQkB,CAAG,IAAMF,CAAS,EACzEG,EAAW,CAACH,EAAW,GAAGC,CAAS,EAAE,IAAIG,GAAKA,EAAE,QAAQ,sBAAuB,MAAM,CAAC,EAGtFC,EAASN,IAAUH,EAAK,OAAS,EAAI,IAAM,iBACjD,MAAO,IAAIO,EAAS,KAAK,EAAE,CAAC,IAAIE,CAAM,EAC1C,CAAC,EAE4B,KAAK,EAAE,EAGpCR,EAAgBD,EAAK,QAAQ,sBAAuB,MAAM,EAG9D,IAAMU,EAAQ,IAAI,OAAO,0BAA0BT,CAAa,yBAA0B,IAAI,EAE9FF,EAAeA,EAAa,QAAQW,EAAQC,GAAU,CAClD,GAAIjB,GAAoBiB,EAAM,OAAS,EAAG,CACtC,IAAMC,EAAQD,EAAM,CAAC,EACfE,EAAOF,EAAMA,EAAM,OAAS,CAAC,EAC7BG,EAAStB,EAAK,OAAOmB,EAAM,OAAS,CAAC,EAC3C,OAAOC,EAAQE,EAASD,CAC5B,CACA,OAAOrB,EAAK,OAAOmB,EAAM,MAAM,CACnC,CAAC,CACL,CAAC,EAEMZ,CACX,CAEA,OAAO,QAAU,CACb,SAAAV,EACA,WAAYF,CAChB","names":["require_dictionary","__commonJSMin","exports","module","dictionary","defaultDictionary","leetMap","censorId","text","options","mask","customWords","keepFirstAndLast","smartMode","wordsToCensor","a","b","censoredText","word","searchPattern","char","index","lowerChar","leetChars","key","variants","c","suffix","regex","match","first","last","middle"]}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var a=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var c=a((A,i)=>{var y=["anjing","asu","babi","bangsat","bajing","bajingan","bedebah","berak","bejat","brengsek","buto","cuk","dancuk","entot","goblok","itil","jancuk","jembut","kontol","kunyuk","lonte","memek","monyet","ngentot","pantat","perek","puki","setan","siak","sinting","tai","tolol"];i.exports=y});var $=a((T,l)=>{var g=c();function w(t,e={}){if(typeof t!="string")return t;let{mask:r="*",customWords:u=[],keepFirstAndLast:d=!1}=e,k=[...new Set([...g,...u])].map(s=>s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")),o=t;return k.forEach(s=>{let p=new RegExp(`(?<=\\s|^|[^a-zA-Z0-9])${s}(?=\\s|$|[^a-zA-Z0-9])`,"gi");o=o.replace(p,n=>{if(d&&n.length>2){let b=n[0],f=n[n.length-1],j=r.repeat(n.length-2);return b+j+f}return r.repeat(n.length)})}),o}l.exports={censorId:w,dictionary:g}});export default $();
1
+ var u=(n,t)=>()=>(t||n((t={exports:{}}).exports,t),t.exports);var p=u((E,k)=>{var w=["anjing","asu","babi","bangsat","bajing","bajingan","bedebah","berak","bejat","brengsek","buto","cuk","dancuk","entot","goblok","itil","jancuk","jembut","kontol","kunyuk","lonte","memek","monyet","ngentot","pantat","perek","puki","setan","siak","sinting","tai","tolol"];k.exports=w});var P=u((L,d)=>{var f=p(),b={4:"a",3:"e",1:"i",0:"o",5:"s",7:"t",8:"b",9:"g","@":"a",$:"s","!":"i","(":"c"};function z(n,t={}){if(typeof n!="string")return n;let{mask:g="*",customWords:h=[],keepFirstAndLast:j=!1,smartMode:$=!1}=t,y=[...new Set([...f,...h])].sort((s,r)=>r.length-s.length),o=n;return y.forEach(s=>{let r;$?r=Array.from(s).map((i,c)=>{let a=i.toLowerCase(),A=Object.keys(b).filter(l=>b[l]===a),x=[a,...A].map(l=>l.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")),C=c===s.length-1?"+":"+[^a-zA-Z0-9]*";return`[${x.join("")}]${C}`}).join(""):r=s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");let m=new RegExp(`(?<=\\s|^|[^a-zA-Z0-9])${r}(?=\\s|$|[^a-zA-Z0-9])`,"gi");o=o.replace(m,e=>{if(j&&e.length>2){let i=e[0],c=e[e.length-1],a=g.repeat(e.length-2);return i+a+c}return g.repeat(e.length)})}),o}d.exports={censorId:z,dictionary:f}});export default P();
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../dictionary.js","../index.js"],"sourcesContent":["/**\r\n * A comprehensive list of common Indonesian profanity, slang, and inappropriate words.\r\n */\r\nconst dictionary = [\r\n \"anjing\",\r\n \"asu\",\r\n \"babi\",\r\n \"bangsat\",\r\n \"bajing\",\r\n \"bajingan\",\r\n \"bedebah\",\r\n \"berak\",\r\n \"bejat\",\r\n \"brengsek\",\r\n \"buto\",\r\n \"cuk\",\r\n \"dancuk\",\r\n \"entot\",\r\n \"goblok\",\r\n \"itil\",\r\n \"jancuk\",\r\n \"jembut\",\r\n \"kontol\",\r\n \"kunyuk\",\r\n \"lonte\",\r\n \"memek\",\r\n \"monyet\",\r\n \"ngentot\",\r\n \"pantat\",\r\n \"perek\",\r\n \"puki\",\r\n \"setan\",\r\n \"siak\",\r\n \"sinting\",\r\n \"tai\",\r\n \"tolol\"\r\n];\r\n\r\nmodule.exports = dictionary;\r\n","const defaultDictionary = require(\"./dictionary\");\r\n\r\n/**\r\n * Censor Indonesian profanity and inappropriate words.\r\n *\r\n * @param {string} text - The input text to be filtered.\r\n * @param {object} options - Filtering options.\r\n * @param {string} options.mask - The character used to mask the words (default is '*').\r\n * @param {string[]} options.customWords - An array of additional words to censor.\r\n * @param {boolean} options.keepFirstAndLast - If true, keeps the first and last letters visible (default is false).\r\n * @returns {string} - The censored text.\r\n */\r\nfunction censorId(text, options = {}) {\r\n if (typeof text !== \"string\") {\r\n return text;\r\n }\r\n\r\n const {\r\n mask = \"*\",\r\n customWords = [],\r\n keepFirstAndLast = false\r\n } = options;\r\n\r\n // Merge default dictionary with custom words\r\n const wordsToCensor = [...new Set([...defaultDictionary, ...customWords])];\r\n\r\n // Escape special characters in words for safety in Regex\r\n const escapedWords = wordsToCensor.map(word =>\r\n word.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\r\n );\r\n\r\n let censoredText = text;\r\n\r\n // Iterate over each word and apply masking\r\n escapedWords.forEach(word => {\r\n // Regex with word boundaries (\\b) and case-insensitive flag (i)\r\n // Note: \\b doesn't work well with non-ASCII characters, but for Indonesian it's generally fine.\r\n // However, for more robustness with various symbols, we use a pattern that matches word start/end.\r\n const regex = new RegExp(`(?<=\\\\s|^|[^a-zA-Z0-9])${word}(?=\\\\s|$|[^a-zA-Z0-9])`, \"gi\");\r\n\r\n censoredText = censoredText.replace(regex, (match) => {\r\n if (keepFirstAndLast && match.length > 2) {\r\n const first = match[0];\r\n const last = match[match.length - 1];\r\n const middle = mask.repeat(match.length - 2);\r\n return first + middle + last;\r\n }\r\n return mask.repeat(match.length);\r\n });\r\n });\r\n\r\n return censoredText;\r\n}\r\n\r\nmodule.exports = {\r\n censorId,\r\n dictionary: defaultDictionary\r\n};\r\n"],"mappings":"8DAAA,IAAAA,EAAAC,EAAA,CAAAC,EAAAC,IAAA,CAGA,IAAMC,EAAa,CACjB,SACA,MACA,OACA,UACA,SACA,WACA,UACA,QACA,QACA,WACA,OACA,MACA,SACA,QACA,SACA,OACA,SACA,SACA,SACA,SACA,QACA,QACA,SACA,UACA,SACA,QACA,OACA,QACA,OACA,UACA,MACA,OACF,EAEAD,EAAO,QAAUC,ICtCjB,IAAAC,EAAAC,EAAA,CAAAC,EAAAC,IAAA,KAAMC,EAAoB,IAY1B,SAASC,EAASC,EAAMC,EAAU,CAAC,EAAG,CAClC,GAAI,OAAOD,GAAS,SAChB,OAAOA,EAGX,GAAM,CACF,KAAAE,EAAO,IACP,YAAAC,EAAc,CAAC,EACf,iBAAAC,EAAmB,EACvB,EAAIH,EAMEI,EAHgB,CAAC,GAAG,IAAI,IAAI,CAAC,GAAGP,EAAmB,GAAGK,CAAW,CAAC,CAAC,EAGtC,IAAIG,GACnCA,EAAK,QAAQ,sBAAuB,MAAM,CAC9C,EAEIC,EAAeP,EAGnB,OAAAK,EAAa,QAAQC,GAAQ,CAIzB,IAAME,EAAQ,IAAI,OAAO,0BAA0BF,CAAI,yBAA0B,IAAI,EAErFC,EAAeA,EAAa,QAAQC,EAAQC,GAAU,CAClD,GAAIL,GAAoBK,EAAM,OAAS,EAAG,CACtC,IAAMC,EAAQD,EAAM,CAAC,EACfE,EAAOF,EAAMA,EAAM,OAAS,CAAC,EAC7BG,EAASV,EAAK,OAAOO,EAAM,OAAS,CAAC,EAC3C,OAAOC,EAAQE,EAASD,CAC5B,CACA,OAAOT,EAAK,OAAOO,EAAM,MAAM,CACnC,CAAC,CACL,CAAC,EAEMF,CACX,CAEAV,EAAO,QAAU,CACb,SAAAE,EACA,WAAYD,CAChB","names":["require_dictionary","__commonJSMin","exports","module","dictionary","require_index","__commonJSMin","exports","module","defaultDictionary","censorId","text","options","mask","customWords","keepFirstAndLast","escapedWords","word","censoredText","regex","match","first","last","middle"]}
1
+ {"version":3,"sources":["../dictionary.js","../index.js"],"sourcesContent":["/**\r\n * A comprehensive list of common Indonesian profanity, slang, and inappropriate words.\r\n */\r\nconst dictionary = [\r\n \"anjing\",\r\n \"asu\",\r\n \"babi\",\r\n \"bangsat\",\r\n \"bajing\",\r\n \"bajingan\",\r\n \"bedebah\",\r\n \"berak\",\r\n \"bejat\",\r\n \"brengsek\",\r\n \"buto\",\r\n \"cuk\",\r\n \"dancuk\",\r\n \"entot\",\r\n \"goblok\",\r\n \"itil\",\r\n \"jancuk\",\r\n \"jembut\",\r\n \"kontol\",\r\n \"kunyuk\",\r\n \"lonte\",\r\n \"memek\",\r\n \"monyet\",\r\n \"ngentot\",\r\n \"pantat\",\r\n \"perek\",\r\n \"puki\",\r\n \"setan\",\r\n \"siak\",\r\n \"sinting\",\r\n \"tai\",\r\n \"tolol\"\r\n];\r\n\r\nmodule.exports = dictionary;\r\n","const defaultDictionary = require(\"./dictionary\");\r\n\r\n// Leetspeak mapping\r\nconst leetMap = {\r\n \"4\": \"a\",\r\n \"3\": \"e\",\r\n \"1\": \"i\",\r\n \"0\": \"o\",\r\n \"5\": \"s\",\r\n \"7\": \"t\",\r\n \"8\": \"b\",\r\n \"9\": \"g\",\r\n \"@\": \"a\",\r\n \"$\": \"s\",\r\n \"!\": \"i\",\r\n \"(\": \"c\"\r\n};\r\n\r\n/**\r\n * Censor Indonesian profanity and inappropriate words.\r\n *\r\n * @param {string} text - The input text to be filtered.\r\n * @param {object} options - Filtering options.\r\n * @param {string} options.mask - The character used to mask the words (default is '*').\r\n * @param {string[]} options.customWords - An array of additional words to censor.\r\n * @param {boolean} options.keepFirstAndLast - If true, keeps the first and last letters visible (default is false).\r\n * @param {boolean} options.smartMode - If true, handles leetspeak and repeated characters (default is false).\r\n * @returns {string} - The censored text.\r\n */\r\nfunction censorId(text, options = {}) {\r\n if (typeof text !== \"string\") {\r\n return text;\r\n }\r\n\r\n const {\r\n mask = \"*\",\r\n customWords = [],\r\n keepFirstAndLast = false,\r\n smartMode = false\r\n } = options;\r\n\r\n // Merge default dictionary with custom words and sort by length descending\r\n const wordsToCensor = [...new Set([...defaultDictionary, ...customWords])].sort((a, b) => b.length - a.length);\r\n\r\n let censoredText = text;\r\n\r\n // Iterate over each word and apply masking\r\n wordsToCensor.forEach(word => {\r\n let searchPattern;\r\n if (smartMode) {\r\n // Create a pattern that allows:\r\n // 1. Any number of repeats for each character (e.g., a+n+j+i+n+g+)\r\n // 2. Optional non-alphanumeric characters between letters (e.g., a[...]*n[...]*j)\r\n // 3. Mapping of leetspeak characters\r\n\r\n const charPatterns = Array.from(word).map((char, index) => {\r\n const lowerChar = char.toLowerCase();\r\n const leetChars = Object.keys(leetMap).filter(key => leetMap[key] === lowerChar);\r\n const variants = [lowerChar, ...leetChars].map(c => c.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"));\r\n\r\n // Only allow non-alphanumeric between characters, not after the last one\r\n const suffix = index === word.length - 1 ? \"+\" : \"+[^a-zA-Z0-9]*\";\r\n return `[${variants.join(\"\")}]${suffix}`;\r\n });\r\n\r\n searchPattern = charPatterns.join(\"\");\r\n } else {\r\n // Standard escaping for regex\r\n searchPattern = word.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\r\n }\r\n\r\n const regex = new RegExp(`(?<=\\\\s|^|[^a-zA-Z0-9])${searchPattern}(?=\\\\s|$|[^a-zA-Z0-9])`, \"gi\");\r\n\r\n censoredText = censoredText.replace(regex, (match) => {\r\n if (keepFirstAndLast && match.length > 2) {\r\n const first = match[0];\r\n const last = match[match.length - 1];\r\n const middle = mask.repeat(match.length - 2);\r\n return first + middle + last;\r\n }\r\n return mask.repeat(match.length);\r\n });\r\n });\r\n\r\n return censoredText;\r\n}\r\n\r\nmodule.exports = {\r\n censorId,\r\n dictionary: defaultDictionary\r\n};\r\n"],"mappings":"8DAAA,IAAAA,EAAAC,EAAA,CAAAC,EAAAC,IAAA,CAGA,IAAMC,EAAa,CACjB,SACA,MACA,OACA,UACA,SACA,WACA,UACA,QACA,QACA,WACA,OACA,MACA,SACA,QACA,SACA,OACA,SACA,SACA,SACA,SACA,QACA,QACA,SACA,UACA,SACA,QACA,OACA,QACA,OACA,UACA,MACA,OACF,EAEAD,EAAO,QAAUC,ICtCjB,IAAAC,EAAAC,EAAA,CAAAC,EAAAC,IAAA,KAAMC,EAAoB,IAGpBC,EAAU,CACZ,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,IAAK,IACL,EAAK,IACL,IAAK,IACL,IAAK,GACT,EAaA,SAASC,EAASC,EAAMC,EAAU,CAAC,EAAG,CAClC,GAAI,OAAOD,GAAS,SAChB,OAAOA,EAGX,GAAM,CACF,KAAAE,EAAO,IACP,YAAAC,EAAc,CAAC,EACf,iBAAAC,EAAmB,GACnB,UAAAC,EAAY,EAChB,EAAIJ,EAGEK,EAAgB,CAAC,GAAG,IAAI,IAAI,CAAC,GAAGT,EAAmB,GAAGM,CAAW,CAAC,CAAC,EAAE,KAAK,CAACI,EAAGC,IAAMA,EAAE,OAASD,EAAE,MAAM,EAEzGE,EAAeT,EAGnB,OAAAM,EAAc,QAAQI,GAAQ,CAC1B,IAAIC,EACAN,EAgBAM,EAVqB,MAAM,KAAKD,CAAI,EAAE,IAAI,CAACE,EAAMC,IAAU,CACvD,IAAMC,EAAYF,EAAK,YAAY,EAC7BG,EAAY,OAAO,KAAKjB,CAAO,EAAE,OAAOkB,GAAOlB,EAAQkB,CAAG,IAAMF,CAAS,EACzEG,EAAW,CAACH,EAAW,GAAGC,CAAS,EAAE,IAAIG,GAAKA,EAAE,QAAQ,sBAAuB,MAAM,CAAC,EAGtFC,EAASN,IAAUH,EAAK,OAAS,EAAI,IAAM,iBACjD,MAAO,IAAIO,EAAS,KAAK,EAAE,CAAC,IAAIE,CAAM,EAC1C,CAAC,EAE4B,KAAK,EAAE,EAGpCR,EAAgBD,EAAK,QAAQ,sBAAuB,MAAM,EAG9D,IAAMU,EAAQ,IAAI,OAAO,0BAA0BT,CAAa,yBAA0B,IAAI,EAE9FF,EAAeA,EAAa,QAAQW,EAAQC,GAAU,CAClD,GAAIjB,GAAoBiB,EAAM,OAAS,EAAG,CACtC,IAAMC,EAAQD,EAAM,CAAC,EACfE,EAAOF,EAAMA,EAAM,OAAS,CAAC,EAC7BG,EAAStB,EAAK,OAAOmB,EAAM,OAAS,CAAC,EAC3C,OAAOC,EAAQE,EAASD,CAC5B,CACA,OAAOrB,EAAK,OAAOmB,EAAM,MAAM,CACnC,CAAC,CACL,CAAC,EAEMZ,CACX,CAEAb,EAAO,QAAU,CACb,SAAAG,EACA,WAAYF,CAChB","names":["require_dictionary","__commonJSMin","exports","module","dictionary","require_index","__commonJSMin","exports","module","defaultDictionary","leetMap","censorId","text","options","mask","customWords","keepFirstAndLast","smartMode","wordsToCensor","a","b","censoredText","word","searchPattern","char","index","lowerChar","leetChars","key","variants","c","suffix","regex","match","first","last","middle"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "censorship-id",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A robust Indonesian profanity filter for detecting and masking inappropriate words.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -41,4 +41,4 @@
41
41
  "tsup": "^8.5.1",
42
42
  "typescript": "^6.0.2"
43
43
  }
44
- }
44
+ }