[
  {
    "path": ".gitattributes",
    "content": "* text=lf"
  },
  {
    "path": ".github/codecov.yml",
    "content": "comment: false\ncoverage:\n  status:\n    patch: off\n    project:\n      default:\n        target: 75%\n        threshold: null\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    labels: [\"dependencies\"]\n    schedule:\n      interval: \"daily\"\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    labels: [\"dependencies\"]\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: test\non:\n  pull_request:\n    paths-ignore:\n      - '*.md'\n  push:\n    branches:\n      - master\n    paths-ignore:\n      - '*.md'\njobs:\n  test:\n    name: test\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    steps:\n      - uses: actions/setup-go@v6\n        with:\n          go-version: 1.25.x\n      - uses: actions/checkout@v5\n      - run: go test ./... -race -coverprofile=coverage.txt -covermode=atomic\n      - name: Codecov\n        uses: codecov/codecov-action@v6.0.0\n        with:\n          files: ./coverage.txt\n          token: ${{ secrets.CODECOV_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea\naccuracy\n/vendor\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022-2023 TwiN\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "![go-away](/.github/assets/go-away.png)\n\n# go-away\n![test](https://github.com/TwiN/go-away/workflows/test/badge.svg)\n[![Go Report Card](https://goreportcard.com/badge/github.com/TwiN/go-away)](https://goreportcard.com/report/github.com/TwiN/go-away)\n[![codecov](https://codecov.io/gh/TwiN/go-away/branch/master/graph/badge.svg)](https://codecov.io/gh/TwiN/go-away)\n[![Go Reference](https://pkg.go.dev/badge/github.com/TwiN/go-away.svg)](https://pkg.go.dev/github.com/TwiN/go-away)\n[![Follow TwiN](https://img.shields.io/github/followers/TwiN?label=Follow&style=social)](https://github.com/TwiN)\n\ngo-away is a stand-alone, lightweight library for detecting and censoring profanities in Go.\n\nThis library must remain **extremely** easy to use. Its original intent of not adding overhead will always remain.\n\n\n## Installation\n```console\ngo get -u github.com/TwiN/go-away\n```\n\n\n## Usage\n```go\npackage main\n\nimport (\n    \"github.com/TwiN/go-away\"\n)\n\nfunc main() {\n    goaway.IsProfane(\"fuck this shit\")                // returns true\n    goaway.ExtractProfanity(\"fuck this shit\")         // returns \"fuck\"\n    goaway.Censor(\"fuck this shit\")                   // returns \"**** this ****\"\n    \n    goaway.IsProfane(\"F   u   C  k th1$ $h!t\")        // returns true\n    goaway.ExtractProfanity(\"F   u   C  k th1$ $h!t\") // returns \"fuck\"\n    goaway.Censor(\"F   u   C  k th1$ $h!t\")           // returns \"*   *   *  * th1$ ****\"\n    \n    goaway.IsProfane(\"@$$h073\")                       // returns true\n    goaway.ExtractProfanity(\"@$$h073\")                // returns \"asshole\"\n    goaway.Censor(\"@$$h073\")                          // returns \"*******\"\n    \n    goaway.IsProfane(\"hello, world!\")                 // returns false\n    goaway.ExtractProfanity(\"hello, world!\")          // returns \"\"\n    goaway.Censor(\"hello, world!\")                    // returns \"hello, world!\"\n\n    buf := &bytes.Buffer{}\n    detector := goaway.NewProfanityDetector()\n    writer := goaway.NewWriter(buf, detector)\n    writer.Write([]byte(\"fuck this shit\"))\n    writer.Flush()\n    print(buf.String())                               // returns \"**** this ****\"  \n}\n```\n\nCalling `goaway.IsProfane(s)`, `goaway.ExtractProfanity(s)` or `goaway.Censor(s)` will use the default profanity detector,\nbut if you'd like to disable leet speak, numerical character or special character sanitization, you have to create a\nProfanityDetector instead:\n```go\nprofanityDetector := goaway.NewProfanityDetector().WithSanitizeLeetSpeak(false).WithSanitizeSpecialCharacters(false).WithSanitizeAccents(false)\nprofanityDetector.IsProfane(\"b!tch\") // returns false because we're not sanitizing special characters\n```\nYou can also disable the default behavior of white space sanitization like so:\n```go\nprofanityDetector := goaway.NewProfanityDetector().WithSanitizeSpaces(false)\nprofanityDetector.IsProfane(\"sh it\") // returns false because we're not sanitizing white spaces\n```\nYou can also require stricter matching by enabling `WithExactWord`:\n```go\nprofanityDetector := NewProfanityDetector().WithExactWord(true).WithSanitizeSpecialCharacters(true)\nprofanityDetector.IsProfane(\"analyst\") // returns false because we match the exact word\nprofanityDetector.IsProfane(\"anal\") // returns true\n```\n\nBy default, the `NewProfanityDetector` constructor uses the default dictionaries for profanities, false positives and false negatives.\nThese dictionaries are exposed as `goaway.DefaultProfanities`, `goaway.DefaultFalsePositives` and `goaway.DefaultFalseNegatives` respectively.\n\nIf you need to load a different dictionary, you could create a new instance of `ProfanityDetector` on this way:\n```go\nprofanities    := []string{\"ass\"}\nfalsePositives := []string{\"bass\"}\nfalseNegatives := []string{\"dumbass\"}\n\nprofanityDetector := goaway.NewProfanityDetector().WithCustomDictionary(profanities, falsePositives, falseNegatives)\n```\n\nYou may also specify custom character replacements using `WithCustomCharacterReplacements` on a `ProfanityDetector`.\nBy default, this is set to `goaway.DefaultCharacterReplacements`.\n\nNote that all character replacements with a value of `' '` are considered as special characters while all characters\nwith a value that is not `' '` are considered to be leetspeak characters. This means that using \n`profanityDetector.WithSanitizeSpecialCharacters(bool)` and `profanityDetector.WithSanitizeLeetSpeak(bool)` will let you\ntoggle which character replacements are executed during the sanitization process.\n\n## Limitations\nCurrently, go-away does not support UTF-8. As such, if the strings you are feeding to this library come from unsanitized user input, you\nare advised to filter out all non-ASCII characters.\n\nIf you'd like to add support for UTF-8, see [#43](https://github.com/TwiN/go-away/issues/43) and [#47](https://github.com/TwiN/go-away/issues/47).\n\n\n## In the background\nWhile using a giant regex query to handle everything would be a way of doing it, as more words \nare added to the list of profanities, that would slow down the filtering considerably.\n\nInstead, the following steps are taken before checking for profanities in a string:\n\n- Numbers are replaced to their letter counterparts (e.g. 1 -> L, 4 -> A, etc)\n- Special characters are replaced to their letter equivalent (e.g. @ -> A, ! -> i)\n- The resulting string has all of its spaces removed to prevent `w  ords  lik e   tha   t`\n- The resulting string has all of its characters converted to lowercase\n- The resulting string has all words deemed as false positives (e.g. `assassin`) removed\n\nIn the future, the following additional steps could also be considered:\n- All non-transformed special characters are removed to prevent `s~tring li~ke tha~~t`\n- All words that have the same character repeated more than twice in a row are removed (e.g. `poooop -> poop`)\n  - NOTE: This is obviously not a perfect approach, as words like `fuuck` wouldn't be detected, but it's better than nothing.\n  - The upside of this method is that we only need to add base bad words, and not all tenses of said bad word. (e.g. the `fuck` entry would support `fucker`, `fucking`, etc.)\n"
  },
  {
    "path": "falsenegatives.go",
    "content": "package goaway\n\n// DefaultFalseNegatives is a list of profanities that are checked for before the DefaultFalsePositives are removed\n//\n// This is reserved for words that may be incorrectly filtered as false positives.\n//\n// Alternatively, words that are long, or that should mark a string as profane no matter what the context is\n// or whether the word is part of another word can also be included.\n//\n// Note that there is a test that prevents words from being in both DefaultProfanities and DefaultFalseNegatives,\nvar DefaultFalseNegatives = []string{\n\t\"asshole\",\n\t\"dumbass\", // ass -> bASS (FP) -> dumBASS\n\t\"nigger\",\n}\n"
  },
  {
    "path": "falsepositives.go",
    "content": "package goaway\n\n// DefaultFalsePositives is a list of words that may wrongly trigger the DefaultProfanities\nvar DefaultFalsePositives = []string{\n\t\"analy\", // analysis, analytics\n\t\"arsenal\",\n\t\"assassin\",\n\t\"assaying\", // was saying\n\t\"assert\",\n\t\"assign\",\n\t\"assimil\",\n\t\"assist\",\n\t\"associat\",\n\t\"assum\", // assuming, assumption, assumed\n\t\"assur\", // assurance\n\t\"banal\",\n\t\"basement\",\n\t\"bass\",\n\t\"cass\",   // cassie, cassandra, carcass\n\t\"butter\", // butter, butterfly\n\t\"butthe\",\n\t\"button\",\n\t\"canvass\",\n\t\"circum\",\n\t\"clitheroe\",\n\t\"cockburn\",\n\t\"cocktail\",\n\t\"cumber\",\n\t\"cumbing\",\n\t\"cumulat\",\n\t\"dickvandyke\",\n\t\"document\",\n\t\"evaluate\",\n\t\"exclusive\",\n\t\"expensive\",\n\t\"explain\",\n\t\"expression\",\n\t\"grape\",\n\t\"grass\",\n\t\"harass\",\n\t\"hass\",\n\t\"horniman\",\n\t\"hotwater\",\n\t\"identit\",\n\t\"kassa\", // kassandra\n\t\"kassi\", // kassie, kassidy\n\t\"lass\",  // class\n\t\"leafage\",\n\t\"libshitz\",\n\t\"magnacumlaude\",\n\t\"mass\",\n\t\"mocha\",\n\t\"pass\", // compass, passion\n\t\"penistone\",\n\t\"peacock\",\n\t\"phoebe\",\n\t\"phoenix\",\n\t\"pushit\",\n\t\"raccoon\",\n\t\"sassy\",\n\t\"saturday\",\n\t\"scrap\", // scrap, scrape, scraping\n\t\"serfage\",\n\t\"sexist\", // systems exist, sexist\n\t\"shoe\",\n\t\"scunthorpe\",\n\t\"shitake\",\n\t\"stitch\",\n\t\"sussex\",\n\t\"therapist\",\n\t\"therapeutic\",\n\t\"tysongay\",\n\t\"wass\",\n\t\"wharfage\",\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/TwiN/go-away\n\ngo 1.25.0\n\nrequire golang.org/x/text v0.36.0\n"
  },
  {
    "path": "go.sum",
    "content": "golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\n"
  },
  {
    "path": "goaway.go",
    "content": "package goaway\n\nimport (\n\t\"strings\"\n\t\"unicode\"\n\n\t\"golang.org/x/text/runes\"\n\t\"golang.org/x/text/transform\"\n\t\"golang.org/x/text/unicode/norm\"\n)\n\nconst (\n\tspace              = \" \"\n\tfirstRuneSupported = ' '\n\tlastRuneSupported  = '~'\n)\n\nvar (\n\tdefaultProfanityDetector *ProfanityDetector\n)\n\n// ProfanityDetector contains the dictionaries as well as the configuration\n// for determining how profanity detection is handled\ntype ProfanityDetector struct {\n\tsanitizeSpecialCharacters bool // Whether to replace characters with the value ' ' in characterReplacements\n\tsanitizeLeetSpeak         bool // Whether to replace characters with a non-' ' value in characterReplacements\n\tsanitizeAccents           bool\n\tsanitizeSpaces            bool\n\texactWord                 bool\n\n\tprofanities    []string\n\tfalseNegatives []string\n\tfalsePositives []string\n\n\tcharacterReplacements map[rune]rune\n}\n\n// NewProfanityDetector creates a new ProfanityDetector\nfunc NewProfanityDetector() *ProfanityDetector {\n\treturn &ProfanityDetector{\n\t\tsanitizeSpecialCharacters: true,\n\t\tsanitizeLeetSpeak:         true,\n\t\tsanitizeAccents:           true,\n\t\tsanitizeSpaces:            true,\n\t\texactWord:                 false,\n\t\tprofanities:               DefaultProfanities,\n\t\tfalsePositives:            DefaultFalsePositives,\n\t\tfalseNegatives:            DefaultFalseNegatives,\n\t\tcharacterReplacements:     DefaultCharacterReplacements,\n\t}\n}\n\n// WithSanitizeLeetSpeak allows configuring whether the sanitization process should also take into account leetspeak\n//\n// Leetspeak characters are characters to be replaced by non-' ' values in the characterReplacements map.\n// For instance, '4' is replaced by 'a' and '3' is replaced by 'e', which means that \"4sshol3\" would be\n// sanitized to \"asshole\", which would be detected as a profanity.\n//\n// By default, this is set to true.\nfunc (g *ProfanityDetector) WithSanitizeLeetSpeak(sanitize bool) *ProfanityDetector {\n\tg.sanitizeLeetSpeak = sanitize\n\treturn g.buildCharacterReplacements()\n}\n\n// WithSanitizeSpecialCharacters allows configuring whether the sanitization process should also take into account\n// special characters.\n//\n// Special characters are characters that are part of the characterReplacements map (DefaultCharacterReplacements by\n// default) and are to be removed during the sanitization step.\n//\n// For instance, \"fu_ck\" would be sanitized to \"fuck\", which would be detected as a profanity.\n//\n// By default, this is set to true.\nfunc (g *ProfanityDetector) WithSanitizeSpecialCharacters(sanitize bool) *ProfanityDetector {\n\tg.sanitizeSpecialCharacters = sanitize\n\treturn g.buildCharacterReplacements()\n}\n\n// WithSanitizeAccents allows configuring of whether the sanitization process should also take into account accents.\n// By default, this is set to true, but since this adds a bit of overhead, you may disable it if your use case\n// is time-sensitive or if the input doesn't involve accents (i.e. if the input can never contain special characters)\nfunc (g *ProfanityDetector) WithSanitizeAccents(sanitize bool) *ProfanityDetector {\n\tg.sanitizeAccents = sanitize\n\treturn g\n}\n\n// WithSanitizeSpaces allows configuring whether the sanitization process should also take into account spaces\nfunc (g *ProfanityDetector) WithSanitizeSpaces(sanitize bool) *ProfanityDetector {\n\tg.sanitizeSpaces = sanitize\n\treturn g\n}\n\n// WithCustomDictionary allows configuring whether the sanitization process should also take into account\n// custom profanities, false positives and false negatives dictionaries.\n// All dictionaries are expected to be lowercased.\nfunc (g *ProfanityDetector) WithCustomDictionary(profanities, falsePositives, falseNegatives []string) *ProfanityDetector {\n\tg.profanities = profanities\n\tg.falsePositives = falsePositives\n\tg.falseNegatives = falseNegatives\n\treturn g\n}\n\n// WithCustomCharacterReplacements allows configuring characters that to be replaced by other characters.\n//\n// Note that all entries that have the value ' ' are considered as special characters while all entries with a value\n// that is not ' ' are considered as leet speak.\n//\n// Defaults to DefaultCharacterReplacements\nfunc (g *ProfanityDetector) WithCustomCharacterReplacements(characterReplacements map[rune]rune) *ProfanityDetector {\n\tg.characterReplacements = characterReplacements\n\treturn g\n}\n\n// WithExactWord allows configuring whether the profanity check process should require exact matches or not.\n// Using this reduces false positives and winds up more permissive.\n//\n// Note: this entails also setting WithSanitizeSpaces(false), since without spaces present exact word matching\n// does not make sense.\nfunc (g *ProfanityDetector) WithExactWord(exactWord bool) *ProfanityDetector {\n\tg.exactWord = exactWord\n\treturn g.WithSanitizeSpaces(false)\n}\n\n// IsProfane takes in a string (word or sentence) and look for profanities.\n// Returns a boolean\nfunc (g *ProfanityDetector) IsProfane(s string) bool {\n\treturn len(g.ExtractProfanity(s)) > 0\n}\n\n// ExtractProfanity takes in a string (word or sentence) and look for profanities.\n// Returns the first profanity found, or an empty string if none are found\nfunc (g *ProfanityDetector) ExtractProfanity(s string) string {\n\ts, _ = g.sanitize(s, false)\n\n\t// Check for false negatives\n\tfor _, word := range g.falseNegatives {\n\t\tif match := strings.Contains(s, word); match {\n\t\t\treturn word\n\t\t}\n\t}\n\t// Remove false positives\n\tfor _, word := range g.falsePositives {\n\t\ts = strings.Replace(s, word, \"\", -1)\n\t}\n\n\tif g.exactWord {\n\t\ttokens := strings.Split(s, space)\n\t\tfor _, token := range tokens {\n\t\t\tif sliceContains(g.profanities, token) {\n\t\t\t\treturn token\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Check for profanities\n\t\tfor _, word := range g.profanities {\n\t\t\tif match := strings.Contains(s, word); match {\n\t\t\t\treturn word\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc sliceContains(words []string, s string) bool {\n\tfor _, word := range words {\n\t\tif strings.EqualFold(s, word) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (g *ProfanityDetector) indexToRune(s string, index int) int {\n\tcount := 0\n\tfor i := range s {\n\t\tif i == index {\n\t\t\tbreak\n\t\t}\n\t\tif i < index {\n\t\t\tcount++\n\t\t}\n\t}\n\treturn count\n}\n\nfunc (g *ProfanityDetector) Censor(s string) string {\n\tcensored := []rune(s)\n\tvar originalIndexes []int\n\ts, originalIndexes = g.sanitize(s, true)\n\truneWordLength := 0\n\n\tg.checkProfanity(&s, &originalIndexes, &censored, g.falseNegatives, &runeWordLength)\n\tg.removeFalsePositives(&s, &originalIndexes, &runeWordLength)\n\tg.checkProfanity(&s, &originalIndexes, &censored, g.profanities, &runeWordLength)\n\n\treturn string(censored)\n}\n\nfunc (g *ProfanityDetector) checkProfanity(s *string, originalIndexes *[]int, censored *[]rune, wordList []string, runeWordLength *int) {\n\tfor _, word := range wordList {\n\t\tcurrentIndex := 0\n\t\t*runeWordLength = len([]rune(word))\n\t\tfor currentIndex != -1 {\n\t\t\tif foundIndex := strings.Index((*s)[currentIndex:], word); foundIndex != -1 {\n\t\t\t\tfor i := 0; i < *runeWordLength; i++ {\n\t\t\t\t\truneIndex := g.indexToRune(*s, currentIndex+foundIndex) + i\n\t\t\t\t\tif runeIndex < len(*originalIndexes) {\n\t\t\t\t\t\t(*censored)[(*originalIndexes)[runeIndex]] = '*'\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcurrentIndex += foundIndex + len([]byte(word))\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (g *ProfanityDetector) removeFalsePositives(s *string, originalIndexes *[]int, runeWordLength *int) {\n\tfor _, word := range g.falsePositives {\n\t\tcurrentIndex := 0\n\t\t*runeWordLength = len([]rune(word))\n\t\tfor currentIndex != -1 {\n\t\t\tif foundIndex := strings.Index((*s)[currentIndex:], word); foundIndex != -1 {\n\t\t\t\tfoundRuneIndex := g.indexToRune(*s, foundIndex)\n\t\t\t\t*originalIndexes = append((*originalIndexes)[:foundRuneIndex], (*originalIndexes)[foundRuneIndex+*runeWordLength:]...)\n\t\t\t\tcurrentIndex += foundIndex + len([]byte(word))\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\t*s = strings.Replace(*s, word, \"\", -1)\n\t}\n}\n\nfunc (g ProfanityDetector) sanitize(s string, rememberOriginalIndexes bool) (string, []int) {\n\ts = strings.ToLower(s)\n\tif g.sanitizeLeetSpeak && !rememberOriginalIndexes && g.sanitizeSpecialCharacters {\n\t\ts = strings.ReplaceAll(s, \"()\", \"o\")\n\t}\n\tsb := strings.Builder{}\n\tfor _, char := range s {\n\t\tif replacement, found := g.characterReplacements[char]; found {\n\t\t\tif g.sanitizeSpecialCharacters && replacement == ' ' {\n\t\t\t\t// If the replacement is a space, and we're sanitizing special characters speak, we replace.\n\t\t\t\tsb.WriteRune(replacement)\n\t\t\t\tcontinue\n\t\t\t} else if g.sanitizeLeetSpeak && replacement != ' ' {\n\t\t\t\t// If the replacement isn't a space, and we're sanitizing leet speak, we replace.\n\t\t\t\tsb.WriteRune(replacement)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tsb.WriteRune(char)\n\t}\n\ts = sb.String()\n\tif g.sanitizeAccents {\n\t\ts = removeAccents(s)\n\t}\n\tvar originalIndexes []int\n\tif rememberOriginalIndexes {\n\t\tfor i, c := range []rune(s) {\n\t\t\t// If spaces aren't being sanitized, appending to the original indices prevents off-by-one errors later on.\n\t\t\tif c != ' ' || !g.sanitizeSpaces {\n\t\t\t\toriginalIndexes = append(originalIndexes, i)\n\t\t\t}\n\t\t}\n\t}\n\tif g.sanitizeSpaces {\n\t\ts = strings.Replace(s, space, \"\", -1)\n\t}\n\treturn s, originalIndexes\n}\n\n// removeAccents strips all accents from characters.\n// Only called if ProfanityDetector.removeAccents is set to true\nfunc removeAccents(s string) string {\n\tremoveAccentsTransformer := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)\n\tfor _, character := range s {\n\t\t// If there's a character outside the range of supported runes, there might be some accented words\n\t\tif character < firstRuneSupported || character > lastRuneSupported {\n\t\t\ts, _, _ = transform.String(removeAccentsTransformer, s)\n\t\t\tbreak\n\t\t}\n\t}\n\treturn s\n}\n\n// buildCharacterReplacements builds characterReplacements if WithSanitizeLeetSpeak or WithSanitizeSpecialCharacters is\n// called.\n//\n// If this is not called, DefaultCharacterReplacements\nfunc (g *ProfanityDetector) buildCharacterReplacements() *ProfanityDetector {\n\tg.characterReplacements = make(map[rune]rune)\n\tif g.sanitizeSpecialCharacters {\n\t\tg.characterReplacements['-'] = ' '\n\t\tg.characterReplacements['_'] = ' '\n\t\tg.characterReplacements['|'] = ' '\n\t\tg.characterReplacements['.'] = ' '\n\t\tg.characterReplacements[','] = ' '\n\t\tg.characterReplacements['('] = ' '\n\t\tg.characterReplacements[')'] = ' '\n\t\tg.characterReplacements['<'] = ' '\n\t\tg.characterReplacements['>'] = ' '\n\t\tg.characterReplacements['\"'] = ' '\n\t\tg.characterReplacements['`'] = ' '\n\t\tg.characterReplacements['~'] = ' '\n\t\tg.characterReplacements['*'] = ' '\n\t\tg.characterReplacements['&'] = ' '\n\t\tg.characterReplacements['%'] = ' '\n\t\tg.characterReplacements['$'] = ' '\n\t\tg.characterReplacements['#'] = ' '\n\t\tg.characterReplacements['@'] = ' '\n\t\tg.characterReplacements['!'] = ' '\n\t\tg.characterReplacements['?'] = ' '\n\t\tg.characterReplacements['+'] = ' '\n\t}\n\tif g.sanitizeLeetSpeak {\n\t\tg.characterReplacements['4'] = 'a'\n\t\tg.characterReplacements['$'] = 's'\n\t\tg.characterReplacements['!'] = 'i'\n\t\tg.characterReplacements['+'] = 't'\n\t\tg.characterReplacements['#'] = 'h'\n\t\tg.characterReplacements['@'] = 'a'\n\t\tg.characterReplacements['0'] = 'o'\n\t\tg.characterReplacements['1'] = 'i'\n\t\tg.characterReplacements['7'] = 'l'\n\t\tg.characterReplacements['3'] = 'e'\n\t\tg.characterReplacements['5'] = 's'\n\t\tg.characterReplacements['<'] = 'c'\n\t}\n\treturn g\n}\n\n// IsProfane checks whether there are any profanities in a given string (word or sentence).\n//\n// Uses the default ProfanityDetector\nfunc IsProfane(s string) bool {\n\tif defaultProfanityDetector == nil {\n\t\tdefaultProfanityDetector = NewProfanityDetector()\n\t}\n\treturn defaultProfanityDetector.IsProfane(s)\n}\n\n// ExtractProfanity takes in a string (word or sentence) and look for profanities.\n// Returns the first profanity found, or an empty string if none are found\n//\n// Uses the default ProfanityDetector\nfunc ExtractProfanity(s string) string {\n\tif defaultProfanityDetector == nil {\n\t\tdefaultProfanityDetector = NewProfanityDetector()\n\t}\n\treturn defaultProfanityDetector.ExtractProfanity(s)\n}\n\n// Censor takes in a string (word or sentence) and tries to censor all profanities found.\n//\n// Uses the default ProfanityDetector\nfunc Censor(s string) string {\n\tif defaultProfanityDetector == nil {\n\t\tdefaultProfanityDetector = NewProfanityDetector()\n\t}\n\treturn defaultProfanityDetector.Censor(s)\n}\n"
  },
  {
    "path": "goaway_bench_test.go",
    "content": "package goaway\n\nimport (\n\t\"testing\"\n)\n\nfunc BenchmarkIsProfaneWhenShortStringHasNoProfanity(b *testing.B) {\n\tfor n := 0; n < b.N; n++ {\n\t\tIsProfane(\"aaaaaaaaaaaaaa\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkIsProfaneWhenShortStringHasProfanityAtTheStart(b *testing.B) {\n\tfor n := 0; n < b.N; n++ {\n\t\tIsProfane(\"fuckaaaaaaaaaa\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkIsProfaneWhenShortStringHasProfanityInTheMiddle(b *testing.B) {\n\tfor n := 0; n < b.N; n++ {\n\t\tIsProfane(\"aaaaafuckaaaaa\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkIsProfaneWhenShortStringHasProfanityAtTheEnd(b *testing.B) {\n\tfor n := 0; n < b.N; n++ {\n\t\tIsProfane(\"aaaaaaaaaafuck\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkIsProfaneWhenMediumStringHasNoProfanity(b *testing.B) {\n\tfor n := 0; n < b.N; n++ {\n\t\tIsProfane(\"How are you doing today?\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkIsProfaneWhenMediumStringHasProfanityAtTheStart(b *testing.B) {\n\tfor n := 0; n < b.N; n++ {\n\t\tIsProfane(\"Shit, you're cute today.\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkIsProfaneWhenMediumStringHasProfanityInTheMiddle(b *testing.B) {\n\tfor n := 0; n < b.N; n++ {\n\t\tIsProfane(\"How are you fu ck doing?\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkIsProfaneWhenMediumStringHasProfanityAtTheEnd(b *testing.B) {\n\tfor n := 0; n < b.N; n++ {\n\t\tIsProfane(\"you're cute today. Fuck.\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkIsProfaneWhenLongStringHasNoProfanity(b *testing.B) {\n\tfor n := 0; n < b.N; n++ {\n\t\tIsProfane(\"Hello John Doe, I hope you're feeling well, as I come today bearing terrible news regarding your favorite chocolate chip cookie brand\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkIsProfaneWhenLongStringHasProfanityAtTheStart(b *testing.B) {\n\tfor n := 0; n < b.N; n++ {\n\t\tIsProfane(\"Fuck John Doe, I hope you're feeling well, as I come today bearing terrible news regarding your favorite chocolate chip cookie brand\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkIsProfaneWhenLongStringHasProfanityInTheMiddle(b *testing.B) {\n\tfor n := 0; n < b.N; n++ {\n\t\tIsProfane(\"Hello John Doe, I hope you're feeling well, as I come today bearing shitty news regarding your favorite chocolate chip cookie brand\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkIsProfaneWhenLongStringHasProfanityAtTheEnd(b *testing.B) {\n\tfor n := 0; n < b.N; n++ {\n\t\tIsProfane(\"Hello John Doe, I hope you're feeling well, as I come today bearing terrible news regarding your favorite chocolate chip cookie bitch\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkProfanityDetector_WithSanitizeAccentsSetToFalseWhenLongStringHasProfanityAtTheStart(b *testing.B) {\n\tprofanityDetector := NewProfanityDetector().WithSanitizeAccents(false)\n\tfor n := 0; n < b.N; n++ {\n\t\tprofanityDetector.IsProfane(\"Fuck John Doe, I hope you're feeling well, as I come today bearing terrible news regarding your favorite chocolate chip cookie brand\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkProfanityDetector_WithSanitizeAccentsSetToFalseWhenLongStringHasProfanityInTheMiddle(b *testing.B) {\n\tprofanityDetector := NewProfanityDetector().WithSanitizeAccents(false)\n\tfor n := 0; n < b.N; n++ {\n\t\tprofanityDetector.IsProfane(\"Hello John Doe, I hope you're feeling well, as I come today bearing shitty news regarding your favorite chocolate chip cookie brand\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkProfanityDetector_WithSanitizeAccentsSetToFalseWhenLongStringHasProfanityAtTheEnd(b *testing.B) {\n\tprofanityDetector := NewProfanityDetector().WithSanitizeAccents(false)\n\tfor n := 0; n < b.N; n++ {\n\t\tprofanityDetector.IsProfane(\"Hello John Doe, I hope you're feeling well, as I come today bearing terrible news regarding your favorite chocolate chip cookie bitch\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkProfanityDetector_Sanitize(b *testing.B) {\n\tprofanityDetector := NewProfanityDetector().WithSanitizeAccents(true).WithSanitizeSpecialCharacters(true).WithSanitizeLeetSpeak(true)\n\tfor n := 0; n < b.N; n++ {\n\t\tprofanityDetector.IsProfane(\"H3ll0 J0hn D0e, 1 h0p3 y0u'r3 f3eling w3ll, as 1 c0me t0d4y b34r1ng sh1tty n3w5 r3g4rd1ng y0ur fav0rite ch0c0l4t3 chip c00kie br4nd\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkCensor(b *testing.B) {\n\tfor n := 0; n < b.N; n++ {\n\t\tCensor(\"Thundercunt c()ck\")\n\t}\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkIsProfaneConcurrently(b *testing.B) {\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tIsProfane(\"aaaaafuckaaaaa\")\n\t\t}\n\t})\n\tb.ReportAllocs()\n}\n\nfunc BenchmarkIsProfaneConcurrently_WithAccents(b *testing.B) {\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tIsProfane(\"ÄšŚ\")\n\t\t}\n\t})\n\tb.ReportAllocs()\n}\n"
  },
  {
    "path": "goaway_test.go",
    "content": "package goaway\n\nimport (\n\t\"testing\"\n)\n\nfunc TestExtractProfanity(t *testing.T) {\n\tdefaultProfanityDetector = nil\n\ttests := []struct {\n\t\tinput             string\n\t\texpectedProfanity string\n\t}{\n\t\t{\n\t\t\tinput:             \"fuck this shit\",\n\t\t\texpectedProfanity: \"fuck\",\n\t\t},\n\t\t{\n\t\t\tinput:             \"F   u   C  k th1$ $h!t\",\n\t\t\texpectedProfanity: \"fuck\",\n\t\t},\n\t\t{\n\t\t\tinput:             \"@$$h073\",\n\t\t\texpectedProfanity: \"asshole\",\n\t\t},\n\t\t{\n\t\t\tinput:             \"hello, world!\",\n\t\t\texpectedProfanity: \"\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tprofanity := ExtractProfanity(tt.input)\n\t\t\tif profanity != tt.expectedProfanity {\n\t\t\t\tt.Errorf(\"expected '%s', got '%s'\", tt.expectedProfanity, profanity)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProfanityDetector_Censor(t *testing.T) {\n\tdefaultProfanityDetector = nil\n\tprofanityDetectorWithSanitizeSpaceDisabled := NewProfanityDetector().WithSanitizeSpaces(false)\n\ttests := []struct {\n\t\tinput                                  string\n\t\texpectedOutput                         string\n\t\texpectedOutputWithoutSpaceSanitization string\n\t}{\n\t\t{\n\t\t\tinput:                                  \"what the fuck\",\n\t\t\texpectedOutput:                         \"what the ****\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"what the ****\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"fuck this\",\n\t\t\texpectedOutput:                         \"**** this\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"**** this\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"one penis, two vaginas, three dicks, four sluts, five whores and a flower\",\n\t\t\texpectedOutput:                         \"one *****, two ******s, three ****s, four ****s, five *****s and a flower\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"one *****, two ******s, three ****s, four ****s, five *****s and a flower\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"Censor doesn't support sanitizing '()' into 'o', because it's two characters. Proof: c()ck. Maybe one day I'll have time to fix it.\",\n\t\t\texpectedOutput:                         \"Censor doesn't support sanitizing '()' into 'o', because it's two characters. Proof: c()ck. Maybe one day I'll have time to fix it.\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"Censor doesn't support sanitizing '()' into 'o', because it's two characters. Proof: c()ck. Maybe one day I'll have time to fix it.\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"fuck shit fuck\",\n\t\t\texpectedOutput:                         \"**** **** ****\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"**** **** ****\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"fuckfuck\",\n\t\t\texpectedOutput:                         \"********\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"********\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"fuck this shit\",\n\t\t\texpectedOutput:                         \"**** this ****\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"**** this ****\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"F   u   C  k th1$ $h!t\",\n\t\t\texpectedOutput:                         \"*   *   *  * th1$ ****\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"F   u   C  k th1$ ****\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"@$$h073\",\n\t\t\texpectedOutput:                         \"*******\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"*******\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"hello, world!\",\n\t\t\texpectedOutput:                         \"hello, world!\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"hello, world!\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"Hey asshole, are y()u an assassin? If not, fuck off.\",\n\t\t\texpectedOutput:                         \"Hey *******, are y()u an assassin? If not, **** off.\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"Hey *******, are y()u an assassin? If not, **** off.\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"I am from Scunthorpe, north Lincolnshire\",\n\t\t\texpectedOutput:                         \"I am from Scunthorpe, north Lincolnshire\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"I am from Scunthorpe, north Lincolnshire\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"He is an associate of mine\",\n\t\t\texpectedOutput:                         \"He is an associate of mine\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"He is an associate of mine\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"But the table is on fucking fire\",\n\t\t\texpectedOutput:                         \"But the table is on ****ing fire\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"But the table is on ****ing fire\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"““““““““““““But the table is on fucking fire“\",\n\t\t\texpectedOutput:                         \"““““““““““““But the table is on ****ing fire“\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"““““““““““““But the table is on ****ing fire“\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"f.u_ck this s.h-i~t\",\n\t\t\texpectedOutput:                         \"*.*_** this *.*-*~*\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"f.u_ck this s.h-i~t\", // This is because special characters get replaced with a space, and because we're not sanitizing spaces...\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"glass\",\n\t\t\texpectedOutput:                         \"glass\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"glass\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"ы\",\n\t\t\texpectedOutput:                         \"ы\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"ы\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"documentdocument\", // false positives (https://github.com/TwiN/go-away/issues/30)\n\t\t\texpectedOutput:                         \"documentdocument\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"documentdocument\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"dumbassdumbass\", // false negatives (https://github.com/TwiN/go-away/issues/30)\n\t\t\texpectedOutput:                         \"**************\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"**************\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"document fuck document fuck\", // FIXME: This is not censored properly\n\t\t\texpectedOutput:                         \"document **** document ****\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"document **** document ****\",\n\t\t},\n\t\t{\n\t\t\tinput:                                  \"Everyone was staring, and someone muttered ‘gyat’ under their breath.\",\n\t\t\texpectedOutput:                         \"Everyone was staring, and someone muttered ‘****’ under their breath.\",\n\t\t\texpectedOutputWithoutSpaceSanitization: \"Everyone was staring, and someone muttered ‘****’ under their breath.\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(\"default_\"+tt.input, func(t *testing.T) {\n\t\t\tcensored := Censor(tt.input)\n\t\t\tif censored != tt.expectedOutput {\n\t\t\t\tt.Errorf(\"expected '%s', got '%s'\", tt.expectedOutput, censored)\n\t\t\t}\n\t\t})\n\t\tt.Run(\"no-space-sanitization_\"+tt.input, func(t *testing.T) {\n\t\t\tcensored := profanityDetectorWithSanitizeSpaceDisabled.Censor(tt.input)\n\t\t\tif censored != tt.expectedOutputWithoutSpaceSanitization {\n\t\t\t\tt.Errorf(\"expected '%s', got '%s'\", tt.expectedOutputWithoutSpaceSanitization, censored)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNoDuplicatesBetweenProfanitiesAndFalseNegatives(t *testing.T) {\n\tfor _, profanity := range DefaultProfanities {\n\t\tfor _, falseNegative := range DefaultFalseNegatives {\n\t\t\tif profanity == falseNegative {\n\t\t\t\tt.Errorf(\"'%s' is already in 'falseNegatives', there's no need to have it in 'profanities' too\", profanity)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestBadWords(t *testing.T) {\n\twords := []string{\"fuck\", \"ass\", \"poop\", \"penis\", \"bitch\"}\n\ttests := []struct {\n\t\tname              string\n\t\tprofanityDetector *ProfanityDetector\n\t}{\n\t\t{\n\t\t\tname:              \"With Default Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector(),\n\t\t},\n\t\t{\n\t\t\tname:              \"With Custom Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomDictionary([]string{\"fuck\", \"ass\", \"poop\", \"penis\", \"bitch\"}, DefaultFalsePositives, DefaultFalseNegatives),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor _, w := range words {\n\t\t\t\tif !tt.profanityDetector.IsProfane(w) {\n\t\t\t\t\tt.Error(\"Expected true, got false from word\", w)\n\t\t\t\t}\n\t\t\t\tif word := tt.profanityDetector.ExtractProfanity(w); len(word) == 0 {\n\t\t\t\t\tt.Error(\"Expected true, got false from word\", w)\n\t\t\t\t} else if word != w {\n\t\t\t\t\tt.Errorf(\"Expected %s, got %s\", w, word)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBadWordsWithSpaces(t *testing.T) {\n\tprofanities := []string{\"fuck\", \"ass\", \"poop\", \"penis\", \"bitch\"}\n\twords := []string{\"fu ck\", \"as s\", \"po op\", \"pe ni s\", \"bit ch\"}\n\ttests := []struct {\n\t\tname              string\n\t\tprofanityDetector *ProfanityDetector\n\t}{\n\t\t{\n\t\t\tname:              \"With Default Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector(),\n\t\t},\n\t\t{\n\t\t\tname:              \"With Custom Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomDictionary(profanities, DefaultFalsePositives, DefaultFalseNegatives),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor _, w := range words {\n\t\t\t\tif !tt.profanityDetector.WithSanitizeSpaces(true).IsProfane(w) {\n\t\t\t\t\tt.Error(\"Expected true because sanitizeSpaces is set to true, got false from word\", w)\n\t\t\t\t}\n\t\t\t\tif tt.profanityDetector.WithSanitizeSpaces(false).IsProfane(w) {\n\t\t\t\t\tt.Error(\"Expected false because sanitizeSpaces is set to false, got true from word\", w)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBadWordsWithAccentedLetters(t *testing.T) {\n\tprofanities := []string{\"fuck\", \"ass\", \"poop\", \"penis\", \"bitch\"}\n\twords := []string{\"fučk\", \"ÄšŚ\", \"pÓöp\", \"pÉnìŚ\", \"bitčh\"}\n\ttests := []struct {\n\t\tname              string\n\t\tprofanityDetector *ProfanityDetector\n\t}{\n\t\t{\n\t\t\tname:              \"With Default Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector(),\n\t\t},\n\t\t{\n\t\t\tname:              \"With Custom Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomDictionary(profanities, DefaultFalsePositives, DefaultFalseNegatives),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor _, w := range words {\n\t\t\t\tif !tt.profanityDetector.WithSanitizeAccents(true).IsProfane(w) {\n\t\t\t\t\tt.Error(\"Expected true because sanitizeAccents is set to true, got false from word\", w)\n\t\t\t\t}\n\t\t\t\tif tt.profanityDetector.WithSanitizeAccents(false).IsProfane(w) {\n\t\t\t\t\tt.Error(\"Expected false because sanitizeAccents is set to false, got true from word\", w)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCensorWithVerySpecialCharacters(t *testing.T) {\n\tprofanities := []string{\"крывавыa\"}\n\twords := []string{\"крывавыa\"}\n\texpectedOutputs := []string{\"********\"}\n\ttests := []struct {\n\t\tname              string\n\t\tprofanityDetector *ProfanityDetector\n\t}{\n\t\t{\n\t\t\tname:              \"With Custom Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomDictionary(profanities, DefaultFalsePositives, DefaultFalseNegatives),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor index, w := range words {\n\t\t\t\tif output := tt.profanityDetector.Censor(w); output != expectedOutputs[index] {\n\t\t\t\t\tt.Errorf(\"Expected %s to return %s, got %s\", w, expectedOutputs[index], output)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSentencesWithBadWords(t *testing.T) {\n\tprofanities := []string{\"fuck\", \"ass\", \"poop\", \"penis\", \"bitch\"}\n\tsentences := []string{\"What the fuck is your problem\", \"Go away, asshole!\"}\n\ttests := []struct {\n\t\tname              string\n\t\tprofanityDetector *ProfanityDetector\n\t}{\n\t\t{\n\t\t\tname:              \"With Default Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector(),\n\t\t},\n\t\t{\n\t\t\tname:              \"With Custom Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomDictionary(profanities, DefaultFalsePositives, DefaultFalseNegatives),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor _, s := range sentences {\n\t\t\t\tif !tt.profanityDetector.IsProfane(s) {\n\t\t\t\t\tt.Error(\"Expected true, got false from sentence\", s)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProfanityDetector_WithCustomCharacterReplacements(t *testing.T) {\n\ttests := []struct {\n\t\tname              string\n\t\tprofanityDetector *ProfanityDetector\n\t\tsentence          string\n\t\tresult            bool\n\t}{\n\t\t{\n\t\t\tname:              \"With default profanity detector\",\n\t\t\tprofanityDetector: NewProfanityDetector(),\n\t\t\tsentence:          \"5#1+\",\n\t\t\tresult:            true, // shit is a profanity\n\t\t},\n\t\t{\n\t\t\tname:              \"With custom character replacements that has leet speak characters\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomCharacterReplacements(map[rune]rune{'(': 'c'}),\n\t\t\tsentence:          \"fu(k\",\n\t\t\tresult:            true, // fuck is a profanity\n\t\t},\n\t\t{\n\t\t\tname:              \"With custom character replacements that has leet speak characters with sanitizeLeetSpeak disabled\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomCharacterReplacements(map[rune]rune{'(': 'c'}).WithSanitizeLeetSpeak(false),\n\t\t\tsentence:          \"fu(k\",\n\t\t\tresult:            false, // fuk isn't a profanity\n\t\t},\n\t\t{\n\t\t\tname:              \"With custom character replacements that has leet speak characters with sanitizeSpecialCharacters disabled\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomCharacterReplacements(map[rune]rune{'(': 'c'}).WithSanitizeSpecialCharacters(false),\n\t\t\tsentence:          \"fu(k\",\n\t\t\tresult:            false, // fu(k isn't a profanity\n\t\t},\n\t\t{\n\t\t\tname:              \"With custom character replacements that has special characters\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomCharacterReplacements(map[rune]rune{'.': ' '}),\n\t\t\tsentence:          \"f.u.c.k\",\n\t\t\tresult:            true,\n\t\t},\n\t\t{\n\t\t\tname:              \"With custom character replacements that has special characters with sanitizeLeetSpeak disabled\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomCharacterReplacements(map[rune]rune{'.': ' '}).WithSanitizeLeetSpeak(false),\n\t\t\tsentence:          \"f.u.c.k\",\n\t\t\tresult:            true, // fuck is a profanity\n\t\t},\n\t\t{\n\t\t\tname:              \"With custom character replacements that has special characters with sanitizeSpecialCharacters disabled\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomCharacterReplacements(map[rune]rune{'.': ' '}).WithSanitizeSpecialCharacters(false),\n\t\t\tsentence:          \"f.u.c.k\",\n\t\t\tresult:            false, // f.u.c.k isn't a profanity\n\t\t},\n\t\t{\n\t\t\tname:              \"With empty character replacement mapping\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomCharacterReplacements(map[rune]rune{}),\n\t\t\tsentence:          \"5#1+\",\n\t\t\tresult:            false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.profanityDetector.IsProfane(tt.sentence)\n\t\t\tif got != tt.result {\n\t\t\t\tt.Errorf(\"Expected %v, got %v from sentence %s\", tt.result, got, tt.sentence)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSneakyBadWords(t *testing.T) {\n\tprofanities := []string{\"fuck\", \"ass\", \"poop\", \"penis\", \"bitch\", \"arse\", \"shit\", \"btch\"}\n\twords := []string{\"A$$\", \"4ss\", \"4s$\", \"a S s\", \"a $ s\", \"@$$h073\", \"f    u     c k\", \"4r5e\", \"5h1t\", \"5hit\", \"a55\", \"ar5e\", \"a_s_s\", \"b!tch\", \"b!+ch\"}\n\ttests := []struct {\n\t\tname              string\n\t\tprofanityDetector *ProfanityDetector\n\t}{\n\t\t{\n\t\t\tname:              \"With Default Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector(),\n\t\t},\n\t\t{\n\t\t\tname:              \"With Custom Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomDictionary(profanities, DefaultFalsePositives, DefaultFalseNegatives),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor _, w := range words {\n\t\t\t\tif !tt.profanityDetector.IsProfane(w) {\n\t\t\t\t\tt.Error(\"Expected true, got false from word\", w)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSentencesWithSneakyBadWords(t *testing.T) {\n\tprofanities := []string{\"poop\", \"asshole\"}\n\tsentences := []string{\n\t\t\"You smell p00p\",\n\t\t\"Go away, a$$h0l3!\",\n\t}\n\ttests := []struct {\n\t\tname              string\n\t\tprofanityDetector *ProfanityDetector\n\t}{\n\t\t{\n\t\t\tname:              \"With Default Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector(),\n\t\t},\n\t\t{\n\t\t\tname:              \"With Custom Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomDictionary(profanities, DefaultFalsePositives, DefaultFalseNegatives),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor _, s := range sentences {\n\t\t\t\tif !tt.profanityDetector.IsProfane(s) {\n\t\t\t\t\tt.Error(\"Expected true, got false from sentence\", s)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNormalWords(t *testing.T) {\n\twords := []string{\"hello\", \"world\", \"whats\", \"up\"}\n\ttests := []struct {\n\t\tname              string\n\t\tprofanityDetector *ProfanityDetector\n\t}{\n\t\t{\n\t\t\tname:              \"With Default Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector(),\n\t\t},\n\t\t{\n\t\t\tname:              \"With Custom Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomDictionary(DefaultProfanities, DefaultFalsePositives, DefaultFalseNegatives),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor _, w := range words {\n\t\t\t\tif tt.profanityDetector.IsProfane(w) {\n\t\t\t\t\tt.Error(\"Expected false, got true from word\", w)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSentencesWithNoProfanities(t *testing.T) {\n\tsentences := []string{\n\t\t\"hello, my friend\",\n\t\t\"what's up?\",\n\t\t\"do you want to play bingo?\",\n\t\t\"who are you?\",\n\t\t\"Better late than never\",\n\t\t\"Bite the bullet\",\n\t\t\"Break a leg\",\n\t\t\"Call it a day\",\n\t\t\"Be careful when you're driving\",\n\t\t\"How are you?\",\n\t\t\"Hurry up!\",\n\t\t\"I don't like her\",\n\t\t\"If you need my help, please let me know\",\n\t\t\"Leave a message after the beep\",\n\t\t\"Thank you\",\n\t\t\"Yes, really\",\n\t\t\"Call me at 9, ok?\",\n\t}\n\n\tfor _, s := range sentences {\n\t\tif IsProfane(s) {\n\t\t\tt.Error(\"Expected false, got false from sentence\", s)\n\t\t}\n\t}\n}\n\nfunc TestFalsePositives(t *testing.T) {\n\tsentences := []string{\n\t\t\"I am from Scunthorpe, north Lincolnshire\",\n\t\t\"He is an associate of mine\",\n\t\t\"Are you an assassin?\",\n\t\t\"But the table is on fire\",\n\t\t\"glass\",\n\t\t\"grass\",\n\t\t\"classic\",\n\t\t\"classification\",\n\t\t\"passion\",\n\t\t\"carcass\",\n\t\t\"cassandra\",\n\t\t\"just push it down the ledge\", // puSH IT\n\t\t\"has steph\",                   // hAS Steph\n\t\t\"was steph\",                   // wAS Steph\n\t\t\"hot water\",                   // hoT WATer\n\t\t\"Phoenix\",                     // pHOEnix\n\t\t\"systems exist\",               // systemS EXist\n\t\t\"saturday\",                    // saTURDay\n\t\t\"therapeutic\",\n\t\t\"press the button\",\n\t}\n\ttests := []struct {\n\t\tname              string\n\t\tprofanityDetector *ProfanityDetector\n\t}{\n\t\t{\n\t\t\tname:              \"With Default Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector(),\n\t\t},\n\t\t{\n\t\t\tname:              \"With Custom Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomDictionary(DefaultProfanities, DefaultFalsePositives, DefaultFalseNegatives),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor _, s := range sentences {\n\t\t\t\tif tt.profanityDetector.IsProfane(s) {\n\t\t\t\t\tt.Error(\"Expected false, got true from:\", s)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestExactWord(t *testing.T) {\n\tacceptSentences := []string{\n\t\t\"I'm an analyst\",\n\t}\n\trejectSentences := []string{\"Go away, ass.\"}\n\ttests := []struct {\n\t\tname              string\n\t\tprofanityDetector *ProfanityDetector\n\t}{\n\t\t{\n\t\t\tname:              \"With Empty FalsePositives\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithExactWord(true).WithSanitizeSpecialCharacters(true).WithCustomDictionary(DefaultProfanities, nil, nil),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor _, s := range acceptSentences {\n\t\t\t\tif tt.profanityDetector.IsProfane(s) {\n\t\t\t\t\tt.Error(\"Expected false, got true from:\", s)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor _, s := range rejectSentences {\n\t\t\t\tif !tt.profanityDetector.IsProfane(s) {\n\t\t\t\t\tt.Error(\"Expected true, got false from:\", s)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFalseNegatives(t *testing.T) {\n\tsentences := []string{\n\t\t\"dumb ass\", // ass -> bASS (FP) -> dumBASS (FFP)\n\t}\n\ttests := []struct {\n\t\tname              string\n\t\tprofanityDetector *ProfanityDetector\n\t}{\n\t\t{\n\t\t\tname:              \"With Default Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector(),\n\t\t},\n\t\t{\n\t\t\tname:              \"With Custom Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomDictionary(DefaultProfanities, DefaultFalsePositives, DefaultFalseNegatives),\n\t\t},\n\t\t{\n\t\t\tname:              \"With Custom Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithExactWord(true).WithCustomDictionary(DefaultProfanities, DefaultFalsePositives, DefaultFalseNegatives),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor _, s := range sentences {\n\t\t\t\tif !tt.profanityDetector.IsProfane(s) {\n\t\t\t\t\tt.Error(\"Expected false, got true from:\", s)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSentencesWithFalsePositivesAndProfanities(t *testing.T) {\n\tsentences := []string{\"You are a shitty associate\", \"Go away, asshole!\"}\n\ttests := []struct {\n\t\tname              string\n\t\tprofanityDetector *ProfanityDetector\n\t}{\n\t\t{\n\t\t\tname:              \"With Default Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector(),\n\t\t},\n\t\t{\n\t\t\tname:              \"With Custom Dictionary\",\n\t\t\tprofanityDetector: NewProfanityDetector().WithCustomDictionary(DefaultProfanities, DefaultFalsePositives, DefaultFalseNegatives),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor _, s := range sentences {\n\t\t\t\tif !tt.profanityDetector.IsProfane(s) {\n\t\t\t\t\tt.Error(\"Expected true, got false from sentence\", s)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// \"The Adventures of Sherlock Holmes\" by Arthur Conan Doyle is in the public domain,\n// which makes it a perfect source to use as reference.\nfunc TestSentencesFromTheAdventuresOfSherlockHolmes(t *testing.T) {\n\tdefaultProfanityDetector = nil\n\tsentences := []string{\n\t\t\"I had called upon my friend, Mr. Sherlock Holmes, one day in the autumn of last year and found him in deep conversation with a very stout, florid-faced, elderly gentleman with fiery red hair.\",\n\t\t\"With an apology for my intrusion, I was about to withdraw when Holmes pulled me abruptly into the room and closed the door behind me.\",\n\t\t\"You could not possibly have come at a better time, my dear Watson, he said cordially\",\n\t\t\"I was afraid that you were engaged.\",\n\t\t\"So I am. Very much so.\",\n\t\t\"Then I can wait in the next room.\",\n\t\t\"Not at all. This gentleman, Mr. Wilson, has been my partner and helper in many of my most successful cases, and I have no doubt that he will be of the utmost use to me in yours also.\",\n\t\t\"The stout gentleman half rose from his chair and gave a bob of greeting, with a quick little questioning glance from his small fat-encircled eyes\",\n\t\t\"Try the settee, said Holmes, relapsing into his armchair and putting his fingertips together, as was his custom when in judicial moods.\",\n\t\t\"I know, my dear Watson, that you share my love of all that is bizarre and outside the conventions and humdrum routine of everyday life.\",\n\t\t\"You have shown your relish for it by the enthusiasm which has prompted you to chronicle, and, if you will excuse my saying so, somewhat to embellish so many of my own little adventures.\",\n\t\t\"You did, Doctor, but none the less you must come round to my view, for otherwise I shall keep on piling fact upon fact on you until your reason breaks down under them and acknowledges me to be right.\",\n\t\t\"Now, Mr. Jabez Wilson here has been good enough to call upon me this morning, and to begin a narrative which promises to be one of the most singular which I have listened to for some time.\",\n\t\t\"You have heard me remark that the strangest and most unique things are very often connected not with the larger but with the smaller crimes, and occasionally\",\n\t\t\"indeed, where there is room for doubt whether any positive crime has been committed.\",\n\t\t\"As far as I have heard it is impossible for me to say whether the present case is an instance of crime or not, but the course of events is certainly among the most singular that I have ever listened to.\",\n\t\t\"Perhaps, Mr. Wilson, you would have the great kindness to recommence your narrative.\",\n\t\t\"I ask you not merely because my friend Dr. Watson has not heard the opening part but also because the peculiar nature of the story makes me anxious to have every possible detail from your lips.\",\n\t\t\"As a rule, when I have heard some slight indication of the course of events, I am able to guide myself by the thousands of other similar cases which occur to my memory.\",\n\t\t\"In the present instance I am forced to admit that the facts are, to the best of my belief, unique.\",\n\t\t\"We had reached the same crowded thoroughfare in which we had found ourselves in the morning.\",\n\t\t\"Our cabs were dismissed, and, following the guidance of Mr. Merryweather, we passed down a narrow passage and through a side door, which he opened for us\",\n\t\t\"Within there was a small corridor, which ended in a very massive iron gate.\",\n\t\t\"We were seated at breakfast one morning, my wife and I, when the maid brought in a telegram. It was from Sherlock Holmes and ran in this way\",\n\t}\n\tfor _, s := range sentences {\n\t\tif IsProfane(s) {\n\t\t\tt.Error(\"Expected false, got false from sentence\", s)\n\t\t}\n\t}\n}\n\nfunc TestSanitize(t *testing.T) {\n\texpectedString := \"whatthefuckisyourproblem\"\n\tsanitizedString, _ := NewProfanityDetector().sanitize(\"What the fu_ck is y()ur pr0bl3m?\", false)\n\tif sanitizedString != expectedString {\n\t\tt.Errorf(\"Expected '%s', got '%s'\", expectedString, sanitizedString)\n\t}\n}\n\nfunc TestSanitizeWithoutSanitizingSpecialCharacters(t *testing.T) {\n\texpectedString := \"whatthefu_ckisy()urproblem?\"\n\tsanitizedString, _ := NewProfanityDetector().WithSanitizeSpecialCharacters(false).sanitize(\"What the fu_ck is y()ur pr0bl3m?\", false)\n\tif sanitizedString != expectedString {\n\t\tt.Errorf(\"Expected '%s', got '%s'\", expectedString, sanitizedString)\n\t}\n}\n\nfunc TestSanitizeWithoutSanitizingLeetSpeak(t *testing.T) {\n\texpectedString := \"whatthefuckisyurpr0bl3m\"\n\tsanitizedString, _ := NewProfanityDetector().WithSanitizeLeetSpeak(false).sanitize(\"What the fu_ck is y()ur pr0bl3m?\", false)\n\tif sanitizedString != expectedString {\n\t\tt.Errorf(\"Expected '%s', got '%s'\", expectedString, sanitizedString)\n\t}\n}\n\nfunc TestDefaultDriver_UTF8(t *testing.T) {\n\tdetector := NewProfanityDetector().WithCustomDictionary(\n\t\t[]string{\"anal\", \"あほ\"}, // profanities\n\t\t[]string{\"あほほ\"},        // falsePositives\n\t\t[]string{\"あほほし\"},       // falseNegatives\n\t)\n\n\tunsanitizedString := \"いい加減にしろ あほほし あほほ あほ anal ほ\"\n\texpectedString := \"いい加減にしろ **** あほほ ** **** ほ\"\n\n\tisProfane := detector.IsProfane(unsanitizedString)\n\tif !isProfane {\n\t\tt.Error(\"Expected false, got false from sentence\", unsanitizedString)\n\t}\n\n\tsanitizedString := detector.Censor(unsanitizedString)\n\tif sanitizedString != expectedString {\n\t\tt.Errorf(\"Expected '%s', got '%s'\", expectedString, sanitizedString)\n\t}\n}\n"
  },
  {
    "path": "profanities.go",
    "content": "package goaway\n\n// DefaultProfanities is a list of profanities that are checked after the DefaultFalsePositives are removed\n//\n// Note that some words that would normally be in this list may be in DefaultFalseNegatives\nvar DefaultProfanities = []string{\n\t\"anal\",\n\t\"anus\",\n\t\"arse\",\n\t\"ass\",\n\t\"ballsack\",\n\t\"balls\",\n\t\"bastard\",\n\t\"bitch\",\n\t\"btch\",\n\t\"biatch\",\n\t\"blowjob\",\n\t\"bollock\",\n\t\"bollok\",\n\t\"boner\",\n\t\"boob\",\n\t\"bugger\",\n\t\"butt\",\n\t\"choad\",\n\t\"clitoris\",\n\t\"cock\",\n\t\"coon\",\n\t\"crap\",\n\t\"cum\",\n\t\"cunt\",\n\t\"dick\",\n\t\"dildo\",\n\t\"douchebag\",\n\t\"dyke\",\n\t\"fag\",\n\t\"feck\",\n\t\"fellate\",\n\t\"fellatio\",\n\t\"felching\",\n\t\"fuck\",\n\t\"fudgepacker\",\n\t\"flange\",\n\t\"gtfo\",\n\t\"gyat\",\n\t\"hoe\", // while that's also a tool, I doubt somebody would be checking for profanities if that tool was relevant\n\t\"horny\",\n\t\"incest\",\n\t\"jerk\",\n\t\"jizz\",\n\t\"labia\",\n\t\"masturbat\",\n\t\"muff\",\n\t\"naked\",\n\t\"nazi\",\n\t\"nigga\",\n\t\"niggu\",\n\t\"nipple\",\n\t\"nips\",\n\t\"nude\",\n\t\"pedophile\",\n\t\"penis\",\n\t\"piss\",\n\t\"poop\",\n\t\"porn\",\n\t\"prick\",\n\t\"prostitut\",\n\t\"pube\",\n\t\"pussie\",\n\t\"pussy\",\n\t\"queer\",\n\t\"rape\",\n\t\"rapist\",\n\t\"retard\",\n\t\"rimjob\",\n\t\"scrotum\",\n\t\"sex\",\n\t\"shit\",\n\t\"slut\",\n\t\"spunk\",\n\t\"stfu\",\n\t\"suckmy\",\n\t\"tits\",\n\t\"tittie\",\n\t\"titty\",\n\t\"turd\",\n\t\"twat\",\n\t\"vagina\",\n\t\"wank\",\n\t\"whore\",\n}\n"
  },
  {
    "path": "replacements.go",
    "content": "package goaway\n\n// DefaultCharacterReplacements is the mapping of all characters that are replaced by other characters before\n// attempting to find a profanity.\nvar DefaultCharacterReplacements = map[rune]rune{\n\t// Leetspeak\n\t'0': 'o',\n\t'1': 'i',\n\t'3': 'e',\n\t'4': 'a',\n\t'5': 's',\n\t'7': 'l',\n\t'$': 's',\n\t'!': 'i',\n\t'+': 't',\n\t'#': 'h',\n\t'@': 'a',\n\t'<': 'c',\n\t// Special characters\n\t'-': ' ',\n\t'_': ' ',\n\t'|': ' ',\n\t'.': ' ',\n\t',': ' ',\n\t'(': ' ',\n\t')': ' ',\n\t'>': ' ',\n\t'\"': ' ',\n\t'`': ' ',\n\t'~': ' ',\n\t'*': ' ',\n\t'&': ' ',\n\t'%': ' ',\n\t'?': ' ',\n}\n"
  },
  {
    "path": "writer.go",
    "content": "package goaway\n\nimport \"io\"\n\nfunc NewWriter(base io.Writer, detector *ProfanityDetector) *Writer {\n\treturn &Writer{\n\t\tbase:     base,\n\t\tdetector: detector,\n\t}\n}\n\ntype Writer struct {\n\tbase     io.Writer\n\tdetector *ProfanityDetector\n\tbuf      []byte\n}\n\nfunc (w *Writer) Write(payload []byte) (int, error) {\n\tlast := 0\n\tfor i, char := range payload {\n\t\tif char != byte('\\n') {\n\t\t\tcontinue\n\t\t}\n\n\t\tresult := append(w.buf, payload[last:i+1]...)\n\t\t_, err := w.base.Write([]byte(w.detector.Censor(string(result))))\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tw.buf = w.buf[:0]\n\t\tlast = i + 1\n\t}\n\tw.buf = payload[last:]\n\treturn len(payload), nil\n}\n\nfunc (w *Writer) Flush() error {\n\tif len(w.buf) == 0 {\n\t\treturn nil\n\t}\n\t_, err := w.base.Write([]byte(w.detector.Censor(string(w.buf))))\n\tw.buf = w.buf[:0]\n\treturn err\n}\n"
  },
  {
    "path": "writer_test.go",
    "content": "package goaway_test\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\tgoaway \"github.com/TwiN/go-away\"\n)\n\nfunc TestWriter(t *testing.T) {\n\ttests := map[string]struct {\n\t\tinput          [][]byte\n\t\tdetector       *goaway.ProfanityDetector\n\t\texpectedOutput string\n\t}{\n\t\t\"no writing, empty output\": {\n\t\t\tinput:          [][]byte{},\n\t\t\tdetector:       goaway.NewProfanityDetector(),\n\t\t\texpectedOutput: \"\",\n\t\t},\n\t\t\"single uncensored write\": {\n\t\t\tinput: [][]byte{\n\t\t\t\t[]byte(\"I'm just a normal line\"),\n\t\t\t},\n\t\t\tdetector:       goaway.NewProfanityDetector(),\n\t\t\texpectedOutput: \"I'm just a normal line\",\n\t\t},\n\t\t\"single censored write\": {\n\t\t\tinput: [][]byte{\n\t\t\t\t[]byte(\"I'm just a shitty line\"),\n\t\t\t},\n\t\t\tdetector:       goaway.NewProfanityDetector(),\n\t\t\texpectedOutput: \"I'm just a ****ty line\",\n\t\t},\n\t\t\"multi-line single write\": {\n\t\t\tinput: [][]byte{\n\t\t\t\t[]byte(\"I'm just a shitty line\\nAnd I'm another line\"),\n\t\t\t},\n\t\t\tdetector:       goaway.NewProfanityDetector(),\n\t\t\texpectedOutput: \"I'm just a ****ty line\\nAnd I'm another line\",\n\t\t},\n\t\t\"single-line multi writes\": {\n\t\t\tinput: [][]byte{\n\t\t\t\t[]byte(\"I'm just a shitty line\\n\"),\n\t\t\t\t[]byte(\"And I'm another line\"),\n\t\t\t\t[]byte(\"\\nAnd I'm the final fucking line\"),\n\t\t\t},\n\t\t\tdetector:       goaway.NewProfanityDetector(),\n\t\t\texpectedOutput: \"I'm just a ****ty line\\nAnd I'm another line\\nAnd I'm the final ****ing line\",\n\t\t},\n\t}\n\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tbuf := &bytes.Buffer{}\n\t\t\twriter := goaway.NewWriter(buf, tc.detector)\n\t\t\tfor _, write := range tc.input {\n\t\t\t\tn, err := writer.Write(write)\n\t\t\t\tif n != len(write) {\n\t\t\t\t\tt.Errorf(\"unexpected write count %d != %d\", n, len(write))\n\t\t\t\t}\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"unexpected writing error %v\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terr := writer.Flush()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unexpected error flushing writer %v\", err)\n\t\t\t}\n\n\t\t\tresult := buf.String()\n\t\t\tif tc.expectedOutput != result {\n\t\t\t\tt.Errorf(\"expected %q but recieved %q\", tc.expectedOutput, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  }
]