├── .editorconfig ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── build.yml │ └── stale.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── _config.yml ├── extraParams.hxml ├── haxelib.json ├── src └── hx │ └── strings │ ├── AnyAsString.hx │ ├── Char.hx │ ├── CharIterator.hx │ ├── Pattern.hx │ ├── RandomStrings.hx │ ├── String8.hx │ ├── StringBuilder.hx │ ├── StringMacros.hx │ ├── Strings.hx │ ├── Version.hx │ ├── ansi │ ├── Ansi.hx │ ├── AnsiColor.hx │ ├── AnsiCursor.hx │ ├── AnsiTextAttribute.hx │ └── AnsiWriter.hx │ ├── collection │ ├── OrderedStringMap.hx │ ├── OrderedStringSet.hx │ ├── SortedStringMap.hx │ ├── SortedStringSet.hx │ ├── StringArray.hx │ ├── StringMap.hx │ └── StringSet.hx │ ├── internal │ ├── Arrays.hx │ ├── Bits.hx │ ├── Either2.hx │ ├── Either3.hx │ ├── Exception.hx │ ├── Macros.hx │ ├── OS.hx │ ├── OneOrMany.hx │ ├── RingBuffer.hx │ └── TriState.hx │ └── spelling │ ├── checker │ ├── AbstractSpellChecker.hx │ ├── EnglishSpellChecker.hx │ ├── GermanSpellChecker.hx │ └── SpellChecker.hx │ ├── dictionary │ ├── Dictionary.hx │ ├── EnglishDictionary.hx │ ├── EnglishDictionary.txt │ ├── GermanDictionary.hx │ ├── GermanDictionary.txt │ ├── InMemoryDictionary.hx │ └── TrainableDictionary.hx │ └── trainer │ ├── AbstractDictionaryTrainer.hx │ ├── DictionaryTrainer.hx │ ├── EnglishDictionaryTrainer.hx │ └── GermanDictionaryTrainer.hx ├── test └── hx │ └── strings │ └── TestRunner.hx ├── tests.hxml └── tools ├── .travis.yml ├── _test-prepare.cmd ├── gen-doc.cmd ├── phantomJS ├── phantom.html └── phantom.js ├── publish-release.cmd ├── test-cpp.cmd ├── test-cppia.cmd ├── test-cs.cmd ├── test-eval.cmd ├── test-flash.cmd ├── test-hl.cmd ├── test-java.cmd ├── test-js.cmd ├── test-jvm.cmd ├── test-lua.cmd ├── test-neko.cmd ├── test-nodejs.cmd ├── test-phantomjs.cmd ├── test-php.cmd └── test-python.cmd /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | indent_style = space 12 | indent_size = 3 13 | 14 | [*.{bat,cmd}] 15 | end_of_line = crlf 16 | 17 | [*.{yml,yaml}] 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | version: 2 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: weekly 8 | day: monday 9 | time: "14:00" 10 | commit-message: 11 | prefix: ci 12 | prefix-development: ci 13 | include: scope 14 | labels: 15 | - dependencies 16 | - gha 17 | - pinned 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 2 | # SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | # https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions 6 | name: Build 7 | 8 | on: 9 | push: 10 | branches-ignore: # build all branches except: 11 | - 'dependabot/**' # prevent GHA triggered twice (once for commit to the branch and once for opening/syncing the PR) 12 | tags-ignore: # don't build tags 13 | - '**' 14 | paths-ignore: 15 | - '**/*.adoc' 16 | - '**/*.md' 17 | - '.editorconfig' 18 | - '.git*' 19 | - '.github/*.yml' 20 | - '.github/workflows/stale.yml' 21 | - 'tools' 22 | pull_request: 23 | paths-ignore: 24 | - '**/*.adoc' 25 | - '**/*.md' 26 | - '.editorconfig' 27 | - '.git*' 28 | - '.github/*.yml' 29 | - '.github/workflows/stale.yml' 30 | - 'tools' 31 | schedule: 32 | # https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows 33 | - cron: '0 15 1 * *' 34 | workflow_dispatch: 35 | # https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_dispatch 36 | inputs: 37 | debug-with-ssh: 38 | description: "Start an SSH session for debugging purposes at the end of the build:" 39 | default: never 40 | type: choice 41 | options: [ always, on_failure, on_failure_or_cancelled, never ] 42 | debug-with-ssh-only-for-actor: 43 | description: "Limit access to the SSH session to the GitHub user that triggered the job." 44 | default: true 45 | type: boolean 46 | debug-with-ssh-only-jobs-matching: 47 | description: "Only start an SSH session for jobs matching this regex pattern:" 48 | default: ".*" 49 | type: string 50 | test-target: 51 | description: "If specified only the given target will be tested" 52 | default: "" 53 | type: choice 54 | options: [ "", cpp, cs, eval, flash, hl, java, jvm, lua, neko, node, php, python ] 55 | 56 | 57 | defaults: 58 | run: 59 | shell: bash 60 | 61 | 62 | jobs: 63 | 64 | ########################################################### 65 | build: 66 | ########################################################### 67 | # https://github.com/vegardit/haxe-reusable-workflows/ 68 | uses: vegardit/haxe-reusable-workflows/.github/workflows/test-with-haxe.yml@dev 69 | strategy: 70 | fail-fast: false 71 | matrix: 72 | os: # https://github.com/actions/runner-images#available-images 73 | - ubuntu-latest 74 | - macos-13 # Intel 75 | - windows-latest 76 | haxe: 77 | - nightly 78 | - latest 79 | - 4.3.7 80 | - 4.2.5 81 | include: 82 | - os: macos-latest # ARM 83 | haxe: 4.3.7 84 | with: 85 | haxe-reusable-workflows-version: dev 86 | 87 | runner-os: ${{ matrix.os }} 88 | haxe-version: ${{ matrix.haxe }} 89 | haxe-libs: haxe-doctest hx3compat 90 | 91 | debug-with-ssh: ${{ inputs.debug-with-ssh || 'never' }} 92 | debug-with-ssh-only-for-actor: ${{ inputs.debug-with-ssh-only-for-actor || false }} 93 | debug-with-ssh-only-jobs-matching: ${{ inputs.debug-with-ssh-only-jobs-matching }} 94 | 95 | test-cpp: | 96 | enabled: ${{ inputs.test-target == '' || inputs.test-target == 'cpp' }} 97 | allow-failure: ${{ matrix.haxe == 'nightly' }} # null-analysis in nightly currently broken for CPP 98 | test-cs: ${{ (inputs.test-target == '' || inputs.test-target == 'cs') && matrix.haxe != 'nightly' }} # Haxe 5 drops C# Support 99 | test-eval: ${{ inputs.test-target == '' || inputs.test-target == 'eval' }} 100 | test-flash: | 101 | enabled: ${{ (inputs.test-target == '' || inputs.test-target == 'flash') && !startsWith(matrix.os, 'macos') }} # FlashPlayer hangs on MacOS 102 | allow-failure: ${{ startsWith(matrix.os, 'ubuntu-') }} # workaround for random "Application crashed with an unhandled SIGSEGV" 103 | test-hl: ${{ inputs.test-target == '' || inputs.test-target == 'hl' }} 104 | test-java: ${{ (inputs.test-target == '' || inputs.test-target == 'java') && matrix.haxe != 'nightly' }} # Haxe 5 drops Java Support 105 | test-jvm: ${{ inputs.test-target == '' || inputs.test-target == 'jvm' }} 106 | test-lua: | 107 | enabled: ${{ inputs.test-target == '' || inputs.test-target == 'lua' }} 108 | allow-failure: ${{ inputs.haxe-version != '4.0.5' }} 109 | test-neko: ${{ inputs.test-target == '' || inputs.test-target == 'neko' }} 110 | test-node: ${{ inputs.test-target == '' || inputs.test-target == 'node' }} 111 | test-php: ${{ inputs.test-target == '' || inputs.test-target == 'php' }} 112 | test-python: ${{ inputs.test-target == '' || inputs.test-target == 'python' }} 113 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions 2 | name: Stale issues 3 | 4 | on: 5 | schedule: 6 | - cron: '0 15 1,15 * *' 7 | workflow_dispatch: 8 | # https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_dispatch 9 | 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | 14 | jobs: 15 | stale: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Git checkout 20 | uses: actions/checkout@v4 # https://github.com/actions/checkout 21 | 22 | - name: Run stale action 23 | uses: actions/stale@v9 # https://github.com/actions/stale 24 | with: 25 | days-before-stale: 90 26 | days-before-close: 14 27 | stale-issue-message: > 28 | This issue has been automatically marked as stale because it has not had 29 | recent activity. It will be closed in 14 days if no further activity occurs. 30 | If the issue is still valid, please add a respective comment to prevent this 31 | issue from being closed automatically. Thank you for your contributions. 32 | stale-issue-label: stale 33 | close-issue-label: wontfix 34 | exempt-issue-labels: | 35 | enhancement 36 | pinned 37 | security 38 | 39 | - name: Run stale action (for enhancements) 40 | uses: actions/stale@v9 # https://github.com/actions/stale 41 | with: 42 | days-before-stale: 360 43 | days-before-close: 14 44 | stale-issue-message: > 45 | This issue has been automatically marked as stale because it has not had 46 | recent activity. It will be closed in 14 days if no further activity occurs. 47 | If the issue is still valid, please add a respective comment to prevent this 48 | issue from being closed automatically. Thank you for your contributions. 49 | stale-issue-label: stale 50 | close-issue-label: wontfix 51 | only-labels: enhancement 52 | exempt-issue-labels: | 53 | pinned 54 | security 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | ## [Unreleased] 9 | 10 | 11 | ## [7.0.6] - 2025-06-07 12 | 13 | ### Fixed 14 | - Fix potential null value issues in `InMemoryDictionary` 15 | 16 | ### Changed 17 | - minimum required Haxe version is now 4.2.x 18 | 19 | 20 | ## [7.0.5] - 2024-03-28 21 | 22 | ### Fixed 23 | - [Issue#14](https://github.com/vegardit/haxe-strings/issues/14) `RandomStrings.randomAsciiAlpha` returns illegal chars (thanks to https://github.com/singpolyma) 24 | 25 | 26 | ## [7.0.4] - 2024-03-18 27 | 28 | ### Fixed 29 | - [Issue#13](https://github.com/vegardit/haxe-strings/issues/13) Null safety error in CharIterator with Haxe 4.3.4 30 | 31 | 32 | ## [7.0.3] - 2023-05-06 33 | 34 | ### Fixed 35 | - `Warning : (WDeprecated) '@:enum abstract' is deprecated in favor of 'enum' with Haxe 4.3 36 | - `Version.compareTo` throws improve cppia support on CPPIA 37 | 38 | ### Changed 39 | - Speed up Strings.(starts|ends)With on Lua 40 | 41 | 42 | ## [7.0.2] - 2021-08-06 43 | 44 | ### Fixed 45 | - lua compatibility issues 46 | 47 | 48 | ## [7.0.1] - 2021-08-05 49 | 50 | ### Fixed 51 | - lua null-safety false positive 52 | 53 | 54 | ## [7.0.0] - 2021-08-04 55 | 56 | ### Added 57 | - method `Strings#toFloatOrNull` 58 | - method `Strings#toIntOrNull` 59 | 60 | ### Changed 61 | - enabled null safety 62 | - `Strings#toEReg()` now throws an exception instead of returning null if input string is null 63 | - changed signature of `Strings#toFloat` from `(String, Null):Null` to `(String, Float):Float` 64 | - changed signature of `Strings#toInt` from `(String, Null):Null` to `(String, Int):Int` 65 | 66 | 67 | ## [6.0.4] - 2021-05-07 68 | 69 | ### Fixed 70 | - [Issue#10](https://github.com/vegardit/haxe-strings/issues/10) `Warning : Std.is is deprecated. Use Std.isOfType instead.` 71 | 72 | 73 | ## [6.0.3] - 2020-07-18 74 | 75 | ### Fixed 76 | - [Issue#9](https://github.com/vegardit/haxe-strings/issues/9) `Warning : Using "is" as an identifier is deprecated` 77 | 78 | 79 | ## [6.0.2] - 2020-07-14 80 | 81 | ### Fixed 82 | - [Issue#8](https://github.com/vegardit/haxe-strings/issues/8) `Warning : __js__ is deprecated, use js.Syntax.code instead` 83 | 84 | 85 | ## [6.0.1] - 2020-04-21 86 | 87 | ### Fixed 88 | - added workarounds for JVM target bugs 89 | - added workaround for eval target bug 90 | 91 | 92 | ## [6.0.0] - 2020-04-20 93 | 94 | ### Added 95 | - property `CharIterator#current:Null` 96 | - method `CharPos#toString()` 97 | - method `StringDiff#toString()` 98 | 99 | ### Changed 100 | - minimum required Haxe version is now 4.x 101 | - removed support for old PHP5 target 102 | - `StringSet.addAll(null)` now throws an exception 103 | 104 | ### Fixed 105 | - `CharIterator#prev()` does not throw EOF as expected 106 | - [PR#5](https://github.com/vegardit/haxe-strings/pull/5) `Ansi.cursor(RestorePos)` performs saves position instead of restoring it 107 | 108 | ### Removed 109 | - deprecated `hx.strings.Paths` module (use [hx.files.Path](https://github.com/vegardit/haxe-files/blob/main/src/hx/files/Path.hx) of the [haxe-files](https://lib.haxe.org/p/haxe-files/) haxelib instead) 110 | 111 | 112 | ## [5.2.4] - 2019-12-10 113 | 114 | ### Changed 115 | - enable `Pattern.MatchingOption#DOTALL` option for HL, Lua, NodeJS, Python targets 116 | - reduce usage of deprecated `haxe.Utf8` class 117 | 118 | 119 | ## [5.2.3] - 2019-09-20 120 | 121 | ### Fixed 122 | - fixes for Haxe 4 RC5 123 | 124 | 125 | ## [5.2.2] - 2018-12-17 126 | 127 | ### Fixed 128 | - Workaround for [Haxe Issue 5336](https://github.com/HaxeFoundation/haxe/issues/5336) "Utf8.compare() for CS treats upper-/lowercase chars differently than other platforms" 129 | - String8 abstract does not work Haxe 4 130 | 131 | ## [5.2.1] - 2018-11-27 132 | 133 | ### Fixed 134 | - "ReferenceError: window is not defined" on node.js 135 | 136 | 137 | ## [5.2.0] - 2018-11-27 138 | 139 | ### Added 140 | - method `Pattern#remove()` 141 | - type `hx.strings.AnyAsString` 142 | 143 | ### Fixed 144 | - `OS.isWindows` does not work with PhantomJS 145 | 146 | 147 | ## [5.1.0] - 2018-11-10 148 | 149 | ### Added 150 | - renderMethod parameter to `Strings#ansiToHtml()` (thanks to https://github.com/emugel) 151 | 152 | ### Fixed 153 | - make Pattern, OrderedStringMap, StringBuilder compatible with Haxe 4 Preview 5 154 | 155 | 156 | ## [5.0.1] - 2018-04-20 157 | 158 | ### Changed 159 | - replaced license header by "SPDX-License-Identifier: Apache-2.0" 160 | - `StringMap` is now usable in macro mode 161 | - deprecated `hx.strings.Paths` 162 | 163 | ### Fixed 164 | - `OS.isWindows` does not work with Node.js 165 | 166 | 167 | ## [5.0.0] - 2017-11-05 168 | 169 | ### Added 170 | - parameter 'interpolationPrefix' to `hx.strings.StringMacros#multiline()` 171 | - property `hx.strings.collection.SortedStringMap#size` 172 | 173 | ### Changed 174 | - minimum required Haxe version is now 3.4.x 175 | - removed workarounds for Haxe 3.2 and lower 176 | - renamed hx.strings.collection.OrderedStringMap#clone() to #copy() for Haxe 4 compatiblity 177 | - renamed hx.strings.collection.StringMap#clone() to #copy() for Haxe 4 compatiblity 178 | - renamed hx.strings.collection.SortedStringMap#clone() to #copy() for Haxe 4 compatiblity 179 | - use php.Syntax.code instead of "untyped __call__" for Haxe 4 compatiblity 180 | 181 | ### Fixed 182 | - [flash] workaround for 'Cannot create Vector without knowing runtime type' 183 | - String8.String8Generator is broken 184 | 185 | 186 | ### Removed 187 | - unused 'comparator' constructor parameter from hx.strings.collection.OrderedStringSet 188 | 189 | 190 | ## [4.0.0] - 2017-05-25 191 | 192 | ### Added 193 | - class hx.strings.CharIterator 194 | - class hx.strings.Strings.CharPos 195 | - class hx.strings.collection.OrderedStringMap 196 | - class hx.strings.collection.OrderedStringSet 197 | - class hx.strings.StringMacros 198 | - function hx.strings.Strings#toCharIterator() 199 | - function hx.strings.collection.StringSet#addAll() 200 | - function hx.strings.collection.StringArray#contains() 201 | - function hx.strings.collection.StringArray#pushAll() 202 | - function hx.strings.StringBuilder#asOutput() 203 | - function hx.strings.Version#isCompatible() 204 | - parameter 'charsToRemove' to hx.strings.Strings#trim...() methods 205 | 206 | ### Changed 207 | - renamed hx.strings.collection.StringTreeMap to hx.strings.collection.SortedStringMap 208 | - replaced hx.strings.CharPos abstract with hx.strings.Strings.CharIndex typedef 209 | - replaced hx.strings.collection.StringMaps class with hx.strings.collection.StringMap abstract 210 | 211 | 212 | ## [3.0.0] - 2017-03-27 213 | 214 | ### Added 215 | - function hx.strings.Pattern.Matcher#reset(str) 216 | - function hx.strings.StringBuilder#insert() 217 | - function hx.strings.StringBuilder#insertAll() 218 | - function hx.strings.StringBuilder#insertChar() 219 | - parameter hx.strings.RandomStrings#randomUUIDv4(separator) 220 | - parameter 'notFoundDefault' to hx.strings.Strings#substring...() methods 221 | 222 | ### Removed 223 | - function hx.strings.StringBuilder#prepend() 224 | - function hx.strings.StringBuilder#prependAll() 225 | - function hx.strings.StringBuilder#prependChar() 226 | 227 | ### Changed 228 | - StringBuilder now uses C#'s native StringBuilder#clear()/#insert() methods 229 | 230 | 231 | ## [2.5.0] - 2017-03-03 232 | 233 | ### Added 234 | - type hx.strings.RandomStrings (#randomUUIDV4(), #randomDigits(), ...) 235 | - type hx.strings.String8 236 | - type hx.strings.collection.StringMaps 237 | - function hx.strings.Strings#containsOnly() 238 | - function hx.strings.Strings#compact() 239 | - function hx.strings.Strings#removeAfter() 240 | - function hx.strings.Strings#removeAt() 241 | - function hx.strings.Strings#removeBefore() 242 | - function hx.strings.Strings#removeFirst() 243 | - function hx.strings.Strings#randomSubstring() 244 | - function hx.strings.collection.StringTreeMap#clone() 245 | - function hx.strings.collection.StringTreeMap#setAll() 246 | 247 | ### Changed 248 | - renamed hx.strings.Strings#insert() to #insertAt() 249 | 250 | 251 | ## [2.4.0] - 2017-02-28 252 | 253 | ### Added 254 | - type hx.strings.collection.StringArray 255 | - function hx.strings.collection.StringSet#isEmpty() 256 | - function hx.strings.collection.StringTreeMap#isEmpty() 257 | 258 | 259 | ## [2.3.0] - 2017-02-25 260 | 261 | ### Added 262 | - function hx.strings.Strings#containsWhitespaces() 263 | - function hx.strings.Strings#insert() 264 | - function hx.strings.Strings#splitAt() 265 | - function hx.strings.Strings#splitEvery() 266 | - Support for Node.js 267 | 268 | 269 | ## [2.2.0] - 2017-01-02 270 | 271 | ### Added 272 | - type hx.strings.Version (Version parsing according SemVer.org 2.0 specification) 273 | - function hx.strings.Char#isAsciiAlphanumeric() 274 | - function hx.strings.Strings#indentLines() 275 | 276 | 277 | ## [2.1.0] - 2016-08-21 278 | 279 | ### Added 280 | - package hx.strings.ansi: type-safe ANSI escape sequence generation 281 | 282 | 283 | ## [2.0.2] - 2016-07-11 284 | 285 | ### Fixed 286 | - [hl] interim workaround for "EReg.hx Unsupported escaped char '/'" 287 | - [cpp] interim fix for static initializer issue 288 | 289 | 290 | ## [2.0.1] - 2016-07-09 291 | 292 | ### Fixed 293 | - "Warning: maybe loop in static generation" 294 | 295 | 296 | ## [2.0.0] - 2016-07-09 297 | 298 | ### Added 299 | - spell checker in package hx.strings.spelling 300 | - type hx.strings.collection.SortedStringSet 301 | - type hx.strings.collection.StringSet 302 | - type hx.strings.collection.StringTreeMap 303 | - type hx.strings.Paths for path related string manipulations 304 | - function hx.strings.Pattern.Matcher#iterate() 305 | - function hx.strings.Strings#ellipsizeLeft() 306 | - function hx.strings.Strings#ellipsizeMiddle() 307 | - function hx.strings.Strings#getLevenshteinDistance() 308 | - function hx.strings.Strings#getFuzzyDistance() 309 | - function hx.strings.Strings#getLongestCommonSubstring() 310 | - function hx.strings.Strings#isLowerCase() 311 | - function hx.strings.Strings#isUpperCase() 312 | - function hx.strings.Strings#left() 313 | - function hx.strings.Strings#right() 314 | - function hx.strings.Strings#removeLeading() 315 | - function hx.strings.Strings#removeTrailing() 316 | - fields hx.strings.Char#CARET/#EXCLAMATION_MARK/and constants for characters 0-9 317 | 318 | ### Changed 319 | - changed license from MIT to Apache License 2.0 320 | - hx.strings.Strings#split8() now allows multiple separators 321 | - slight performance improvement in hx.strings.StringBuilder 322 | - renamed hx.strings.Strings#stripAnsi() to hx.strings.Strings#removeAnsi() 323 | - renamed hx.strings.Strings#stripTags() to hx.strings.Strings#removeTags() 324 | - renamed hx.strings.Strings#ltrim() to hx.strings.Strings#trimLeft() 325 | - renamed hx.strings.Strings#rstrip() to hx.strings.Strings#trimRight() 326 | - renamed hx.strings.Strings#abbreviate() to hx.strings.Strings#ellipsizeRight() 327 | - renamed hx.strings.Strings#hex() to hx.strings.Strings#toHex() 328 | - moved hx.strings.Strings#PATH_SEPARATOR to hx.strings.Paths#DIRECTORY_SEPARATOR 329 | - moved hx.strings.Strings#globToEReg() to hx.strings.Paths#globToEReg() 330 | - moved hx.strings.Strings#globToPattern() to hx.strings.Paths#globToPattern() 331 | - moved hx.strings.Strings#globToRegEx() to hx.strings.Paths#globToRegEx() 332 | 333 | ### Fixed 334 | - hx.strings.Char.toLowerCase() was broken for character I 335 | 336 | 337 | ## [1.2.0] - 2016-06-21 338 | 339 | ### Added 340 | - function hx.strings.Strings#endsWithAny() 341 | - function hx.strings.Strings#endsWithAnyIgnoreCase() 342 | - function hx.strings.Strings#startsWithAny() 343 | - function hx.strings.Strings#startsWithAnyIgnoreCase() 344 | - function hx.strings.Strings#toTitle() 345 | - parameter 'algorithm' to hx.strings.Strings#hashCode() 346 | 347 | ### Fixed 348 | - hx.strings.StringBuilder#addChar() with values between 128 and 255 didn't work on all platforms as expected 349 | 350 | 351 | ## [1.1.0] - 2016-06-11 352 | 353 | ### Added 354 | - type hx.strings.Pattern for threadsafe pattern matching 355 | - function hx.strings.Strings#abbreviate() 356 | - function hx.strings.Strings#globToPattern() 357 | - function hx.strings.Strings#substringBetween() 358 | - function hx.strings.Strings#substringBetweenIgnoreCase() 359 | - function hx.strings.Strings#toBool() 360 | - function hx.strings.Strings#toFloat() 361 | - function hx.strings.Strings#toInt() 362 | - function hx.strings.Strings#toPattern() 363 | - function hx.strings.Strings#wrap() 364 | - function hx.strings.StringBuilder#isEmpty() 365 | 366 | 367 | ## [1.0.0] - 2016-06-05 368 | 369 | ### Added 370 | - Initial release 371 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | https://vegardit.com/about/legal/. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest in contributing to this project! Whether it's a bug report, new feature, correction, or additional documentation, we greatly value feedback and contributions from our community. 4 | 5 | We want to make contributing as easy and transparent as possible. 6 | 7 | Please read through this document before submitting any contributions to ensure your contribution goes to the correct code repository and we have all the necessary information to effectively respond to your request. 8 | 9 | 10 | ## Code of Conduct 11 | 12 | Our code of conduct is described in [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). 13 | 14 | 15 | ## Reporting Bugs/Feature Requests 16 | 17 | We use GitHub issues to track bugs and feature requests. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. 18 | 19 | 20 | ## Contributing via Pull Requests 21 | 22 | Contributions via pull requests are much appreciated. 23 | 24 | Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the **main** branch. 27 | 1. You check existing open and recently merged pull requests to make sure someone else hasn't already addressed the issue. 28 | 29 | To send us a pull request, please: 30 | 31 | 1. Fork our repository. 32 | 1. Modify the source while focusing on the specific change you are contributing. 33 | 1. Commit to your fork using clear, descriptive commit messages. 34 | 1. Send us a pull request, answering any default questions in the pull request interface. 35 | 36 | GitHub provides additional documentation on [forking a repository](https://help.github.com/articles/fork-a-repo/) and [creating a pull request](https://help.github.com/articles/creating-a-pull-request/) 37 | 38 | 39 | ## License 40 | 41 | By contributing your code, you agree to license your contribution under the [Apache License 2.0](LICENSE.txt). -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | -------------------------------------------------------------------------------- /extraParams.hxml: -------------------------------------------------------------------------------- 1 | --macro hx.strings.internal.Macros.addDefines() 2 | --macro hx.strings.internal.Macros.configureNullSafety() 3 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haxe-strings", 3 | "url": "https://github.com/vegardit/haxe-strings", 4 | "license": "Apache", 5 | "classPath": "src/", 6 | "tags": [ 7 | "utility","string","collection","semver","ansi","random","version", 8 | "cross","cpp","cs","flash","hashlink","hl","java","javascript","js","lua","neko","nodejs","php","python", 9 | "vegardit" 10 | ], 11 | "description": "A haxelib for consistent cross-platform UTF-8 string manipulation. Contains comprehensive String utility functions, SemVer.org version parsing, spell checker, ANSI escape sequence builder, RandomUUID generator, thread-safe regular expression API, StringBuilder, SortedStringMap, SortedStringSet, camelcase string formatting, filepath normalizing and globbing (wildcards).", 12 | "contributors": ["vegardit"], 13 | "releasenote": "See https://github.com/vegardit/haxe-strings/blob/main/CHANGELOG.md", 14 | "version": "7.0.6", 15 | "dependencies": {} 16 | } 17 | -------------------------------------------------------------------------------- /src/hx/strings/AnyAsString.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings; 7 | 8 | /** 9 | *

10 |  * >>> (function(){var str:AnyAsString = 1;      return str; })() == "1"
11 |  * >>> (function(){var str:AnyAsString = true;   return str; })() == "true"
12 |  * >>> (function(){var str:AnyAsString = "cat";  return str; })() == "cat"
13 |  * >>> (function(){var str:AnyAsString = [1, 2]; return str; })() == "[1,2]"
14 |  * 
15 | */ 16 | @:noDoc @:dox(hide) 17 | @:noCompletion 18 | abstract AnyAsString(String) from String to String { 19 | 20 | @:from 21 | inline 22 | static function fromBool(value:Bool):AnyAsString 23 | return value ? "true" : "false"; 24 | 25 | @:from 26 | inline 27 | static function fromAny(value:Dynamic):AnyAsString 28 | return Std.string(value); 29 | } 30 | -------------------------------------------------------------------------------- /src/hx/strings/CharIterator.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings; 7 | 8 | import haxe.io.Eof; 9 | import haxe.io.Input; 10 | import hx.strings.Strings.CharPos; 11 | import hx.strings.internal.Bits; 12 | import hx.strings.internal.RingBuffer; 13 | import hx.strings.internal.TriState; 14 | 15 | using hx.strings.Strings; 16 | 17 | class CharIterator { 18 | 19 | /** 20 | *

 21 |     * >>> CharIterator.fromString(null).hasNext()          == false
 22 |     * >>> CharIterator.fromString("").hasNext()            == false
 23 |     * >>> CharIterator.fromString("cat").hasNext()         == true
 24 |     * >>> CharIterator.fromString("cat").next().toString() == "c"
 25 |     * >>> CharIterator.fromString("はい").next().toString() == "は"
 26 |     * 
27 | * 28 | * @param prevBufferSize number of characters the iterator can go backwards 29 | */ 30 | inline 31 | public static function fromString(chars:Null, prevBufferSize = 0):CharIterator { 32 | if (chars == null) return NullCharIterator.INSTANCE; 33 | return new StringCharIterator(chars, prevBufferSize); 34 | } 35 | 36 | 37 | /** 38 | *

 39 |     * >>> CharIterator.fromArray(null).hasNext()                           == false
 40 |     * >>> CharIterator.fromArray(Strings.toChars("")).hasNext()            == false
 41 |     * >>> CharIterator.fromArray(Strings.toChars("cat")).hasNext()         == true
 42 |     * >>> CharIterator.fromArray(Strings.toChars("cat")).next().toString() == "c"
 43 |     * >>> CharIterator.fromArray(Strings.toChars("はい")).next().toString() == "は"
 44 |     * 
45 | * 46 | * @param prevBufferSize number of characters the iterator can go backwards 47 | */ 48 | inline 49 | public static function fromArray(chars:Null>, prevBufferSize = 0):CharIterator { 50 | if (chars == null) return NullCharIterator.INSTANCE; 51 | return new ArrayCharIterator(chars, prevBufferSize); 52 | } 53 | 54 | 55 | /** 56 | * Read characters from an ASCII or Utf8-encoded input. 57 | * 58 | *

 59 |     * >>> CharIterator.fromInput(null).hasNext()          == false
 60 |     * >>> CharIterator.fromInput(new haxe.io.StringInput("")).hasNext()            == false
 61 |     * >>> CharIterator.fromInput(new haxe.io.StringInput("cat")).hasNext()         == true
 62 |     * >>> CharIterator.fromInput(new haxe.io.StringInput("cat")).next().toString() == "c"
 63 |     * >>> CharIterator.fromInput(new haxe.io.StringInput("はい")).next().toString() == "は"
 64 |     * 
65 | * 66 | * @param prevBufferSize number of characters the iterator can go backwards 67 | */ 68 | inline 69 | public static function fromInput(chars:Null, prevBufferSize = 0):CharIterator { 70 | if (chars == null) return NullCharIterator.INSTANCE; 71 | return new InputCharIterator(chars, prevBufferSize); 72 | } 73 | 74 | 75 | /** 76 | *

 77 |     * >>> CharIterator.fromIterator(null).hasNext()                                      == false
 78 |     * >>> CharIterator.fromIterator(Strings.toChars("").iterator()).hasNext()            == false
 79 |     * >>> CharIterator.fromIterator(Strings.toChars("cat").iterator()).hasNext()         == true
 80 |     * >>> CharIterator.fromIterator(Strings.toChars("cat").iterator()).next().toString() == "c"
 81 |     * >>> CharIterator.fromIterator(Strings.toChars("はい").iterator()).next().toString() == "は"
 82 |     * 
83 | * 84 | * @param prevBufferSize number of characters the iterator can go backwards 85 | */ 86 | inline 87 | public static function fromIterator(chars:Null>, prevBufferSize = 0):CharIterator { 88 | if (chars == null) return NullCharIterator.INSTANCE; 89 | return new IteratorCharIterator(chars, prevBufferSize); 90 | } 91 | 92 | 93 | var index = -1; 94 | var line = 0; 95 | var col = 0; 96 | var currChar:Char = -1; 97 | 98 | final prevBuffer:Null>; 99 | var prevBufferPrevIdx = -1; 100 | var prevBufferNextIdx = -1; 101 | 102 | 103 | var prevBufferLength(get,never):Int; 104 | inline function get_prevBufferLength():Int 105 | return @:nullSafety(Off) prevBuffer.length; 106 | 107 | 108 | public var current(get, never):Null; 109 | inline function get_current() 110 | return index > -1 ? currChar : null; 111 | 112 | 113 | public var pos(get, never):CharPos; 114 | inline function get_pos() 115 | return new CharPos(index, line, col); 116 | 117 | 118 | inline function new(prevBufferSize:Int) 119 | prevBuffer = prevBufferSize > 0 ? new RingBuffer(prevBufferSize + 1 /*currChar*/) : null; 120 | 121 | 122 | inline 123 | public function hasPrev():Bool 124 | return prevBufferPrevIdx > -1; 125 | 126 | 127 | /** 128 | * Moves to the previous character in the input sequence and returns it. 129 | * 130 | * @throws haxe.io.Eof if no previous character is available 131 | */ 132 | public final function prev():Char { 133 | if (!hasPrev()) 134 | throw new Eof(); 135 | 136 | final prevChar = @:nullSafety(Off) prevBuffer[prevBufferPrevIdx]; 137 | currChar = prevChar.char; 138 | index = prevChar.index; 139 | line = prevChar.line; 140 | col = prevChar.col; 141 | 142 | prevBufferNextIdx = prevBufferPrevIdx + 1 < prevBufferLength ? prevBufferPrevIdx + 1 : -1; 143 | prevBufferPrevIdx--; 144 | return currChar; 145 | } 146 | 147 | 148 | inline 149 | public function hasNext():Bool 150 | return prevBufferNextIdx > -1 ? true : !isEOF(); 151 | 152 | 153 | /** 154 | * Moves to the next character in the input sequence and returns it. 155 | * 156 | * @throws haxe.io.Eof if no more characters are available 157 | */ 158 | public final function next():Char { 159 | if (prevBufferNextIdx > -1) { 160 | var prevChar = @:nullSafety(Off) prevBuffer[prevBufferNextIdx]; 161 | currChar = prevChar.char; 162 | index = prevChar.index; 163 | line = prevChar.line; 164 | col = prevChar.col; 165 | prevBufferPrevIdx = prevBufferNextIdx - 1; 166 | prevBufferNextIdx = prevBufferNextIdx + 1 < prevBufferLength ? prevBufferNextIdx + 1 : -1; 167 | return currChar; 168 | } 169 | 170 | if (isEOF()) 171 | throw new Eof(); 172 | 173 | if (currChar == Char.LF || currChar < 0) { 174 | line++; 175 | col=0; 176 | } 177 | 178 | index++; 179 | col++; 180 | currChar = getChar(); 181 | 182 | if (prevBuffer != null) { 183 | prevBuffer.add(new CharWithPos(currChar, index, col, line)); 184 | prevBufferPrevIdx = prevBufferLength - 2; 185 | prevBufferNextIdx = -1; 186 | } 187 | 188 | return currChar; 189 | } 190 | 191 | 192 | /** 193 | * @return the char at the current position 194 | */ 195 | function getChar():Char throw "Not implemented" ; 196 | 197 | function isEOF():Bool throw "Not implemented"; 198 | } 199 | 200 | 201 | @:noDoc @:dox(hide) 202 | private class CharWithPos extends CharPos { 203 | public final char:Char; 204 | 205 | public function new(char:Char, index:CharIndex, line:Int, col:Int) { 206 | super(index, line, col); 207 | this.char = char; 208 | } 209 | } 210 | 211 | 212 | @:noDoc @:dox(hide) 213 | private class NullCharIterator extends CharIterator { 214 | 215 | public static final INSTANCE = new NullCharIterator(); 216 | 217 | inline 218 | function new() 219 | super(0); 220 | 221 | 222 | override 223 | function isEOF():Bool 224 | return true; 225 | } 226 | 227 | 228 | @:noDoc @:dox(hide) 229 | private class ArrayCharIterator extends CharIterator { 230 | final chars:Array; 231 | final charsMaxIndex:Int; 232 | 233 | 234 | public function new(chars:Array, prevBufferSize:Int) { 235 | super(prevBufferSize); 236 | this.chars = chars; 237 | charsMaxIndex = chars.length -1; 238 | } 239 | 240 | 241 | override 242 | function isEOF():Bool 243 | return index >= charsMaxIndex; 244 | 245 | 246 | override 247 | function getChar():Char 248 | return chars[index]; 249 | } 250 | 251 | 252 | @:noDoc @:dox(hide) 253 | private class IteratorCharIterator extends CharIterator { 254 | final chars:Iterator; 255 | 256 | 257 | public function new(chars:Iterator, prevBufferSize:Int) { 258 | super(prevBufferSize); 259 | this.chars = chars; 260 | } 261 | 262 | override 263 | function isEOF():Bool 264 | return !chars.hasNext(); 265 | 266 | 267 | override 268 | function getChar(): Char 269 | return chars.next(); 270 | } 271 | 272 | 273 | @:noDoc @:dox(hide) 274 | private class InputCharIterator extends CharIterator { 275 | var byteIndex = 0; 276 | final input:Input; 277 | var currCharIndex = -1; 278 | var nextChar:Char = -1; 279 | var nextCharAvailable = TriState.UNKNOWN; 280 | 281 | public function new(chars:Input, prevBufferSize:Int) { 282 | super(prevBufferSize); 283 | this.input = chars; 284 | } 285 | 286 | 287 | override 288 | function isEOF():Bool { 289 | if (nextCharAvailable == UNKNOWN) { 290 | try { 291 | nextChar = readUtf8Char(); 292 | nextCharAvailable = TRUE; 293 | } catch (ex:haxe.io.Eof) { 294 | nextCharAvailable = FALSE; 295 | } 296 | } 297 | return nextCharAvailable != TRUE; 298 | } 299 | 300 | 301 | override 302 | function getChar():Char { 303 | if(index != currCharIndex) { 304 | currCharIndex = index; 305 | nextCharAvailable = UNKNOWN; 306 | return nextChar; 307 | } 308 | return currChar; 309 | } 310 | 311 | /** 312 | * http://www.fileformat.info/info/unicode/utf8.htm 313 | * @throws exception if an unexpected byte was found 314 | */ 315 | inline 316 | function readUtf8Char():Char { 317 | var byte1 = input.readByte(); 318 | byteIndex++; 319 | if (byte1 <= 127) 320 | return byte1; 321 | 322 | /* 323 | * determine the number of bytes composing this UTF char 324 | * and clear the control bits from the first byte. 325 | */ 326 | byte1 = Bits.clearBit(byte1, 8); 327 | byte1 = Bits.clearBit(byte1, 7); 328 | var totalBytes = 2; 329 | 330 | final isBit6Set = Bits.getBit(byte1, 6); 331 | var isBit5Set = false; 332 | if(isBit6Set) { 333 | byte1 = Bits.clearBit(byte1, 6); 334 | totalBytes++; 335 | 336 | isBit5Set = Bits.getBit(byte1, 5); 337 | if(isBit5Set) { 338 | byte1 = Bits.clearBit(byte1, 5); 339 | totalBytes++; 340 | 341 | if (Bits.getBit(byte1, 4)) 342 | throw 'Valid UTF-8 byte expected at position [$byteIndex] but found byte with value [$byte1]!'; 343 | } 344 | } 345 | 346 | var result:Int = (byte1 << 6*(totalBytes-1)); 347 | 348 | /* 349 | * read the second byte 350 | */ 351 | final byte2 = readUtf8MultiSequenceByte(); 352 | result += (byte2 << 6*(totalBytes-2)); 353 | 354 | /* 355 | * read the third byte 356 | */ 357 | if(isBit6Set) { 358 | final byte3 = readUtf8MultiSequenceByte(); 359 | result += (byte3 << 6*(totalBytes-3)); 360 | 361 | /* 362 | * read the fourth byte 363 | */ 364 | if(isBit5Set) { 365 | final byte4 = readUtf8MultiSequenceByte(); 366 | result += (byte4 << 6*(totalBytes-4)); 367 | } 368 | } 369 | 370 | // UTF8-BOM marker http://unicode.org/faq/utf_bom.html#bom4 371 | if (index == 0 && result == 65279) 372 | return readUtf8Char(); 373 | 374 | return result; 375 | } 376 | 377 | 378 | inline 379 | function readUtf8MultiSequenceByte():Int { 380 | final byte = input.readByte(); 381 | byteIndex++; 382 | 383 | if (!Bits.getBit(byte, 8)) 384 | throw 'Valid UTF-8 multi-sequence byte expected at position [$byteIndex] but found byte with value [$byte]!'; 385 | 386 | if (Bits.getBit(byte, 7)) 387 | throw 'Valid UTF-8 multi-sequence byte expected at position [$byteIndex] but found byte with value [$byte]!'; 388 | 389 | return Bits.clearBit(byte, 8); 390 | } 391 | } 392 | 393 | 394 | @:noDoc @:dox(hide) 395 | private class StringCharIterator extends CharIterator { 396 | final chars:String; 397 | final charsMaxIndex:Int; 398 | 399 | 400 | public function new(chars:String, prevBufferSize:Int) { 401 | super(prevBufferSize); 402 | this.chars = chars; 403 | charsMaxIndex = chars.length8() - 1; 404 | } 405 | 406 | 407 | override 408 | function isEOF():Bool 409 | return index >= charsMaxIndex; 410 | 411 | 412 | override 413 | function getChar():Char 414 | return Strings._charCodeAt8Unsafe(chars, index); 415 | } 416 | -------------------------------------------------------------------------------- /src/hx/strings/Pattern.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings; 7 | 8 | import hx.strings.internal.Either3; 9 | 10 | using hx.strings.Strings; 11 | 12 | /** 13 | * Thread-safe API for regex pattern matching backed by Haxe's EReg class. 14 | * 15 | * UTF8 matching (EReg's 'u' flag) is enabled by default. 16 | * 17 | * @see http://haxe.org/manual/std-regex.html 18 | */ 19 | @immutable 20 | @threadSafe 21 | class Pattern { 22 | 23 | public final pattern:String; 24 | public final options:String; 25 | final ereg:EReg; 26 | 27 | 28 | /** 29 | * @param pattern regular expression 30 | * @param options matching options 31 | */ 32 | public static function compile(pattern:String, ?options:Either3>):Pattern { 33 | if(options == null) 34 | return new Pattern(pattern, ""); 35 | 36 | return new Pattern(pattern, switch(options.value) { 37 | case a(str): @:nullSafety(Off) 38 | str.toLowerCase8() 39 | // remove unsupported flags 40 | .filterChars((ch) -> ch == "i" || ch == "m" || ch == "g" 41 | #if (cpp || flash || java || neko || php) 42 | || ch == "s" 43 | #end 44 | ); 45 | case b(opt): Std.string(opt); 46 | case c(arr): arr.filter((m) -> m != null /* remove null enties */).join(""); 47 | }); 48 | } 49 | 50 | 51 | function new(pattern:String, options:String) { 52 | this.pattern = pattern; 53 | this.options = options; 54 | this.ereg = new EReg(pattern, options); 55 | 56 | // explicitly enable UTF8 57 | this.options += "u"; 58 | } 59 | 60 | 61 | /** 62 | *

 63 |     * >>> Pattern.compile(".*").matcher("a").matches() == true
 64 |     * 
65 | * 66 | * @return a matcher (not thread-safe) that works on the given input string 67 | */ 68 | inline 69 | public function matcher(str:String):Matcher 70 | return new MatcherImpl(ereg, pattern, options, str); 71 | 72 | 73 | /** 74 | * If MatchingOption.MATCH_ALL was specified, replaces all matches with replaceWith. 75 | * Otherwise only the first match. 76 | * 77 | *

 78 |     * >>> Pattern.compile("[.]"     ).replace("a.b.c", ":") == "a:b.c"
 79 |     * >>> Pattern.compile("[.]", "g").replace("a.b.c", ":") == "a:b:c"
 80 |     * 
81 | */ 82 | inline 83 | public function replace(str:String, replaceWith:String):String 84 | return ereg.replace(str, replaceWith); 85 | 86 | 87 | /** 88 | * If MatchingOption.MATCH_ALL was specified, removes all matches. 89 | * Otherwise only the first match. 90 | * 91 | *

 92 |     * >>> Pattern.compile("[.]"     ).remove("a.b.c") == "ab.c"
 93 |     * >>> Pattern.compile("[.]", "g").remove("a.b.c") == "abc"
 94 |     * 
95 | */ 96 | inline 97 | public function remove(str:String):String 98 | return ereg.replace(str, ""); 99 | 100 | 101 | /** 102 | * Uses matches as separator to split the string. 103 | * 104 | *

105 |     * >>> Pattern.compile("[.]"     ).split("a.b.c") == [ "a", "b.c" ]
106 |     * >>> Pattern.compile("[.]", "g").split("a.b.c") == [ "a", "b", "c" ]
107 |     * 
108 | */ 109 | inline 110 | public function split(str:String):Array 111 | return ereg.split(str); 112 | } 113 | 114 | 115 | /** 116 | * Performs match operations on a string by interpreting a regular expression pattern. 117 | * 118 | * Instances are created via the hx.strings.Pattern#matcher() function. 119 | * 120 | * @author Sebastian Thomschke, Vegard IT GmbH 121 | */ 122 | @notThreadSafe 123 | interface Matcher { 124 | 125 | /** 126 | * Iterates over all matches and invokes the onMatch function for each match. 127 | */ 128 | function iterate(onMatch:Matcher -> Void):Void; 129 | 130 | /** 131 | * Iterates over all matches and invokes the mapper function for each match. 132 | * 133 | * @return a string with all matches replaced by the mapper 134 | */ 135 | function map(mapper:Matcher -> String):String; 136 | 137 | /** 138 | * If no match attempt was made before Matcher#matches() will be excuted implicitly. 139 | * 140 | * @return the substring captured by the n-th group of the current match. 141 | * If n is 0, then returns the whole string of the current match. 142 | * 143 | * @throws exception if no capturing group with the given index n exists 144 | */ 145 | function matched(n:Int = 0):String; 146 | 147 | /** 148 | * If no match attempt was made before Matcher#matches() will be excuted implicitly. 149 | * 150 | * @return the position of the current match 151 | * 152 | * @throws exception if no match was found 153 | * 154 | *

155 |     * >>> Pattern.compile("c").matcher("abcde").matchedPos() == { pos: 2, len: 1 }
156 |     * 
157 | */ 158 | function matchedPos(): {pos:Int, len:Int}; 159 | 160 | /** 161 | * Attempts to match the string against the pattern. 162 | * 163 | * @return true if at least one match has been found 164 | * 165 | *

166 |     * >>> Pattern.compile(".*").matcher("a").matches() == true
167 |     * 
168 | */ 169 | function matches():Bool; 170 | 171 | /** 172 | * Attempts to match the given region of the string against the pattern. 173 | * 174 | * @return true if at least one match has been found 175 | * 176 | *

177 |     * >>> Pattern.compile("b").matcher("aba").matchesInRegion(0)    == true
178 |     * >>> Pattern.compile("b").matcher("aba").matchesInRegion(0, 1) == false
179 |     * >>> Pattern.compile("b").matcher("aba").matchesInRegion(0, 2) == true
180 |     * >>> Pattern.compile("b").matcher("aba").matchesInRegion(1)    == true
181 |     * >>> Pattern.compile("b").matcher("aba").matchesInRegion(2)    == false
182 |     * 
183 | */ 184 | function matchesInRegion(pos:Int, len:Int=-1):Bool; 185 | 186 | /** 187 | * Resets the matcher with the given input string 188 | * 189 | *

190 |     * >>> Pattern.compile("b").matcher("abcb").reset("abCB").substringAfterMatch()  == "CB"
191 |     * 
192 | * 193 | * @return self reference 194 | */ 195 | function reset(str:String):Matcher; 196 | 197 | /** 198 | * If no match attempt was made before Matcher#matches() will be excuted implicitly. 199 | * 200 | * @return the substring after the current match or "" if no match was found 201 | * 202 | *

203 |     * >>> Pattern.compile("b"     ).matcher("abcb").substringAfterMatch() == "cb"
204 |     * >>> Pattern.compile("b", "g").matcher("abcb").substringAfterMatch() == "cb"
205 |     * >>> Pattern.compile("d", "g").matcher("abcb").substringAfterMatch() == ""
206 |     * 
207 | */ 208 | function substringAfterMatch():String; 209 | 210 | /** 211 | * If no match attempt was made before Matcher#matches() will be excuted implicitly. 212 | * 213 | * @return the substring before the current match or "" if no match was found 214 | * 215 | *

216 |     * >>> Pattern.compile("b"     ).matcher("abcb").substringBeforeMatch() == "a"
217 |     * >>> Pattern.compile("b", "g").matcher("abcb").substringBeforeMatch() == "a"
218 |     * >>> Pattern.compile("d", "g").matcher("abcb").substringBeforeMatch() == ""
219 |     * 
220 | */ 221 | function substringBeforeMatch():String; 222 | } 223 | 224 | 225 | /** 226 | * Options for compilation of regular expression patterns. 227 | * 228 | * @author Sebastian Thomschke, Vegard IT GmbH 229 | */ 230 | #if (haxe_ver < 4.3) @:enum #else enum #end 231 | abstract MatchingOption(String) { 232 | 233 | /** 234 | * case insensitive matching 235 | */ 236 | final IGNORE_CASE = "i"; 237 | 238 | /** 239 | * multiline matching, in the sense of that ^ and $ represent the beginning and end of a line 240 | */ 241 | final MULTILINE = "m"; 242 | 243 | #if !(cs || (js && !nodejs)) 244 | /** 245 | * the dot . will also match new lines 246 | */ 247 | final DOTALL = "s"; 248 | #end 249 | 250 | /** 251 | * All map, split and replace operations are performed on all matches within the given string 252 | */ 253 | final MATCH_ALL = "g"; 254 | } 255 | 256 | @:nullSafety(Strict) 257 | private class MatcherImpl implements Matcher { 258 | final ereg:EReg; 259 | var isMatch:Null; 260 | var str:String; 261 | 262 | 263 | public function new(ereg:EReg, pattern:String, options:String, str:String) { 264 | this.ereg = @:nullSafety(Off) _cloneEReg(ereg, pattern, options); 265 | this.str = str; 266 | } 267 | 268 | 269 | inline 270 | public function reset(str:String):Matcher { 271 | this.str = str; 272 | this.isMatch = null; 273 | return this; 274 | } 275 | 276 | 277 | public function iterate(onMatch:Matcher -> Void):Void { 278 | var startAt = 0; 279 | while(ereg.matchSub(str, startAt)) { 280 | isMatch = true; 281 | var matchedPos = ereg.matchedPos(); 282 | onMatch(this); 283 | startAt = matchedPos.pos + matchedPos.len; 284 | } 285 | isMatch = false; 286 | } 287 | 288 | 289 | public function map(mapper:Matcher -> String):String { 290 | return ereg.map(str, function(ereg) { 291 | isMatch = true; 292 | return mapper(this); 293 | }); 294 | } 295 | 296 | 297 | public function matched(n:Int = 0):String { 298 | if(!matches()) throw "No string matched"; 299 | 300 | final result = ereg.matched(n); 301 | 302 | #if (cs || php) // workaround for targets with non-compliant implementation 303 | if(result == null) throw 'Group $n not found.'; 304 | #end 305 | 306 | return result; 307 | } 308 | 309 | #if python @:nullSafety(Off) #end // TODO 310 | public function matches():Bool { 311 | if (isMatch == null) isMatch = ereg.match(str); 312 | return isMatch; 313 | } 314 | 315 | 316 | inline 317 | public function matchesInRegion(pos:Int, len:Int = -1):Bool { 318 | #if eval 319 | // https://github.com/HaxeFoundation/haxe/issues/9333 320 | if (len < 0) 321 | return isMatch = ereg.matchSub(str, pos); 322 | #end 323 | return isMatch = ereg.matchSub(str, pos, len); 324 | } 325 | 326 | 327 | public function matchedPos(): {pos:Int, len:Int} { 328 | if(!matches()) throw "No string matched"; 329 | return ereg.matchedPos(); 330 | } 331 | 332 | 333 | public function substringAfterMatch():String { 334 | if(!matches()) return ""; 335 | return ereg.matchedRight(); 336 | } 337 | 338 | 339 | public function substringBeforeMatch():String { 340 | if(!matches()) return ""; 341 | return ereg.matchedLeft(); 342 | } 343 | 344 | 345 | @:nullSafety(Off) 346 | inline 347 | function _cloneEReg(from:EReg, pattern:String, options:String):EReg { 348 | // partially copy internal state (if possible) to reuse the inner pre-compiled pattern instance 349 | // and avoid expensive reparsing of the pattern string 350 | #if (neko || lua || cpp) 351 | final clone = Type.createEmptyInstance(EReg); 352 | Reflect.setField(clone, "r", Reflect.field(from, "r")); 353 | Reflect.setField(clone, "global", Reflect.field(from, "global")); 354 | #elseif java 355 | final clone = Type.createEmptyInstance(EReg); 356 | Reflect.setField(clone, "pattern", pattern); 357 | Reflect.setField(clone, "matcher", Reflect.field(from, "matcher")); 358 | Reflect.setField(clone, "isGlobal", Reflect.field(from, "isGlobal")); 359 | #elseif cs 360 | final clone = Type.createEmptyInstance(EReg); 361 | Reflect.setField(clone, "regex", Reflect.field(from, "regex")); 362 | Reflect.setField(clone, "isGlobal", Reflect.field(from, "isGlobal")); 363 | #elseif php 364 | final clone = Type.createEmptyInstance(EReg); 365 | Reflect.setField(clone, "pattern", pattern); 366 | Reflect.setField(clone, "options", Reflect.field(from, "options")); 367 | Reflect.setField(clone, "global", Reflect.field(from, "global")); 368 | Reflect.setField(clone, "re", Reflect.field(from, "re")); 369 | #elseif python 370 | final clone = Type.createEmptyInstance(EReg); 371 | Reflect.setField(clone, "pattern", Reflect.field(from, "pattern")); 372 | Reflect.setField(clone, "global", Reflect.field(from, "global")); 373 | #else 374 | // not reusing internal state on 375 | // - untested targets 376 | // - targets where the compiled pattern and matcher not separated internally (js, flash) 377 | // - targets where cloning results in runtime errors (hl) 378 | final clone = new EReg(pattern, options); 379 | #end 380 | return clone; 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /src/hx/strings/RandomStrings.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings; 7 | 8 | import hx.strings.internal.Bits; 9 | import hx.strings.internal.Either2; 10 | 11 | using hx.strings.Strings; 12 | 13 | /** 14 | * Utility functions to generate random strings. 15 | */ 16 | class RandomStrings { 17 | 18 | static var DIGITS = "0123456789".toChars(); 19 | 20 | static function _genAsciiAlpha() { 21 | final chars = new Array(); 22 | for (i in 65...91) 23 | chars.push(i); 24 | for (i in 97...123) 25 | chars.push(i); 26 | return chars; 27 | } 28 | 29 | static final ASCII_ALPHA = _genAsciiAlpha(); 30 | static final ASCII_ALPHA_NUMERIC = DIGITS.concat(ASCII_ALPHA); 31 | 32 | static inline final MAX_INT = 2147483647; 33 | 34 | 35 | /** 36 | *

 37 |     * >>> RandomStrings.randomAsciiAlpha(-1) throws "[count] must be positive value"
 38 |     * >>> RandomStrings.randomAsciiAlpha(0)  == ""
 39 |     * >>> RandomStrings.randomAsciiAlpha(4).length == 4
 40 |     * >>> RandomStrings.randomAsciiAlpha(50) == ~/^[a-z]+$/i
 41 |     * 
42 | */ 43 | inline 44 | public static function randomAsciiAlpha(length:Int):String 45 | return random(length, ASCII_ALPHA); 46 | 47 | 48 | /** 49 | *

 50 |     * >>> RandomStrings.randomAsciiAlphaNumeric(-1) throws "[count] must be positive value"
 51 |     * >>> RandomStrings.randomAsciiAlphaNumeric(0)  == ""
 52 |     * >>> RandomStrings.randomAsciiAlphaNumeric(4).length == 4
 53 |     * >>> RandomStrings.randomAsciiAlpha(50) == ~/^[a-z0-9]+$/i
 54 |     * 
55 | */ 56 | inline 57 | public static function randomAsciiAlphaNumeric(length:Int):String 58 | return random(length, ASCII_ALPHA_NUMERIC); 59 | 60 | 61 | /** 62 | *

 63 |     * >>> RandomStrings.randomDigits(-1) throws "[count] must be positive value"
 64 |     * >>> RandomStrings.randomDigits(0)  == ""
 65 |     * >>> RandomStrings.randomDigits(4).length == 4
 66 |     * >>> Strings.containsOnly(RandomStrings.randomDigits(50), "0123456789") == true
 67 |     * 
68 | */ 69 | inline 70 | public static function randomDigits(length:Int):String 71 | return random(length, DIGITS); 72 | 73 | 74 | /** 75 | * Generates a random string based on the characters of the given string or character array. 76 | * 77 | *

 78 |     * >>> RandomStrings.random(0, "a")  == ""
 79 |     * >>> RandomStrings.random(4, "a")  == "aaaa"
 80 |     * >>> RandomStrings.random(0, [65]) == ""
 81 |     * >>> RandomStrings.random(4, [65]) == "AAAA"
 82 |     * >>> Strings.containsOnly(RandomStrings.random(50, "aBc"), "aBc") == true
 83 |     * >>> RandomStrings.random(-1, "a")  throws "[count] must be positive value"
 84 |     * >>> RandomStrings.random(-1, [65]) throws "[count] must be positive value"
 85 |     * >>> RandomStrings.random(1, null)  throws "[chars] must not be null"
 86 |     * >>> RandomStrings.random(1, [])    throws "[chars] must not be empty"
 87 |     * 
88 | */ 89 | public static function random(length:Int, chars:Either2>):String { 90 | if (length == 0) 91 | return ""; 92 | 93 | if (length < 0) 94 | throw "[count] must be positive value"; 95 | 96 | if (chars == null) 97 | throw "[chars] must not be null"; 98 | 99 | final charsArray:Array = switch(chars.value) { 100 | case a(str): str.toChars(); 101 | case b(chars): chars; 102 | } 103 | 104 | if (charsArray.length == 0) 105 | throw "[chars] must not be empty"; 106 | 107 | final result = new StringBuilder(); 108 | for (i in 0...length) 109 | result.addChar(charsArray[Math.floor(charsArray.length * Math.random())]); 110 | 111 | return result.toString(); 112 | } 113 | 114 | 115 | /** 116 | * Returns a random substring from the given string. 117 | *

118 |     * >>> RandomStrings.randomSubstring(null)     throws "[str] must not be null or empty"
119 |     * >>> RandomStrings.randomSubstring("")       throws "[str] must not be null or empty"
120 |     * >>> RandomStrings.randomSubstring(" ", 2)   throws "[substringLength] must not be larger than str.length"
121 |     * >>> RandomStrings.randomSubstring("dog", 3) == "dog"
122 |     * >>> ["ab", "bc"].indexOf(RandomStrings.randomSubstring("abc", 2)) > -1
123 |     * 
124 | * 125 | * @return a random substring from the given string. 126 | */ 127 | public static function randomSubstring(str:String, substringLength:Int = 1):String { 128 | if (str.isEmpty()) 129 | throw "[str] must not be null or empty"; 130 | 131 | if (substringLength < 1) 132 | throw "[substringLength] must not be smaller than 1"; 133 | 134 | final len = str.length8(); 135 | 136 | if (substringLength > len) 137 | throw "[substringLength] must not be larger than str.length"; 138 | 139 | if (substringLength == len) 140 | return str; 141 | 142 | final startAt = Math.floor((len - substringLength + 1) * Math.random()); 143 | return str.substr8(startAt, substringLength); 144 | } 145 | 146 | 147 | /** 148 | * Generates a Version 4 UUID, e.g. "dcdfd0b2-a5e8-4748-8333-58a5e420bc5e". 149 | * See https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_.28random.29 150 | * 151 | *

152 |     * >>> RandomStrings.randomUUIDv4().length == 36
153 |     * >>> Strings.containsOnly(RandomStrings.randomUUIDv4(),    "01234567890abcdef-") == true
154 |     * >>> Strings.containsOnly(RandomStrings.randomUUIDv4(":"), "01234567890abcdef:") == true
155 |     * >>> ~/[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[ab89][a-f0-9]{3}-[a-f0-9]{12}/.match(RandomStrings.randomUUIDv4()) == true
156 |     * 
157 | * 158 | * @param separator string to separate the UUID parts, default is a dash - 159 | */ 160 | public static function randomUUIDv4(separator:String = "-"):String { 161 | if (separator == null) 162 | throw "[separator] must not be null"; 163 | 164 | // set variant bits (i.e. 10xx) according to RFC4122 4.1.1. Variant: http://www.ietf.org/rfc/rfc4122.txt 165 | var variantByte = Math.floor(Math.random() * 16); 166 | variantByte = Bits.setBit(variantByte, 4); // set the 4th bit to 1 167 | variantByte = Bits.clearBit(variantByte, 3); // set the 3nd bit to 0 168 | 169 | return ( 170 | StringTools.hex(Math.floor(Math.random() * 65536), 4) + // 171 | StringTools.hex(Math.floor(Math.random() * 65536), 4) + // 172 | 173 | separator + // 174 | 175 | StringTools.hex(Math.floor(Math.random() * 65536), 4) + // 176 | 177 | separator + // 178 | 179 | "4" + // Version 4 indicator 180 | StringTools.hex(Math.floor(Math.random() * 4096), 3) + // 181 | 182 | separator + // 183 | 184 | StringTools.hex(variantByte) + // 185 | StringTools.hex(Math.floor(Math.random() * 4096), 3) + // 186 | 187 | separator + // 188 | 189 | StringTools.hex(Math.floor(Math.random() * 65536), 4) + // 190 | StringTools.hex(Math.floor(Math.random() * 65536), 4) + // 191 | StringTools.hex(Math.floor(Math.random() * 65536), 4) // 192 | ).toLowerCase(); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/hx/strings/String8.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings; 7 | 8 | using hx.strings.Strings; 9 | 10 | #if !macro 11 | /** 12 | * Implemented as an abstract type over String. 13 | * 14 | * All exposed methods are UTF-8 compatible and have consistent behavior across platforms. 15 | * 16 | * The methods are auto generated based on the static methods provided by the `hx.strings.Strings` class. 17 | * 18 | * Example usage: 19 | *
 20 |  * var str:String8 = "myString";
 21 |  * str.length();  // --> this is not the Haxe internal `length()` method but an implementation that with UTF-8 strings across platforms
 22 |  * 
23 | */ 24 | @:nullSafety(Off) 25 | @:build(hx.strings.String8.String8Generator.generateMethods()) 26 | abstract String8(String) from String to String { 27 | 28 | /** 29 | * String#length variant with cross-platform UTF-8 support. 30 | */ 31 | public var length(get, never):Int; 32 | inline function get_length():Int 33 | return Strings.length8(this); 34 | } 35 | 36 | #else 37 | 38 | import haxe.macro.Context; 39 | import haxe.macro.Expr; 40 | import haxe.macro.Type; 41 | 42 | @:noCompletion 43 | @:dox(hide) 44 | class String8Generator { 45 | 46 | macro 47 | public static function generateMethods():Array { 48 | final contextFields = Context.getBuildFields(); 49 | final contextPos = Context.currentPos(); 50 | 51 | final delegateClass:ClassType = switch(Context.getType("hx.strings.Strings")) { 52 | case TInst(t, _): t.get(); 53 | default: Context.fatalError("hx.strings.Strings isn't a class.", contextPos); 54 | } 55 | 56 | for (delegateField in delegateClass.statics.get()) { 57 | if (!delegateField.isPublic) 58 | continue; 59 | 60 | if (delegateField.name == "length8") 61 | continue; 62 | 63 | switch(delegateField.type) { 64 | case TFun(args, ret): 65 | if (args.length == 0) 66 | continue; 67 | 68 | // ignore methods whose first argument doesn't take a string 69 | switch(args[0].t) { 70 | case TInst(t, params): 71 | if ("String" != t.toString()) 72 | continue; 73 | case TAbstract(t, params): 74 | if ("hx.strings.internal.AnyAsString" != t.toString()) 75 | continue; 76 | default: 77 | } 78 | 79 | /* 80 | * generate method args declaration of delegating method 81 | */ 82 | final delegateTFunc:TFunc = delegateField.expr() == null ? null : switch(delegateField.expr().expr) { 83 | case TFunction(func): func; 84 | default: Context.fatalError("Should never be reached.", contextPos); 85 | }; 86 | final generatedArgs = new Array(); 87 | final delegateArgs = ["this"]; 88 | for (i in 1...args.length) { 89 | final defaultValue = delegateTFunc == null ? null : 90 | // delegateTFunc.args[i].value in Haxe 4 Preview 5 is TypedExpr 91 | delegateTFunc.args[i].value == null ? null : 92 | switch(delegateTFunc.args[i].value.expr) { 93 | case TConst(constant): 94 | switch(constant) { 95 | case TBool(val): Context.makeExpr(val, contextPos); 96 | case TString(val): Context.makeExpr(val, contextPos); 97 | case TFloat(val): Context.makeExpr(val, contextPos); 98 | case TNull: Context.makeExpr(null, contextPos); 99 | case TInt(val): 100 | switch(delegateTFunc.args[i].v.t) { 101 | case TAbstract(t, params): 102 | if (t.toString() == "hx.strings.StringNotFoundDefault") { 103 | switch(val) { 104 | case 1: macro { StringNotFoundDefault.NULL; }; 105 | case 2: macro { StringNotFoundDefault.EMPTY; }; 106 | case 3: macro { StringNotFoundDefault.INPUT; }; 107 | default: null; 108 | } 109 | } else 110 | Context.makeExpr(val, contextPos); 111 | default: 112 | Context.makeExpr(val, contextPos); 113 | } 114 | default: null; 115 | } 116 | default: null; 117 | }; 118 | final arg = args[i]; 119 | generatedArgs.push({ 120 | name: arg.name, 121 | opt: arg.opt, 122 | value: defaultValue, 123 | type: Context.toComplexType(arg.t) 124 | }); 125 | delegateArgs.push(arg.name); 126 | } 127 | 128 | /* 129 | * generate generic type declaration of delegating method 130 | */ 131 | final generatedGenericParams = new Array(); 132 | for (param in delegateField.params) { 133 | generatedGenericParams.push({name: param.name}); 134 | } 135 | 136 | /* 137 | * generate full declaration of delegating method 138 | */ 139 | final delegateName = delegateField.name; 140 | contextFields.push({ 141 | name: delegateName.endsWith("8") ? delegateName.substringBeforeLast("8") : delegateName, 142 | doc: delegateField.doc, 143 | meta: delegateField.meta.get(), 144 | access: [APublic, AInline], 145 | kind: FFun({ 146 | args: generatedArgs, 147 | params: generatedGenericParams, 148 | ret: Context.toComplexType(ret), 149 | expr: Context.parseInlineString("return Strings." + delegateName + "(" + delegateArgs.join(",") + ")", contextPos) 150 | }), 151 | pos: contextPos 152 | }); 153 | 154 | #if debug 155 | trace('[DEBUG] Generated String8#$delegateName(${delegateArgs.slice(1).join(", ")})'); 156 | #end 157 | default: 158 | continue; 159 | } 160 | } 161 | 162 | return contextFields; 163 | } 164 | } 165 | #end 166 | -------------------------------------------------------------------------------- /src/hx/strings/StringBuilder.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings; 7 | 8 | import haxe.io.BytesOutput; 9 | import haxe.io.Output; 10 | 11 | using hx.strings.Strings; 12 | 13 | /** 14 | * An UTF-8 and fluent API supporting alternative to StringBuf 15 | * with additional functions such as clear(), insert(), isEmpty() 16 | *
17 | * This implementation tries to avoid the creation of intermediate String objects as much as possible. 18 | */ 19 | @:nullSafety(Strict) 20 | @notThreadSafe 21 | class StringBuilder { 22 | 23 | var sb = new StringBuf(); 24 | 25 | #if !(java_src || cs) 26 | var pre:Null> = null; 27 | var len:Int = 0; 28 | #end 29 | 30 | 31 | inline 32 | public function new(?initialContent:AnyAsString) 33 | if (initialContent != null) 34 | add(initialContent); 35 | 36 | 37 | /** 38 | * Important: Invocation may result in temporary string object generation during invocation 39 | * on platforms except Java. 40 | * 41 | *

 42 |     * >>> new StringBuilder("").length                       == 0
 43 |     * >>> new StringBuilder("はい").add("は").add("い").length == 4
 44 |     * >>> new StringBuilder("ab").insert(0, "cd").insert(2, "はい").insertChar(1, 32).insertChar(4, 32).length == 8
 45 |     * 
46 | * 47 | * @return the length of the string representation of all added items 48 | */ 49 | public var length(get, never): Int; 50 | inline 51 | function get_length():Int 52 | #if (java_src || cs) 53 | return sb.length; 54 | #else 55 | return len; 56 | #end 57 | 58 | 59 | /** 60 | *

 61 |     * >>> new StringBuilder(null).add(null).toString() == "null"
 62 |     * >>> new StringBuilder("hi").add(null).toString() == "hinull"
 63 |     * >>> new StringBuilder("hi").add(1).toString()    == "hi1"
 64 |     * 
65 | * 66 | * @return this for chained operations 67 | */ 68 | public function add(item:Null):StringBuilder { 69 | sb.add(item == null ? "null" : item); 70 | #if !(java_src || cs) 71 | len += item.length8(); 72 | #end 73 | return this; 74 | } 75 | 76 | /** 77 | *

 78 |     * >>> new StringBuilder("").addChar(223).toString()     == "ß"
 79 |     * >>> new StringBuilder("hi").addChar(12399).toString() == "hiは"
 80 |     * 
81 | * 82 | * @return this for chained operations 83 | */ 84 | #if java inline #end 85 | public function addChar(ch:Char):StringBuilder { 86 | #if (java || flash || cs || python) 87 | sb.addChar(ch); 88 | #else 89 | if (ch.isAscii()) 90 | sb.addChar(ch); 91 | else 92 | sb.add(ch.toString()); 93 | #end 94 | 95 | #if !(java_src || cs) 96 | len++; 97 | #end 98 | return this; 99 | } 100 | 101 | 102 | /** 103 | *

104 |     * >>> new StringBuilder("hi").addAll([1,2]).toString() == "hi12"
105 |     * >>> new StringBuilder("hi").addAll([1,2]).length     == 4
106 |     * 
107 | * @return this for chained operations 108 | */ 109 | public function addAll(items:Array):StringBuilder { 110 | for (item in items) { 111 | sb.add(item); 112 | #if !(java_src || cs) 113 | len += item.length8(); 114 | #end 115 | } 116 | return this; 117 | } 118 | 119 | 120 | /** 121 | * Resets the builders internal buffer 122 | * 123 | *

124 |     * >>> new StringBuilder("hi").clear().add(1).toString() == "1"
125 |     * 
126 | * 127 | * @return this for chained operations 128 | */ 129 | public function clear():StringBuilder { 130 | #if java_src 131 | untyped __java__("this.sb.b.setLength(0)"); 132 | #elseif cs 133 | untyped __cs__("this.sb.b.Clear()"); 134 | #else 135 | pre = null; 136 | sb = new StringBuf(); 137 | len = 0; 138 | #end 139 | return this; 140 | } 141 | 142 | 143 | /** 144 | *

145 |     * >>> new StringBuilder("").isEmpty()    == true
146 |     * >>> new StringBuilder("cat").isEmpty() == false
147 |     * 
148 | * 149 | * @return true if no chars/strings have been added to the string builder yet 150 | */ 151 | inline 152 | public function isEmpty():Bool 153 | return length == 0; 154 | 155 | 156 | /** 157 | * adds the "\n" new line character 158 | * 159 | * @return this for chained operations 160 | */ 161 | public function newLine():StringBuilder { 162 | sb.add(Strings.NEW_LINE_NIX); 163 | 164 | #if !(java_src || cs) 165 | len ++; 166 | #end 167 | return this; 168 | } 169 | 170 | 171 | /** 172 | *

173 |     * >>> new StringBuilder("hi").insert(0, "はい").toString() == "はいhi"
174 |     * >>> new StringBuilder("hi").insert(1, "はい").toString() == "hはいi"
175 |     * >>> new StringBuilder("hi").insert(2, "はい").toString() == "hiはい"
176 |     * >>> new StringBuilder("hi").insert(0, "はい").insert(1, "はい").toString() == "ははいいhi"
177 |     * >>> new StringBuilder("hi").insert(0, "ho").insert(1, "はい").toString() == "hはいohi"
178 |     * >>> new StringBuilder("hi").insert(0, "ho").insert(1, "はい").toString() == "hはいohi"
179 |     * >>> new StringBuilder("hi").insert(0, "ho").insert(2, "はい").toString() == "hoはいhi"
180 |     * >>> new StringBuilder("hi").insert(0, "ho").insert(3, "はい").toString() == "hohはいi"
181 |     * >>> new StringBuilder("hi").insert(0, "ho").insert(4, "はい").toString() == "hohiはい"
182 |     * >>> new StringBuilder("hi").insert(3, " ").toString() throws "[pos] must not be greater than this.length"
183 |     * 
184 | * 185 | * @return this for chained operations 186 | * @throws exception if pos is out-of range (i.e. < 0 or > this.length) 187 | */ 188 | public function insert(pos:CharIndex, item:AnyAsString):StringBuilder { 189 | if (pos < 0) throw "[pos] must not be negative"; 190 | if (pos > this.length) throw "[pos] must not be greater than this.length"; 191 | 192 | if (pos == this.length) { 193 | add(item); 194 | return this; 195 | } 196 | 197 | #if java_src 198 | untyped __java__("this.sb.b.insert(pos, item)"); 199 | #elseif cs 200 | untyped __cs__("this.sb.b.Insert(pos, item)"); 201 | #else 202 | if (pos == 0) { 203 | if (pre == null) pre = []; 204 | pre.unshift(item); 205 | len += item.length8(); 206 | return this; 207 | } 208 | 209 | // insert the item into the pre[] array if required 210 | var pre_len = 0; 211 | if (pre != null) { 212 | final pre = this.pre; // TODO workaround for null-safety bug 213 | final i = pre.length; 214 | for(i in 0...pre.length) { 215 | final next_pre_len = pre_len + pre[i].length8(); 216 | if (next_pre_len == pos) { 217 | pre.insert(i + 1, item); 218 | len += item.length8(); 219 | return this; 220 | } 221 | if (next_pre_len > pos) { 222 | final preSplitted:Array = cast pre[i].splitAt(pos - pre_len); 223 | pre[i] = preSplitted[0]; 224 | pre.insert(i + 1, item); 225 | pre.insert(i + 2, preSplitted[1]); 226 | len += item.length8(); 227 | return this; 228 | } 229 | pre_len = next_pre_len; 230 | } 231 | } 232 | 233 | if (sb.length == 0) { 234 | add(item); 235 | return this; 236 | } 237 | 238 | final sbSplitted:Array = cast sb.toString().splitAt(pos - pre_len); 239 | sb = new StringBuf(); 240 | sb.add(sbSplitted[0]); 241 | sb.add(item); 242 | len += item.length8(); 243 | sb.add(sbSplitted[1]); 244 | #end 245 | return this; 246 | } 247 | 248 | 249 | /** 250 | *

251 |     * >>> new StringBuilder("hi").insertChar(0, Char.of("は")).toString() == "はhi"
252 |     * >>> new StringBuilder("hi").insertChar(1, Char.of("は")).toString() == "hはi"
253 |     * >>> new StringBuilder("hi").insertChar(2, Char.of("は")).toString() == "hiは"
254 |     * >>> new StringBuilder("hi").insertChar(0, Char.of("は")).insertChar(1, 32).toString() == "は hi"
255 |     * >>> new StringBuilder("hi").insert(0, "ho").insertChar(1, Char.of("は")).toString() == "hはohi"
256 |     * >>> new StringBuilder("hi").insert(0, "ho").insertChar(1, Char.of("は")).toString() == "hはohi"
257 |     * >>> new StringBuilder("hi").insert(0, "ho").insertChar(2, Char.of("は")).toString() == "hoはhi"
258 |     * >>> new StringBuilder("hi").insert(0, "ho").insertChar(3, Char.of("は")).toString() == "hohはi"
259 |     * >>> new StringBuilder("hi").insert(0, "ho").insertChar(4, Char.of("は")).toString() == "hohiは"
260 |     * >>> new StringBuilder("hi").insertChar(3, 32).toString() throws "[pos] must not be greater than this.length"
261 |     * 
262 | * 263 | * @return this for chained operations 264 | * @throws exception if pos is out-of range (i.e. < 0 or > this.length) 265 | */ 266 | public function insertChar(pos:CharIndex, ch:Char):StringBuilder { 267 | if (pos < 0) throw "[pos] must not be negative"; 268 | if (pos > this.length) throw "[pos] must not be greater than this.length"; 269 | 270 | if (pos == this.length) { 271 | addChar(ch); 272 | return this; 273 | } 274 | 275 | #if java_src 276 | untyped __java__("this.sb.b.insert(pos, (char)ch)"); 277 | #elseif cs 278 | untyped __cs__("this.sb.b.Insert(pos, (System.Char)ch)"); 279 | #else 280 | if (pos == 0) { 281 | if (pre == null) pre = []; 282 | pre.unshift(ch); 283 | len++; 284 | return this; 285 | } 286 | 287 | // insert the char into the pre[] array if required 288 | var pre_len = 0; 289 | if (pre != null) { 290 | final pre = this.pre; // TODO workaround for null-safety bug 291 | final i = pre.length; 292 | for(i in 0...pre.length) { 293 | final next_pre_len = pre_len + pre[i].length8(); 294 | if (next_pre_len == pos) { 295 | pre.insert(i + 1, ch); 296 | len++; 297 | return this; 298 | } 299 | if (next_pre_len > pos) { 300 | final preSplitted:Array = cast pre[i].splitAt(pos - pre_len); 301 | pre[i] = preSplitted[0]; 302 | pre.insert(i + 1, ch); 303 | pre.insert(i + 2, preSplitted[1]); 304 | len++; 305 | return this; 306 | } 307 | pre_len = next_pre_len; 308 | } 309 | } 310 | 311 | if (sb.length == 0) { 312 | addChar(ch); 313 | return this; 314 | } 315 | 316 | final sbSplitted:Array = cast sb.toString().splitAt(pos - pre_len); 317 | sb = new StringBuf(); 318 | sb.add(sbSplitted[0]); 319 | addChar(ch); 320 | sb.add(sbSplitted[1]); 321 | #end 322 | return this; 323 | } 324 | 325 | 326 | /** 327 | *

328 |     * >>> new StringBuilder("hi").insertAll(0, [1,2]).toString() == "12hi"
329 |     * >>> new StringBuilder("hi").insertAll(1, [1,2]).toString() == "h12i"
330 |     * >>> new StringBuilder("hi").insertAll(2, [1,2]).toString() == "hi12"
331 |     * 
332 | * 333 | * @return this for chained operations 334 | */ 335 | public function insertAll(pos:CharIndex, items:Array):StringBuilder { 336 | if (pos < 0) throw "[pos] must not be negative"; 337 | if (pos > this.length) throw "[pos] must not be greater than this.length"; 338 | 339 | if (pos == this.length) { 340 | addAll(items); 341 | return this; 342 | } 343 | 344 | #if (java_src || cs) 345 | var i = items.length; 346 | while (i-- > 0) { 347 | final item = items[i]; 348 | #if java_src 349 | untyped __java__("this.sb.b.insert(pos, item)"); 350 | #else 351 | untyped __cs__("this.sb.b.Insert(pos, item)"); 352 | #end 353 | } 354 | #else 355 | if (pos == 0) { 356 | if (pre == null) pre = []; 357 | final pre = this.pre; // TODO workaround for null-safety bug 358 | var i = items.length; 359 | while (i-- > 0) { 360 | final item = items[i]; 361 | pre.unshift(item); 362 | len += item.length8(); 363 | } 364 | return this; 365 | } 366 | 367 | // insert the items into the pre[] array if required 368 | var pre_len = 0; 369 | if (pre != null) { 370 | final pre = this.pre; // TODO workaround for null-safety bug 371 | final i = pre.length; 372 | for(i in 0...pre.length) { 373 | final next_pre_len = pre_len + pre[i].length8(); 374 | if (next_pre_len == pos) { 375 | var j = items.length; 376 | while (j-- > 0) { 377 | final item = items[j]; 378 | pre.insert(i + 1, item); 379 | len += item.length8(); 380 | } 381 | return this; 382 | } 383 | if (next_pre_len > pos) { 384 | final preSplitted:Array = cast pre[i].splitAt(pos - pre_len); 385 | pre[i] = preSplitted[0]; 386 | pre.insert(i + 1, preSplitted[1]); 387 | var j = items.length; 388 | while (j-- > 0) { 389 | final item = items[j]; 390 | pre.insert(i + 1, item); 391 | len += item.length8(); 392 | } 393 | return this; 394 | } 395 | pre_len = next_pre_len; 396 | } 397 | } 398 | 399 | if (sb.length == 0) { 400 | for(item in items) 401 | add(item); 402 | return this; 403 | } 404 | 405 | final sbSplitted:Array = cast sb.toString().splitAt(pos - pre_len); 406 | sb = new StringBuf(); 407 | sb.add(sbSplitted[0]); 408 | for (item in items) { 409 | sb.add(item); 410 | len += item.length8(); 411 | } 412 | sb.add(sbSplitted[1]); 413 | #end 414 | return this; 415 | } 416 | 417 | 418 | /** 419 | *

420 |     * >>> ({var sb=new StringBuilder("1"); final out=sb.asOutput(); out.writeByte(Char.TWO); out.writeString("3"); sb; }).toString() == "123"
421 |     * 
422 | * 423 | * @return a haxe.ui.Output wrapper object around this instance 424 | */ 425 | public function asOutput():Output 426 | return new OutputWrapper(this); 427 | 428 | 429 | /** 430 | *

431 |     * >>> new StringBuilder().toString()     == ""
432 |     * >>> new StringBuilder("").toString()   == ""
433 |     * >>> new StringBuilder(null).toString() == ""
434 |     * >>> new StringBuilder("hi").toString() == "hi"
435 |     * 
436 | * 437 | * @return a new string object representing the builder's content 438 | */ 439 | #if java_src inline #end 440 | public function toString():String { 441 | #if (java_src || cs) 442 | return sb.toString(); 443 | #else 444 | if (pre == null) 445 | return sb.toString(); 446 | final str = pre.join("") + sb.toString(); 447 | clear(); 448 | add(str); 449 | return str; 450 | #end 451 | } 452 | } 453 | 454 | 455 | @:nullSafety(Strict) 456 | private class OutputWrapper extends Output { 457 | 458 | private final sb:StringBuilder; 459 | private var bo:Null; 460 | 461 | inline 462 | public function new(sb:StringBuilder) 463 | this.sb = sb; 464 | 465 | 466 | override 467 | public function flush() { 468 | if (bo != null && bo.length > 0) { 469 | sb.add(bo.getBytes().toString()); 470 | bo == null; 471 | } 472 | } 473 | 474 | 475 | override 476 | public function writeByte(c:Int):Void { 477 | if (bo == null) bo = new BytesOutput(); 478 | bo.writeByte(c); 479 | } 480 | 481 | 482 | @:dox(hide) 483 | override 484 | function writeString(str:String, ?encoding:haxe.io.Encoding) { 485 | flush(); 486 | sb.add(str); 487 | } 488 | } 489 | -------------------------------------------------------------------------------- /src/hx/strings/StringMacros.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings; 7 | 8 | import haxe.macro.*; 9 | 10 | class StringMacros { 11 | 12 | /** 13 | * Returns the content of the multi-line comment between the methods invocation brackets ( and ). 14 | * Variables with the notation ${varName} are not interpolated. 15 | * 16 | * E.g. 17 | * 18 | *

19 |      * var myString = StringMacros.multiline(/*
20 |      * This is a multi-line string wrapped in a comment.
21 |      * Special characters like ' or " don't need to be escaped.
22 |      * Variables ${blabla} are not interpolated.
23 |      * */
24 |      * 
25 | * 26 | * @param interpolationPrefix if set, e.g. to "$" Haxe variable interpolation for $var and ${var} be performed 27 | */ 28 | macro 29 | public static function multiline(interpolationPrefix:String = "", trimLeft:Bool = true):ExprOf { 30 | final pos = Context.currentPos(); 31 | 32 | final posInfo = Context.getPosInfos(pos); 33 | final str:String = sys.io.File.getContent(Context.resolvePath(posInfo.file)).substring(posInfo.min, posInfo.max); 34 | 35 | final start = str.indexOf("/*"); 36 | if(start < 0) Context.error("Cannot find multi-line comment start marker '/*'.", pos); 37 | 38 | final end = str.lastIndexOf("*/"); 39 | if(end < 0) Context.error("Cannot find multi-line comment end marker '*/'.", pos); 40 | if(end < start) Context.error("Multi-line comment end marker most be placed after start marker.", pos); 41 | 42 | var comment:String = str.substring(start + 2, end); 43 | 44 | comment = Strings.trimRight(comment, "\t "); 45 | comment = Strings.replaceAll(comment, "\r", ""); 46 | 47 | if (Strings.startsWith(comment, Strings.NEW_LINE_WIN)) 48 | comment = comment.substr(Strings.NEW_LINE_WIN.length); 49 | else if (Strings.startsWith(comment, Strings.NEW_LINE_NIX)) 50 | comment = comment.substr(Strings.NEW_LINE_NIX.length); 51 | 52 | if(trimLeft) { 53 | final lines:Array = comment.split("\n"); 54 | var indent = 9999; 55 | for (l in lines) { 56 | for (i in 0...l.length) { 57 | if (l.charCodeAt(i) != 32) { 58 | if (i < indent) { 59 | indent = i; 60 | break; 61 | } 62 | } 63 | } 64 | } 65 | if (indent > 0) { 66 | comment = [ for (l in lines) l.substr(indent) ].join("\n"); 67 | } 68 | } 69 | 70 | if (Strings.endsWith(comment, "\n")) 71 | comment = comment.substring(0, comment.length - 1); 72 | 73 | if (Strings.isNotEmpty(interpolationPrefix) && Strings.contains(comment, interpolationPrefix)) { 74 | if (interpolationPrefix != "$") { 75 | comment = Strings.replaceAll(comment, interpolationPrefix + interpolationPrefix, "THIS_IS_ESCAPED"); 76 | comment = Strings.replaceAll(comment, interpolationPrefix, "THIS_IS_TO_INTERPOLATE"); 77 | comment = Strings.replaceAll(comment, "$", "$$"); 78 | comment = Strings.replaceAll(comment, "THIS_IS_ESCAPED", Strings.replaceAll(interpolationPrefix, "$", "$$")); 79 | comment = Strings.replaceAll(comment, "THIS_IS_TO_INTERPOLATE", "$"); 80 | } 81 | return MacroStringTools.formatString(comment, pos); 82 | } 83 | 84 | return macro @:pos(pos) $v{comment}; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/hx/strings/ansi/Ansi.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.ansi; 7 | 8 | import hx.strings.ansi.AnsiColor; 9 | import hx.strings.ansi.AnsiTextAttribute; 10 | import hx.strings.ansi.AnsiWriter.StringBuf_StringBuilder_or_Output; 11 | 12 | 13 | /** 14 | * https://en.wikipedia.org/wiki/ANSI_escape_code 15 | * http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/c327.html 16 | * http://ascii-table.com/ansi-escape-sequences.php 17 | */ 18 | class Ansi { 19 | 20 | /** 21 | * ANSI escape sequence header 22 | */ 23 | public static inline final ESC = "\x1B["; 24 | 25 | 26 | /** 27 | * sets the given text attribute 28 | */ 29 | inline 30 | public static function attr(attr:AnsiTextAttribute):String 31 | return ESC + (attr) + "m"; 32 | 33 | 34 | /** 35 | * set the text background color 36 | * 37 | *

 38 |     * >>> Ansi.bg(RED) == "\x1B[41m"
 39 |     * 
40 | */ 41 | inline 42 | public static function bg(color:AnsiColor):String 43 | return ESC + "4" + color + "m"; 44 | 45 | 46 | /** 47 | *

 48 |     * >>> Ansi.cursor(MoveUp(5)) == "\x1B[5A"
 49 |     * >>> Ansi.cursor(GoToPos(5,5)) == "\x1B[5;5H"
 50 |     * 
51 | */ 52 | public static function cursor(cmd:AnsiCursor):String { 53 | return switch(cmd) { 54 | case GoToHome: Ansi.ESC + "H"; 55 | case GoToPos(line, column): Ansi.ESC + line + ";" + column + "H"; 56 | case MoveUp(lines): Ansi.ESC + lines + "A"; 57 | case MoveDown(lines): Ansi.ESC + lines + "B"; 58 | case MoveRight(columns): Ansi.ESC + columns + "C"; 59 | case MoveLeft(columns): Ansi.ESC + columns + "D"; 60 | case SavePos: Ansi.ESC + "s"; 61 | case RestorePos: Ansi.ESC + "u"; 62 | } 63 | } 64 | 65 | 66 | /** 67 | * Clears the screen and moves the cursor to the home position 68 | */ 69 | inline 70 | public static function clearScreen():String 71 | return ESC + "2J"; 72 | 73 | 74 | /** 75 | * Clear all characters from current position to the end of the line including the character at the current position 76 | */ 77 | inline 78 | public static function clearLine():String 79 | return ESC + "K"; 80 | 81 | 82 | /** 83 | * set the text foreground color 84 | * 85 | *

 86 |     * >>> Ansi.fg(RED) == "\x1B[31m"
 87 |     * 
88 | */ 89 | inline 90 | public static function fg(color:AnsiColor):String 91 | return ESC + "3" + color + "m"; 92 | 93 | 94 | /** 95 | *

 96 |     * >>> Ansi.writer(new StringBuf()          ).fg(GREEN).attr(ITALIC).write("Hello").attr(RESET).out.toString()            == "\x1B[32m\x1B[3mHello\x1B[0m"
 97 |     * >>> Ansi.writer(new StringBuilder()      ).fg(GREEN).attr(ITALIC).write("Hello").attr(RESET).out.toString()            == "\x1B[32m\x1B[3mHello\x1B[0m"
 98 |     * >>> Ansi.writer(new haxe.io.BytesOutput()).fg(GREEN).attr(ITALIC).write("Hello").attr(RESET).out.getBytes().toString() == "\x1B[32m\x1B[3mHello\x1B[0m"
 99 |     * >>> ({var out=new StringBuf();           Ansi.writer(out).out == out;}) == true
100 |     * >>> ({var out=new StringBuilder();       Ansi.writer(out).out == out;}) == true
101 |     * >>> ({var out=new haxe.io.BytesOutput(); Ansi.writer(out).out == out;}) == true
102 |     * 
103 | * 104 | * @param out: StringBuf, haxe.io.Output or hx.strings.StringBuilder 105 | */ 106 | inline 107 | public static function writer(out:StringBuf_StringBuilder_or_Output):AnsiWriter 108 | return new AnsiWriter(out); 109 | } 110 | -------------------------------------------------------------------------------- /src/hx/strings/ansi/AnsiColor.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.ansi; 7 | 8 | #if (haxe_ver < 4.3) @:enum #else enum #end 9 | abstract AnsiColor(Int) { 10 | final BLACK = 0; 11 | final RED = 1; 12 | final GREEN = 2; 13 | final YELLOW = 3; 14 | final BLUE = 4; 15 | final MAGENTA = 5; 16 | final CYAN = 6; 17 | final WHITE = 7; 18 | final DEFAULT = 9; 19 | } 20 | -------------------------------------------------------------------------------- /src/hx/strings/ansi/AnsiCursor.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.ansi; 7 | 8 | enum AnsiCursor { 9 | GoToHome; 10 | GoToPos(line:Int, column:Int); 11 | MoveUp(lines:Int); 12 | MoveDown(lines:Int); 13 | MoveRight(columns:Int); 14 | MoveLeft(columns:Int); 15 | SavePos; 16 | RestorePos; 17 | } 18 | -------------------------------------------------------------------------------- /src/hx/strings/ansi/AnsiTextAttribute.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.ansi; 7 | 8 | /** 9 | * https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes 10 | */ 11 | #if (haxe_ver < 4.3) @:enum #else enum #end 12 | abstract AnsiTextAttribute(Int) { 13 | 14 | /** 15 | * All colors/text-attributes off 16 | */ 17 | final RESET = 0; 18 | 19 | final INTENSITY_BOLD = 1; 20 | 21 | /** 22 | * Not widely supported. 23 | */ 24 | final INTENSITY_FAINT = 2; 25 | 26 | /** 27 | * Not widely supported. 28 | */ 29 | final ITALIC = 3; 30 | 31 | final UNDERLINE_SINGLE = 4; 32 | 33 | final BLINK_SLOW = 5; 34 | 35 | /** 36 | * Not widely supported. 37 | */ 38 | final BLINK_FAST = 6; 39 | 40 | final NEGATIVE = 7; 41 | 42 | /** 43 | * Not widely supported. 44 | */ 45 | final HIDDEN = 8; 46 | 47 | /** 48 | * Not widely supported. 49 | */ 50 | final STRIKETHROUGH = 9; 51 | 52 | /** 53 | * Not widely supported. 54 | */ 55 | final UNDERLINE_DOUBLE = 21; 56 | 57 | final INTENSITY_OFF = 22; 58 | 59 | final ITALIC_OFF = 23; 60 | 61 | final UNDERLINE_OFF = 24; 62 | 63 | final BLINK_OFF = 25; 64 | 65 | final NEGATIVE_OFF = 27; 66 | 67 | final HIDDEN_OFF = 28; 68 | 69 | final STRIKTHROUGH_OFF = 29; 70 | } 71 | -------------------------------------------------------------------------------- /src/hx/strings/ansi/AnsiWriter.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.ansi; 7 | 8 | import hx.strings.AnyAsString; 9 | 10 | class AnsiWriter { 11 | 12 | final _out:StringBuf_StringBuilder_or_Output; 13 | public var out(get, never):T; 14 | inline function get_out():T return _out.out; 15 | 16 | 17 | inline 18 | public function new(out:StringBuf_StringBuilder_or_Output) 19 | this._out = out; 20 | 21 | 22 | /** 23 | * sets the given text attribute 24 | */ 25 | inline 26 | public function attr(attr:AnsiTextAttribute) 27 | return write(Ansi.attr(attr)); 28 | 29 | 30 | /** 31 | * set the text background color 32 | */ 33 | inline 34 | public function bg(color:AnsiColor):AnsiWriter 35 | return write(Ansi.bg(color)); 36 | 37 | 38 | /** 39 | * Clears the screen and moves the cursor to the home position 40 | */ 41 | inline 42 | public function clearScreen():AnsiWriter 43 | return write(Ansi.clearScreen()); 44 | 45 | 46 | /** 47 | * Clear all characters from current position to the end of the line including the character at the current position 48 | */ 49 | inline 50 | public function clearLine():AnsiWriter 51 | return write(Ansi.clearLine()); 52 | 53 | 54 | inline 55 | public function cursor(cmd:AnsiCursor):AnsiWriter 56 | return write(Ansi.cursor(cmd)); 57 | 58 | 59 | /** 60 | * set the text foreground color 61 | */ 62 | inline 63 | public function fg(color:AnsiColor):AnsiWriter 64 | return write(Ansi.fg(color)); 65 | 66 | 67 | /** 68 | * flushes any buffered data 69 | */ 70 | inline 71 | public function flush():AnsiWriter { 72 | _out.flush(); 73 | return this; 74 | } 75 | 76 | 77 | inline 78 | public function write(str:AnyAsString):AnsiWriter { 79 | _out.write(str); 80 | return this; 81 | } 82 | } 83 | 84 | 85 | @:noCompletion 86 | @:noDoc @:dox(hide) 87 | @:forward 88 | abstract StringBuf_StringBuilder_or_Output(StringWriter) { 89 | 90 | inline 91 | function new(writer:StringWriter) 92 | this = writer; 93 | 94 | @:from inline static function fromStringBuilder (out:T) return new StringBuf_StringBuilder_or_Output(new StringBuilderStringWriter(out)); 95 | @:from inline static function fromStringBuf (out:T) return new StringBuf_StringBuilder_or_Output(new StringBufStringWriter(out)); 96 | @:from inline static function fromOutput (out:T) return new StringBuf_StringBuilder_or_Output(new OutputStringWriter(out)); 97 | } 98 | 99 | 100 | @:noDoc @:dox(hide) 101 | private interface StringWriter { 102 | public var out(default, null):T; 103 | public function flush():Void; 104 | public function write(str:String):Void; 105 | } 106 | 107 | 108 | @:noDoc @:dox(hide) 109 | private class OutputStringWriter implements StringWriter { 110 | 111 | public var out(default, null):T; 112 | 113 | inline 114 | public function new(out:T) 115 | this.out = out; 116 | 117 | public function flush() 118 | out.flush(); 119 | 120 | public function write(str:String) 121 | out.writeString(str); 122 | } 123 | 124 | 125 | @:noDoc @:dox(hide) 126 | private class StringBufStringWriter implements StringWriter { 127 | 128 | public var out(default, null):T; 129 | 130 | inline 131 | public function new(out:T) 132 | this.out = out; 133 | 134 | public function flush() {}; 135 | 136 | public function write(str:String) 137 | out.add(str); 138 | } 139 | 140 | 141 | @:noDoc @:dox(hide) 142 | private class StringBuilderStringWriter implements StringWriter { 143 | 144 | public var out(default, null):T; 145 | 146 | inline 147 | public function new(out:T) 148 | this.out = out; 149 | 150 | public function flush() {}; 151 | 152 | public function write(str:String) { 153 | #if (cs||java) 154 | cast(out, StringBuilder).add(str); 155 | #else 156 | out.add(str); 157 | #end 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/hx/strings/collection/OrderedStringMap.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.collection; 7 | 8 | import hx.strings.StringBuilder; 9 | 10 | /** 11 | * A map with String keys ordered by insertion. 12 | */ 13 | @:forward 14 | abstract OrderedStringMap(OrderedStringMapImpl) from OrderedStringMapImpl { 15 | 16 | inline 17 | public function new() 18 | this = new OrderedStringMapImpl(); 19 | 20 | 21 | @:to 22 | function __toStringMap():StringMap 23 | return cast this; 24 | 25 | 26 | @:arrayAccess 27 | @:noCompletion 28 | @:noDoc @:dox(hide) 29 | inline 30 | public function __arrayGet(key:String):Null 31 | return this.get(key); 32 | 33 | 34 | @:arrayAccess 35 | @:noCompletion 36 | @:noDoc @:dox(hide) 37 | inline 38 | public function __arrayWrite(key:String, value:V):V { 39 | this.set(key, value); 40 | return value; 41 | } 42 | } 43 | 44 | 45 | @:noDoc @:dox(hide) 46 | @:noCompletion 47 | class OrderedStringMapImpl implements haxe.Constraints.IMap { 48 | 49 | @:allow(hx.strings.collection.ValueIterator) 50 | var __keys:Array; 51 | final __map = new StringMap(); 52 | 53 | 54 | public var size(get, never):Int; 55 | inline 56 | private function get_size():Int 57 | return __keys.length; 58 | 59 | 60 | inline 61 | public function new() 62 | clear(); 63 | 64 | 65 | /** 66 | *

 67 |     * >>> ({var m = new OrderedStringMap(); m.set("1", 1); m.clear(); m; }).isEmpty() == true
 68 |     * 
69 | */ 70 | inline 71 | public function clear():Void { 72 | __keys = new Array(); 73 | __map.clear(); 74 | } 75 | 76 | 77 | /** 78 | *

 79 |     * >>> new OrderedStringMap().copy() != null
 80 |     * 
81 | */ 82 | inline 83 | public function copy():OrderedStringMapImpl { 84 | final clone = new OrderedStringMapImpl(); 85 | for (k => v in this) 86 | clone.set(k, v); 87 | return clone; 88 | } 89 | 90 | 91 | inline 92 | public function exists(key:String):Bool 93 | return __map.exists(key); 94 | 95 | 96 | /** 97 | *

 98 |     * >>> ({var m = new OrderedStringMap(); m.set("1", 10); m["1"]; }) == 10
 99 |     * 
100 | */ 101 | inline 102 | public function get(key:String):Null 103 | return __map.get(key); 104 | 105 | 106 | /** 107 | *

108 |     * >>> new OrderedStringMap().isEmpty() == true
109 |     * >>> ({var m = new OrderedStringMap(); m.set("1", 1); m; }).isEmpty() == false
110 |     * 
111 | */ 112 | inline 113 | public function isEmpty():Bool 114 | return !this.iterator().hasNext(); 115 | 116 | 117 | inline 118 | public function iterator():Iterator 119 | return new ValueIterator(this); 120 | 121 | 122 | inline 123 | public function keys():Iterator 124 | return __keys.iterator(); 125 | 126 | 127 | inline 128 | public function keyValueIterator():KeyValueIterator 129 | return new haxe.iterators.MapKeyValueIterator(this); 130 | 131 | 132 | public function remove(key:String):Bool { 133 | if (__map.remove(key)) { 134 | __keys.remove(key); 135 | return true; 136 | } 137 | return false; 138 | } 139 | 140 | 141 | /** 142 | * Sets the value for the given key. Does not change the position of the key in case it existed already. 143 | */ 144 | public function set(key:String, value:V):Void { 145 | final isNew = !__map.exists(key); 146 | __map.set(key, value); 147 | if (isNew) 148 | __keys.push(key); 149 | } 150 | 151 | 152 | public function toString() : String { 153 | final sb = new StringBuilder("{"); 154 | var isFirst = true; 155 | for(key => v in this) { 156 | if(isFirst) 157 | isFirst = false; 158 | else 159 | sb.add(", "); 160 | sb.add(key).add(" => ").add(v); 161 | } 162 | sb.add("}"); 163 | return sb.toString(); 164 | } 165 | } 166 | 167 | 168 | private class ValueIterator { 169 | 170 | final map:OrderedStringMap; 171 | var pos = -1; 172 | 173 | 174 | inline 175 | public function new(map:OrderedStringMap) 176 | this.map = map; 177 | 178 | 179 | inline 180 | public function hasNext():Bool 181 | return pos + 1 < map.__keys.length; 182 | 183 | 184 | inline 185 | public function next():V 186 | return cast map.get(map.__keys[++pos]); 187 | } 188 | -------------------------------------------------------------------------------- /src/hx/strings/collection/OrderedStringSet.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.collection; 7 | 8 | import haxe.ds.StringMap; 9 | 10 | import hx.strings.internal.Either2; 11 | 12 | /** 13 | * hx.strings.collection.OrderedStringMap backed set implementation that maintains insertion order. 14 | * 15 | *

16 |  * >>> new OrderedStringSet(["", "c", "a", "b", "a"]).toArray()  ==  [ "", "c", "a", "b" ]
17 |  * >>> new OrderedStringSet(["", "c", "a", "b", "a"]).toString() == '[ "", "c", "a", "b" ]'
18 |  * 
19 | */ 20 | class OrderedStringSet extends StringSet { 21 | 22 | 23 | inline 24 | public function new(?initialItems:Either2>) 25 | super(initialItems); 26 | 27 | 28 | override 29 | function _initMap():Void 30 | map = new OrderedStringMap(); 31 | } 32 | -------------------------------------------------------------------------------- /src/hx/strings/collection/SortedStringMap.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.collection; 7 | 8 | import haxe.ds.BalancedTree; 9 | 10 | import hx.strings.Strings; 11 | 12 | /** 13 | * A map with sorted String keys. 14 | */ 15 | @:forward 16 | abstract SortedStringMap(SortedStringMapImpl) from SortedStringMapImpl { 17 | 18 | inline 19 | public function new(?comparator:String -> String -> Int) 20 | this = new SortedStringMapImpl(comparator); 21 | 22 | 23 | @:to 24 | function __toStringMap():StringMap 25 | return cast this; 26 | 27 | 28 | @:arrayAccess 29 | @:noCompletion 30 | @:noDoc @:dox(hide) 31 | public inline function __arrayGet(key:String) 32 | return this.get(key); 33 | 34 | 35 | @:arrayAccess 36 | @:noCompletion 37 | @:noDoc @:dox(hide) 38 | inline 39 | public function __arrayWrite(key:String, value:V):V { 40 | this.set(key, value); 41 | return value; 42 | } 43 | } 44 | 45 | 46 | /** 47 | *

 48 |  * >>> ({var m = new SortedStringMap(); m.set("1", 1); m.clear(); m; }).isEmpty() == true
 49 |  * 
50 | */ 51 | @:noDoc @:dox(hide) 52 | @:noCompletion 53 | class SortedStringMapImpl extends BalancedTree implements haxe.Constraints.IMap { 54 | 55 | final cmp:String -> String -> Int; 56 | 57 | 58 | /** 59 | * IMPORTANT: There is currently no native support for getting the size of a map, 60 | * therefore this is emulated for now by using an iterator - which impacts performance. 61 | * 62 | *

 63 |     * >>> new SortedStringMap().size == 0
 64 |     * >>> ({var m = new SortedStringMap(); m.set("1", 1); m.set("2", 1); m; }).size == 2
 65 |     * 
66 | */ 67 | public var size(get, never):Int; 68 | inline 69 | function get_size():Int { 70 | var count = 0; 71 | final it = this.keys(); 72 | while (it.hasNext()) { 73 | it.next(); 74 | count++; 75 | } 76 | return count; 77 | } 78 | 79 | 80 | /** 81 | * @param comparator used for sorting the String keys. Default is the UTF8 supporting Strings#compare() method 82 | */ 83 | public function new(?comparator:(String, String) -> Int) { 84 | super(); 85 | if (comparator == null) 86 | #if jvm 87 | // TODO workaround for java.lang.IllegalAccessError: no such method: hx.strings.Strings.compare(String,String)int/invokeStatic 88 | this.cmp = (s1, s2) -> Strings.compare(s1,s2); 89 | #else 90 | this.cmp = Strings.compare; 91 | #end 92 | else 93 | this.cmp = comparator; 94 | 95 | } 96 | 97 | 98 | @:arrayAccess 99 | @:noCompletion 100 | @:noDoc @:dox(hide) 101 | inline 102 | public function __arrayWrite(k:String, v:V):V { 103 | this.set(k, v); 104 | return v; 105 | } 106 | 107 | 108 | /** 109 | *

110 |     * >>> new SortedStringMap().copy() != null
111 |     * 
112 | */ 113 | override 114 | public function copy():SortedStringMapImpl { 115 | final clone = new SortedStringMapImpl(); 116 | for (k => v in this) 117 | clone.set(k, v); 118 | return clone; 119 | } 120 | 121 | 122 | inline 123 | override 124 | function compare(s1:String, s2:String):Int 125 | return cmp(s1, s2); 126 | 127 | 128 | /** 129 | *

130 |     * >>> ({var m = new SortedStringMap(); m.set("1", 10); m["1"]; }) == 10
131 |     * 
132 | */ 133 | @:arrayAccess 134 | override 135 | public function get(key:String):Null 136 | return super.get(key); 137 | 138 | 139 | /** 140 | *

141 |     * >>> new SortedStringMap().isEmpty() == true
142 |     * >>> ({var m = new SortedStringMap(); m.set("1", 1); m; }).isEmpty() == false
143 |     * 
144 | */ 145 | inline 146 | public function isEmpty():Bool 147 | return !this.iterator().hasNext(); 148 | 149 | 150 | /** 151 | * Copies all key-value pairs from the source map into this map. 152 | * 153 | * @param replace if true existing key-value pairs are replaced otherwise they will be skipped 154 | * @return the number of copied key-value pairs 155 | */ 156 | inline 157 | public function setAll(source:StringMap, replace:Bool = true):Int { 158 | final m:StringMap = this; 159 | return m.setAll(source, replace); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/hx/strings/collection/SortedStringSet.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.collection; 7 | 8 | /** 9 | * hx.strings.collection.SortedStringMap backed sorted set implementation. 10 | * 11 | *

12 |  * >>> new SortedStringSet(["", "c", "a", "b", "a"]).toArray()  ==  [ "", "a", "b", "c" ]
13 |  * >>> new SortedStringSet(["", "c", "a", "b", "a"]).toString() == '[ "", "a", "b", "c" ]'
14 |  * 
15 | */ 16 | class SortedStringSet extends StringSet { 17 | 18 | final cmp:Null String -> Int>; 19 | 20 | 21 | public function new(?initialItems:Array, ?comparator:String -> String -> Int) { 22 | cmp = comparator; 23 | 24 | @:nullSafety(Off) // TODO no idea why null-safety check fails 25 | super(initialItems); 26 | } 27 | 28 | 29 | override 30 | function _initMap():Void 31 | map = new SortedStringMap(cmp); 32 | } 33 | -------------------------------------------------------------------------------- /src/hx/strings/collection/StringArray.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.collection; 7 | 8 | import hx.strings.internal.Either2; 9 | 10 | /** 11 | * Abstract of Array with additional functionality. 12 | * 13 | *

 14 |  * >>> new StringArray().length == 0
 15 |  * >>> ({var a:StringArray = ["a", "b"]; a;}).length == 2
 16 |  * 
17 | */ 18 | @:forward 19 | abstract StringArray(Array) from Array to Array { 20 | 21 | inline 22 | public function new(?initialItems:Either2>) { 23 | this = []; 24 | if (initialItems != null) 25 | pushAll(initialItems); 26 | } 27 | 28 | /** 29 | * the first element of the array or null if empty 30 | * 31 | *

 32 |     * >>> new StringArray(["a", "b"]).first == "a"
 33 |     * 
34 | */ 35 | public var first(get, never):Null; 36 | inline 37 | function get_first():Null 38 | return isEmpty() ? null : this[0]; 39 | 40 | 41 | /** 42 | * the last element of the array or null if empty 43 | * 44 | *

 45 |     * >>> new StringArray(["a", "b"]).last == "b"
 46 |     * 
47 | */ 48 | public var last(get, never):Null; 49 | inline 50 | function get_last():Null 51 | return isEmpty() ? null : this[this.length - 1]; 52 | 53 | 54 | /** 55 | *

 56 |     * >>> new StringArray(["a", "b"]).contains("b") == true
 57 |     * >>> new StringArray(["a", "b"]).contains("c") == false
 58 |     * 
59 | */ 60 | public function contains(str:String):Bool 61 | return this.indexOf(str) > -1; 62 | 63 | 64 | /** 65 | *

 66 |     * >>> new StringArray().isEmpty() == true
 67 |     * >>> ({var a:StringArray = ["a", "b"]; a.isEmpty();})  == false
 68 |     * 
69 | */ 70 | inline 71 | public function isEmpty():Bool 72 | return this.length == 0; 73 | 74 | 75 | /** 76 | *

 77 |     * >>> ({var a:StringArray = ["a", "b"]; a.clear(); a;}).length == 0
 78 |     * 
79 | */ 80 | inline 81 | public function clear():Void { 82 | while (this.length > 0) 83 | this.pop(); 84 | } 85 | 86 | 87 | public function pushAll(items:Either2>):Void { 88 | if (items == null) 89 | throw "[items] must not be null!"; 90 | 91 | switch(items.value) { 92 | case a(set): 93 | for (str in set) 94 | this.push(str); 95 | 96 | case b(array): 97 | for (str in array) 98 | this.push(str); 99 | } 100 | } 101 | 102 | 103 | /** 104 | *

105 |     * >>> ({var a:StringArray = ["b", "a"]; a.sortAscending(); a;}) == ["a", "b"]
106 |     * 
107 | */ 108 | inline 109 | public function sortAscending():Void 110 | this.sort(Strings.compare); 111 | 112 | 113 | /** 114 | *

115 |     * >>> ({var a:StringArray = ["a", "b"]; a.sortDescending(); a;}) == ["b", "a"]
116 |     * 
117 | */ 118 | inline 119 | public function sortDescending():Void 120 | this.sort((s1, s2) -> -1 * Strings.compare(s1, s2)); 121 | } 122 | -------------------------------------------------------------------------------- /src/hx/strings/collection/StringMap.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.collection; 7 | 8 | import hx.strings.internal.Macros; 9 | 10 | /** 11 | * Abstract on haxe.Constraints.IMap[String, V] 12 | */ 13 | @:forward 14 | abstract StringMap(haxe.Constraints.IMap) from haxe.Constraints.IMap to haxe.Constraints.IMap { 15 | 16 | inline 17 | public function new() 18 | this = new Map(); 19 | 20 | 21 | @:to 22 | function __toMap():Map 23 | return cast this; 24 | 25 | 26 | @:arrayAccess 27 | @:noCompletion 28 | @:noDoc @:dox(hide) 29 | inline 30 | public function __arrayGet(key:String):Null 31 | return this.get(key); 32 | 33 | 34 | @:arrayAccess 35 | @:noCompletion 36 | @:noDoc @:dox(hide) 37 | inline 38 | public function __arrayWrite(k:String, v:V):V { 39 | this.set(k, v); 40 | return v; 41 | } 42 | 43 | /** 44 | * IMPORTANT: There is currently no native support for getting the size of a map, 45 | * therefore this is emulated for now by using an iterator - which impacts performance. 46 | * 47 | *

 48 |     * >>> new StringMap().size == 0
 49 |     * >>> ({var m = new StringMap(); m.set("1", 1); m.set("2", 1); m;}) == ["1" => 1, "2" => 1]
 50 |     * 
51 | */ 52 | public var size(get, never):Int; 53 | inline 54 | function get_size():Int { 55 | var count = 0; 56 | final it = this.keys(); 57 | while (it.hasNext()) { 58 | it.next(); 59 | count++; 60 | } 61 | return count; 62 | } 63 | 64 | /** 65 | *

 66 |     * >>> new StringMap().copy() != null
 67 |     * 
68 | */ 69 | public function copy():StringMap { 70 | if (Std.isOfType(this, SortedStringMap.SortedStringMapImpl)) { 71 | final m:SortedStringMap = cast this; 72 | return m.copy(); 73 | } 74 | 75 | if (Std.isOfType(this, OrderedStringMap.OrderedStringMapImpl)) { 76 | final m:OrderedStringMap = cast this; 77 | return m.copy(); 78 | } 79 | 80 | final clone:StringMap = new StringMap(); 81 | for (k => v in this) 82 | clone.set(k, v); 83 | return clone; 84 | } 85 | 86 | 87 | /** 88 | *

 89 |     * >>> new StringMap().isEmpty() == true
 90 |     * >>> ({var m = new StringMap(); m.set("1", 1); m; }).isEmpty() == false
 91 |     * 
92 | */ 93 | inline 94 | public function isEmpty():Bool 95 | return !this.iterator().hasNext(); 96 | 97 | 98 | /** 99 | * Copies all key-value pairs from the source map into this map. 100 | * 101 | *

102 |     * >>> ({var m = new StringMap(); m.setAll(["1" => 1, "2" => 1]); m;}) == ["1" => 1, "2" => 1]
103 |     * >>> new StringMap().setAll(null) throws "[items] must not be null!"
104 |     * 
105 | * 106 | * @param replace if true existing key-value pairs are replaced otherwise they will be skipped 107 | * @return the number of copied key-value pairs 108 | */ 109 | public function setAll(items:StringMap, replace:Bool = true):Int { 110 | if (items == null) 111 | throw "[items] must not be null!"; 112 | 113 | var count = 0; 114 | if(replace) { 115 | for (k => v in items) { 116 | this.set(k, v); 117 | count++; 118 | } 119 | } else { 120 | for (k => v in items) { 121 | if(!this.exists(k)) { 122 | this.set(k, v); 123 | count++; 124 | } 125 | } 126 | } 127 | return count; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/hx/strings/collection/StringSet.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.collection; 7 | 8 | import hx.strings.AnyAsString; 9 | import hx.strings.collection.StringMap; 10 | import hx.strings.internal.Either2; 11 | 12 | 13 | /** 14 | * haxe.ds.StringMap backed set implementation. 15 | * 16 | * Each added string is guaranteed to only be present once in the collection. 17 | */ 18 | class StringSet { 19 | 20 | private static final EMPTY_MAP = new StringMap(); 21 | 22 | var map:StringMap = EMPTY_MAP; 23 | 24 | 25 | /** 26 | *

 27 |     * >>> new StringSet().size                     == 0
 28 |     * >>> new StringSet(["a", "b", "c", "a"]).size == 3
 29 |     * 
30 | */ 31 | public var size(default, null):Int = 0; 32 | 33 | 34 | inline 35 | public function new(?initialItems:Either2>) { 36 | _initMap(); 37 | 38 | if (initialItems != null) 39 | addAll(initialItems); 40 | } 41 | 42 | 43 | function _initMap():Void 44 | map = new StringMap(); 45 | 46 | 47 | /** 48 | *

 49 |     * >>> new StringSet(["", "a", "b"]).add("")   == false
 50 |     * >>> new StringSet(["", "a", "b"]).add("a")  == false
 51 |     * >>> new StringSet(["", "a", "b"]).add("c")  == true
 52 |     * >>> new StringSet(["", "a", "b"]).add(null) throws "[item] must not be null!"
 53 |     * 
54 | * @return true if item was added, false if it was already present 55 | */ 56 | public function add(item:AnyAsString):Bool { 57 | if (item == null) 58 | throw "[item] must not be null!"; 59 | 60 | if (contains(item)) 61 | return false; 62 | 63 | map.set(item, true); 64 | size++; 65 | return true; 66 | } 67 | 68 | 69 | /** 70 | *

 71 |     * >>> new StringSet(["", "a", "b"]).addAll(["a", "b"]) == 0
 72 |     * >>> new StringSet(["", "a", "b"]).addAll(["a", "c"]) == 1
 73 |     * >>> new StringSet(["", "a", "b"]).addAll(["c", "d"]) == 2
 74 |     * >>> new StringSet(["", "a", "b"]).addAll(null) throws "[items] must not be null!"
 75 |     * 
76 | * 77 | * @return number of added items 78 | */ 79 | public function addAll(items:Either2>):Int { 80 | if (items == null) 81 | throw "[items] must not be null!"; 82 | 83 | var count = 0; 84 | switch(items.value) { 85 | case a(set): 86 | if (set == null) 87 | return 0; 88 | for (str in set) { 89 | if (str != null && add(str)) 90 | count++; 91 | } 92 | 93 | case b(array): 94 | if (array == null) 95 | return 0; 96 | for (str in array) { 97 | if (str != null && add(str)) 98 | count++; 99 | } 100 | } 101 | return count; 102 | } 103 | 104 | 105 | /** 106 | * Empties the set. 107 | */ 108 | inline 109 | public function clear():Void 110 | map.clear(); 111 | 112 | 113 | /** 114 | *

115 |     * >>> new StringSet(["", "a", "b"]).contains("")   == true
116 |     * >>> new StringSet(["", "a", "b"]).contains("a")  == true
117 |     * >>> new StringSet(["", "a", "b"]).contains("c")  == false
118 |     * >>> new StringSet(["", "a", "b"]).contains(null) == false
119 |     * 
120 | * 121 | * @return true if the item is present 122 | */ 123 | inline 124 | public function contains(item:Null):Bool 125 | return item == null ? false : map.exists(item); 126 | 127 | 128 | /** 129 | *

130 |     * >>> new StringSet(          ).isEmpty() == true
131 |     * >>> new StringSet([]        ).isEmpty() == true
132 |     * >>> new StringSet(["a", "b"]).isEmpty() == false
133 |     * 
134 | */ 135 | inline 136 | public function isEmpty():Bool 137 | return size == 0; 138 | 139 | 140 | /** 141 | * @return an Iterator over the items. No particular order is guaranteed. 142 | */ 143 | inline 144 | public function iterator():Iterator 145 | return map.keys(); 146 | 147 | 148 | /** 149 | *

150 |     * >>> new StringSet(["", "a", "b"]).remove("")   == true
151 |     * >>> new StringSet(["", "a", "b"]).remove("a")  == true
152 |     * >>> new StringSet(["", "a", "b"]).remove("c")  == false
153 |     * >>> new StringSet(["", "a", "b"]).remove(null) == false
154 |     * 
155 | * 156 | * @return true if the item was removed, false if it was not present 157 | */ 158 | public function remove(item:Null):Bool { 159 | if (item == null) 160 | return false; 161 | 162 | if (map.remove(item)) { 163 | size--; 164 | return true; 165 | } 166 | return false; 167 | } 168 | 169 | 170 | /** 171 | *

172 |     * >>> new StringSet([]).toArray()     == [ ]
173 |     * >>> new StringSet([null]).toArray() == [ ]
174 |     * >>> new StringSet([""]).toArray()   == [ "" ]
175 |     * >>> new StringSet(["a"]).toArray()  == [ "a" ]
176 |     * 
177 | */ 178 | public function toArray():StringArray 179 | return [ for (k in map.keys()) k ]; 180 | 181 | 182 | /** 183 | *

184 |     * >>> new StringSet([]).toString()     == "[]"
185 |     * >>> new StringSet([null]).toString() == "[]"
186 |     * >>> new StringSet([""]).toString()   == '[ "" ]'
187 |     * >>> new StringSet(["b"]).toString()  == '[ "b" ]'
188 |     * 
189 | */ 190 | public function toString():String { 191 | if (size == 0) return "[]"; 192 | return '[ "' + toArray().join('", "') + '" ]'; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/hx/strings/internal/Arrays.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.internal; 7 | 8 | /** 9 | * IMPORTANT: This class it not part of the API. Direct usage is discouraged. 10 | */ 11 | @:noDoc @:dox(hide) 12 | @:noCompletion 13 | class Arrays { 14 | 15 | /** 16 | *

17 |     * >>> Arrays.first(["1", "2"]) == "1"
18 |     * >>> Arrays.first([])         == null
19 |     * >>> Arrays.first(null)       == null
20 |     * 
21 | */ 22 | #if !hl 23 | // see https://github.com/HaxeFoundation/haxe/issues/6071 24 | inline 25 | #end 26 | public static function first(items:Array):Null 27 | return (items == null || items.length == 0) ? null : items[0]; 28 | 29 | 30 | /** 31 | *

32 |     * >>> Arrays.unique(["1", "1", "2"]) == ["1", "2"]
33 |     * 
34 | */ 35 | public static function unique(items:Array):Array { 36 | final filtered = new Array(); 37 | 38 | for (i in items) 39 | if (filtered.indexOf(i) == -1) filtered.push(i); 40 | return filtered; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/hx/strings/internal/Bits.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.internal; 7 | 8 | /** 9 | * IMPORTANT: This class it not part of the API. Direct usage is discouraged. 10 | */ 11 | class Bits { 12 | 13 | /** 14 | *

15 |     * >>> Bits.clearBit(5, 1) == 4
16 |     * >>> Bits.clearBit(4, 1) == 4
17 |     * 
18 | */ 19 | inline 20 | public static function clearBit(num:Int, bitPos:Int):Int 21 | return num & ~(1 << (bitPos - 1)); 22 | 23 | 24 | /** 25 | *

26 |     * >>> Bits.setBit(5, 2) == 7
27 |     * >>> Bits.setBit(7, 2) == 7
28 |     * 
29 | */ 30 | inline 31 | public static function setBit(num:Int, bitPos:Int):Int 32 | return num | 1 << (bitPos -1); 33 | 34 | 35 | /** 36 | *

37 |     * >>> Bits.toggleBit(5, 1) == 4
38 |     * >>> Bits.toggleBit(4, 1) == 5
39 |     * 
40 | */ 41 | inline 42 | public static function toggleBit(num:Int, bitPos:Int):Int 43 | return num ^ 1 << (bitPos - 1); 44 | 45 | 46 | /** 47 | *

48 |     * >>> Bits.getBit(5, 1) == true
49 |     * >>> Bits.getBit(5, 2) == false
50 |     * >>> Bits.getBit(5, 3) == true
51 |     * >>> Bits.getBit(5, 4) == false
52 |     * 
53 | */ 54 | inline 55 | public static function getBit(num:Int, bitPos:Int):Bool 56 | return 1 == ((num >> (bitPos - 1)) & 1); 57 | } 58 | -------------------------------------------------------------------------------- /src/hx/strings/internal/Either2.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.internal; 7 | 8 | /** 9 | * IMPORTANT: This class it not part of the API. Direct usage is discouraged. 10 | */ 11 | @:noDoc @:dox(hide) 12 | @:noCompletion 13 | abstract Either2(_Either2) { 14 | 15 | inline 16 | public function new(value:_Either2) 17 | this = value; 18 | 19 | 20 | public var value(get,never):_Either2; 21 | inline 22 | function get_value():_Either2 23 | return this; 24 | 25 | 26 | @:from 27 | inline 28 | static function fromA(value:A):Either2 29 | return new Either2(a(value)); 30 | 31 | 32 | @:from 33 | inline 34 | static function fromB(value:B):Either2 35 | return new Either2(b(value)); 36 | } 37 | 38 | @:noDoc @:dox(hide) 39 | private enum _Either2 { 40 | a(v:A); 41 | b(v:B); 42 | } 43 | -------------------------------------------------------------------------------- /src/hx/strings/internal/Either3.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.internal; 7 | 8 | /** 9 | * IMPORTANT: This class it not part of the API. Direct usage is discouraged. 10 | */ 11 | @:noDoc @:dox(hide) 12 | @:noCompletion 13 | abstract Either3(_Either3) { 14 | 15 | inline 16 | public function new(value:_Either3) 17 | this = value; 18 | 19 | 20 | public var value(get,never):_Either3; 21 | inline 22 | function get_value():_Either3 23 | return this; 24 | 25 | 26 | @:from 27 | inline 28 | static function fromA(value:A):Either3 29 | return new Either3(a(value)); 30 | 31 | 32 | @:from 33 | inline 34 | static function fromB(value:B):Either3 35 | return new Either3(b(value)); 36 | 37 | 38 | @:from 39 | inline 40 | static function fromC(value:C):Either3 41 | return new Either3(c(value)); 42 | } 43 | 44 | 45 | @:noDoc @:dox(hide) 46 | private enum _Either3 { 47 | a(v:A); 48 | b(v:B); 49 | c(v:C); 50 | } 51 | -------------------------------------------------------------------------------- /src/hx/strings/internal/Exception.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.internal; 7 | 8 | import haxe.CallStack; 9 | 10 | class Exception { 11 | 12 | static inline final INDENT = #if java "\t" #else " " #end; 13 | 14 | inline 15 | public static function capture(ex:Dynamic) { 16 | return new Exception(ex); 17 | } 18 | 19 | 20 | public final cause:Dynamic; 21 | public final causeStackTrace:Array; 22 | 23 | 24 | function new(cause:Dynamic) { 25 | this.cause = cause; 26 | this.causeStackTrace = CallStack.exceptionStack(); 27 | } 28 | 29 | 30 | #if (!python) inline #end 31 | public function rethrow() { 32 | #if neko 33 | neko.Lib.rethrow(cause); // Neko has proper support 34 | #elseif python 35 | //python.Syntax.pythonCode("raise"); // rethrows the last but not necessarily the captured exception 36 | python.Syntax.code('raise Exception(self.toString()) from None'); 37 | #else 38 | // cpp.Lib.rethrow(cause); // swallows stacktrace 39 | // cs.Lib.rethrow(this); // throw/rethrow swallows complete stacktrace 40 | // js.Lib.rethrow(); // rethrows the last but not necessarily the captured exception 41 | // php.Lib.rethrow(cause); // swallows stacktrace 42 | throw this.toString(); 43 | #end 44 | } 45 | 46 | 47 | #if python @:keep #end 48 | public function toString():String { 49 | final sb = new StringBuf(); 50 | sb.add("rethrown exception:\n"); 51 | sb.add(INDENT); sb.add("--------------------\n"); 52 | sb.add(INDENT); sb.add("| Exception : "); sb.add(cause); sb.add("\n"); 53 | #if lua @:nullSafety(Off) #end 54 | for (item in CallStack.toString(causeStackTrace).split("\n")) { 55 | if (item == "") continue; 56 | sb.add(INDENT); 57 | sb.add(StringTools.replace(item, "Called from", "| at")); 58 | sb.add("\n"); 59 | } 60 | sb.add(INDENT); sb.add("--------------------"); 61 | return sb.toString(); 62 | } 63 | } -------------------------------------------------------------------------------- /src/hx/strings/internal/Macros.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.internal; 7 | 8 | import haxe.macro.*; 9 | 10 | /** 11 | * IMPORTANT: This class it not part of the API. Direct usage is discouraged. 12 | */ 13 | @:noDoc @:dox(hide) 14 | @:noCompletion 15 | class Macros { 16 | 17 | static var __static_init = { 18 | #if (haxe_ver < 4.2) 19 | throw 'ERROR: Haxe 4.2.x or higher is required!'; 20 | #end 21 | 22 | #if (php && !php7) 23 | throw 'ERROR: For PHP the php7 target is required!'; 24 | #end 25 | }; 26 | 27 | 28 | macro 29 | public static function addDefines() { 30 | final def = Context.getDefines(); 31 | 32 | if (def.exists("java") && !def.exists("jvm")) { 33 | trace("[INFO] Setting compiler define 'java_src'."); 34 | Compiler.define("java_src"); 35 | } 36 | return macro {} 37 | } 38 | 39 | 40 | macro 41 | public static function configureNullSafety() { 42 | haxe.macro.Compiler.nullSafety("hx.strings", StrictThreaded); 43 | return macro {} 44 | } 45 | 46 | 47 | /** 48 | * Embeds the given file as resource. 49 | * 50 | * Using addResource("com/example/resources/items.txt", "Items"); 51 | * is the same as specifying the Haxe option -resource com/example/resources/items.txt@Items 52 | * 53 | * See http://haxe.org/manual/cr-resources.html for more details about embedding resources. 54 | */ 55 | macro 56 | public static function addResource(filePath:String, ?resourceName:String):Expr { 57 | if (resourceName == null || resourceName.length == 0) 58 | resourceName = filePath; 59 | 60 | if (!sys.FileSystem.exists(filePath)) { 61 | for (cp in Context.getClassPath()) { 62 | final path = cp + filePath; 63 | if (sys.FileSystem.exists(path)) { 64 | filePath = path; 65 | break; 66 | } 67 | } 68 | } 69 | trace('[INFO] Embedding file [$filePath] as resource with name [$resourceName]...'); 70 | Context.addResource(resourceName, sys.io.File.getBytes(filePath)); 71 | 72 | // return a no-op expression 73 | return macro {}; 74 | } 75 | 76 | 77 | /** 78 | * Implements assignment destructuring, see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment 79 | * 80 | * Usage: 81 | *

 82 |     * Macros.unpack([a,b,c] = ["1", "2", "3"]);
 83 |     * trace(a);
 84 |     *
 85 |     * // or
 86 |     * var list = ["1", "2", "3"];
 87 |     * Macros.unpack([a,b,c] = list);
 88 |     * trace(a);
 89 |     *
 90 |     * // or
 91 |     * Macros.unpack([prefix,value] = str.split(":"));
 92 |     * trace(prefix);
 93 |     * 
94 | */ 95 | macro 96 | public static function unpack(e:Expr) { 97 | final assignments = new Array(); 98 | switch(e.expr) { 99 | case EBinop(OpAssign, varsExpr, valuesExpr): 100 | final varNames = new Array(); 101 | switch varsExpr { 102 | case macro $a{varDecls}: 103 | for (varDecl in varDecls) { 104 | switch(varDecl) { 105 | case macro $i{varName}: 106 | varNames.push(varName); 107 | default: 108 | Context.error("Invalid variable name.", varDecl.pos); 109 | } 110 | } 111 | default: 112 | Context.error("Array of variable names expected.", varsExpr.pos); 113 | } 114 | 115 | var idx = -1; 116 | switch (valuesExpr.expr) { 117 | case ECall(_): 118 | assignments.push(macro @:mergeBlock { 119 | final __unpack_return_values = $valuesExpr; 120 | }); 121 | for (varName in varNames) { 122 | idx++; 123 | assignments.push(macro @:mergeBlock { 124 | var $varName = __unpack_return_values[$v{idx}]; 125 | }); 126 | }; 127 | 128 | case EArrayDecl(values): 129 | for (varName in varNames) { 130 | final value = values[++idx]; 131 | assignments.push(macro @:mergeBlock { 132 | final $varName=${value}; 133 | }); 134 | }; 135 | 136 | case EConst(CIdent(refName)): 137 | for (varName in varNames) { 138 | idx++; 139 | assignments.push(macro @:mergeBlock { 140 | final $varName = $i{refName}[$v{idx}]; 141 | }); 142 | }; 143 | 144 | default: 145 | Context.error("Expected a variable reference, an array or a function call.", valuesExpr.pos); 146 | } 147 | 148 | default: 149 | Context.error("Assignment operator = is missing!", e.pos); 150 | } 151 | 152 | return macro @:mergeBlock $b{assignments}; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/hx/strings/internal/OS.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.internal; 7 | 8 | /** 9 | * IMPORTANT: This class it not part of the API. Direct usage is discouraged. 10 | */ 11 | @:noDoc @:dox(hide) 12 | @:noCompletion 13 | class OS { 14 | 15 | #if js 16 | static final isNodeJS:Bool = js.Syntax.code("(typeof process !== 'undefined') && (typeof process.release !== 'undefined') && (process.release.name === 'node')"); 17 | #end 18 | 19 | public static var isWindows(default, never):Bool = { 20 | final os:String = 21 | #if flash 22 | flash.system.Capabilities.os 23 | #elseif js 24 | isNodeJS ? js.Syntax.code("process.platform") : js.Browser.navigator.platform 25 | #else 26 | Sys.systemName() 27 | #end; 28 | #if python @:nullSafety(Off) #end // TODO https://github.com/HaxeFoundation/haxe/issues/10273 29 | ~/win/i.match(os); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/hx/strings/internal/OneOrMany.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.internal; 7 | 8 | /** 9 | * IMPORTANT: This class it not part of the API. Direct usage is discouraged. 10 | */ 11 | @:noDoc @:dox(hide) 12 | @:noCompletion 13 | @:forward 14 | abstract OneOrMany(Array) from Array to Array { 15 | 16 | @:from 17 | inline 18 | static function fromSingle(value:T):OneOrMany 19 | return [value]; 20 | } 21 | -------------------------------------------------------------------------------- /src/hx/strings/internal/RingBuffer.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.internal; 7 | 8 | /** 9 | * IMPORTANT: This class it not part of the API. Direct usage is discouraged. 10 | * 11 | *

 12 |  * >>> ({var b = new RingBuffer(2); b.add("a"); b.add("b"); b.add("c"); b; }).toArray() == [ "b", "c" ]
 13 |  * 
14 | */ 15 | @:forward 16 | abstract RingBuffer(RingBufferImpl) { 17 | 18 | inline 19 | public function new(size:Int) 20 | this = new RingBufferImpl(size); 21 | 22 | 23 | @:arrayAccess 24 | inline 25 | public function get(index:Int):V 26 | return this.get(index); 27 | } 28 | 29 | 30 | @:noDoc @:dox(hide) 31 | @:noCompletion 32 | private class RingBufferImpl { 33 | 34 | #if flash 35 | // using Array instead of Vector as workaround for https://github.com/HaxeFoundation/haxe/issues/6529 36 | final buffer:Array = []; 37 | #else 38 | final buffer:haxe.ds.Vector; 39 | #end 40 | var bufferStartIdx = 0; 41 | var bufferEndIdx = -1; 42 | var bufferMaxIdx:Int; 43 | 44 | 45 | public var length(default, null):Int = 0; 46 | public final size:Int; 47 | 48 | #if hl @:nullSafety(Off) #end // TODO https://github.com/HaxeFoundation/haxe/issues/10312 49 | #if python @:nullSafety(Off) #end // TODO 50 | public function new(size:Int) { 51 | if (size < 1) 52 | throw "[size] must be > 0"; 53 | 54 | #if !flash 55 | buffer = new haxe.ds.Vector(size); 56 | #end 57 | this.size = size; 58 | bufferMaxIdx = size - 1; 59 | } 60 | 61 | 62 | public function add(item:V) { 63 | if (length == size) { 64 | bufferEndIdx = bufferStartIdx; 65 | bufferStartIdx++; 66 | if (bufferStartIdx > bufferMaxIdx) 67 | bufferStartIdx = 0; 68 | } else { 69 | bufferEndIdx++; 70 | length++; 71 | } 72 | buffer[bufferEndIdx] = item; 73 | } 74 | 75 | 76 | public function get(index:Int):V { 77 | if (index < 0 || index > bufferMaxIdx) 78 | throw '[index] $index is out of bound'; 79 | 80 | var realIdx = bufferStartIdx + index; 81 | if (realIdx > bufferMaxIdx) 82 | realIdx -= length; 83 | #if php @:nullSafety(Off) #end // TODO 84 | return buffer[realIdx]; 85 | } 86 | 87 | 88 | public function iterator(): Iterator 89 | return new RingBufferIterator(this); 90 | 91 | 92 | public function toArray():Array { 93 | final arr = new Array(); 94 | for (i in this) 95 | arr.push(i); 96 | return arr; 97 | } 98 | } 99 | 100 | 101 | @:noDoc @:dox(hide) 102 | @:noCompletion 103 | private class RingBufferIterator { 104 | 105 | final buff:RingBufferImpl; 106 | var idx = -1; 107 | 108 | 109 | inline 110 | public function new(buff:RingBufferImpl) 111 | this.buff = buff; 112 | 113 | 114 | inline 115 | public function hasNext():Bool 116 | return idx + 1 < buff.length; 117 | 118 | 119 | inline 120 | public function next():V { 121 | idx++; 122 | return buff.get(idx); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/hx/strings/internal/TriState.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.internal; 7 | 8 | /** 9 | * IMPORTANT: This class it not part of the API. Direct usage is discouraged. 10 | */ 11 | #if (haxe_ver < 4.3) @:enum #else enum #end 12 | abstract TriState(Null) from Null to Null { 13 | final TRUE = true; 14 | final FALSE = false; 15 | final UNKNOWN = null; 16 | } 17 | -------------------------------------------------------------------------------- /src/hx/strings/spelling/checker/AbstractSpellChecker.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.spelling.checker; 7 | 8 | import haxe.Timer; 9 | import hx.strings.collection.StringSet; 10 | import hx.strings.spelling.dictionary.Dictionary; 11 | import hx.strings.internal.Arrays; 12 | 13 | using hx.strings.Strings; 14 | 15 | /** 16 | * Partially implemented spell checker class that provides shared functionality to subclasses. 17 | */ 18 | @:abstract 19 | class AbstractSpellChecker implements SpellChecker { 20 | 21 | final alphabet:Array; 22 | final dict:Dictionary; 23 | 24 | 25 | /** 26 | * @param alphabet used by #generateEdits() to generate word variations 27 | */ 28 | public function new(dictionary:Dictionary, alphabet:String) { 29 | if (dictionary == null) throw "[dictionary] must not be null!"; 30 | if (alphabet == null) throw "[alphabet] must not be null!"; 31 | this.dict = dictionary; 32 | this.alphabet = Arrays.unique(alphabet.toChars()); 33 | } 34 | 35 | 36 | public function correctText(text:String, timeoutMS:Int = 500):String 37 | throw "Not implemented"; 38 | 39 | 40 | public function correctWord(word:String, timeoutMS:Int = 500):String { 41 | final timeoutAt = haxe.Timer.stamp() + (timeoutMS / 1000); 42 | 43 | if(dict.exists(word)) 44 | return word; 45 | 46 | var candidate:Null = null; 47 | var candidatePopularity:Int = 0; 48 | 49 | var edits = generateEdits(word, timeoutAt); 50 | 51 | for (edit in edits) { 52 | var editPopularity = dict.popularity(edit); 53 | if (editPopularity > candidatePopularity) { 54 | candidate = edit; 55 | candidatePopularity = editPopularity; 56 | } 57 | } 58 | if (candidate != null) 59 | return candidate; 60 | 61 | if (timeoutAt < Timer.stamp()) 62 | return word; 63 | 64 | // check for words that are 2 edits away from the given input word 65 | for (edit in edits) { 66 | if (timeoutAt < Timer.stamp()) break; 67 | 68 | for (edit2 in generateEdits(edit, timeoutAt)) { 69 | var edit2Popularity = dict.popularity(edit2); 70 | if (edit2Popularity > candidatePopularity) { 71 | candidate = edit2; 72 | candidatePopularity = edit2Popularity; 73 | } 74 | } 75 | } 76 | return candidate == null ? word : candidate; 77 | } 78 | 79 | 80 | /** 81 | * @return the a list of word variations that are 1 character edit away from the given input string 82 | */ 83 | function generateEdits(word:String, timeoutAt:Float):Array { 84 | final edits = new Array(); 85 | final wordLen = word.length8(); 86 | for (i in 0...wordLen) { 87 | // generate a word variation by leaving out 1 of the word's characters 88 | edits.push(word.substring8(0, i) + word.substring8(i + 1)); 89 | 90 | // generate a word variation by switching the order of two characters 91 | edits.push(word.substring8(0, i) + word.substring8(i + 1, i + 2) + word.substring8(i, i + 1) + word.substring8(i + 2)); 92 | 93 | for (char in alphabet) { 94 | // generate a word variation by replacing one character 95 | edits.push(word.substring8(0, i) + char + word.substring8(i + 1)); 96 | 97 | // generate a word variation by adding one character 98 | edits.push(word.substring8(0, i) + char + word.substring8(i)); 99 | } 100 | 101 | if (timeoutAt < Timer.stamp()) break; 102 | } 103 | return edits; 104 | } 105 | 106 | 107 | public function suggestWords(word:String, max:Int = 3, timeoutMS:Int = 1000):Array { 108 | final timeoutAt = haxe.Timer.stamp() + (timeoutMS / 1000); 109 | var candidates = new Array<{word:String, popularity:Int}>(); 110 | 111 | final edits = generateEdits(word, timeoutAt); 112 | for (edit in edits) { 113 | final editPopularity = dict.popularity(edit); 114 | if (editPopularity > 0) 115 | candidates.push({word:edit,popularity:editPopularity}); 116 | } 117 | 118 | candidates.sort((a, b) -> a.popularity > b.popularity ? -1 : a.popularity == b.popularity ? 0 : 1); 119 | final result = Arrays.unique([for (candidate in candidates) candidate.word]); 120 | 121 | if (result.length < max) { 122 | candidates = new Array<{word:String, popularity:Int}>(); 123 | 124 | // check for words that are 2 edits away from the given input word 125 | final edit2s = new StringSet(); 126 | for (edit in edits) { 127 | for (edit2 in generateEdits(edit, timeoutAt)) { 128 | // short cut 129 | if (result.indexOf(edit2) > -1 || edit2s.contains(edit2)) 130 | continue; 131 | edit2s.add(edit2); 132 | final edit2Popularity = dict.popularity(edit2); 133 | if (edit2Popularity > 0) 134 | candidates.push({word:edit2,popularity:edit2Popularity}); 135 | } 136 | } 137 | candidates.sort((a, b) -> a.popularity > b.popularity ? -1 : a.popularity == b.popularity ? 0 : 1); 138 | for (candidate in candidates) { 139 | if(result.length < max) 140 | result.push(candidate.word); 141 | else 142 | break; 143 | } 144 | } 145 | 146 | return Arrays.unique(result).slice(0, max); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/hx/strings/spelling/checker/EnglishSpellChecker.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.spelling.checker; 7 | 8 | import hx.strings.spelling.dictionary.Dictionary; 9 | import hx.strings.spelling.dictionary.EnglishDictionary; 10 | 11 | using hx.strings.Strings; 12 | 13 | /** 14 | * Spell checker implementation with English language specific parsing behaviour. 15 | */ 16 | class EnglishSpellChecker extends AbstractSpellChecker{ 17 | 18 | /** 19 | * default instance that uses the pre-trained hx.strings.spelling.dictionary.EnglishDictionary 20 | * 21 | *

22 |     * >>> EnglishSpellChecker.INSTANCE.correctWord("speling")  == "spelling"
23 |     * >>> EnglishSpellChecker.INSTANCE.correctWord("SPELING")  == "spelling"
24 |     * >>> EnglishSpellChecker.INSTANCE.correctWord("SPELLING") == "spelling"
25 |     * >>> EnglishSpellChecker.INSTANCE.correctWord("spell1ng") == "spelling"
26 |     * >>> EnglishSpellChecker.INSTANCE.correctText("sometinG zEems realy vrong!", 3000) == "something seems really wrong!"
27 |     * >>> EnglishSpellChecker.INSTANCE.suggestWords("absance", 3, 3000) == [ "absence", "advance", "balance" ]
28 |     * 
29 | */ 30 | public static final INSTANCE = new EnglishSpellChecker(EnglishDictionary.INSTANCE); 31 | 32 | 33 | inline 34 | public function new(dictionary:Dictionary) 35 | super(dictionary, "abcdefghijklmnopqrstuvwxyz"); 36 | 37 | 38 | override 39 | public function correctText(text:String, timeoutMS:Int = 1000):String { 40 | final result = new StringBuilder(); 41 | final currentWord = new StringBuilder(); 42 | 43 | final chars = text.toChars(); 44 | final len = chars.length; 45 | for (i in 0...len) { 46 | var ch:Char = chars[i]; 47 | // treat a-z and 0-9 as characters of potential words to capture OCR errors like "m1nd" or "m0ther" or "1'll" 48 | if (ch.isAsciiAlpha() || ch.isDigit()) { 49 | currentWord.addChar(ch); 50 | } else if (currentWord.length > 0) { 51 | if (ch == Char.SINGLE_QUOTE) { 52 | final chNext:Char = i < len - 1 ? chars[i + 1] : -1; 53 | final chNextNext:Char = i < len - 2 ? chars[i + 2] : -1; 54 | 55 | // handle "don't" / "can't" 56 | if (chNext == 116 /*t*/ && !chNextNext.isAsciiAlpha()) { 57 | currentWord.addChar(ch); 58 | 59 | // handle "we'll" 60 | } else if (chNext == 108 /*l*/ && chNextNext == 108 /*l*/) { 61 | currentWord.addChar(ch); 62 | 63 | } else { 64 | result.add(correctWord(currentWord.toString(), timeoutMS)); 65 | currentWord.clear(); 66 | result.addChar(ch); 67 | } 68 | } else { 69 | result.add(correctWord(currentWord.toString(), timeoutMS)); 70 | currentWord.clear(); 71 | result.addChar(ch); 72 | } 73 | } else { 74 | result.addChar(ch); 75 | } 76 | } 77 | 78 | if (currentWord.length > 0) 79 | result.add(correctWord(currentWord.toString(), timeoutMS)); 80 | 81 | return result.toString(); 82 | } 83 | 84 | 85 | override 86 | public function correctWord(word:String, timeoutMS:Int = 1000):String { 87 | if(dict.exists(word)) 88 | return word; 89 | 90 | final wordLower = word.toLowerCase8(); 91 | if (dict.exists(wordLower)) 92 | return wordLower; 93 | 94 | final result = super.correctWord(wordLower, timeoutMS); 95 | return result == wordLower ? word : result; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/hx/strings/spelling/checker/GermanSpellChecker.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.spelling.checker; 7 | 8 | import hx.strings.spelling.checker.AbstractSpellChecker; 9 | import hx.strings.spelling.dictionary.Dictionary; 10 | import hx.strings.spelling.dictionary.GermanDictionary; 11 | 12 | using hx.strings.Strings; 13 | 14 | /** 15 | * Spell checker implementation with German language specific parsing behaviour. 16 | */ 17 | class GermanSpellChecker extends AbstractSpellChecker { 18 | 19 | /** 20 | * default instance that uses the pre-trained hx.strings.spelling.dictionary.GermanDictionary 21 | * 22 | *

 23 |     * >>> GermanSpellChecker.INSTANCE.correctWord("schreibweise")  == "Schreibweise"
 24 |     * >>> GermanSpellChecker.INSTANCE.correctWord("Schreibwiese")  == "Schreibweise"
 25 |     * >>> GermanSpellChecker.INSTANCE.correctWord("SCHREIBWEISE")  == "Schreibweise"
 26 |     * >>> GermanSpellChecker.INSTANCE.correctWord("SCHRIBWEISE")   == "Schreibweise"
 27 |     * >>> GermanSpellChecker.INSTANCE.correctWord("Schre1bweise")  == "Schreibweise"
 28 |     * >>> GermanSpellChecker.INSTANCE.correctText("etwaz kohmische Aepfel ligen vör der Thür", 3000) == "etwas komische Äpfel liegen vor der Tür"
 29 |     * >>> GermanSpellChecker.INSTANCE.suggestWords("Kiche", 3, 3000) == [ "Kirche", "Küche", "Eiche" ]
 30 |     * 
31 | */ 32 | public static final INSTANCE = new GermanSpellChecker(GermanDictionary.INSTANCE); 33 | 34 | final alphabetUpper:Array; 35 | 36 | 37 | public function new(dictionary:Dictionary) { 38 | super(dictionary, "abcdefghijklmnopqrstuvwxyzäöüß"); 39 | 40 | alphabetUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜ".toChars(); 41 | } 42 | 43 | 44 | function replaceUmlaute(word:String):String { 45 | // replace oe with ö, ae with ä and ue with ü 46 | final word = word.replaceAll("oe", "ö") 47 | .replaceAll("ae", "ä") 48 | .replaceAll("ue", "ü") 49 | .replaceAll("OE", "Ö") 50 | .replaceAll("AE", "Ä") 51 | .replaceAll("UE", "Ü"); 52 | 53 | if (word.startsWith("Oe")) 54 | return "Ö" + word.substr8(2); 55 | 56 | if (word.startsWith("Ae")) 57 | return "Ä" + word.substr8(2); 58 | 59 | if (word.startsWith("Ue")) 60 | return "Ü" + word.substr8(2); 61 | 62 | return word; 63 | } 64 | 65 | 66 | override 67 | public function correctText(text:String, timeoutMS:Int = 1000):String { 68 | final result = new StringBuilder(); 69 | final currentWord = new StringBuilder(); 70 | 71 | final chars = text.toChars(); 72 | final len = chars.length; 73 | for (i in 0...len) { 74 | final ch:Char = chars[i]; 75 | // treat a-z and 0-9 as characters of potential words to capture OCR errors like "me1n" or "M0ntag" 76 | if (ch.isAsciiAlpha() || ch.isDigit() || ch == "ä" || ch == "ö" || ch == "ü" || ch == "Ä" || ch == "Ö" || ch == "Ü" || ch == "ß") { 77 | currentWord.addChar(ch); 78 | } else if (currentWord.length > 0) { 79 | result.add(correctWord(currentWord.toString(), timeoutMS)); 80 | currentWord.clear(); 81 | result.addChar(ch); 82 | } else { 83 | result.addChar(ch); 84 | } 85 | } 86 | 87 | if (currentWord.length > 0) 88 | result.add(correctWord(currentWord.toString(), timeoutMS)); 89 | 90 | return result.toString(); 91 | } 92 | 93 | 94 | override 95 | public function correctWord(word:String, timeoutMS:Int = 1000):String { 96 | if(dict.exists(word)) 97 | return word; 98 | 99 | final wordWithUmlaute = replaceUmlaute(word); 100 | if(dict.exists(wordWithUmlaute)) 101 | return wordWithUmlaute; 102 | 103 | if (word.isUpperCase()) { // special handling for all uppercase words 104 | var wordLower = word.toLowerCase8(); 105 | final wordFirstCharUpper = wordLower.toUpperCaseFirstChar(); 106 | final pLower = dict.popularity(wordLower); 107 | final pCapitalized = dict.popularity(wordFirstCharUpper); 108 | if (pLower == 0 && pCapitalized == 0) 109 | return super.correctWord(wordFirstCharUpper, timeoutMS); 110 | return pCapitalized > pLower ? wordFirstCharUpper : wordLower; 111 | } 112 | 113 | return super.correctWord(word, timeoutMS); 114 | } 115 | 116 | 117 | override 118 | public function suggestWords(word:String, max:Int = 3, timeoutMS:Int = 1000):Array { 119 | if(!dict.exists(word)) { 120 | final wordWithUmlaute = replaceUmlaute(word); 121 | if(dict.exists(wordWithUmlaute)) 122 | return super.suggestWords(wordWithUmlaute, max, timeoutMS); 123 | } 124 | return super.suggestWords(word, max, timeoutMS); 125 | } 126 | 127 | 128 | override 129 | function generateEdits(word:String, timeoutAt:Float):Array { 130 | final edits = super.generateEdits(word, timeoutAt); 131 | 132 | // add 1st char upper case variation 133 | for (upper in alphabetUpper) 134 | edits.push(upper + word.substr8(1)); 135 | 136 | return edits; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/hx/strings/spelling/checker/SpellChecker.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.spelling.checker; 7 | 8 | /** 9 | * Word frequency/popularity based spell checker inspired by Peter Norvig's article 10 | * "How to Write a Spelling Corrector" http://www.norvig.com/spell-correct.html 11 | */ 12 | interface SpellChecker { 13 | 14 | /** 15 | * @param timeoutMS approximate maximum time in milliseconds per word the spell checker can try to find the best correction 16 | * 17 | * @return the text with each word replaced by it's best known correction 18 | */ 19 | function correctText(text:String, timeoutMS:Int = 1000):String; 20 | 21 | /** 22 | * @param timeoutMS approximate maximum time in milliseconds the spell checker can try to find the best correction 23 | * 24 | * @return the best known correction for the given word or the word itself if no correction is available 25 | */ 26 | function correctWord(word:String, timeoutMS:Int = 1000):String; 27 | 28 | /** 29 | * @param max maximum number of suggestions to return, may be less 30 | * @param timeoutMS approximate maximum time in milliseconds the spell checker can try to find the best corrections 31 | * 32 | * @return an array of the best known corrections for the given word ordered by popularity. the array may be empty in case no corrections are available 33 | */ 34 | function suggestWords(word:String, max:Int = 3, timeoutMS:Int = 1000):Array; 35 | } 36 | -------------------------------------------------------------------------------- /src/hx/strings/spelling/dictionary/Dictionary.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.spelling.dictionary; 7 | 8 | /** 9 | * Dictionary holding words and their popularity/frequency. 10 | */ 11 | interface Dictionary { 12 | 13 | /** 14 | * @return true if the given word exists 15 | */ 16 | function exists(word:String):Bool; 17 | 18 | /** 19 | * @return the popularity score of the given word or 0 if the word does not exist in the dictionary 20 | */ 21 | function popularity(word:String):Int; 22 | 23 | /** 24 | * @return number of words know to this dictionary 25 | */ 26 | function size():Int; 27 | 28 | /** 29 | * @return an iterator over all words known by the dictionary, no particular order is guranteed 30 | */ 31 | function words():Iterator; 32 | } 33 | -------------------------------------------------------------------------------- /src/hx/strings/spelling/dictionary/EnglishDictionary.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.spelling.dictionary; 7 | 8 | import haxe.Resource; 9 | import haxe.io.BytesInput; 10 | import hx.strings.internal.Macros; 11 | 12 | /** 13 | * A pre-trained English in-memory dictionary. 14 | * 15 | * Trained using http://www.norvig.com/big.txt, see http://www.norvig.com/spell-correct.html for details. 16 | */ 17 | class EnglishDictionary extends InMemoryDictionary { 18 | 19 | public static final INSTANCE = new EnglishDictionary(); 20 | 21 | 22 | public function new() { 23 | super(); 24 | 25 | Macros.addResource("hx/strings/spelling/dictionary/EnglishDictionary.txt", "EnglishDictionary"); 26 | 27 | // workaround to prevent strange error: AttributeError: type object 'python_Lib' has no attribute 'lineEnd' 28 | #if python python.Lib; #end 29 | 30 | // not using loadWordsFromResource for full DCE support 31 | trace('[INFO] Loading words from embedded [EnglishDictionary]...'); 32 | loadWordsFromResource("EnglishDictionary"); 33 | } 34 | 35 | 36 | override 37 | public function toString() 38 | return 'EnglishDictionary[words=$dictSize]'; 39 | } 40 | -------------------------------------------------------------------------------- /src/hx/strings/spelling/dictionary/GermanDictionary.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.spelling.dictionary; 7 | 8 | import haxe.Resource; 9 | import haxe.io.BytesInput; 10 | import hx.strings.internal.Macros; 11 | 12 | /** 13 | * A pre-trained German in-memory dictionary. 14 | * 15 | * Contains most common 30.000 words determined and weighted by analyzing: 16 | * 1) free German books (https://www.gutenberg.org/browse/languages/de), 17 | * 2) German movie subtitles (http://opensubtitles.org), 18 | * 3) some German newspaper articles, 19 | * 4) the Top 10000 German word list of the University Leipzig (http://wortschatz.uni-leipzig.de/html/wliste.html), and 20 | * 5) the Free German Dictionary (https://sourceforge.net/projects/germandict/) 21 | */ 22 | class GermanDictionary extends InMemoryDictionary { 23 | 24 | public static final INSTANCE = new GermanDictionary(); 25 | 26 | public function new() { 27 | super(); 28 | 29 | Macros.addResource("hx/strings/spelling/dictionary/GermanDictionary.txt", "GermanDictionary"); 30 | 31 | // workaround to prevent strange error: AttributeError: type object 'python_Lib' has no attribute 'lineEnd' 32 | #if python python.Lib; #end 33 | 34 | // not using loadWordsFromResource for full DCE support 35 | trace('[INFO] Loading words from embedded [GermanDictionary]...'); 36 | loadWordsFromResource("GermanDictionary"); 37 | } 38 | 39 | override 40 | public function toString() 41 | return 'GermanDictionary[words=$dictSize]'; 42 | } 43 | -------------------------------------------------------------------------------- /src/hx/strings/spelling/dictionary/InMemoryDictionary.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.spelling.dictionary; 7 | 8 | import haxe.Resource; 9 | import haxe.io.BytesInput; 10 | import haxe.io.Input; 11 | import haxe.io.Output; 12 | import hx.strings.internal.Exception; 13 | 14 | import hx.strings.collection.StringMap; 15 | 16 | using hx.strings.Strings; 17 | 18 | /** 19 | * Hash map based dictionary. 20 | */ 21 | class InMemoryDictionary implements TrainableDictionary { 22 | 23 | /** 24 | * key = word, value = popularity score 25 | */ 26 | final dict = new StringMap(); 27 | var dictSize = 0; 28 | 29 | 30 | inline 31 | public function new() { 32 | } 33 | 34 | 35 | public function clear():Void { 36 | dict.clear(); 37 | dictSize = 0; 38 | } 39 | 40 | 41 | inline 42 | public function exists(word:String):Bool 43 | return dict.exists(word); 44 | 45 | 46 | public function popularity(word:String):Int { 47 | final p = dict.get(word); 48 | return p == null ? 0 : p; 49 | } 50 | 51 | 52 | public function train(word:String):Int { 53 | final p = popularity(word) + 1; 54 | dict.set(word, p); 55 | if (p == 1) 56 | dictSize++; 57 | return p; 58 | } 59 | 60 | 61 | public function remove(word:String):Bool { 62 | if (dict.remove(word)) { 63 | dictSize--; 64 | return true; 65 | } 66 | return false; 67 | } 68 | 69 | 70 | inline 71 | public function size():Int 72 | return dictSize; 73 | 74 | 75 | public function trimTo(n:Int):Int { 76 | if (dictSize <= n) 77 | return 0; 78 | 79 | final arr = [for (word => popularity in dict) {word:word, popularity:popularity}]; 80 | arr.sort((a, b) -> a.popularity > b.popularity ? -1 : a.popularity == b.popularity ? 0 : 1); 81 | 82 | #if python @:nullSafety(Off) #end // TODO 83 | final removables = arr.slice(n); 84 | 85 | for (r in removables) 86 | remove(r.word); 87 | return removables.length; 88 | } 89 | 90 | 91 | /** 92 | * Exports all words and their popularity to the given output stream 93 | */ 94 | public function exportWordsToOutput(out:Output, autoClose:Bool=true):Void { 95 | final words = [for (word in dict.keys()) word]; 96 | words.sort(Strings.compare); 97 | 98 | for (word in words) 99 | out.writeString('$word:${dict[word]}\n'); 100 | 101 | if(autoClose) 102 | out.close(); 103 | } 104 | 105 | 106 | #if sys 107 | /** 108 | * Exports all words and their popularity to the given file 109 | */ 110 | public function exportWordsToFile(filePath:String):Void { 111 | trace('[INFO] Exporting words to file [$filePath]...'); 112 | exportWordsToOutput(sys.io.File.write(filePath)); 113 | } 114 | 115 | 116 | /** 117 | * Loads all words and their popularity from the given file 118 | * 119 | * @return number of loaded entries 120 | */ 121 | public function loadWordsFromFile(filePath:String):Int { 122 | trace('[INFO] Loading words from file [$filePath]...'); 123 | return loadWordsFromInput(sys.io.File.read(filePath)); 124 | } 125 | #end 126 | 127 | 128 | /** 129 | * Loads all words and their popularity from the given Haxe resource 130 | * 131 | * @return number of loaded entries 132 | */ 133 | public function loadWordsFromResource(resourceName:String):Int { 134 | trace('[INFO] Loading words from resource [$resourceName]...'); 135 | final bytes = Resource.getBytes(resourceName); 136 | if (bytes == null) 137 | throw 'Resource [$resourceName] not found!'; 138 | return loadWordsFromInput(new BytesInput(bytes)); 139 | } 140 | 141 | 142 | /** 143 | * Loads all words and their popularity from the given input stream 144 | * 145 | * @return number of loaded entries 146 | */ 147 | public function loadWordsFromInput(input:Input, autoClose:Bool=true):Int { 148 | var lineNo = 0; 149 | var line = ""; 150 | var count = 0; 151 | try { 152 | while (true) { 153 | lineNo++; 154 | line = input.readLine(); 155 | final tuple = line.split8(":", 2); 156 | if (tuple.length < 2) { 157 | trace('[WARN] Skipping line #$lineNo which misses the colon (:) separator'); 158 | continue; 159 | } 160 | final word = tuple[0]; 161 | final popularity = tuple[1].toInt(0); 162 | if (popularity < 1) { 163 | trace('[WARN] Skipping line #$lineNo with popularity < 1'); 164 | continue; 165 | } 166 | if (!exists(word)) dictSize++; 167 | count++; 168 | dict.set(word, popularity); 169 | } 170 | } catch(ex:haxe.io.Eof) { 171 | // expected --> https://github.com/HaxeFoundation/haxe/issues/5418 172 | } catch (e:Dynamic) { 173 | final ex = Exception.capture(e); 174 | trace('[ERROR] Exception while reading line #$lineNo. Previous line content was [$line].'); 175 | if (autoClose) input.close(); 176 | ex.rethrow(); 177 | } 178 | if (autoClose) input.close(); 179 | return count; 180 | } 181 | 182 | 183 | public function toString() 184 | return 'InMemoryDictionary[words=$dictSize]'; 185 | 186 | 187 | inline 188 | public function words():Iterator 189 | return dict.keys(); 190 | } 191 | -------------------------------------------------------------------------------- /src/hx/strings/spelling/dictionary/TrainableDictionary.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.spelling.dictionary; 7 | 8 | /** 9 | * A modifiable/trainable dictionary. 10 | */ 11 | interface TrainableDictionary extends Dictionary { 12 | 13 | /** 14 | * removes all trained words 15 | */ 16 | function clear():Void; 17 | 18 | /** 19 | * removes the given word from the dictionary 20 | * 21 | * @return true if the word was removed, false if the word didn't exist 22 | */ 23 | function remove(word:String):Bool; 24 | 25 | /** 26 | * adds the word to the dictionary or if it exists already increases it's popularity score 27 | * 28 | * @return the new popularity score 29 | */ 30 | function train(word:String):Int; 31 | 32 | /** 33 | * Only leaves the n-most popular words in the dictionary 34 | * 35 | * @return number of removed words 36 | */ 37 | function trimTo(n:Int):Int; 38 | } 39 | -------------------------------------------------------------------------------- /src/hx/strings/spelling/trainer/AbstractDictionaryTrainer.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.spelling.trainer; 7 | 8 | import haxe.io.BytesInput; 9 | import haxe.io.Input; 10 | import haxe.Resource; 11 | 12 | import hx.strings.collection.StringMap; 13 | import hx.strings.spelling.dictionary.TrainableDictionary; 14 | 15 | using hx.strings.Strings; 16 | 17 | /** 18 | * Partially implemented dictionary trainer class that provides shared functionality to subclasses. 19 | */ 20 | @:abstract 21 | @threadSafe 22 | class AbstractDictionaryTrainer implements DictionaryTrainer { 23 | 24 | var vocabular:StringMap; 25 | 26 | #if sys 27 | public function trainWithFile(dictionary:TrainableDictionary, filePath:String, ignoreUnknownWords:Bool = false):Int { 28 | trace('[INFO] Training with file [$filePath]...'); 29 | return trainWithInput(dictionary, sys.io.File.read(filePath), ignoreUnknownWords); 30 | } 31 | #end 32 | 33 | public function trainWithInput(dictionary:TrainableDictionary, input:Input, ignoreUnknownWords:Bool = false, autoClose:Bool = true):Int { 34 | var lineNo = 0; 35 | var line = ""; 36 | var count = 0; 37 | try { 38 | while (true) { 39 | lineNo++; 40 | line = input.readLine(); 41 | count += trainWithString(dictionary, line, ignoreUnknownWords); 42 | } 43 | } catch(ex:haxe.io.Eof) { 44 | // expected --> https://github.com/HaxeFoundation/haxe/issues/5418 45 | if(autoClose) input.close(); 46 | } catch (e:Dynamic) { 47 | final ex = Exception.capture(e); 48 | trace('[ERROR] Exception while reading line #$lineNo. Previous line content was [$line].'); 49 | if (autoClose) input.close(); 50 | ex.rethrow(); 51 | } 52 | return count; 53 | } 54 | 55 | public function trainWithResource(dictionary:TrainableDictionary, resourceName:String, ignoreUnknownWords:Bool = false):Int { 56 | trace('[INFO] Training with resource [$resourceName]...'); 57 | return trainWithInput(dictionary, new BytesInput(Resource.getBytes(resourceName)), ignoreUnknownWords); 58 | } 59 | 60 | public function trainWithString(dictionary:TrainableDictionary, content:String, ignoreUnknownWords:Bool = false):Int 61 | throw "Not implemented"; 62 | } 63 | -------------------------------------------------------------------------------- /src/hx/strings/spelling/trainer/DictionaryTrainer.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.spelling.trainer; 7 | 8 | import hx.strings.spelling.dictionary.TrainableDictionary; 9 | 10 | /** 11 | * A dictionary trainer can train/populate a dictionary by analyzing provided reference texts. 12 | */ 13 | interface DictionaryTrainer { 14 | 15 | #if sys 16 | /** 17 | * Populates the dictionary with words found in the given UTF-8 encoded file. 18 | * 19 | * @param ignoreUnknownWords if set to true only words already present in the dictionary are trained 20 | * 21 | * @return count of words found in the file 22 | */ 23 | function trainWithFile(dictionary:TrainableDictionary, filePath:String, ignoreUnknownWords:Bool = false):Int; 24 | #end 25 | 26 | /** 27 | * Populates the dictionary with words found in the given UTF-8 encoded Haxe input stream. 28 | * 29 | * @param ignoreUnknownWords if set to true only words already present in the dictionary are trained 30 | * 31 | * @return count of words found in the resource 32 | */ 33 | function trainWithInput(dictionary:TrainableDictionary, input:haxe.io.Input, ignoreUnknownWords:Bool = false, autoClose:Bool = true):Int; 34 | 35 | /** 36 | * Populates the dictionary with words found in the given UTF-8 encoded Haxe resource. 37 | * 38 | * @param ignoreUnknownWords if set to true only words already present in the dictionary are trained 39 | * 40 | * @return count of words found in the resource 41 | */ 42 | function trainWithResource(dictionary:TrainableDictionary, resourceName:String, ignoreUnknownWords:Bool = false):Int; 43 | 44 | /** 45 | * Populates the dictionary with words found in the given UTF-8 encoded content 46 | * 47 | * @param ignoreUnknownWords if set to true only words already present in the dictionary are trained 48 | * 49 | * @return count of words found in the string 50 | */ 51 | function trainWithString(dictionary:TrainableDictionary, content:String, ignoreUnknownWords:Bool = false):Int; 52 | } 53 | -------------------------------------------------------------------------------- /src/hx/strings/spelling/trainer/EnglishDictionaryTrainer.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.spelling.trainer; 7 | 8 | import hx.strings.spelling.dictionary.TrainableDictionary; 9 | 10 | using hx.strings.Strings; 11 | 12 | /** 13 | * A dictionary trainer with English language specific parsing behaviour. 14 | */ 15 | @threadSafe 16 | class EnglishDictionaryTrainer extends AbstractDictionaryTrainer { 17 | 18 | public static final INSTANCE = new EnglishDictionaryTrainer(); 19 | 20 | 21 | inline 22 | function new() { 23 | } 24 | 25 | 26 | inline 27 | function isValidWordChar(ch:Char):Bool 28 | return ch.isAsciiAlpha(); 29 | 30 | 31 | function trainWord(dictionary:TrainableDictionary, word:StringBuilder, ignoreUnknownWords:Bool):Int { 32 | var w = word.toString(); 33 | if (w == "I" || w == "O") { // only accept single char uppercase words 'I' and 'O' 34 | if (ignoreUnknownWords && !dictionary.exists(w)) 35 | return 0; 36 | 37 | dictionary.train(w); 38 | return 1; 39 | } 40 | w = w.toLowerCase8(); 41 | 42 | if (w.length == 1 && w != "a") { // only accept single char lowercase word 'a' 43 | // ignore 44 | return 0; 45 | } 46 | if (ignoreUnknownWords && !dictionary.exists(w)) 47 | return 0; 48 | 49 | dictionary.train(w); 50 | return 1; 51 | } 52 | 53 | 54 | override 55 | public function trainWithString(dictionary:TrainableDictionary, content:String, ignoreUnknownWords:Bool = false):Int { 56 | if (dictionary == null) throw "[dictionary] must not be null!"; 57 | 58 | final chars = content.toChars(); 59 | final len = chars.length; 60 | var count = 0; 61 | var currentWord = new StringBuilder(); 62 | for (i in 0...len) { 63 | final ch = chars[i]; 64 | if (isValidWordChar(ch)) { 65 | currentWord.addChar(ch); 66 | } else if (currentWord.length > 0) { 67 | if (ch == Char.SINGLE_QUOTE) { 68 | final chNext:Char = i < len - 1 ? chars[i + 1] : -1; 69 | final chNextNext:Char = i < len - 2 ? chars[i + 2] : -1; 70 | 71 | // handle "don't" / "can't" 72 | if (chNext == 116 /*t*/ && !isValidWordChar(chNextNext)) { 73 | currentWord.addChar(ch); 74 | 75 | // handle "we'll" 76 | } else if (chNext == 108 /*l*/ && chNextNext == 108 /*l*/) { 77 | currentWord.addChar(ch); 78 | 79 | } else { 80 | count += trainWord(dictionary, currentWord, ignoreUnknownWords); 81 | currentWord.clear(); 82 | } 83 | } else { 84 | count += trainWord(dictionary, currentWord, ignoreUnknownWords); 85 | currentWord.clear(); 86 | } 87 | } 88 | } 89 | 90 | if (currentWord.length > 0) 91 | count += trainWord(dictionary, currentWord, ignoreUnknownWords); 92 | 93 | return count; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/hx/strings/spelling/trainer/GermanDictionaryTrainer.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings.spelling.trainer; 7 | 8 | import hx.strings.spelling.dictionary.TrainableDictionary; 9 | 10 | using hx.strings.Strings; 11 | 12 | /** 13 | * A dictionary trainer with German language specific parsing behaviour. 14 | */ 15 | @threadSafe 16 | class GermanDictionaryTrainer extends AbstractDictionaryTrainer { 17 | 18 | public static var INSTANCE(default, never) = new GermanDictionaryTrainer(); 19 | 20 | static var SPECIAL_CHARS = [ Char.of("ä"), Char.of("Ä"), Char.of("ö"), Char.of("Ö"), Char.of("ü"), Char.of("Ü"), Char.of("ß") ]; 21 | 22 | function new() { 23 | } 24 | 25 | inline 26 | function isValidWordChar(ch:Char) 27 | return ch.isAsciiAlpha() || SPECIAL_CHARS.indexOf(ch) > -1; 28 | 29 | 30 | function trainWord(dictionary:TrainableDictionary, word:StringBuilder, ignoreUnknownWords:Bool):Int { 31 | final w = word.toString(); 32 | if (w.length == 1 || w.startsWith("ß") || w.isUpperCase() /* ignore all uppercase words */) 33 | return 0; 34 | 35 | if (ignoreUnknownWords && !dictionary.exists(w)) 36 | return 0; 37 | 38 | dictionary.train(w); 39 | return 1; 40 | } 41 | 42 | 43 | override 44 | public function trainWithString(dictionary:TrainableDictionary, content:String, ignoreUnknownWords:Bool = false):Int { 45 | if (dictionary == null) throw "[dictionary] must not be null!"; 46 | 47 | final chars = content.toChars(); 48 | final len = chars.length; 49 | var count = 0; 50 | var currentWord = new StringBuilder(); 51 | for (i in 0...len) { 52 | final ch = chars[i]; 53 | if (isValidWordChar(ch)) { 54 | currentWord.addChar(ch); 55 | } else if (currentWord.length > 0) { 56 | count += trainWord(dictionary, currentWord, ignoreUnknownWords); 57 | currentWord.clear(); 58 | } 59 | } 60 | 61 | if (currentWord.length > 0) 62 | count += trainWord(dictionary, currentWord, ignoreUnknownWords); 63 | 64 | return count; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/hx/strings/TestRunner.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | * SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | package hx.strings; 7 | 8 | import haxe.io.Eof; 9 | import hx.doctest.DocTestRunner; 10 | import hx.strings.Pattern; 11 | import hx.strings.collection.OrderedStringMap; 12 | import hx.strings.collection.SortedStringMap; 13 | import hx.strings.collection.StringMap; 14 | import hx.strings.spelling.checker.*; 15 | import hx.strings.spelling.dictionary.*; 16 | import hx.strings.spelling.trainer.*; 17 | 18 | import hx.strings.StringMacros.multiline; 19 | 20 | using hx.strings.Strings; 21 | 22 | @:build(hx.doctest.DocTestGenerator.generateDocTests()) 23 | @:keep // prevent DCEing of manually created testXYZ() methods 24 | class TestRunner extends DocTestRunner { 25 | 26 | public static function main() { 27 | var runner = new TestRunner(); 28 | runner.runAndExit(); 29 | } 30 | 31 | function new() { 32 | super(); 33 | } 34 | 35 | public function testMultiline():Void { 36 | 37 | assertEquals(multiline(/*foobar*/), "foobar"); 38 | 39 | var value = "foo"; 40 | assertEquals(multiline( /*${value}*/), "${value}"); 41 | assertEquals(multiline("$" /*${value} $${bar} $$$${bar}*/), "foo ${bar} $${bar}"); 42 | assertEquals(multiline("$$" /*$${value} ${bar} $$$${bar}*/), "foo ${bar} $${bar}"); 43 | assertEquals(multiline("@" /*@{value} ${bar} $${bar} @@{bar}*/), "foo ${bar} $${bar} @{bar}"); 44 | assertEquals(multiline("@@" /*@@{value} ${bar} $${bar} @{bar}*/), "foo ${bar} $${bar} @{bar}"); 45 | 46 | assertEquals(multiline("$" /* 47 | ${value} 48 | line2 49 | */), "foo\nline2"); 50 | 51 | assertEquals(multiline("$" /* 52 | ${value} 53 | line2 54 | 55 | */), "foo\nline2\n"); 56 | 57 | assertEquals(multiline("$", false/* 58 | ${value} 59 | line2 60 | */), " foo\n line2"); 61 | 62 | assertEquals(multiline("$", false/* 63 | ${value} 64 | line2 65 | 66 | */), " foo\n line2\n"); 67 | } 68 | 69 | 70 | public function testCharIterator_WithPrevBuffer():Void { 71 | var it = CharIterator.fromString("1234567890", 4); 72 | 73 | assertFalse(it.hasPrev()); 74 | try { it.prev(); fail(); } catch (e:Eof) { }; 75 | assertEquals(it.current, null); 76 | assertTrue(it.hasNext()); 77 | 78 | assertEquals(it.next(), Char.of("1")); 79 | assertEquals(it.current, Char.of("1")); 80 | assertEquals(it.pos.col, 1); 81 | assertEquals(it.pos.index, 0); 82 | assertEquals(it.pos.line, 1); 83 | assertFalse(it.hasPrev()); 84 | try { it.prev(); fail(); } catch (e:Eof) { }; 85 | 86 | assertEquals(it.next(), Char.of("2")); 87 | assertEquals(it.current, Char.of("2")); 88 | assertEquals(it.pos.col, 2); 89 | assertEquals(it.pos.index, 1); 90 | assertEquals(it.pos.line, 1); 91 | assertTrue(it.hasPrev()); 92 | 93 | assertEquals(it.prev(), Char.of("1")); 94 | assertEquals(it.next(), Char.of("2")); 95 | assertEquals(it.next(), Char.of("3")); 96 | assertEquals(it.next(), Char.of("4")); 97 | assertEquals(it.next(), Char.of("5")); 98 | assertEquals(it.next(), Char.of("6")); 99 | assertEquals(it.prev(), Char.of("5")); 100 | assertEquals(it.prev(), Char.of("4")); 101 | assertEquals(it.prev(), Char.of("3")); 102 | assertEquals(it.prev(), Char.of("2")); 103 | assertFalse(it.hasPrev()); 104 | assertEquals(it.next(), Char.of("3")); 105 | assertEquals(it.next(), Char.of("4")); 106 | assertEquals(it.next(), Char.of("5")); 107 | assertEquals(it.next(), Char.of("6")); 108 | assertEquals(it.next(), Char.of("7")); 109 | assertEquals(it.next(), Char.of("8")); 110 | assertEquals(it.prev(), Char.of("7")); 111 | assertEquals(it.prev(), Char.of("6")); 112 | assertEquals(it.prev(), Char.of("5")); 113 | assertEquals(it.prev(), Char.of("4")); 114 | assertFalse(it.hasPrev()); 115 | assertEquals(it.next(), Char.of("5")); 116 | assertEquals(it.next(), Char.of("6")); 117 | assertEquals(it.next(), Char.of("7")); 118 | assertEquals(it.next(), Char.of("8")); 119 | assertEquals(it.next(), Char.of("9")); 120 | assertEquals(it.next(), Char.of("0")); 121 | assertFalse(it.hasNext()); 122 | try { it.next(); fail(); } catch (e:Eof) { }; 123 | assertEquals(it.current, Char.of("0")); 124 | } 125 | 126 | 127 | public function testCharIterator_WithoutPrevBuffer():Void { 128 | var it = CharIterator.fromString("1234567890", 0); 129 | assertFalse(it.hasPrev()); 130 | try { it.prev(); fail(); } catch (e:Eof) { }; 131 | 132 | assertTrue(it.hasNext()); 133 | assertEquals(it.next(), Char.of("1")); 134 | assertFalse(it.hasPrev()); 135 | try { it.prev(); fail(); } catch (e:Eof) { }; 136 | } 137 | 138 | 139 | public function testPattern():Void { 140 | { 141 | var p:Pattern = Pattern.compile("DOG", [IGNORE_CASE, MATCH_ALL]); 142 | var m:Matcher = p.matcher("dogcatdog"); 143 | 144 | assertEquals(m.matchedPos(), { pos: 0, len: 3 }); 145 | assertEquals(m.matches(), true); 146 | assertEquals(m.matched(), "dog"); 147 | assertEquals(m.matched(0), "dog"); 148 | try { m.matched(1); fail(); } catch (e:Dynamic) {}; 149 | assertEquals(m.map(function(m) return "cat"), "catcatcat"); 150 | } 151 | 152 | { 153 | var p:Pattern = Pattern.compile("(D(.)G)", [IGNORE_CASE, MATCH_ALL]); 154 | var m:Matcher = p.matcher("dOgcatdAg"); 155 | 156 | assertEquals(m.matchedPos(), { pos: 0, len: 3 }); 157 | assertEquals(m.matches(), true); 158 | assertEquals(m.matched(), "dOg"); 159 | assertEquals(m.matched(0), "dOg"); 160 | assertEquals(m.matched(1), "dOg"); 161 | assertEquals(m.matched(2), "O"); 162 | try { m.matched(3); fail(); } catch (e:Dynamic) { }; 163 | var i = 0; 164 | assertEquals(m.map(function(m) { 165 | i++; 166 | if(i==1) assertEquals(m.matchedPos(), { pos: 0, len: 3 }); 167 | if(i==2) assertEquals(m.matchedPos(), { pos: 6, len: 3 }); 168 | return "cat"; } 169 | ), "catcatcat"); 170 | } 171 | 172 | { 173 | var p:Pattern = Pattern.compile("DOG", [IGNORE_CASE, MATCH_ALL]); 174 | var m:Matcher = p.matcher("cowcatcow"); 175 | 176 | try { m.matchedPos(); fail(); } catch (e:Dynamic) {}; 177 | assertEquals(m.matches(), false); 178 | try { m.matched(); fail(); } catch (e:Dynamic) { }; 179 | try { m.matchedPos(); fail(); } catch (e:Dynamic) { }; 180 | try { m.matched(0); fail(); } catch (e:Dynamic) {}; 181 | assertEquals(m.map(function(m) return "cat"), "cowcatcow"); 182 | } 183 | 184 | { 185 | var p:Pattern = Pattern.compile("DOG", [IGNORE_CASE]); 186 | var m:Matcher = p.matcher("dogcatdog"); 187 | 188 | assertEquals(m.matchedPos(), { pos: 0, len: 3 }); 189 | assertEquals(m.matches(), true); 190 | assertEquals(m.matched(), "dog"); 191 | assertEquals(m.matched(0), "dog"); 192 | assertEquals(m.map(function(m) return "cat"), "catcatdog"); 193 | } 194 | 195 | { 196 | var p:Pattern = Pattern.compile("DOG", [IGNORE_CASE]); 197 | var m:Matcher = p.matcher("dogcatdog"); 198 | 199 | var matches = new Array(); 200 | m.iterate(function(m) matches.push(m.matched())); 201 | assertEquals(matches, ["dog", "dog"]); 202 | } 203 | } 204 | 205 | 206 | public function testPatternDOTALL():Void { 207 | { 208 | var p:Pattern = Pattern.compile(".+"); 209 | var m:Matcher = p.matcher("foo\nbar"); 210 | assertEquals("foo", m.matched(0)); 211 | } 212 | 213 | #if !(cs || (js && !nodejs)) 214 | { 215 | var p:Pattern = Pattern.compile(".+", [DOTALL]); 216 | var m:Matcher = p.matcher("foo\nbar"); 217 | assertEquals("foo\nbar", m.matched(0)); 218 | } 219 | #end 220 | } 221 | 222 | 223 | public function testString8():Void { 224 | var str:String8 = "test"; 225 | assertTrue(str.endsWith("est")); 226 | assertTrue(str.startsWith("tes")); 227 | } 228 | 229 | 230 | public function testVersion():Void { 231 | var v1:Version = "1.1.1"; 232 | var v1_B:Version = "1.1.1"; 233 | var v2:Version = "1.1.2"; 234 | 235 | /* 236 | * Testing operator overloading etc 237 | */ 238 | assertEquals(v1, v1_B); 239 | assertTrue(v1 == v1_B); 240 | assertFalse(v1 != v1_B); 241 | assertFalse(v1 > v1_B); 242 | assertFalse(v1 < v1_B); 243 | assertTrue(v1 <= v1_B); 244 | assertTrue(v1 >= v1_B); 245 | 246 | assertEquals(v1_B, v1); 247 | assertTrue(v1_B == v1); 248 | assertFalse(v1_B != v1); 249 | assertFalse(v1_B > v1); 250 | assertFalse(v1_B < v1); 251 | assertTrue(v1_B <= v1); 252 | assertTrue(v1_B >= v1); 253 | 254 | assertNotEquals(v1, v2); 255 | assertFalse(v1 == v2); 256 | assertTrue(v1 != v2); 257 | assertFalse(v1 > v2); 258 | assertTrue(v1 < v2); 259 | assertTrue(v1 <= v2); 260 | assertFalse(v1 >= v2); 261 | 262 | assertNotEquals(v2, v1); 263 | assertFalse(v2 == v1); 264 | assertTrue(v2 != v1); 265 | assertTrue(v2 > v1); 266 | assertFalse(v2 < v1); 267 | assertFalse(v2 <= v1); 268 | assertTrue(v2 >= v1); 269 | 270 | /* 271 | * testing using Version as Map keys 272 | */ 273 | var map:Map = new Map(); 274 | map.set(v1, true); 275 | 276 | assertTrue(map.exists(v1)); 277 | assertTrue(map.exists(v1_B)); 278 | assertFalse(map.exists(v2)); 279 | 280 | map.set(v1_B, true); 281 | 282 | var mapLen = 0; 283 | for (key in map.keys()) 284 | mapLen++; 285 | assertEquals(1, mapLen); 286 | } 287 | 288 | 289 | public function testStringMapCopy() { 290 | var ssm:StringMap = new SortedStringMap(); 291 | assertTrue(Std.isOfType(ssm, SortedStringMapImpl)); 292 | assertTrue(Std.isOfType(ssm.copy(), SortedStringMapImpl)); 293 | 294 | var osm:OrderedStringMap = new OrderedStringMap(); 295 | assertTrue(Std.isOfType(osm, OrderedStringMapImpl)); 296 | assertTrue(Std.isOfType(osm.copy(), OrderedStringMapImpl)); 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /tests.hxml: -------------------------------------------------------------------------------- 1 | extraParams.hxml 2 | -cp src 3 | -cp test 4 | -main hx.strings.TestRunner 5 | -lib haxe-doctest 6 | -dce full 7 | -debug 8 | -D dump=pretty 9 | -D swf-script-timeout=360 10 | -------------------------------------------------------------------------------- /tools/.travis.yml: -------------------------------------------------------------------------------- 1 | # old build file for Travis CI 2 | dist: bionic 3 | 4 | language: haxe 5 | 6 | os: 7 | - linux 8 | - osx 9 | 10 | git: 11 | depth: 2 12 | 13 | cache: 14 | directories: 15 | - $HOME/haxe/lib 16 | - $HOME/.cache/luarocks 17 | 18 | haxe: 19 | - "4.0.5" 20 | - "4.1.5" 21 | - development 22 | 23 | env: 24 | - HAXE_TARGET=cpp 25 | - HAXE_TARGET=cs 26 | - HAXE_TARGET="interp -D eval-stack" 27 | - HAXE_TARGET=flash 28 | - HAXE_TARGET=hl 29 | - HAXE_TARGET=java 30 | - HAXE_TARGET="java -D jvm" 31 | - HAXE_TARGET=js 32 | - HAXE_TARGET=lua 33 | - HAXE_TARGET=neko 34 | - HAXE_TARGET=node 35 | - HAXE_TARGET=php7 36 | - HAXE_TARGET=python 37 | 38 | matrix: 39 | fast_finish: true 40 | allow_failures: 41 | - haxe: development 42 | - env: HAXE_TARGET=flash # see https://github.com/travis-ci/travis-ci/issues/8481 43 | - env: HAXE_TARGET=lua # https://github.com/HaxeFoundation/haxe/issues/9323 44 | - os: osx 45 | exclude: 46 | - haxe: development 47 | os: osx 48 | 49 | install: 50 | #- haxelib git tink_io https://github.com/haxetink/tink_io pure 51 | #- haxelib git tink_cli https://github.com/haxetink/tink_cli pure 52 | #- haxelib git travix https://github.com/back2dos/travix && pushd . && cd $(haxelib config)travix/git && haxe build-neko.hxml && popd 53 | #- haxelib run travix install 54 | - haxelib dev haxe-strings . 55 | - haxelib install travix 56 | - haxelib install haxe-doctest 57 | # install node 12 LTS as workaround for https://github.com/back2dos/travix/issues/120 58 | - |- 59 | if [ "$HAXE_TARGET" == "js" ] && [ "$TRAVIS_OS_NAME" == "linux" ]; then 60 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash && \ 61 | source $HOME/.nvm/nvm.sh && \ 62 | nvm install --lts && \ 63 | nvm use --lts 64 | fi 65 | 66 | script: 67 | - haxelib run travix $HAXE_TARGET 68 | -------------------------------------------------------------------------------- /tools/_test-prepare.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | pushd . 7 | 8 | REM cd into project root 9 | cd %~dp0.. 10 | 11 | 12 | if exist dump\%1 ( 13 | echo Cleaning [dump\%1]... 14 | rd /s /q dump\%1 15 | ) 16 | if exist target\%1 ( 17 | echo Cleaning [target\%1]... 18 | rd /s /q target\%1 19 | ) 20 | md target\%1 21 | shift 22 | 23 | REM install common libs 24 | echo Checking required haxelibs... 25 | for %%i in (haxe-doctest) do ( 26 | haxelib list | findstr %%i >NUL 27 | if errorlevel 1 ( 28 | echo Installing [%%i]... 29 | haxelib install %%i 30 | ) 31 | ) 32 | 33 | goto :eof 34 | 35 | REM install additional libs 36 | :iterate 37 | 38 | if "%~1"=="" goto :eof 39 | 40 | haxelib list | findstr %1 >NUL 41 | if errorlevel 1 ( 42 | echo Installing [%1]... 43 | haxelib install %1 44 | ) 45 | 46 | shift 47 | goto iterate 48 | -------------------------------------------------------------------------------- /tools/gen-doc.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | REM generates API documentation using dox at \target\site 7 | 8 | setlocal 9 | 10 | set TOP_LEVEL_PACKAGE=hx.strings 11 | set OWNER=https://vegardit.com 12 | 13 | REM cd into project root 14 | pushd . 15 | cd %~dp0.. 16 | 17 | REM extract GIT URL from haxelib.json 18 | for /f "tokens=*" %%a in ( 'findstr url haxelib.json' ) do (set textLine=%%a) 19 | set REPO_URL=%textLine:"url": "=% 20 | set REPO_URL=%REPO_URL:",=% 21 | set REPO_URL=%REPO_URL:"=% 22 | echo REPO_URL=%REPO_URL% 23 | 24 | REM extract project version from haxelib.json 25 | for /f "tokens=*" %%a in ( 'findstr version haxelib.json' ) do (set textLine=%%a) 26 | set PROJECT_VERSION=%textLine:"version": "=% 27 | set PROJECT_VERSION=%PROJECT_VERSION:",=% 28 | set PROJECT_VERSION=%PROJECT_VERSION:"=% 29 | echo PROJECT_VERSION=%PROJECT_VERSION% 30 | 31 | REM extract project name from haxelib.json 32 | for /f "tokens=*" %%a in ( 'findstr name haxelib.json' ) do (set textLine=%%a) 33 | set PROJECT_NAME=%textLine:"name": "=% 34 | set PROJECT_NAME=%PROJECT_NAME:",=% 35 | set PROJECT_NAME=%PROJECT_NAME:"=% 36 | 37 | REM extract project description from haxelib.json 38 | for /f "tokens=*" %%a in ( 'findstr description haxelib.json' ) do (set textLine=%%a) 39 | set PROJECT_DESCRIPTION=%textLine:"description": "=% 40 | set PROJECT_DESCRIPTION=%PROJECT_DESCRIPTION:",=% 41 | set PROJECT_DESCRIPTION=%PROJECT_DESCRIPTION:"=% 42 | 43 | haxelib list | findstr dox >NUL 44 | if %errorlevel% neq 0 ( 45 | echo Installing [dox]... 46 | haxelib install dox 47 | ) 48 | 49 | if exist target\site ( 50 | echo Cleaning target\site... 51 | del target\doc.xml >NUL 52 | rd /s /q target\site 53 | ) 54 | 55 | echo Analyzing source code... 56 | haxe -cp src --no-output -D doc-gen -xml target\doc.xml --macro include('%TOP_LEVEL_PACKAGE%') || goto :eof 57 | 58 | REM https://github.com/HaxeFoundation/dox/wiki/Commandline-arguments-overview 59 | echo Generating HTML files... 60 | haxelib run dox ^ 61 | --title "%PROJECT_NAME% %PROJECT_VERSION% API documentation" ^ 62 | --toplevel-package "%TOP_LEVEL_PACKAGE%" ^ 63 | -D description "%PROJECT_NAME%: %PROJECT_DESCRIPTION%" ^ 64 | -D source-path "%REPO_URL%/tree/main/src" ^ 65 | -D themeColor 0x1690CC ^ 66 | -D version "%PROJECT_VERSION%" ^ 67 | -D website "%OWNER%" ^ 68 | -ex "^%TOP_LEVEL_PACKAGE:.=\.%\.internal" ^ 69 | -i target\doc.xml ^ 70 | -o target\site || goto :eof 71 | 72 | set INDEX_HTML=%CD%\target\site\index.html 73 | 74 | echo. 75 | echo Documentation generated at [file:///%INDEX_HTML:\=/%]... 76 | 77 | :eof 78 | popd 79 | endlocal 80 | -------------------------------------------------------------------------------- /tools/phantomJS/phantom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | phantomJS test 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tools/phantomJS/phantom.js: -------------------------------------------------------------------------------- 1 | // based on https://github.com/back2dos/travix/blob/master/src/travix/js/runPhantom.js 2 | var fs = require('fs'); 3 | var system = require('system'); 4 | var webpage = require('webpage'); 5 | 6 | var path = system.args[0].split('\\').join('/').split('/'); 7 | fs.changeWorkingDirectory(path.slice(0, path.length - 1).join('/')); 8 | 9 | var page = webpage.create(); 10 | 11 | page.onConsoleMessage = function(msg) { 12 | console.log(msg); 13 | }; 14 | 15 | page.onCallback = function(data) { 16 | if(!data) return; 17 | switch (data.cmd) { 18 | case 'doctest:exit': 19 | phantom.exit(data.exitCode); 20 | break; 21 | default: 22 | // ignore 23 | break; 24 | } 25 | } 26 | 27 | page.open("phantom.html", function (status) { 28 | if(status != "success") 29 | console.log("Loading " + url + "faild: " + status); 30 | }); 31 | -------------------------------------------------------------------------------- /tools/publish-release.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | REM creates a new release in GitHub and haxelib.org 7 | 8 | where zip.exe /Q 9 | if %errorlevel% neq 0 ( 10 | echo Required command 'zip' not found. Download from http://www.info-zip.org/Zip.html#Downloads 11 | exit /b 1 12 | ) 13 | 14 | where curl.exe /Q 15 | if %errorlevel% neq 0 ( 16 | echo Required command 'curl' not found. Download from https://curl.se/windows/ 17 | exit /b 1 18 | ) 19 | 20 | if [%GITHUB_ACCESS_TOKEN%] == [] ( 21 | echo Required environment variable GITHUB_ACCESS_TOKEN is not set! 22 | exit /b 1 23 | ) 24 | 25 | setlocal 26 | set DRAFT=false 27 | set PREPRELEASE=false 28 | 29 | REM cd into project root 30 | pushd . 31 | cd %~dp0.. 32 | 33 | REM extract GIT URL from haxelib.json 34 | for /f "tokens=*" %%a in ( 'findstr url haxelib.json' ) do (set textLine=%%a) 35 | set REPO_URL=%textLine:"url": "=% 36 | set REPO_URL=%REPO_URL:",=% 37 | set REPO_URL=%REPO_URL:"=% 38 | echo REPO_URL=%REPO_URL% 39 | 40 | REM extract repo name from haxelib.json 41 | set REPO_NAME=%REPO_URL:https://github.com/=% 42 | echo REPO_NAME=%REPO_NAME% 43 | 44 | REM extract project version from haxelib.json 45 | for /f "tokens=*" %%a in ( 'findstr version haxelib.json' ) do (set textLine=%%a) 46 | set PROJECT_VERSION=%textLine:"version": "=% 47 | set PROJECT_VERSION=%PROJECT_VERSION:",=% 48 | set PROJECT_VERSION=%PROJECT_VERSION:"=% 49 | echo PROJECT_VERSION=%PROJECT_VERSION% 50 | 51 | REM extract release note from haxelib.json 52 | for /f "tokens=*" %%a in ( 'findstr releasenote haxelib.json' ) do (set textLine=%%a) 53 | set RELEASE_NOTE=%textLine:"releasenote": "=% 54 | set RELEASE_NOTE=%RELEASE_NOTE:",=% 55 | set RELEASE_NOTE=%RELEASE_NOTE:"=% 56 | echo RELEASE_NOTE=%RELEASE_NOTE% 57 | 58 | if not exist target mkdir target 59 | 60 | REM create haxelib release 61 | if exist target\haxelib-upload.zip ( 62 | del target\haxelib-upload.zip 63 | ) 64 | echo Building haxelib release... 65 | zip target\haxelib-upload.zip src extraParams.hxml haxelib.json LICENSE.txt CONTRIBUTING.md README.md -r -9 || goto :eof 66 | 67 | REM create github release https://developer.github.com/v3/repos/releases/#create-a-release 68 | echo Creating GitHub release https://github.com/%REPO_NAME%/releases/tag/v%PROJECT_VERSION%... 69 | ( 70 | echo { 71 | echo "tag_name":"v%PROJECT_VERSION%", 72 | echo "name":"v%PROJECT_VERSION%", 73 | echo "target_commitish":"main", 74 | echo "body":"%RELEASE_NOTE%", 75 | echo "draft":%DRAFT%, 76 | echo "prerelease":%PREPRELEASE% 77 | echo } 78 | )>target\github_release.json 79 | curl -sSfL --header "Authorization: token %GITHUB_ACCESS_TOKEN%" -d @target/github_release.json "https://api.github.com/repos/%REPO_NAME%/releases" || goto :eof 80 | 81 | REM submit haxelib release 82 | echo Submitting haxelib release... 83 | haxelib submit target\haxelib-upload.zip 84 | 85 | :eof 86 | popd 87 | endlocal 88 | -------------------------------------------------------------------------------- /tools/test-cpp.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | call %~dp0_test-prepare.cmd cpp hxcpp 7 | 8 | echo Compiling... 9 | haxe %~dp0..\tests.hxml -D HXCPP_CHECK_POINTER -cpp target\cpp 10 | set rc=%errorlevel% 11 | popd 12 | if not %rc% == 0 exit /b %rc% 13 | 14 | echo Testing... 15 | "%~dp0..\target\cpp\TestRunner-Debug.exe" 16 | -------------------------------------------------------------------------------- /tools/test-cppia.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | call %~dp0_test-prepare.cmd cppia hxcpp 7 | 8 | echo Compiling... 9 | haxe %~dp0..\tests.hxml -D HXCPP_CHECK_POINTER -cppia "%~dp0..\target\cppia\TestRunner.cppia" 10 | set rc=%errorlevel% 11 | popd 12 | if not %rc% == 0 exit /b %rc% 13 | 14 | echo Testing... 15 | haxelib run hxcpp "%~dp0..\target\cppia\TestRunner.cppia" 16 | -------------------------------------------------------------------------------- /tools/test-cs.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | call %~dp0_test-prepare.cmd cs hxcs 7 | 8 | echo Compiling... 9 | haxe %~dp0..\tests.hxml -cs target\cs 10 | set rc=%errorlevel% 11 | popd 12 | if not %rc% == 0 exit /b %rc% 13 | 14 | echo Testing... 15 | mono "%~dp0..\target\cs\bin\TestRunner-Debug.exe" 16 | -------------------------------------------------------------------------------- /tools/test-eval.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | call %~dp0_test-prepare.cmd eval 7 | 8 | echo Testing... 9 | haxe %~dp0..\tests.hxml --interp -D eval-stack 10 | set rc=%errorlevel% 11 | popd 12 | if not %rc% == 0 exit /b %rc% 13 | -------------------------------------------------------------------------------- /tools/test-flash.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | call %~dp0_test-prepare.cmd flash 7 | 8 | echo Compiling... 9 | haxe %~dp0..\tests.hxml ^ 10 | -D no-swf-compress ^ 11 | -D swf-script-timeout=180 ^ 12 | -swf-version 11.5 ^ 13 | -swf target\flash\TestRunner.swf 14 | set rc=%errorlevel% 15 | popd 16 | if not %rc% == 0 exit /b %rc% 17 | 18 | REM enable Flash logging 19 | ( 20 | echo ErrorReportingEnable=1 21 | echo TraceOutputFileEnable=1 22 | ) > "%HOME%\mm.cfg" 23 | 24 | REM add the flash target directory as trusted source to prevent "Only trusted local files may cause the Flash Player to exit." 25 | call :normalize_path %~dp0..\target 26 | set target_dir_absolute=%RETVAL% 27 | set "fptrust_dir=%HOME%\AppData\Roaming\Macromedia\Flash Player\#Security\FlashPlayerTrust" 28 | REM https://stackoverflow.com/questions/905226/what-is-equivalent-to-linux-mkdir-p-in-windows 29 | setlocal enableextensions 30 | if not exist "%fptrust_dir%" ( md "%fptrust_dir%" ) 31 | endlocal 32 | ( 33 | echo %target_dir_absolute%\flash 34 | ) > "%fptrust_dir%\HaxeDoctest.cfg" 35 | 36 | echo Testing... 37 | for /f "delims=" %%A in ('where flashplayer_*_sa_debug.exe') do set "flashplayer_path=%%A" 38 | %flashplayer_path% "%~dp0..\target\flash\TestRunner.swf" 39 | set rc=%errorlevel% 40 | 41 | REM printing log file 42 | type "%HOME%\AppData\Roaming\Macromedia\Flash Player\Logs\flashlog.txt" 43 | 44 | exit /b %rc% 45 | 46 | :normalize_path 47 | SET RETVAL=%~dpfn1 48 | exit /b 49 | -------------------------------------------------------------------------------- /tools/test-hl.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | call %~dp0_test-prepare.cmd hl 7 | 8 | echo Compiling... 9 | haxe %~dp0..\tests.hxml -hl target\hl\TestRunner.hl 10 | set rc=%errorlevel% 11 | popd 12 | if not %rc% == 0 exit /b %rc% 13 | 14 | echo Testing... 15 | hl "%~dp0..\target\hl\TestRunner.hl" 16 | -------------------------------------------------------------------------------- /tools/test-java.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | call %~dp0_test-prepare.cmd java hxjava 7 | 8 | echo Compiling... 9 | haxe %~dp0..\tests.hxml -java target\java 10 | set rc=%errorlevel% 11 | popd 12 | if not %rc% == 0 exit /b %rc% 13 | 14 | echo Testing... 15 | java -jar "%~dp0..\target\java\TestRunner-Debug.jar" 16 | -------------------------------------------------------------------------------- /tools/test-js.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | call %~dp0_test-prepare.cmd js phantomjs 7 | 8 | echo Compiling... 9 | haxe %~dp0..\tests.hxml -js target\js\TestRunner.js 10 | set rc=%errorlevel% 11 | popd 12 | if not %rc% == 0 exit /b %rc% 13 | 14 | echo Testing [Execution in WegPage Context]... 15 | phantomjs "%~dp0phantomJS\phantom.js" 16 | -------------------------------------------------------------------------------- /tools/test-jvm.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | call %~dp0_test-prepare.cmd java hxjava 7 | 8 | echo Compiling... 9 | haxe %~dp0..\tests.hxml -D jvm -java target\java 10 | set rc=%errorlevel% 11 | popd 12 | if not %rc% == 0 exit /b %rc% 13 | 14 | echo Testing... 15 | java -jar "%~dp0..\target\java\TestRunner-Debug.jar" 16 | -------------------------------------------------------------------------------- /tools/test-lua.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | call %~dp0_test-prepare.cmd lua 7 | 8 | echo Compiling... 9 | haxe %~dp0..\tests.hxml -D luajit -lua target\lua\TestRunner.lua 10 | set rc=%errorlevel% 11 | popd 12 | if not %rc% == 0 exit /b %rc% 13 | 14 | echo Testing... 15 | lua "%~dp0..\target\lua\TestRunner.lua" 16 | -------------------------------------------------------------------------------- /tools/test-neko.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | call %~dp0_test-prepare.cmd neko 7 | 8 | echo Compiling... 9 | haxe %~dp0..\tests.hxml -neko target\neko\TestRunner.n 10 | set rc=%errorlevel% 11 | popd 12 | if not %rc% == 0 exit /b %rc% 13 | 14 | echo Testing... 15 | neko "%~dp0..\target\neko\TestRunner.n" 16 | -------------------------------------------------------------------------------- /tools/test-nodejs.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | call %~dp0_test-prepare.cmd js hxnodejs 7 | 8 | echo Compiling... 9 | haxe %~dp0..\tests.hxml -lib hxnodejs -D nodejs -js target\js\TestRunner.js 10 | set rc=%errorlevel% 11 | popd 12 | if not %rc% == 0 exit /b %rc% 13 | 14 | echo Testing... 15 | node "%~dp0..\target\js\TestRunner.js" 16 | -------------------------------------------------------------------------------- /tools/test-phantomjs.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | call %~dp0_test-prepare.cmd js phantomjs 7 | 8 | echo Compiling... 9 | haxe %~dp0..\tests.hxml -lib phantomjs -js target\js\TestRunner.js 10 | set rc=%errorlevel% 11 | popd 12 | if not %rc% == 0 exit /b %rc% 13 | 14 | echo Testing [Direct Execution]... 15 | phantomjs "%~dp0..\target\js\TestRunner.js" 16 | -------------------------------------------------------------------------------- /tools/test-php.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | call %~dp0_test-prepare.cmd php 7 | 8 | echo Compiling... 9 | haxe %~dp0..\tests.hxml -php target\php 10 | set rc=%errorlevel% 11 | popd 12 | if not %rc% == 0 exit /b %rc% 13 | 14 | echo Testing... 15 | %PHP7_HOME%\php "%~dp0..\target\php\index.php" 16 | -------------------------------------------------------------------------------- /tools/test-python.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com) and contributors 3 | REM SPDX-FileContributor: Sebastian Thomschke, Vegard IT GmbH 4 | REM SPDX-License-Identifier: Apache-2.0 5 | 6 | call %~dp0_test-prepare.cmd python 7 | 8 | echo Compiling... 9 | haxe %~dp0..\tests.hxml -python target\python\TestRunner.py 10 | set rc=%errorlevel% 11 | popd 12 | if not %rc% == 0 exit /b %rc% 13 | 14 | echo Testing... 15 | python "%~dp0..\target\python\TestRunner.py" 16 | --------------------------------------------------------------------------------