├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── assets ├── DynamicTextField │ ├── RotationJoint.png │ └── folderholder ├── MathTextField │ ├── math-bold.ttf │ └── math-regular.ttf └── TextTools │ ├── sans.ttf │ └── serif.ttf ├── compile.hxml ├── haxelib.json ├── hxformat.json ├── include.xml └── src ├── .gitignore ├── BidiTools.hx ├── Main.hx ├── TextTools.hx └── texter ├── flixel ├── FlxInputTextRTL.hx ├── FlxSuperText.hx ├── FlxTextButton.hx └── _internal │ └── FlxInputText.hx ├── general ├── Char.hx ├── CharTools.hx ├── Emoji.hx ├── TextTools.hx ├── bidi │ ├── Bidi.hx │ └── TextAttribute.hx ├── markdown │ ├── Markdown.hx │ ├── MarkdownBlocks.hx │ ├── MarkdownEffect.hx │ ├── MarkdownPatterns.hx │ └── MarkdownVisualizer.hx └── math │ ├── MathAttribute.hx │ └── MathLexer.hx └── openfl ├── DynamicTextField.hx ├── MathTextField.hx ├── TextFieldRTL.hx └── _internal ├── DrawableTextField.hx ├── JointGraphic.hx ├── JointManager.hx └── TextFieldCompatibility.hx /.gitignore: -------------------------------------------------------------------------------- 1 | export 2 | .vscode 3 | project.xml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2.4.0 2 | === 3 | 4 | **TextTools:** 5 | 6 | - added `insert()` 7 | - deprecated `indexesFromArray()` 8 | - added `indexesOfSubs()` as a replacement - replaced for a cleaner, more understandable name. 9 | - added `fonts` field, containing sans & serif, multilingual fonts. 10 | - fixes `loremIpsum()`not taking the extra `length` argument into account 11 | - fixed documentation formatting & typos 12 | - added missing documentation to `remove()`, `replace()`, `contains()`, `reverse()` 13 | 14 | 15 | **MathTextField - new Class!** 16 | 17 | this class provides the ability to display mathematical forms of strings. no special syntax is required (e.g. this: `f(x) = 2x + 5` is valid) 18 | 19 | **MathLexer - new Class!** 20 | 21 | `MathLexer` is an "internal" class, used by mathematical displays inside the `texter` library to display mathematical expressions in a more advanced and natural way. Theres nothing preventing you from using it yourself, but it might not be as straight forward as just getting some text and putting it through a function. If you want to make a mathematical expression display of your own, you can look at the implementations that already exist inside this library. 22 | 23 | - added `getMathAttributes` 24 | - added `splitBlocks` 25 | - added `reorderAttributes` 26 | - added `resetAttributesOrder` 27 | - added `removeDuplicates` 28 | - added `condenseWhitespaces` 29 | - added `extractTextFromAttributes` 30 | 31 | 32 | **DynamicTextField:** 33 | 34 | - added `borderSize` 35 | - added `virtualBorderSize` 36 | - added missing implementation for `resizable` 37 | - fixed weird behavior when dragging the sides of the text field, while its rotated 38 | 39 | **TextFieldRTL** 40 | 41 | - removed deprecation notice. this class is repurposed :) 42 | - all selection related bugs should not appear anymore. 43 | - now relies on an external, BiDi algorithm. 44 | 45 | 2.3.2 (September 5th, 2022) 46 | === 47 | 48 | - fixed allRtlLetters containing whitespaces 49 | - fixed FlxInputTextRTL assuming RTL direction 50 | - fixed FlxInputTextRTL backspace not working correctly 51 | - fixed FlxInputTextRTL typing getting messy after typing a whitespaces 52 | 53 | 2.3.1 (September 4th, 2022) 54 | === 55 | 56 | - fix for FlxInputTextRTL and TextFieldRTL typing letters incorrectly 57 | 58 | 2.3.0 (August 26th, 2022) 59 | === 60 | 61 | **Bidi - new class!** 62 | 63 | The `Bidi` class provides methods that help getting directional information from a string: 64 | 65 | - `process(text:String)` - runs the Bidi algorithm on `text`. if the resulting string will be re-processed by the algorithm, the text should still remain correct. 66 | - `unbidify(text:String)` - runs the Bidi algorithms with some changes, "reversing" the effect on the initial Bidi processing. 67 | - `getTextAttributes(text:String)` - more of an internal function, but can be used to get an "AST" of the text's directional components. 68 | - `processTextAttributes(attributes:Array)` - takes an array of attributes (the AST) itself, and gets the correctly bidified text out of it. 69 | 70 | 71 | **TextAttribute** 72 | 73 | In addition to the `Bidi` class, this enum was added: 74 | 75 | - `Bidified` 76 | - `LineDirection(letterType:TextDirection);` 77 | - `Rtl(string:String)` 78 | - `Ltr(string:String)` 79 | - `SoftChar(string:String, generalDirection:TextDirection)` 80 | - `LineEnd()` 81 | 82 | `Bidified` serves no purpose, but to tell the bidi processor which text has been processed and which text has'nt. 83 | 84 | 85 | **BidiTools - new class!** 86 | 87 | BidiTools is a class containing cross framework tools to work with Bidi texts. currently, only OpenFL is supported, but more frameworks will be added in the future 88 | if requested/PRed. it currently has 2 methods: 89 | 90 | - `bidifyString()` - similar to `Bidi.process()` 91 | - `attachBidifier()` ` + 2 overloads` - attaches an engine, which eases working with Bidi texts in text fields. currently supports text fields of type: 92 | - `openfl.text.TextField` 93 | - `texter.openfl.DynamicTextField` 94 | - `texter.openfl.DynamicTextField` 95 | 96 | 97 | **CharTools** 98 | 99 | - added `allRtlLetters` - an array of all letters written RTL. this also contains more "obscure" languages, like aramaic. 100 | - added `softChars` 101 | - added `isRTL()` 102 | - added `isSoft()` - checks if a char is whithout a specific direction 103 | 104 | 105 | **TextTools** 106 | 107 | - added `remove()` 108 | - added `replace()` 109 | - added `reverse()` 110 | 111 | **DynamicTextField** 112 | 113 | - added `onDragged` - a callback, triggered whenever the textfield stopped dragging. contains previous, and current position. 114 | - added `onResized` - a callback, triggered whenever the textfield has been resized. contains previous, and current dimensions & position. 115 | - added `onRotated` - a callback, triggered whenever the textfield has been rotated. contains previous and current rotation, in degrees. Notice - this accounts for rotation around center. 116 | - added conversion methods from textField sizes to object sizes: 117 | - `textFieldWidthToObjectWidth()` 118 | - `textFieldHeightToObjectWidth()` 119 | - `objectWidthToTextFieldWidth()` 120 | - `objectHeightToOTextFieldWidth()` 121 | - rotation joint now has a better graphic. if not rendered, copy it from `assets/DynamicTextField` into you project, at `assets/texter/DynamicTextField` 122 | 123 | 2.2.0 (August 16, 2022) 124 | === 125 | 126 | **DynamicTextField - new class!** 127 | 128 | `DynamicTextField` is a new class that allows you to create a text field that can dynamically resize, move, and rotate. This class is kind of a superset of the TextField class. 129 | 130 | properties: 131 | 132 | - `resizable` 133 | - `draggable` 134 | - `rotatable` 135 | - `currentlyDragging` 136 | - `matchTextSize` 137 | - `jointGraphics` 138 | - `hasFocus` 139 | - `textFieldWidth` 140 | - `textFieldHeight` 141 | - `textField` 142 | - `hideControlsWhenUnfocused` 143 | - (other `TextField` properties and functions) 144 | 145 | **MarkdownVisualizer** 146 | 147 | - added `darkMode` property to `VisualConfig` 148 | 149 | 150 | 2.1.0 (May 5, 2022) 151 | === 152 | 153 | **TextTools:** 154 | 155 | - added `splitOnParagraph()` 156 | - added `multiply()` to repeat a string a number of times 157 | - added `subtract()` to remove the last occurrence of a string from a string 158 | - added `sortByLength()` to sort a list of strings by their length 159 | - added `sortByValue()` to sort a list of floats by their value 160 | - added `sortByIntValue()` to sort a list of integers by their value 161 | - added `getLineIndexOfChar()` to get the line index of a substring in a string 162 | - added `contains()` to check if a string contains a substring 163 | - added `countOccurrencesOf()` to count the number of occurrences of a substring in a string 164 | - added `loremIpsumText` property to `TextTools` 165 | - moved `src.texter.general.TextTools` to `src.TextTools` 166 | 167 | 2.0.4 (April 18, 2022) 168 | === 169 | 170 | - fixed inability to compile to c++ 171 | - renovated FlxInputText, should now uniformly support multiline text across platforms 172 | - fixed visual glitches when visualizing markdown alignment 173 | 174 | 2.0.2 & 2.0.3 (April 13, 2022) 175 | === 176 | 177 | **Markdown:** 178 | 179 | added more supported markup to the markdown interpreter: 180 | 181 | - `\t` - tab 182 | - `` - alignment 183 | - ` ` or `\` at the end of the line - newline character 184 | 185 | fixed interpreter faults with `ParagraphGap` 186 | 187 | **MarkdownPatterns:** 188 | 189 | added a couple more patterns to match markdown's markup: 190 | 191 | - `doubleSpaceNewlineEReg` 192 | - `backslashNewlineEReg` 193 | - `alignmentEReg` 194 | - `linkEReg` (fix) 195 | 196 | **TextFieldRTL** 197 | 198 | - removed unused imports 199 | 200 | 2.0.1 (April 13, 2022) 201 | === 202 | 203 | - fixed some README markup issues & mistakes 204 | - minimal re-write of FlxInputText, should now fully work with touch devices 205 | - removed framework specific import from MarkdownVIsualizer, preventing the library from working without OpenFL 206 | - fixed documentation 207 | 208 | 209 | 2.0.0 (April 12, 2022) - Major Update! 210 | === 211 | I promised for every major update to have a new framework supported, and the new framework is 212 | ### 🥁 213 | ### 🥁 214 | ### 🥁 215 | **OpenFL!** 216 | ### **New Features:** 217 | 218 | **TextFieldRTL - new class!** 219 | `TextFieldRTL` is an "extention" of `TextField` that adds support for multiple things, such as **right-to-left text** and **built-in markdown visualization**. 220 | 221 | It also adds some convenience methods & fields for working with the object, that `TextField` doesn't have. 222 | 223 | - added `autoAlign` property - aligns the text according to the first strongly typed character 224 | - added `openingDirection` (read-only) - specifies the base direction of the text 225 | - added `alignment` property, similar to `autoSize` but more understandable 226 | - added `overlay` property, you can now draw on top of the text 227 | - added `underlay` property, you can now draw below the text 228 | - added `markdownText` property - you can set this to make the text display things in markdown format 229 | - `caretIndex` is now an editable property 230 | - added `hasFocus` property for easy focus access 231 | - added `insertSubstring()` 232 | - added `getCaretIndexAtPoint()` 233 | - added `getCaretIndexOfMouse()` 234 | - RTL text input is now supported on platforms other then the web 235 | - the text selection's background now has a nicer & more natural look 236 | - extended markdown visualization support. also supports: 237 | - Horizontal Rules 238 | - Code Background 239 | - Strikethrough 240 | 241 | 242 | **CharTools:** 243 | 244 | - added `charFromValue` map 245 | - added `charToValue` map 246 | - `fromCharArray` and `toCharArray` now use `Char`s instead of `String`s 247 | 248 | 249 | **Markdown - new class! features:** 250 | 251 | - added a field that gives access to the visualizer - `visualizer` 252 | - added access to all markdown patterns via `Markdown.patterns`. more information in `MarkdownPatterns` 253 | - added `syntaxBlocks` field - you can redefine highlight parsers there. 254 | - added `interpret()` - a cross-platform, cross-framework markdown interpreter based on ADTs (algebric data types) from `MarkdownEffect.hx` 255 | - added `visualizeMarkdown` - a (soon to be) cross-framework method to display markdown visuals 256 | 257 | **MarkdownPatterns - new class!** 258 | 259 | `MarkdownPatterns` is a class consisting of the following markdown patterns: (more will be added in the future) 260 | 261 | - `hRuledTitleEReg` 262 | - `linkEReg` 263 | - `codeEReg` 264 | - `codeblockEReg` 265 | - `tildeCodeblockEReg` 266 | - `tabCodeblockEReg` 267 | - `imageEReg` 268 | - `listItemEReg` 269 | - `unorderedListItemEReg` 270 | - `titleEReg` 271 | - `hRuleEReg` 272 | - `astBoldEReg` 273 | - `boldEReg` 274 | - `strikeThroughEReg` 275 | - `italicEReg` 276 | - `astItalicEReg` 277 | - `mathEReg` 278 | - `parSepEReg` 279 | - `emojiEReg` 280 | - `indentEReg` 281 | 282 | **MarkdownVisualizer - new class!** 283 | 284 | `MarkdownVisualizer` is a class consisting of the framework-specific markdown visualization methods. For now, only supports visualization for: 285 | 286 | - OpenFL (via `TextField`, `TextFieldRTL`) 287 | 288 | **MarkdownBlocks - new class!** 289 | 290 | `MarkdownBlocks` is the class that handles the code block's syntax highlighting in markdown. 291 | It provides a user friendly way to edit the syntax, and all syntax handlers can be redefined with `MarkdownBlocks.parseLang = function(...)` 292 | 293 | For now, syntax highlighting is only available (out-of-the-box) for theses languages: 294 | 295 | - JSON 296 | - Haxe 297 | - C# 298 | - C 299 | 300 | More will be added in the future :) 301 | 302 | **TextTools - new class!** 303 | 304 | `TextTools` is a class containing static methods for manipulating text. it contains: 305 | 306 | - `replaceFirst()` - replaces the first occurrence of a substring inside a string 307 | - `replaceFirst()` - replaces the last occurrence of a substring inside a string 308 | - `filter()` - filters a string according to the `EReg` or `String` supplied 309 | - `multiply()` - multiplies a string by `X` times 310 | - `indexesOf()` finds and reports all occurrences of a substring inside a string 311 | - `indexesFromArray()` finds and reports all occurrences of the supplied substrings inside a string 312 | - `indexesFromEReg()` finds and reports all occurences of the findings of a regex pattern in a string. 313 | 314 | **Emoji - new class!** 315 | 316 | the `Emoji` class is pretty simple, yet powerful. it has mapping for very single emoji that github support: 317 | 318 | - :joy: turns into 😂 319 | - :flushed: turns into 😳 320 | 321 | 322 | etc. 323 | 324 | I want to thank **PXShadow** for providing the map itself and making this possible 325 | 326 | ### Bug Fixes: 327 | 328 | **FlxInputTextRTL:** 329 | 330 | - removed bulky and old code 331 | - fixed lag spikes when the textfield is selected for a long time 332 | 333 | **FlxInputText** 334 | - fixed first char nor disappearing after deleting all of the text (JS) 335 | - fixed multiline crashing the app on JS 336 | - fixed weird bugs with the height's consistency 337 | - fixed horizontal scrolling behaving weirdly on multiline text 338 | 339 | 1.1.4 (March 20, 2022) 340 | === 341 | ### **Bug Fix:** 342 | - fixed pasting an image/empty text from the clipboard adding `null` to the text 343 | 344 | 1.1.3 (March 20, 2022) 345 | === 346 | ### **New Features:** 347 | 348 | - moved `CharTools` and `WordWrapper` into the folder `general` 349 | 350 | ### FlxInputTextRTL 351 | 352 | - added support for pasting text from the clipboard (LTR text only) 353 | - added event dispatching for when `home` and `end` buttons are pressed 354 | - `getCharBoundaries` is now a public field, and was optimized a bit more to report more acurate bounds 355 | - `getCaretIndexAtPoint` is now a public field and should be more accurate 356 | 357 | 358 | ### **Bug Fixes:** 359 | 360 | ### FlxInputTextRTL 361 | 362 | - fixed sound tray activating when pressing `-` or `=` while having focus 363 | - fixed `getCharBoundaries` crashing when the text contains only `space` chars & `enter`s 364 | 365 | 366 | 1.1.2 (February 29, 2022) 367 | === 368 | 369 | ### **Bug Fixes:** 370 | 371 | - removed testing files 372 | - fixed incorrect markdown syntax in CHANGELOG 373 | - `About Copying` in `README.md` has been removed since it isn't relevant 374 | - `LICENSE` file was changed to `LICENSE.md` 375 | 376 | 1.1.1 (February 28, 2022) 377 | === 378 | 379 | ### **New Features:** 380 | 381 | ### FlxInputTextRTL 382 | 383 | - Added field `openingDirection` to get the base direction of the text. is not related to `alignment` 384 | 385 | ### CharTools 386 | 387 | - Added an `generalMarks` - an `Array` of all **common** text marks (math/grammer characters) 388 | 389 | 390 | ### **Bug Fixes:** 391 | 392 | ### FlxInputTextRTL 393 | 394 | - Fixed `getCaretIndexAtPoint()` reporting incorrect index when pressing between the lines of text 395 | - Fixed a crash where `getCharBoundaries()` reports null for `rect.width` 396 | - Fixed a crash after trying to wordwrap lots of `spacebar`s 397 | - Fixed enter alignment being incorrect when the text is aligned to the right 398 | - Fixed `spacebar` not moving when switching between languages of different direction 399 | - Fixed `getCharBoundaries()` reporting inaccurate dimensions when pressing `spacebar` 400 | - Fixed text aligning & "sticking" to the left when softly typed chars are being typed 401 | - Fixed a crash when pressing between word-wrapped lines 402 | - Fixed misplacing of punctuation marks in RTL languages 403 | - Fixed caret sticking to the outline of the input text when `text = ""` 404 | - Fixed `up` & `down` keys behaving incorrectly 405 | - Fixed caret being **graphically** misplaces when lots of `spacebar`s are being typed 406 | - Fixed textbox cutting "tall" letters (`l`, `j`, `f`, `t`...) 407 | 408 | ### CharTools 409 | 410 | - Changed `numericChars`'s `EReg` to `~/[0-9]/g` 411 | 412 | 413 | 1.1.0 (February 24, 2022) 414 | === 415 | ### **New Features:** 416 | 417 | ### CharTools 418 | 419 | - added chars for text direction manipulation. 420 | - added more regular chars 421 | - added documentation 422 | 423 | ### FlxTextButton 424 | 425 | - class has been reworked and extra fields were added 426 | - documentation has been added to all class methods & fields 427 | - now extends `FlxSpriteGroup` to support more label types. 428 | - will also use `FlxInputTextRTL` at its core to support input for the button's text. 429 | 430 | ### FlxInputTextRTL 431 | 432 | - added `autoAlign` field to enable alignment based on the first char 433 | - text background now matches the size of the font isntead of being a fixed size 434 | 435 | ### README 436 | 437 | - fixed typos 438 | - moved the roadmap to `Roadmaps.md` 439 | 440 | 441 | ### **Bug Fixes:** 442 | 443 | ### FlxInputTextRTL 444 | 445 | - didnt support `enter` callback, now supported. 446 | - `enter` button on RTL languages now behaves correctly. 447 | - BiDi on non-JS platform now behaves correctly 448 | - fixed caret positioning reseting to (0,0) when pressing enter 449 | - fixed caret positioning reseting to (0,0) when pressing spacebar twice in a row 450 | - fixed a crash when pressing spacebar twice and then enter 451 | - fixed text background expanding too far vertically when pressing enter 452 | - `getCharBoundaries()` is now supposed to report accurate boundaries for the specified char 453 | - fixed text input slowdown when many chars are displayed 454 | 455 | 456 | 1.0.0 (February 21, 2022) - **Official Release!** 457 | === 458 | ### **New Features:** 459 | 460 | ### CharTools - new class! features: 461 | 462 | - added an EReg of RTL letters 463 | - added an EReg of numeric chars 464 | 465 | ### FlxInputTextRTL** - new class! features: 466 | 467 | - added multi-language support 468 | - added RTL support 469 | - added BiDi support (Bi-Directional text support) 470 | - added support for more unicodes 471 | 472 | ### FlxTextButton** - **new class!** features: 473 | 474 | - simplified button methods & fields with FlxText base 475 | - easy button disabling/enabling 476 | 477 | 478 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ShaharMS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # texter 2 | 3 | ## I'll start with a story 4 | 5 | About 5 month ago, just a month after I started programming in haxeflixel, I wanted to make an app that needed text input, specificly of type RTL. 6 | 7 | for about 2 months I tried to find some existing (decent) RTL support, but didn't find any that were good enough. 8 | 9 | ### It was the time I decided to take this duty upon myself - to add more support for text input (in that time - only in haxeflixel) 10 | 11 | It might seem like I'm exaggerating, but trust me, it took me a **while** to make progress, but when I did, I started making (good) progress. 12 | I figured that I'm not the only person that needs those fixes, **and thats how and why I created this library.** 13 | 14 | ### Over-time, I wanted to add more things to the library 15 | 16 | A month or so after the release of this library, I found myself needing a bunch more things - (markdown text, Char...) and this library expanded and expanded. 17 | 18 | Today, it is no longer only an RTL-support focused library, but a general-purpose text-related library. 19 | 20 | Does this mean I abandoned the project of cross-framework RTL? Of course not - but now, the library is **W A Y** more useful, providing visual markdown text, cross-platform cross-framework markdown interpreter, Emojis and more to come! 21 | 22 | 23 | ### **Can I Help/Contribute?** 24 | Of course! Any help is greatly appreciated! You can help with: 25 | - Fixing bugs 26 | - Writing/fixing documentation 27 | - Making code more readable, simpler & shorter (don't worry, I think my code is pretty understandable ;) ) 28 | - Writing code for the library 29 | - Adding projects that you think are useful 30 | 31 | And more that pops up in you mind! 32 | 33 | # Installation 34 | 35 | #### To install the latest stable version: 36 | ``` 37 | haxelib install texter 38 | ``` 39 | 40 | #### To install the newer - but maybe unstable git version: 41 | ``` 42 | haxelib git texter https://github.com/ShaharMS/texter.git 43 | ``` 44 | -------------------------------------------------------------------------------- /assets/DynamicTextField/RotationJoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaharMS/texter/21cbcfff60587b3b343ccfcc70d73146702200f8/assets/DynamicTextField/RotationJoint.png -------------------------------------------------------------------------------- /assets/DynamicTextField/folderholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaharMS/texter/21cbcfff60587b3b343ccfcc70d73146702200f8/assets/DynamicTextField/folderholder -------------------------------------------------------------------------------- /assets/MathTextField/math-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaharMS/texter/21cbcfff60587b3b343ccfcc70d73146702200f8/assets/MathTextField/math-bold.ttf -------------------------------------------------------------------------------- /assets/MathTextField/math-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaharMS/texter/21cbcfff60587b3b343ccfcc70d73146702200f8/assets/MathTextField/math-regular.ttf -------------------------------------------------------------------------------- /assets/TextTools/sans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaharMS/texter/21cbcfff60587b3b343ccfcc70d73146702200f8/assets/TextTools/sans.ttf -------------------------------------------------------------------------------- /assets/TextTools/serif.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaharMS/texter/21cbcfff60587b3b343ccfcc70d73146702200f8/assets/TextTools/serif.ttf -------------------------------------------------------------------------------- /compile.hxml: -------------------------------------------------------------------------------- 1 | --class-path src 2 | --main Main 3 | --interp 4 | --library lime 5 | --library openfl 6 | --define reordered_attributes=1 -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "texter", 3 | "url" : "https://github.com/ShaharMS/texter", 4 | "license": "MIT", 5 | "tags": ["text", "input", "rtl", "ltr", "char", "textinput", "flixel", "openfl", "bidi", "hebrew", "arabic", "markdown", "textfield", "FlxInputTextRTL", "dynamic", "string"], 6 | "description": "Advanced text tools for Haxe game engines & frameworks.", 7 | "version": "2.3.0", 8 | "classPath": "src/", 9 | "releasenote": "DynamicTextField improvements, Complete Bidi algorithm implementation. More details in CHANGELOG.md", 10 | "contributors": [ "ShaharMS" ] 11 | } -------------------------------------------------------------------------------- /hxformat.json: -------------------------------------------------------------------------------- 1 | { 2 | "lineEnds": { 3 | "leftCurly": "both", 4 | "rightCurly": "both", 5 | "objectLiteralCurly": { 6 | "leftCurly": "after" 7 | } 8 | }, 9 | "sameLine": { 10 | "ifElse": "next", 11 | "doWhile": "next", 12 | "tryBody": "next", 13 | "tryCatch": "next" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /include.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 |
6 | 7 | 8 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | Main.hx -------------------------------------------------------------------------------- /src/BidiTools.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | #if openfl 4 | import texter.openfl.TextFieldRTL; 5 | import texter.openfl.DynamicTextField; 6 | import openfl.text.TextField; 7 | import openfl.events.FocusEvent; 8 | import openfl.events.Event; 9 | import lime.system.Clipboard; 10 | import openfl.events.TextEvent; 11 | import lime.ui.KeyModifier; 12 | import lime.ui.KeyCode; 13 | #end 14 | import texter.general.CharTools; 15 | import texter.general.bidi.Bidi; 16 | 17 | using TextTools; 18 | 19 | /** 20 | This class provides useful tools to add support for Right-to-Left Texts. 21 | 22 | to use it, I recommand adding the following line to the top of your file: 23 | 24 | using BidiTools; 25 | 26 | **/ 27 | #if openfl 28 | @:access(openfl.text.TextField) 29 | @:access(openfl.text._internal.TextEngine) 30 | #end 31 | class BidiTools 32 | { 33 | /** 34 | Returns the correct form a bidirectional text script should be represented: 35 | 36 | | Expression | Becomes | 37 | | ---------- | ------- | 38 | | "ltr" | "ltr" | 39 | | "לאמשל ןימי" | "ימין לשמאל" | 40 | | " - לאמשל ןימי" | "ימין לשמאל - " | 41 | 42 | #### Things to know 43 | - you can combine rtl and ltr text 44 | - You can process the same text twice, prefixed or postfixed, and you will get the same result, or a corrected one for the extra chars. insertions and deletions are not corrected. 45 | - numbers might change position, but that happens to make the text more readable 46 | 47 | @param text the text to be processed. 48 | **/ 49 | public static function bidifyString(text:String) 50 | { 51 | return Bidi.process(text); 52 | } 53 | 54 | #if openfl 55 | public static function __attachOpenFL(textfield:TextField):Void 56 | { 57 | function getBidified(text:String):String 58 | { 59 | var processed = text; 60 | // var offset = 0; 61 | // for (i in 1...text.length) { 62 | // var previousCharPos = textfield.getCharBoundaries(i + offset - 1); 63 | // var charPos = textfield.getCharBoundaries(i + offset); 64 | // if (charPos.y > previousCharPos.y && text.charAt(i + offset - 1) != "\n") { 65 | // processed.insert("\n", i + offset); 66 | // offset++; 67 | // } 68 | // } 69 | return Bidi.process(processed); 70 | } 71 | 72 | var unbidified:String = textfield.text; 73 | var nText = new TextField(); 74 | function displayChanges(e:Event) 75 | { 76 | nText.defaultTextFormat = textfield.defaultTextFormat; 77 | nText.width = textfield.width; 78 | nText.x = textfield.x; 79 | nText.y = textfield.y + textfield.height; 80 | nText.height = textfield.height; 81 | nText.text = getBidified(textfield.text); 82 | } 83 | 84 | function displayBidifiedBelow(e) 85 | { 86 | var tf = textfield.defaultTextFormat; 87 | tf.align = if (CharTools.isRTL(textfield.text.charAt(0)) && tf.align != "center") "right" else tf.align; 88 | textfield.text = unbidified; 89 | nText.defaultTextFormat = textfield.defaultTextFormat; 90 | nText.width = textfield.width; 91 | nText.x = textfield.x; 92 | nText.y = textfield.y + textfield.height; 93 | nText.height = textfield.height; 94 | nText.multiline = textfield.multiline; 95 | nText.wordWrap = textfield.wordWrap; 96 | nText.text = getBidified(textfield.text); 97 | textfield.parent.addChild(nText); 98 | textfield.addEventListener(Event.CHANGE, displayChanges); 99 | } 100 | 101 | function applyBidified(e) 102 | { 103 | unbidified = textfield.text; 104 | textfield.text = nText.text; 105 | textfield.parent.removeChild(nText); 106 | } 107 | 108 | function invoke(fromEvent = false, e:Event) 109 | { 110 | if (fromEvent) 111 | textfield.removeEventListener(Event.ADDED_TO_STAGE, invoke.bind(true)); 112 | textfield.addEventListener(FocusEvent.FOCUS_IN, displayBidifiedBelow); 113 | textfield.addEventListener(FocusEvent.FOCUS_OUT, applyBidified); 114 | } 115 | 116 | if (textfield.stage == null) 117 | { 118 | textfield.addEventListener(Event.ADDED_TO_STAGE, invoke.bind(true)); 119 | } 120 | else 121 | { 122 | invoke(false, null); 123 | } 124 | } 125 | 126 | public static function __attachLiveOpenFL(textfield:TextField):Void 127 | { 128 | var startingLetter = ""; 129 | var currentlyOppositeDirection = false; 130 | final getStartingDirection = () -> 131 | { 132 | return CharTools.isRTL(startingLetter) ? RTL : LTR; 133 | } 134 | final setOppositeDirection = () -> 135 | { 136 | trace((CharTools.isRTL(textfield.text.charAt(textfield.caretIndex)) 137 | || (currentlyOppositeDirection && textfield.text.charAt(textfield.caretIndex) == " ") 138 | || (CharTools.generalMarks.contains(textfield.text.charAt(textfield.caretIndex)) && currentlyOppositeDirection))); 139 | return currentlyOppositeDirection = (CharTools.isRTL(textfield.text.charAt(textfield.caretIndex)) 140 | || (currentlyOppositeDirection && textfield.text.charAt(textfield.caretIndex) == " ") 141 | || (CharTools.generalMarks.contains(textfield.text.charAt(textfield.caretIndex)) && currentlyOppositeDirection)); 142 | } 143 | function manageTextInput(letter:String) 144 | { 145 | // if the user didnt intend to edit the text, dont do anything 146 | if (textfield.stage.focus != textfield) 147 | return; 148 | // if the caret is broken for some reason, fix it 149 | if (textfield.caretIndex < 0) 150 | textfield.setSelection(0, 0); 151 | // set up the letter - remove null chars, add rtl mark to letters from RTL languages 152 | var t:String = "", 153 | hasConverted:Bool = false, 154 | addedSpace:Bool = false; 155 | #if !js 156 | if (letter != null) 157 | { 158 | if (getStartingDirection() == LTR) 159 | { 160 | // logic for general RTL letters, spacebar, punctuation marks 161 | if (CharTools.isRTL(letter) 162 | || (currentlyOppositeDirection && letter == " ") 163 | || (CharTools.generalMarks.contains(letter) && currentlyOppositeDirection)) 164 | { 165 | t = CharTools.RLO + letter; 166 | currentlyOppositeDirection = true; 167 | } 168 | // logic for when the user converted from RTL to LTR 169 | else if (currentlyOppositeDirection) 170 | { 171 | t = letter; 172 | currentlyOppositeDirection = false; 173 | hasConverted = true; 174 | 175 | // after conversion, the caret needs to move itself to he end of the RTL text. 176 | // the last spacebar also needs to be moved 177 | if (textfield.text.charAt(textfield.caretIndex) == " ") 178 | { 179 | t = CharTools.PDF + " " + letter; 180 | textfield.text = textfield.text.substring(0, textfield.caretIndex) 181 | + textfield.text.substring(textfield.caretIndex + 1, textfield.text.length); 182 | addedSpace = true; 183 | } 184 | textfield.setSelection(textfield.caretIndex + 1, textfield.caretIndex + 1); 185 | 186 | while (CharTools.isRTL(textfield.text.charAt(textfield.caretIndex)) 187 | || CharTools.isSoft(textfield.text.charAt(textfield.caretIndex)) 188 | && textfield.caretIndex < textfield.text.length) 189 | textfield.setSelection(textfield.caretIndex + 1, textfield.caretIndex + 1); 190 | } 191 | // logic for everything else - LTR letters, special chars... 192 | else 193 | { 194 | t = letter; 195 | } 196 | } 197 | else 198 | { 199 | // logic for general RTL letters, spacebar, punctuation marks 200 | if (CharTools.isLTR(letter) 201 | || (currentlyOppositeDirection && letter == " ") 202 | || (CharTools.generalMarks.contains(letter) && currentlyOppositeDirection)) 203 | { 204 | t = letter; 205 | currentlyOppositeDirection = true; 206 | } 207 | // logic for when the user converted from RTL to LTR 208 | else if (currentlyOppositeDirection) 209 | { 210 | t = letter; 211 | currentlyOppositeDirection = false; 212 | hasConverted = true; 213 | 214 | // after conversion, the caret needs to move itself to he end of the RTL text. 215 | // the last spacebar also needs to be moved 216 | if (textfield.text.charAt(textfield.caretIndex - 1) == " ") 217 | { 218 | t = letter + " "; 219 | textfield.text = textfield.text.substring(0, textfield.caretIndex - 1) 220 | + textfield.text.substring(textfield.caretIndex, textfield.text.length); 221 | addedSpace = true; 222 | } 223 | textfield.setSelection(textfield.caretIndex - 1, textfield.caretIndex - 1); 224 | 225 | while (textfield.caretIndex > 0 226 | && (CharTools.isLTR(textfield.text.charAt(textfield.caretIndex - 1)) 227 | || CharTools.isSoft(textfield.text.charAt(textfield.caretIndex - 1)))) 228 | { 229 | textfield.setSelection(textfield.caretIndex - 1, textfield.caretIndex - 1); 230 | } 231 | } 232 | // logic for everything else - LTR letters, special chars... 233 | else 234 | { 235 | t = CharTools.RLO + letter; 236 | } 237 | } 238 | } 239 | else 240 | ""; 241 | #else 242 | t = letter; 243 | #end 244 | if (t.length > 0 && (textfield.maxChars == 0 || (textfield.text.length + t.length) < textfield.maxChars)) 245 | { 246 | if (!CharTools.isSoft(letter) && (textfield.text.length == 0 || startingLetter == "")) 247 | startingLetter = letter.charAt(0); 248 | final oc = textfield.caretIndex; 249 | textfield.replaceSelectedText(t); 250 | textfield.setSelection(oc + 1, oc + 1); 251 | if (getStartingDirection() == LTR) 252 | { 253 | if (hasConverted) 254 | textfield.setSelection(textfield.caretIndex + 1, textfield.caretIndex + 1); 255 | if (addedSpace) 256 | textfield.setSelection(textfield.caretIndex + 1, textfield.caretIndex + 1); 257 | } 258 | else 259 | { 260 | if (hasConverted) 261 | textfield.setSelection(textfield.caretIndex - 1, textfield.caretIndex - 1); 262 | if (addedSpace) 263 | textfield.setSelection(textfield.caretIndex - 1, textfield.caretIndex - 1); 264 | } 265 | trace(getStartingDirection()); 266 | } 267 | } 268 | 269 | function manageKeyDown(key:KeyCode, modifier:KeyModifier):Void 270 | { 271 | switch (key) 272 | { 273 | case RETURN, NUMPAD_ENTER: 274 | if (textfield.__textEngine.multiline) 275 | { 276 | if (((currentlyOppositeDirection && getStartingDirection() == LTR) 277 | || (!currentlyOppositeDirection && getStartingDirection() == RTL)) 278 | && textfield.selectionBeginIndex == textfield.selectionEndIndex) 279 | { 280 | // If we just insert a newline, everything would go one line down and leave an empty line at the top 281 | // we need to go through the letters until we hit something LTR, and insert a newline before that. 282 | // special case: if we have a spacebar before that LTR letter, we should insert the newline before that spacebar 283 | var spacebarDefecit = 0; // used to traverse back on spacebars 284 | while (CharTools.isRTL(textfield.text.charAt(textfield.caretIndex)) 285 | || CharTools.isSoft(textfield.text.charAt(textfield.caretIndex)) 286 | && textfield.caretIndex != textfield.text.length) 287 | { 288 | trace(textfield.text.charAt(textfield.caretIndex)); 289 | textfield.setSelection(textfield.caretIndex + 1, textfield.caretIndex + 1); 290 | if (textfield.text.charAt(textfield.caretIndex) == " ") 291 | spacebarDefecit++ 292 | else 293 | spacebarDefecit = 0; 294 | } 295 | textfield.setSelection(textfield.caretIndex - spacebarDefecit, textfield.caretIndex - spacebarDefecit); 296 | } 297 | 298 | var te = new TextEvent(TextEvent.TEXT_INPUT, true, true, "\n"); 299 | 300 | textfield.dispatchEvent(te); 301 | 302 | if (!te.isDefaultPrevented()) 303 | { 304 | textfield.__replaceSelectedText("\n", true); 305 | 306 | textfield.dispatchEvent(new Event(Event.CHANGE, true)); 307 | } 308 | } 309 | else 310 | { 311 | textfield.__stopCursorTimer(); 312 | textfield.__startCursorTimer(); 313 | } 314 | 315 | case BACKSPACE, DELETE: 316 | setOppositeDirection(); 317 | if (key == BACKSPACE && !currentlyOppositeDirection || key == DELETE && currentlyOppositeDirection) 318 | { 319 | if (textfield.__selectionIndex == textfield.__caretIndex && textfield.__caretIndex > 0) 320 | textfield.__selectionIndex = textfield.__caretIndex - 1; 321 | 322 | if (textfield.__selectionIndex != textfield.__caretIndex) 323 | { 324 | textfield.replaceSelectedText(""); 325 | textfield.__selectionIndex = textfield.__caretIndex; 326 | 327 | textfield.dispatchEvent(new Event(Event.CHANGE, true)); 328 | } 329 | else 330 | { 331 | textfield.__stopCursorTimer(); 332 | textfield.__startCursorTimer(); 333 | } 334 | } 335 | else if (key == DELETE && !currentlyOppositeDirection || key == BACKSPACE && currentlyOppositeDirection) 336 | { 337 | if (textfield.__selectionIndex == textfield.__caretIndex && textfield.__caretIndex < textfield.__text.length) 338 | textfield.__selectionIndex = textfield.__caretIndex + 1; 339 | 340 | if (textfield.__selectionIndex != textfield.__caretIndex) 341 | { 342 | textfield.replaceSelectedText(""); 343 | textfield.__selectionIndex = textfield.__caretIndex; 344 | 345 | textfield.dispatchEvent(new Event(Event.CHANGE, true)); 346 | } 347 | else 348 | { 349 | textfield.__stopCursorTimer(); 350 | textfield.__startCursorTimer(); 351 | } 352 | } 353 | case LEFT if (textfield.selectable): 354 | if (#if mac modifier.metaKey #elseif js modifier.metaKey || modifier.ctrlKey #else modifier.ctrlKey #end) 355 | textfield.__caretBeginningOfPreviousLine(); 356 | else 357 | textfield.__caretPreviousCharacter(); 358 | 359 | if (!modifier.shiftKey) 360 | textfield.__selectionIndex = textfield.__caretIndex; 361 | 362 | textfield.setSelection(textfield.__selectionIndex, textfield.__caretIndex); 363 | setOppositeDirection(); 364 | 365 | case RIGHT if (textfield.selectable): 366 | if (#if mac modifier.metaKey #elseif js modifier.metaKey || modifier.ctrlKey #else modifier.ctrlKey #end) 367 | textfield.__caretBeginningOfNextLine(); 368 | else 369 | textfield.__caretNextCharacter(); 370 | 371 | if (!modifier.shiftKey) 372 | textfield.__selectionIndex = textfield.__caretIndex; 373 | 374 | textfield.setSelection(textfield.__selectionIndex, textfield.__caretIndex); 375 | setOppositeDirection(); 376 | 377 | case DOWN if (textfield.selectable): 378 | if (#if mac modifier.metaKey #elseif js modifier.metaKey || modifier.ctrlKey #else modifier.ctrlKey #end) 379 | { 380 | textfield.__caretIndex = textfield.__text.length; 381 | } 382 | else 383 | { 384 | textfield.__caretNextLine(); 385 | } 386 | 387 | if (!modifier.shiftKey) 388 | { 389 | textfield.__selectionIndex = textfield.__caretIndex; 390 | } 391 | 392 | textfield.setSelection(textfield.__selectionIndex, textfield.__caretIndex); 393 | setOppositeDirection(); 394 | 395 | case UP if (textfield.selectable): 396 | if (#if mac modifier.metaKey #elseif js modifier.metaKey || modifier.ctrlKey #else modifier.ctrlKey #end) 397 | { 398 | textfield.__caretIndex = 0; 399 | } 400 | else 401 | { 402 | textfield.__caretPreviousLine(); 403 | } 404 | 405 | if (!modifier.shiftKey) 406 | { 407 | textfield.__selectionIndex = textfield.__caretIndex; 408 | } 409 | 410 | textfield.setSelection(textfield.__selectionIndex, textfield.__caretIndex); 411 | setOppositeDirection(); 412 | 413 | case HOME if (textfield.selectable): 414 | if (#if mac modifier.metaKey #elseif js modifier.metaKey || modifier.ctrlKey #else modifier.ctrlKey #end) 415 | { 416 | textfield.__caretIndex = 0; 417 | } 418 | else 419 | { 420 | textfield.__caretBeginningOfLine(); 421 | } 422 | 423 | if (!modifier.shiftKey) 424 | { 425 | textfield.__selectionIndex = textfield.__caretIndex; 426 | } 427 | 428 | textfield.setSelection(textfield.__selectionIndex, textfield.__caretIndex); 429 | setOppositeDirection(); 430 | 431 | case END if (textfield.selectable): 432 | if (#if mac modifier.metaKey #elseif js modifier.metaKey || modifier.ctrlKey #else modifier.ctrlKey #end) 433 | { 434 | textfield.__caretIndex = textfield.__text.length; 435 | } 436 | else 437 | { 438 | textfield.__caretEndOfLine(); 439 | } 440 | 441 | if (!modifier.shiftKey) 442 | { 443 | textfield.__selectionIndex = textfield.__caretIndex; 444 | } 445 | 446 | textfield.setSelection(textfield.__selectionIndex, textfield.__caretIndex); 447 | setOppositeDirection(); 448 | 449 | case C: 450 | #if lime 451 | if (#if mac modifier.metaKey #elseif js modifier.metaKey || modifier.ctrlKey #else modifier.ctrlKey #end) 452 | { 453 | if (textfield.__caretIndex != textfield.__selectionIndex) 454 | { 455 | Clipboard.text = textfield.__text.substring(textfield.__caretIndex, textfield.__selectionIndex); 456 | } 457 | } 458 | #end 459 | 460 | case X: 461 | #if lime 462 | if (#if mac modifier.metaKey #elseif js modifier.metaKey || modifier.ctrlKey #else modifier.ctrlKey #end) 463 | { 464 | if (textfield.__caretIndex != textfield.__selectionIndex) 465 | { 466 | Clipboard.text = textfield.__text.substring(textfield.__caretIndex, textfield.__selectionIndex); 467 | 468 | textfield.replaceSelectedText(""); 469 | textfield.dispatchEvent(new Event(Event.CHANGE, true)); 470 | } 471 | } 472 | #end 473 | 474 | #if !js 475 | case V: 476 | #if lime 477 | if (#if mac modifier.metaKey #else modifier.ctrlKey #end) 478 | { 479 | if (Clipboard.text != null) 480 | { 481 | var te = new TextEvent(TextEvent.TEXT_INPUT, true, true, Clipboard.text); 482 | 483 | textfield.dispatchEvent(te); 484 | 485 | if (!te.isDefaultPrevented()) 486 | { 487 | textfield.__replaceSelectedText(#if !js bidifyString(Clipboard.text) #else Clipboard.text #end, true); 488 | 489 | textfield.dispatchEvent(new Event(Event.CHANGE, true)); 490 | } 491 | } 492 | } 493 | else 494 | { 495 | // TODO: does this need to occur? 496 | textfield.__textEngine.textFormatRanges[textfield.__textEngine.textFormatRanges.length - 1].end = textfield.__text.length; 497 | } 498 | #end 499 | #end 500 | 501 | case A if (textfield.selectable): 502 | if (#if mac modifier.metaKey #elseif js modifier.metaKey || modifier.ctrlKey #else modifier.ctrlKey #end) 503 | { 504 | textfield.setSelection(0, textfield.__text.length); 505 | } 506 | 507 | default: 508 | } 509 | } 510 | 511 | function invoke(fromEvent = false, e:Event) 512 | { 513 | if (fromEvent) 514 | textfield.removeEventListener(Event.ADDED_TO_STAGE, invoke.bind(true)); 515 | @:privateAccess textfield.__inputEnabled = true; 516 | textfield.addEventListener(FocusEvent.FOCUS_IN, e -> 517 | { 518 | textfield.stage.window.onTextInput.remove(@:privateAccess textfield.window_onTextInput); 519 | textfield.stage.window.onKeyDown.remove(@:privateAccess textfield.window_onKeyDown); 520 | textfield.stage.window.onTextInput.remove(manageTextInput); 521 | textfield.stage.window.onTextInput.add(manageTextInput); 522 | textfield.stage.window.onKeyDown.remove(manageKeyDown); 523 | textfield.stage.window.onKeyDown.add(manageKeyDown); 524 | }); 525 | textfield.stage.window.onTextInput.remove(@:privateAccess textfield.window_onTextInput); 526 | textfield.stage.window.onKeyDown.remove(@:privateAccess textfield.window_onKeyDown); 527 | textfield.stage.window.onTextInput.remove(manageTextInput); 528 | textfield.stage.window.onTextInput.add(manageTextInput); 529 | textfield.stage.window.onKeyDown.remove(manageKeyDown); 530 | textfield.stage.window.onKeyDown.add(manageKeyDown); 531 | } 532 | 533 | if (textfield.stage == null) 534 | { 535 | textfield.addEventListener(Event.ADDED_TO_STAGE, invoke.bind(true)); 536 | } 537 | else 538 | { 539 | invoke(false, null); 540 | } 541 | } 542 | 543 | public static overload extern inline function attachBidifier(textfield:TextField):Void 544 | { 545 | __attachLiveOpenFL(textfield); 546 | } 547 | 548 | public static overload extern inline function attachBidifier(textfield:DynamicTextField):Void 549 | { 550 | __attachOpenFL(textfield.textField); 551 | } 552 | 553 | public static overload extern inline function attachBidifier(textfield:TextFieldRTL) 554 | { 555 | __attachOpenFL(textfield); 556 | } 557 | #end 558 | } 559 | -------------------------------------------------------------------------------- /src/Main.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import texter.general.math.MathLexer; 4 | import haxe.Timer; 5 | import texter.general.bidi.Bidi; 6 | using TextTools; 7 | class Main { 8 | static function main() { 9 | #if false 10 | var timer = haxe.Timer.stamp(); 11 | var processed = Bidi.process(" 12 | - שלום עולם 13 | שלום לכם זה RTL וזה LTR 14 | my name is שחר and היום אני בן 16 15 | - hello world"); 16 | trace(processed); 17 | trace(Bidi.unbidify(processed)); 18 | trace('Processing Time: ${Timer.stamp() - timer}'); 19 | #else 20 | //var eq = "f(x) = 5x + (444x)/(30542315) + 61x"; 21 | //trace(MathLexer.condenseAttributes(MathLexer.getMathAttributes(eq))); 22 | //trace(MathLexer.extractTextFromAttributes(MathLexer.condenseAttributes(MathLexer.getMathAttributes(eq)))); 23 | //var e = "(4x + 5) * 3x + (2x + 5) * (2x + 5)/(4x + 6)"; 24 | //trace( 25 | // MathLexer.reorderAttributes( 26 | // MathLexer.getMathAttributes(e) 27 | // ).join("\n") 28 | //); 29 | trace( 30 | //MathLexer.extractTextFromAttributes( 31 | MathLexer.resetAttributesOrder( 32 | MathLexer.splitBlocks( 33 | MathLexer.getMathAttributes( 34 | "((12345x) + 4)/(3490)" 35 | ) 36 | ) 37 | ) 38 | //) 39 | ); 40 | // trace( 41 | // MathLexer.extractTextFromAttributes( 42 | // MathLexer.resetAttributesOrder( 43 | // MathLexer.splitBlocks( 44 | // MathLexer.getMathAttributes( 45 | // "((12345x) + 4)/(3490)" 46 | // ) 47 | // ) 48 | // ) 49 | // ) 50 | // ); 51 | #end 52 | } 53 | } -------------------------------------------------------------------------------- /src/TextTools.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | /** 4 | * `TextTools` is a class containing static methods for manipulating text. 5 | * 6 | * you can use it by doing 7 | * 8 | * using TextTools; 9 | * 10 | * and enjoy not having to write more text manipulation functions ever again :D 11 | */ 12 | class TextTools 13 | { 14 | /** 15 | * The `fonts` field contains paths to fonts of different style, all of them supporting 16 | * almost any language you'd throw at them :). 17 | * 18 | * Right now, only the `sans` and `serif` fonts are available. 19 | * 20 | * usage: 21 | * ```haxe 22 | * var textFormat = new TextFormat(TextTools.fonts.sans); 23 | * textField.defaultTextFormat = textFormat; 24 | * ``` 25 | */ 26 | public static var fonts(default, null):MultilangFonts = @:privateAccess new MultilangFonts(); 27 | 28 | /** 29 | * replaces the last occurrence of `replace` in `string` with `by`. 30 | * 31 | * @param string the string to replace in 32 | * @param replace the string to replace 33 | * @param by the replacement string 34 | * @return the string with the last occurrence of `replace` replaced by `by` 35 | */ 36 | public static function replaceLast(string:String, replace:String, by:String):String 37 | { 38 | final place = string.lastIndexOf(replace); 39 | var result = string.substring(0, place) + by + string.substring(place + replace.length); 40 | return result; 41 | } 42 | 43 | /** 44 | * replaces the first occurrence of `replace` in `string` with `by`. 45 | * 46 | * @param string the string to replace in 47 | * @param replace the string to replace 48 | * @param by the replacement string 49 | * @return the string with the first occurrence of `replace` replaced by `by` 50 | */ 51 | public static function replaceFirst(string:String, replace:String, by:String):String 52 | { 53 | final place = string.indexOf(replace); 54 | var result = string.substring(0, place) + by + string.substring(place + replace.length); 55 | return result; 56 | } 57 | 58 | /** 59 | * splits `string` on the first occurrence of `delimiter` and returns the array of the two parts. 60 | * 61 | * @param string the string to split 62 | * @param delimiter the string to split on 63 | * @return the array of the two parts 64 | */ 65 | public static function splitOnFirst(string:String, delimiter:String):Array 66 | { 67 | final place = string.indexOf(delimiter); 68 | var result = new Array(); 69 | result.push(string.substring(0, place)); 70 | result.push(string.substring(place + delimiter.length)); 71 | return result; 72 | } 73 | 74 | /** 75 | * splits `string` on the last occurrence of `delimiter` and returns the array 76 | * of the two parts. 77 | * 78 | * @param string the string to split 79 | * @param delimiter the string to split on 80 | * @return the array of the two parts 81 | */ 82 | public static function splitOnLast(string:String, delimiter:String):Array 83 | { 84 | final place = string.lastIndexOf(delimiter); 85 | var result = new Array(); 86 | result.push(string.substring(0, place)); 87 | result.push(string.substring(place + delimiter.length)); 88 | return result; 89 | } 90 | 91 | /** 92 | * Splits a text into paragraphs, determined by HTML/Markdown markup 93 | * (double newline or

). 94 | * 95 | * @param text the text to split into he array of paragraphs. 96 | * @return an array containing the paragraphs. 97 | */ 98 | public static inline function splitOnParagraph(text:String):Array 99 | { 100 | return ~/

|<\/p>|\n\n|\r\n\r\n/g.split(text); 101 | } 102 | 103 | /** 104 | * filters a string according to the contents of `filter`: 105 | * 106 | * - if `filter` is a string, it can be use as one of 2 things 107 | * 108 | * - if the string contains a regex filter it will re-call the function with the string passed as an EReg 109 | * - if the string does not contain the filter it can be one of 3 3 things: 110 | * 111 | * - if the string is empty, nothing will be filtered 112 | * - if the string is "alpha", it will filter out all non-alphabetic characters 113 | * - if the string is "numeric", it will filter out all non-numeric characters 114 | * - if the string is "alphanumeric", it will filter out all non-alphanumeric characters 115 | * 116 | * - if `filter` is an EReg, it will be used to filter the string 117 | * 118 | * @param text the text to filter 119 | * @param filter the actual filter; can be a string or an EReg 120 | * @return the filtered string 121 | */ 122 | public static function filter(text:String, filter:Dynamic):String 123 | { 124 | if (filter is EReg) 125 | { 126 | var pattern:EReg = cast filter; 127 | text = pattern.replace(text, ""); 128 | return text; 129 | } 130 | var patternType:String = cast filter; 131 | if (replaceFirst(text, "/", "") != patternType) 132 | { // someone decided to be quirky and pass an EReg as a string 133 | var regexDetector:EReg = ~/^~?\/(.*)\/(.*)$/s; 134 | regexDetector.match(patternType); 135 | return filter(text, new EReg(regexDetector.matched(1), regexDetector.matched(2))); 136 | } 137 | switch patternType.toLowerCase() 138 | { 139 | case "alpha": 140 | return filter(text, new EReg("[^a-zA-Z]", "g")); 141 | case "alphanumeric": 142 | return filter(text, new EReg("[^a-zA-Z0-9]", "g")); 143 | case "numeric": 144 | return filter(text, new EReg("[^0-9]", "g")); 145 | } 146 | return text; 147 | } 148 | 149 | /** 150 | * Returns an array containing the start & end indexes of all occurences of `sub`. 151 | * 152 | * the reported indxes are from `startIndex`, up to but not including `endIndex`. 153 | * 154 | * @param string The string containing the `sub` 155 | * @param sub The `sub` itself 156 | * @return An Array f all indexes 157 | */ 158 | public static function indexesOf(string:String, sub:String):Array<{startIndex:Int, endIndex:Int}> 159 | { 160 | var indexArray:Array<{startIndex:Int, endIndex:Int}> = []; 161 | var removedLength = 0, index = string.indexOf(sub); 162 | while (index != -1) 163 | { 164 | indexArray.push({startIndex: index + removedLength, endIndex: index + sub.length + removedLength - 1}); 165 | removedLength += sub.length; 166 | string = string.substring(0, index) + string.substring(index + sub.length, string.length); 167 | index = string.indexOf(sub); 168 | } 169 | return indexArray; 170 | } 171 | 172 | /** 173 | * repoort all occurences of the elements inside `sub` in `string`. 174 | * 175 | * @param string the string to search in 176 | * @param subs an array of substrings to search for 177 | * @return an array of all positions of the substrings, from startIndex, up to but not including endIndex 178 | */ 179 | public static function indexesOfSubs(string:String, subs:Array):Array<{startIndex:Int, endIndex:Int}> 180 | { 181 | var indexArray:Array<{startIndex:Int, endIndex:Int}> = [], 182 | orgString = string; 183 | for (sub in subs) 184 | { 185 | var removedLength = 0, index = string.indexOf(sub); 186 | while (index != -1) 187 | { 188 | indexArray.push({startIndex: index + removedLength, endIndex: index + sub.length + removedLength}); 189 | removedLength += sub.length; 190 | string = string.substring(0, index) + string.substring(index + sub.length, string.length); 191 | index = string.indexOf(sub); 192 | } 193 | string = orgString; 194 | } 195 | return indexArray; 196 | } 197 | 198 | /** 199 | * repoort all occurences of the elements inside `sub` in `string`. 200 | * 201 | * @param string the string to search in 202 | * @param subs an array of substrings to search for 203 | * @return an array of all positions of the substrings, from startIndex, up to but not including endIndex 204 | */ 205 | @:deprecated("TextTools.indexesFromArray is deprecated. Use TextTools.indexesOfSubs() instead") 206 | public static function indexesFromArray(string:String, subs:Array) 207 | return indexesOfSubs(string, subs); 208 | 209 | /** 210 | * reports all occurences of the findings of `ereg` in `string`. 211 | * NOTICE: avoid using eregs with the global flag, as they will only report the first substring found. 212 | * @param string the string to search in 213 | * @param ereg the EReg to use as the searching engine 214 | * @return an array of all positions of the substrings, from startIndex, up to but not including endIndex 215 | */ 216 | public static function indexesFromEReg(string:String, ereg:EReg):Array<{startIndex:Int, endIndex:Int}> 217 | { 218 | var indexArray:Array<{startIndex:Int, endIndex:Int}> = []; 219 | while (ereg.match(string)) 220 | { 221 | var info = ereg.matchedPos(); 222 | string = ereg.replace(string, multiply("⨔", info.len)); 223 | indexArray.push({startIndex: info.pos, endIndex: info.pos + info.len}); 224 | } 225 | 226 | return indexArray; 227 | } 228 | 229 | /** 230 | * Multiplies `string` by `times`. 231 | * 232 | * When multiplied by a number equal/less than 0, it returns an empty string. 233 | * 234 | * example: 235 | * ```haxe 236 | * var foo = "foo"; 237 | * var bar = TextTools.multiply(foo, 3); 238 | * trace(bar); // foofoofoo 239 | * 240 | * //if you have `using TextTools` at the top of the file: 241 | * bar = foo.multiply(0); 242 | * trace(bar); // "" 243 | * ``` 244 | * 245 | * @param string the string to multiply 246 | * @param by the number of times to multiply 247 | * @return the multiplied string 248 | */ 249 | public static function multiply(string:String, times:Int):String 250 | { 251 | final stringcopy = string; 252 | if (times <= 0) 253 | return ""; 254 | while (--times > 0) 255 | { 256 | string += stringcopy; 257 | } 258 | return string; 259 | } 260 | 261 | /** 262 | * Subtracts a part of a string from another string. 263 | * Itll try to find the last occurence of `by` in `string`, and remove it. 264 | * 265 | * @param string the string to subtract from 266 | * @param by the string to subtract 267 | */ 268 | public static inline function subtract(string:String, by:String):String 269 | { 270 | return replaceLast(string, by, ""); 271 | } 272 | 273 | /** 274 | * Creates a lorem ipsum text the following modifiers. 275 | * 276 | * @param paragraphs the amount of paragraphs to generate 277 | * @param length **Optional** - the total length of the text. 278 | */ 279 | public static inline function loremIpsum(paragraphs:Int = 1, length:Int = -1):String 280 | { 281 | var loremArray = splitOnParagraph(StringTools.replace(loremIpsumText, "\t", "")); 282 | var loremText = loremArray.join("\n\n"); 283 | if (paragraphs > loremArray.length) 284 | { 285 | var multiplier = Math.ceil(paragraphs / loremArray.length); 286 | loremText = multiply(loremIpsumText, multiplier); 287 | loremArray = splitOnParagraph(loremText); 288 | } 289 | while (loremArray.length > paragraphs) 290 | loremArray.pop(); 291 | var loremString = loremArray.join("\n\n"); 292 | if (length != -1) 293 | { 294 | return loremString.substring(0, length); 295 | } 296 | return loremString; 297 | } 298 | 299 | /** 300 | * Sorts an array of strings by the string's length, whith the shortest strings first. 301 | * 302 | * @param array an array of strings to be sorted 303 | * @return the sorted array 304 | */ 305 | public static function sortByLength(array:Array):Array 306 | { 307 | array.sort(function(a:String, b:String):Int 308 | { 309 | return a.length - b.length; 310 | }); 311 | return array; 312 | } 313 | 314 | /** 315 | * Sorts an array of floats by the float's value, whith the lowest values first. 316 | * 317 | * @param array an array of floats to be sorted 318 | * @return the sorted array 319 | */ 320 | @:deprecated public static function sortByValue(array:Array):Array 321 | { 322 | array.sort(function(a:Float, b:Float):Int 323 | { 324 | return Std.int(a - b); 325 | }); 326 | return array; 327 | } 328 | 329 | /** 330 | * 331 | * Sorts an array of ints by the int's value, whith the lowest values first. 332 | * @param array an array of ints to be sorted 333 | * @return the sorted array 334 | */ 335 | @:deprecated public static function sortByIntValue(array:Array):Array 336 | { 337 | array.sort(function(a:Int, b:Int):Int 338 | { 339 | return a - b; 340 | }); 341 | return array; 342 | } 343 | 344 | /** 345 | * Gets the 0-based line index of the char in position `index` 346 | * 347 | * @param string the string to search in 348 | * @param index The character's position within the string 349 | * @return The 0-based line index of the char in position `index` 350 | */ 351 | public static function getLineIndexOfChar(string:String, index:Int):Int 352 | { 353 | final lines = string.split("\n"); 354 | var lineIndex = 0; 355 | for (i in 0...lines.length) 356 | { 357 | if (index < lines[i].length) 358 | { 359 | lineIndex = i; 360 | break; 361 | } 362 | index -= lines[i].length; 363 | } 364 | return lineIndex; 365 | } 366 | 367 | /** 368 | * Searches for occurrences of `sub` in `string`, and returns the number of occurrences. 369 | * @param string the string to search in 370 | * @param sub the substring to search for 371 | * @return The amount of times `sub` was found in `string` 372 | */ 373 | public static function countOccurrencesOf(string:String, sub:String):Int 374 | { 375 | var count = 0; 376 | while (contains(string, sub)) 377 | { 378 | count++; 379 | string = replaceFirst(string, sub, ""); 380 | } 381 | return count; 382 | } 383 | 384 | /** 385 | Returns `true` if `string` contains `contains` and `false` otherwise. 386 | 387 | When `contains` is `null`, the function returns `false`. 388 | When `contains` is `""`, the function returns `true` 389 | **/ 390 | public static function contains(string:String, contains:String):Bool 391 | { 392 | if (string == null) 393 | return false; 394 | return string.indexOf(contains) != -1; 395 | } 396 | 397 | /** 398 | * Removes all occurrences of `sub` inside of `string`. 399 | * 400 | * @param string the string to remove from 401 | * @param sub the substring to find, and remove. 402 | * @return the resulting string. 403 | */ 404 | public static function remove(string:String, sub:String):String 405 | { 406 | return replace(string, sub, ""); 407 | } 408 | 409 | /** 410 | Replace all occurrences of the String `replace` in the String `string` by the 411 | String `with`. 412 | 413 | If `replace` is the empty String `""`, `with` is inserted after each character 414 | of `string` except the last one. 415 | 416 | If `with` is also an empty String (`""`), `string` remains unchanged. 417 | 418 | If `replace` or `with` are null, the string remains unchanged. 419 | **/ 420 | public static function replace(string:String, replace:String, with:String):String 421 | { 422 | if (replace == null || with == null) 423 | return string; 424 | return StringTools.replace(string, replace, with); 425 | } 426 | 427 | /** 428 | * Reverses a given the string without allocating an array. 429 | * 430 | * @param string the string to reverse 431 | * @return the reversed string. 432 | */ 433 | public static function reverse(string:String):String 434 | { 435 | var returnedString = ''; 436 | for (i in 1...string.length + 1) 437 | { 438 | returnedString += string.charAt(string.length - 1); 439 | } 440 | return returnedString; 441 | } 442 | 443 | /** 444 | Inserts `substring` **after** `at` 445 | **/ 446 | public static function insert(string:String, substring:String, at:Int) 447 | { 448 | return string.substring(0, at + 1) + substring + string.substring(at + 1); 449 | } 450 | 451 | public static var loremIpsumText(default, null):String = " 452 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque finibus condimentum magna, eget porttitor libero aliquam non. Praesent commodo, augue nec hendrerit tincidunt, urna felis lobortis mi, non cursus libero tellus quis tellus. Vivamus ornare convallis tristique. Integer nec ornare libero. Phasellus feugiat facilisis faucibus. Vivamus porta id neque id placerat. Proin convallis vel felis et pharetra. Quisque magna justo, ullamcorper quis scelerisque eu, tincidunt vitae lectus. Nunc sed turpis justo. Aliquam porttitor, purus sit amet faucibus bibendum, ligula elit molestie purus, eu volutpat turpis sapien ac tellus. Fusce mauris arcu, volutpat ut aliquam ut, ultrices id ante. Morbi quis consectetur turpis. Integer semper lacinia urna id laoreet. 453 | 454 | Ut mollis eget eros eu tempor. Phasellus nulla velit, sollicitudin eget massa a, tristique rutrum turpis. Vestibulum in dolor at elit pellentesque finibus. Nulla pharetra felis a varius molestie. Nam magna lectus, eleifend ac sagittis id, ornare id nibh. Praesent congue est non iaculis consectetur. Nullam dictum augue sit amet dignissim fringilla. Aenean semper justo velit. Sed nec lectus facilisis, sodales diam eget, imperdiet nunc. Quisque elementum nulla non orci interdum pharetra id quis arcu. Phasellus eu nunc lectus. Nam tellus tortor, pellentesque eget faucibus eu, laoreet quis odio. Pellentesque posuere in enim a blandit. 455 | 456 | Duis dignissim neque et ex iaculis, ac consequat diam gravida. In mi ex, blandit eget velit non, euismod feugiat arcu. Nulla nec fermentum neque, eget elementum mauris. Vivamus urna ligula, faucibus at facilisis sed, commodo sit amet urna. Sed porttitor feugiat purus ac tincidunt. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aliquam sollicitudin lacinia turpis quis placerat. Donec eget velit nibh. Duis vehicula orci lectus, eget rutrum arcu tincidunt et. Vestibulum ut pharetra lectus. Quisque lacinia nunc rhoncus neque venenatis consequat. Nulla rutrum ultricies sapien, sed semper lectus accumsan nec. Phasellus commodo faucibus lacinia. Donec auctor condimentum ligula. Sed quis viverra mauris. 457 | 458 | Quisque maximus justo dui, eget pretium lorem accumsan ac. Praesent eleifend faucibus orci et varius. Ut et molestie turpis, eu porta neque. Quisque vehicula, libero in tincidunt facilisis, purus eros pulvinar leo, sit amet eleifend justo ligula tempor lectus. Donec ac tortor sed ipsum tincidunt pulvinar id nec eros. In luctus purus cursus est dictum, ac sollicitudin turpis maximus. Maecenas a nisl velit. Nulla gravida lectus vel ultricies gravida. Proin vel bibendum magna. Donec aliquam ultricies quam, quis tempor nunc pharetra ut. 459 | 460 | Pellentesque sit amet dui est. Aliquam erat volutpat. Integer vitae ullamcorper est, ut eleifend augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Quisque congue velit felis, vitae elementum nulla faucibus id. Donec lectus nibh, commodo eget nunc id, feugiat sagittis massa. In hac habitasse platea dictumst. Pellentesque volutpat molestie ultrices. 461 | "; 462 | } 463 | 464 | enum TextDirection 465 | { 466 | RTL; 467 | LTR; 468 | UNDETERMINED; 469 | } 470 | 471 | private class MultilangFonts 472 | { 473 | function new() {} 474 | 475 | /** 476 | * A `sans` font that supports many languages, of any direction. to use in your text field, do: 477 | * ```haxe 478 | * var textFormat = new TextFormat(TextTools.fonts.sans); 479 | * textField.defaultTextFormat = textFormat; 480 | * ``` 481 | */ 482 | public var sans(default, null):String = "assets/texter/TextTools/sans.ttf"; 483 | 484 | /** 485 | * A `sans` font that supports many languages, of any direction. to use in your text field, do: 486 | * ```haxe 487 | * var textFormat = new TextFormat(TextTools.fonts.serif); 488 | * textField.defaultTextFormat = textFormat; 489 | * ``` 490 | */ 491 | public var serif(default, null):String = "assets/texter/TextTools/serif.ttf"; 492 | } 493 | -------------------------------------------------------------------------------- /src/texter/flixel/FlxInputTextRTL.hx: -------------------------------------------------------------------------------- 1 | #if flixel 2 | package texter.flixel; 3 | 4 | import lime.ui.KeyCode; 5 | import lime.ui.KeyModifier; 6 | import openfl.events.KeyboardEvent; 7 | import texter.general.CharTools; 8 | import flixel.FlxG; 9 | import openfl.desktop.Clipboard; 10 | import texter.flixel._internal.FlxInputText; 11 | import texter.general.TextTools.TextDirection; 12 | 13 | using StringTools; 14 | 15 | /** 16 | * Reguar FlxInputText with extended support for: 17 | * - Multilanguage 18 | * - Bi-directional text 19 | * - Copy/Paste 20 | * - Auto-align 21 | * - Multilne 22 | */ 23 | class FlxInputTextRTL extends FlxInputText 24 | { 25 | /** 26 | Whether the text is aligned according to the first typed character: 27 | 28 | - if the character is from a RTL language - `alignment` will be set to `RIGHT`. 29 | - if the character is from any other language - `alignment` will be set to `LEFT`. 30 | - if the character is not from any specific language - `alignment` will be set to `UNDETERMINED`. 31 | 32 | 33 | 34 | **`autoAlign` does not default to a certine direction when set to `false`**. it will 35 | use the last direction it remembers when this `FlxInputTextRTL` was created/when `autoAlign` was still true; 36 | **/ 37 | public var autoAlign(default, set):Bool = true; 38 | 39 | /** 40 | Specifies the direction of the starting character inside this text input. 41 | 42 | the text direction will only be set according to `openingDirection` if `autoAlign` is set to true. 43 | 44 | `openingDirection` is decided after the first strongly typed character is typed. a table to help: 45 | 46 | | Character Group | Type | Direction | 47 | | :---: | :---:| :---: | 48 | | punctuation marks (see `CharTools.generalMarks`) | softly typed | UNDETERMINED | 49 | | LTR languages (English, Spanish, French, German...) | strongly typed | LTR | 50 | | RTL languages (Arabic, Hebrew, Sorani, Urdu...) | strongly typed | RTL | 51 | **/ 52 | public var openingDirection(default, null):TextDirection = UNDETERMINED; 53 | 54 | var currentlyRTL:Bool = false; 55 | var currentlyNumbers:Bool = false; 56 | 57 | /** 58 | Creates a new text input with extra features & bug fixes that the regular `FlxInputText` doesnt have: 59 | 60 | - multiline 61 | - multiple languages 62 | - LTR & RTL support both in the same text input 63 | - fully working caret 64 | 65 | @param X The X position of the text. 66 | @param Y The Y position of the text. 67 | @param Width The width of the text object (height is determined automatically). 68 | @param Text The actual text you would like to display initially. 69 | @param size Initial size of the font 70 | @param TextColor The color of the text 71 | @param BackgroundColor The color of the background (FlxColor.TRANSPARENT for no background color) 72 | @param EmbeddedFont Whether this text field uses embedded fonts or not 73 | **/ 74 | public function new(X:Float = 0, Y:Float = 0, Width:Int = 150, Text:String = '', size:Int = 8, TextColor:Int = flixel.util.FlxColor.BLACK, 75 | BackgroundColor:Int = flixel.util.FlxColor.WHITE, EmbeddedFont:Bool = true) 76 | { 77 | super(X, Y, Width, Text, size, TextColor, BackgroundColor, EmbeddedFont); 78 | wordWrap = true; 79 | FlxG.stage.window.onTextInput.add(regularKeysDown, false, 1); 80 | FlxG.stage.window.onKeyDown.add(specialKeysDown, false, 2); 81 | #if js 82 | FlxG.stage.window.onFocusOut.add(() -> hasFocus = false); 83 | #end 84 | } 85 | 86 | #if js 87 | override function update(elapsed:Float) 88 | { 89 | if (FlxG.keys.justPressed.SPACE) 90 | { 91 | regularKeysDown(" "); 92 | } 93 | super.update(elapsed); 94 | } 95 | #end 96 | 97 | override function set_hasFocus(newFocus:Bool):Bool 98 | { 99 | FlxG.stage.window.textInputEnabled = true; 100 | FlxG.sound.soundTrayEnabled = !newFocus; 101 | return super.set_hasFocus(newFocus); 102 | } 103 | 104 | /** 105 | The original `onKeyDown` from `FlxInputText` is replaced with two functions - 106 | 107 | | Function | Job | 108 | | --- | --- | 109 | | **`specialKeysDown(KeyCode, KeyModifier)`** | used to get "editing" keys (backspace, capslock, arrow keys...) | 110 | | **`regularKeysDown(String)`** | used to get "input" keys - regular letters of all languages and directions | 111 | **/ 112 | override function onKeyDown(e:KeyboardEvent) 113 | return; 114 | 115 | /** 116 | This function replaces `onKeyDown` with support for `delete`, `backspace`, arrow keys and more. 117 | `specialKeysDown()` is one of two functions, and is using `window.onKeyDown` to get button 118 | presses, so pay attention to that when overriding. 119 | 120 | @param key the keycode of the current key that was presses according to lime's `window.onKeyDown` 121 | @param modifier information about modifying buttons and if theyre on or not - `ctrl`, `shift`, `alt`, `capslock`... 122 | **/ 123 | function specialKeysDown(key:KeyCode, modifier:KeyModifier) 124 | { 125 | // if the user didnt intend to edit the text, dont do anything 126 | if (!hasFocus) 127 | return; 128 | // handle copy-paste 129 | if (modifier.ctrlKey) 130 | { 131 | if (key == KeyCode.V) 132 | { 133 | // paste text 134 | var clipboardText = Clipboard.generalClipboard.getData(TEXT_FORMAT); 135 | if (clipboardText == null) 136 | return; 137 | if (currentlyRTL) 138 | { 139 | text = insertSubstring(text, clipboardText, caretIndex); 140 | } 141 | else 142 | { 143 | text = insertSubstring(text, clipboardText, caretIndex); 144 | caretIndex += clipboardText.length; 145 | if (caretIndex > text.length) 146 | caretIndex = text.length; 147 | } 148 | } 149 | } 150 | // this keys break the caret and place it in caretIndex -1 151 | if (modifier.altKey || modifier.shiftKey || modifier.ctrlKey || modifier.metaKey) 152 | return; 153 | 154 | // fix the caret if its broken 155 | if (caretIndex < 0) 156 | caretIndex = 0; 157 | 158 | if (key == KeyCode.RIGHT && caretIndex < text.length) 159 | caretIndex++; 160 | if (key == KeyCode.LEFT && caretIndex > 0) 161 | caretIndex--; 162 | if (key == KeyCode.DOWN) 163 | { 164 | // here we get the line the caret is on, the amount of letters in it and where is the caret relative to it 165 | var currentLine = textField.getLineIndexOfChar(caretIndex), 166 | letterLineIndex = caretIndex - textField.getLineOffset(currentLine); 167 | // here we get stats about the next line and where to put the caret 168 | if (letterLineIndex > textField.getLineLength(currentLine + 1)) 169 | letterLineIndex = textField.getLineLength(currentLine + 1); 170 | caretIndex = textField.getLineOffset(currentLine + 1) + letterLineIndex; 171 | } 172 | if (key == KeyCode.UP) 173 | { 174 | // here we get the line the caret is on, the amount of letters in it and where is the caret relative to it 175 | var currentLine = textField.getLineIndexOfChar(caretIndex), 176 | letterLineIndex = caretIndex - textField.getLineOffset(currentLine); 177 | // here we get stats about the next line and where to put the caret 178 | if (letterLineIndex > textField.getLineLength(currentLine - 1)) 179 | letterLineIndex = textField.getLineLength(currentLine - 1); 180 | caretIndex = textField.getLineOffset(currentLine - 1) + letterLineIndex; 181 | } 182 | else if (key == KeyCode.BACKSPACE) 183 | { 184 | if (caretIndex > 0) 185 | { 186 | #if !js 187 | if (CharTools.isRTL(text.charAt(caretIndex + 1)) || CharTools.isRTL(text.charAt(caretIndex))) 188 | { 189 | text = text.substring(0, caretIndex) + text.substring(caretIndex + 1); 190 | } 191 | else 192 | { 193 | caretIndex--; 194 | text = text.substring(0, caretIndex) + text.substring(caretIndex + 1); 195 | } 196 | #else 197 | caretIndex--; 198 | text = text.substring(0, caretIndex) + text.substring(caretIndex + 1); 199 | #end 200 | 201 | onChange(FlxInputText.BACKSPACE_ACTION); 202 | } 203 | } 204 | else if (key == KeyCode.DELETE) 205 | { 206 | #if !js 207 | if (text.length > 0 && caretIndex < text.length) 208 | { 209 | if (CharTools.isRTL(text.charAt(caretIndex + 1)) || CharTools.isRTL(text.charAt(caretIndex))) 210 | { 211 | text = text.substring(0, caretIndex - 1) + text.substring(caretIndex); 212 | caretIndex--; 213 | } 214 | else 215 | { 216 | text = text.substring(0, caretIndex) + text.substring(caretIndex + 1); 217 | } 218 | onChange(FlxInputText.DELETE_ACTION); 219 | } 220 | #else 221 | text = text.substring(0, caretIndex) + text.substring(caretIndex + 1); 222 | #end 223 | } 224 | else if (key == 13) 225 | { 226 | caretIndex++; 227 | if (!currentlyRTL) 228 | { 229 | text = insertSubstring(text, "\n", caretIndex - 1); 230 | } 231 | else 232 | { 233 | var insertionIndex = 0; 234 | insertionIndex = caretIndex; 235 | // starts a search for the last RTL char and places the "\n" there 236 | // if the string ends and theres still no last RTL char, "\n" will be insterted at length. 237 | while (CharTools.isRTL(text.charAt(insertionIndex)) || text.charAt(insertionIndex) == " " && insertionIndex != text.length) 238 | insertionIndex++; 239 | text = insertSubstring(text, "\n", insertionIndex); 240 | caretIndex = insertionIndex + 1; 241 | } 242 | onChange(FlxInputText.ENTER_ACTION); 243 | } 244 | else if (key == KeyCode.END) 245 | { 246 | caretIndex = text.length; 247 | onChange(FlxInputText.END_ACTION); 248 | } 249 | else if (key == KeyCode.HOME) 250 | { 251 | caretIndex = 0; 252 | onChange(FlxInputText.HOME_ACTION); 253 | } 254 | } 255 | 256 | /** 257 | * This function replaces `onKeyDown` with support for RTL & LTR letter input 258 | * `regularKeysDown()` is one of two functions, and is using `window.onKeyDown` to get button 259 | * presses, so pay attention to that when overriding. 260 | * @param letter the letter outputted from the current key-press according to lime's `window.onTextInput` 261 | */ 262 | function regularKeysDown(letter:String) 263 | { 264 | // if the user didnt intend to edit the text, dont do anything 265 | if (!hasFocus) 266 | return; 267 | // if the caret is broken for some reason, fix it 268 | if (caretIndex < 0) 269 | caretIndex = 0; 270 | // set up the letter - remove null chars, add rtl mark to letters from RTL languages 271 | var t:String = "", hasConverted:Bool = false, addedSpace:Bool = false; 272 | #if !js 273 | if (letter != null) 274 | { 275 | // logic for general RTL letters, spacebar, punctuation mark 276 | if (CharTools.isRTL(letter) || (currentlyRTL && letter == " ") || (CharTools.generalMarks.contains(letter) && currentlyRTL)) 277 | { 278 | currentlyNumbers = false; 279 | t = CharTools.RLO + letter; 280 | currentlyRTL = true; 281 | if (openingDirection == UNDETERMINED || text == "") 282 | { 283 | if (autoAlign) 284 | alignment = RIGHT; 285 | openingDirection = RTL; 286 | } 287 | } 288 | // logic for when the user converted from RTL to LTR 289 | else if (currentlyRTL) 290 | { 291 | t = letter; 292 | currentlyRTL = false; 293 | hasConverted = true; 294 | 295 | // after conversion, the caret needs to move itself to he end of the RTL text. 296 | // the last spacebar also needs to be moved 297 | if (text.charAt(caretIndex) == " ") 298 | { 299 | t = CharTools.PDF + " " + letter; 300 | text = text.substring(0, caretIndex) + text.substring(caretIndex, text.length); 301 | addedSpace = true; 302 | } 303 | caretIndex++; 304 | 305 | while (CharTools.isRTL(text.charAt(caretIndex)) || text.charAt(caretIndex) == " " && caretIndex != text.length) 306 | caretIndex++; 307 | } 308 | // logic for everything else - LTR letters, special chars... 309 | else 310 | { 311 | t = letter; 312 | if (openingDirection == UNDETERMINED || text == "") 313 | { 314 | if (autoAlign) 315 | alignment = LEFT; 316 | if (CharTools.generalMarks.contains(t)) 317 | openingDirection = UNDETERMINED 318 | else 319 | openingDirection = LTR; 320 | } 321 | } 322 | } 323 | else 324 | ""; 325 | #else 326 | t = letter; 327 | #end 328 | if (t.length > 0 && (maxLength == 0 || (text.length + t.length) < maxLength)) 329 | { 330 | caretIndex += t.length; 331 | text = insertSubstring(text, t, caretIndex - 1); 332 | if (hasConverted) 333 | caretIndex++; 334 | if (addedSpace) 335 | caretIndex++; 336 | onChange(FlxInputText.INPUT_ACTION); 337 | } 338 | } 339 | 340 | function set_autoAlign(value:Bool):Bool 341 | { 342 | if (!value) 343 | return value; 344 | if (!CharTools.isRTL(text.charAt(0))) 345 | { 346 | alignment = LEFT; 347 | } 348 | else 349 | { 350 | alignment = RIGHT; 351 | } 352 | return value; 353 | } 354 | } 355 | #end 356 | -------------------------------------------------------------------------------- /src/texter/flixel/FlxSuperText.hx: -------------------------------------------------------------------------------- 1 | #if (flixel && false) 2 | package texter.flixel; 3 | 4 | import flixel.group.FlxSpriteGroup; 5 | 6 | 7 | /** 8 | * A text class that has some extra fancy visual settings to it. 9 | * 10 | * uses FlxInputTextRTL under the hood to support both RTL and LTR input. 11 | * 12 | * INCOMPLETE - waiting for FlxInputTextRTL's RTL WordWrap 13 | */ 14 | class FlxSuperText extends FlxSpriteGroup { 15 | 16 | public function new(x:Float, y:Float, length:Int, size:Int) 17 | { 18 | super(x, y); 19 | } 20 | 21 | override function draw() 22 | { 23 | super.draw(); 24 | } 25 | } 26 | #end -------------------------------------------------------------------------------- /src/texter/flixel/FlxTextButton.hx: -------------------------------------------------------------------------------- 1 | #if flixel 2 | package texter.flixel; 3 | 4 | import flixel.group.FlxSpriteGroup; 5 | import flixel.ui.FlxButton; 6 | import flixel.FlxG; 7 | 8 | /** 9 | * A text that calls a function when clicked 10 | * Behaves like a regular `FlxInputTextRTL`, but 11 | * with extra button functions. 12 | */ 13 | class FlxTextButton extends FlxSpriteGroup 14 | { 15 | /** 16 | An Instance of FlxInputTextRTL, will handle the 17 | text visulas & input. 18 | **/ 19 | public var label:FlxInputTextRTL; 20 | 21 | /** 22 | The current state of the button: 23 | 24 | | State | Situation | 25 | | --- | --- | 26 | | `FlxButton.NORMAL` | when the button isn't overlapped by the mouse/touchpoint | 27 | | `FlxButton.HIGHLIGHT` | when the mouse overlaps the button, but isn't pressed | 28 | | `FlxButton.PRESSED` | when the mouse/touchpoint not only overlap the button, but are also pressed | 29 | **/ 30 | public var status(get, null):Int; 31 | 32 | /** 33 | The button's callback for when the user presses it 34 | **/ 35 | public var onClick:Void->Void; 36 | 37 | /** 38 | #### For INPUT mode only 39 | The button's callback for when the user presses enter while the text is focused 40 | **/ 41 | public var onEnter:Void->Void; 42 | 43 | public function new(x:Float = 0, y:Float = 0, width:Int = 0, text:String = "", size:Int = 8, ?OnClick:Void->Void = null, ?OnEnter:Void->Void = null) 44 | { 45 | super(x, y); 46 | if (OnClick == null) 47 | onClick == () -> return; 48 | else 49 | onClick = OnClick; 50 | 51 | if (OnEnter == null) 52 | onEnter == () -> return; 53 | else 54 | onEnter = OnEnter; 55 | 56 | label = new FlxInputTextRTL(0, 0, width, text, size); 57 | add(label); 58 | } 59 | 60 | function get_status():Int 61 | { 62 | #if FLX_MOUSE 63 | if (FlxG.mouse.overlaps(this) && FlxG.mouse.pressed) 64 | return FlxButton.PRESSED; 65 | if (FlxG.mouse.overlaps(this)) 66 | return FlxButton.HIGHLIGHT; 67 | return FlxButton.NORMAL; 68 | #else 69 | for (touch in FlxG.touches.list) 70 | { 71 | if (touch.overlaps(this)) 72 | return FlxButton.PRESSED; 73 | return FlxButton.NORMAL; 74 | } 75 | #end 76 | } 77 | 78 | override function update(elapsed:Float) 79 | { 80 | super.update(elapsed); 81 | #if !mobile 82 | if (FlxG.keys.justPressed.ENTER && label.hasFocus) 83 | onEnter(); 84 | if (FlxG.mouse.overlaps(this) && FlxG.mouse.justReleased) 85 | onClick(); 86 | #end 87 | } 88 | } 89 | 90 | enum LabelType 91 | { 92 | INPUT; 93 | REGULAR; 94 | } 95 | #end 96 | -------------------------------------------------------------------------------- /src/texter/flixel/_internal/FlxInputText.hx: -------------------------------------------------------------------------------- 1 | package texter.flixel._internal; 2 | 3 | #if flixel 4 | import flash.errors.Error; 5 | import flash.events.KeyboardEvent; 6 | import flash.geom.Rectangle; 7 | import flixel.FlxG; 8 | import flixel.FlxSprite; 9 | import flixel.math.FlxPoint; 10 | import flixel.math.FlxRect; 11 | import flixel.text.FlxText; 12 | import flixel.util.FlxColor; 13 | import flixel.util.FlxDestroyUtil; 14 | import flixel.util.FlxTimer; 15 | 16 | using StringTools; 17 | /** 18 | * FlxInputText v1.11, ported to Haxe 19 | * 20 | * by **larsiusprime, (Lars Doucet)** 21 | * 22 | * http://github.com/haxeflixel/flixel-ui 23 | 24 | * 25 | * FlxInputText v1.10, Input text field extension for Flixel 26 | * 27 | * by **Gama11, Mr_Walrus, nitram_cero (Martín Sebastián Wain)** 28 | * 29 | * http://forums.flixel.org/index.php/topic,272.0.html 30 | 31 | * 32 | * Copyright (c) 2009 Martín Sebastián Wain 33 | * License: Creative Commons Attribution 3.0 United States 34 | * @link http://creativecommons.org/licenses/by/3.0/us/ 35 | 36 | * 37 | * 38 | * **WARNING** - used here as an engine, so some features may be altered 39 | */ 40 | class FlxInputText extends FlxText 41 | { 42 | public static inline var NO_FILTER:Int = 0; 43 | public static inline var ONLY_ALPHA:Int = 1; 44 | public static inline var ONLY_NUMERIC:Int = 2; 45 | public static inline var ONLY_ALPHANUMERIC:Int = 3; 46 | public static inline var CUSTOM_FILTER:Int = 4; 47 | 48 | public static inline var ALL_CASES:Int = 0; 49 | public static inline var UPPER_CASE:Int = 1; 50 | public static inline var LOWER_CASE:Int = 2; 51 | 52 | public static inline var BACKSPACE_ACTION:String = "backspace"; // press backspace 53 | 54 | public static inline var DELETE_ACTION:String = "delete"; // press delete 55 | 56 | public static inline var ENTER_ACTION:String = "enter"; // press enter 57 | 58 | public static inline var INPUT_ACTION:String = "input"; // manually edit 59 | 60 | public static inline var HOME_ACTION:String = "home"; // press home 61 | 62 | public static inline var END_ACTION:String = "end"; // press end 63 | 64 | /** 65 | * This regular expression will filter out (remove) everything that matches. 66 | * Automatically sets filterMode = FlxInputText.CUSTOM_FILTER. 67 | */ 68 | public var customFilterPattern(default, set):EReg; 69 | 70 | function set_customFilterPattern(cfp:EReg) 71 | { 72 | customFilterPattern = cfp; 73 | filterMode = CUSTOM_FILTER; 74 | return customFilterPattern; 75 | } 76 | 77 | /** 78 | * A function called whenever the value changes from user input, or enter is pressed 79 | */ 80 | public var callback:(String, String) -> Void; 81 | 82 | /** 83 | * Whether or not the textbox has a background 84 | */ 85 | public var background:Bool = false; 86 | 87 | /** 88 | * The caret's color. Has the same color as the text by default. 89 | */ 90 | public var caretColor(default, set):Int; 91 | 92 | function set_caretColor(i:Int):Int 93 | { 94 | caretColor = i; 95 | dirty = true; 96 | return caretColor; 97 | } 98 | 99 | public var caretWidth(default, set):Int = 1; 100 | 101 | function set_caretWidth(i:Int):Int 102 | { 103 | caretWidth = i; 104 | dirty = true; 105 | return caretWidth; 106 | } 107 | 108 | /** 109 | * Whether or not the textfield is a password textfield 110 | */ 111 | public var passwordMode(get, set):Bool; 112 | 113 | /** 114 | * Whether or not the text box is the active object on the screen. 115 | */ 116 | public var hasFocus(default, set):Bool = false; 117 | 118 | /** 119 | * The position of the selection cursor. An index of 0 means the carat is before the character at index 0. 120 | */ 121 | public var caretIndex(default, set):Int = 0; 122 | 123 | /** 124 | * callback that is triggered when this text field gets focus 125 | * @since 2.2.0 126 | */ 127 | public var focusGained:Void->Void = () -> return; 128 | 129 | /** 130 | * callback that is triggered when this text field loses focus 131 | * @since 2.2.0 132 | */ 133 | public var focusLost:Void->Void = () -> return; 134 | 135 | /** 136 | * The Case that's being enforced. Either ALL_CASES, UPPER_CASE or LOWER_CASE. 137 | */ 138 | public var forceCase(default, set):Int = ALL_CASES; 139 | 140 | /** 141 | * Set the maximum length for the field (e.g. "3" 142 | * for Arcade type hi-score initials). 0 means unlimited. 143 | */ 144 | public var maxLength(default, set):Int = 0; 145 | 146 | /** 147 | * Change the amount of lines that are allowed. 148 | */ 149 | public var lines(default, set):Int; 150 | 151 | /** 152 | * Defines what text to filter. It can be NO_FILTER, ONLY_ALPHA, ONLY_NUMERIC, ONLY_ALPHA_NUMERIC or CUSTOM_FILTER 153 | * (Remember to append "FlxInputText." as a prefix to those constants) 154 | */ 155 | public var filterMode(default, set):Int = NO_FILTER; 156 | 157 | /** 158 | * The color of the fieldBorders 159 | */ 160 | public var fieldBorderColor(default, set):Int = FlxColor.BLACK; 161 | 162 | /** 163 | * The thickness of the fieldBorders 164 | */ 165 | public var fieldBorderThickness(default, set):Int = 1; 166 | 167 | /** 168 | * The color of the background of the textbox. 169 | */ 170 | public var backgroundColor(default, set):Int = FlxColor.WHITE; 171 | 172 | /** 173 | * A FlxSprite representing the background sprite 174 | */ 175 | private var backgroundSprite:FlxSprite; 176 | 177 | /** 178 | * A timer for the flashing caret effect. 179 | */ 180 | private var _caretTimer:FlxTimer; 181 | 182 | /** 183 | * A FlxSprite representing the flashing caret when editing text. 184 | */ 185 | private var caret:FlxSprite; 186 | 187 | /** 188 | * A FlxSprite representing the fieldBorders. 189 | */ 190 | private var fieldBorderSprite:FlxSprite; 191 | 192 | /** 193 | * The left- and right- most fully visible character indeces 194 | */ 195 | private var _scrollBoundIndeces:{left:Int, right:Int} = {left: 0, right: 0}; 196 | 197 | // workaround to deal with non-availability of getCharIndexAtPoint or getCharBoundaries on cpp/neko targets 198 | private var _charBoundaries:Array; 199 | 200 | /** 201 | * Stores last input text scroll. 202 | */ 203 | private var lastScroll:Int; 204 | 205 | /** 206 | * @param X The X position of the text. 207 | * @param Y The Y position of the text. 208 | * @param Width The width of the text object (height is determined automatically). 209 | * @param Text The actual text you would like to display initially. 210 | * @param size Initial size of the font 211 | * @param TextColor The color of the text 212 | * @param BackgroundColor The color of the background (FlxColor.TRANSPARENT for no background color) 213 | * @param EmbeddedFont Whether this text field uses embedded fonts or not 214 | */ 215 | public function new(X:Float = 0, Y:Float = 0, Width:Int = 150, Text:String = '', size:Int = 8, TextColor:Int = FlxColor.BLACK, 216 | BackgroundColor:Int = FlxColor.WHITE, EmbeddedFont:Bool = true) 217 | { 218 | super(X, Y, Width, Text, size, EmbeddedFont); 219 | backgroundColor = BackgroundColor; 220 | 221 | if (BackgroundColor != FlxColor.TRANSPARENT) 222 | background = true; 223 | 224 | caretColor = color = TextColor; 225 | 226 | caret = new FlxSprite(); 227 | caret.makeGraphic(caretWidth, Std.int(size + 2)); 228 | _caretTimer = new FlxTimer(); 229 | 230 | caretIndex = 0; 231 | hasFocus = false; 232 | fieldBorderSprite = new FlxSprite(X, Y); 233 | backgroundSprite = new FlxSprite(X, Y); 234 | if (!background) 235 | fieldBorderSprite.visible = backgroundSprite.visible = false; 236 | 237 | lines = 1; 238 | FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); 239 | 240 | text = Text; 241 | 242 | calcFrame(); 243 | } 244 | 245 | /** 246 | * Clean up memory 247 | */ 248 | override public function destroy():Void 249 | { 250 | FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); 251 | 252 | backgroundSprite = FlxDestroyUtil.destroy(backgroundSprite); 253 | fieldBorderSprite = FlxDestroyUtil.destroy(fieldBorderSprite); 254 | callback = null; 255 | 256 | #if sys 257 | if (_charBoundaries != null) 258 | { 259 | while (_charBoundaries.length > 0) 260 | { 261 | _charBoundaries.pop(); 262 | } 263 | _charBoundaries = null; 264 | } 265 | #end 266 | 267 | super.destroy(); 268 | } 269 | 270 | /** 271 | * Draw the caret in addition to the text. 272 | */ 273 | override public function draw():Void 274 | { 275 | drawSprite(fieldBorderSprite); 276 | drawSprite(backgroundSprite); 277 | 278 | super.draw(); 279 | 280 | // In case caretColor was changed 281 | 282 | if (caretColor != caret.color || caret.height != size + 2) 283 | { 284 | caret.color = caretColor; 285 | } 286 | 287 | drawSprite(caret); 288 | } 289 | 290 | /** 291 | * Helper function that makes sure sprites are drawn up even though they haven't been added. 292 | * @param Sprite The Sprite to be drawn. 293 | */ 294 | private function drawSprite(Sprite:FlxSprite):Void 295 | { 296 | if (Sprite != null && Sprite.visible) 297 | { 298 | Sprite.scrollFactor = scrollFactor; 299 | Sprite.cameras = cameras; 300 | Sprite.draw(); 301 | } 302 | } 303 | 304 | /** 305 | * Check for mouse input every tick. 306 | */ 307 | override public function update(elapsed:Float):Void 308 | { 309 | super.update(elapsed); 310 | 311 | #if FLX_MOUSE 312 | // Set focus and caretIndex as a response to mouse press 313 | 314 | if (FlxG.mouse.justPressed) 315 | { 316 | var hadFocus:Bool = hasFocus; 317 | if (FlxG.mouse.overlaps(this)) 318 | { 319 | caretIndex = getCaretIndex(); 320 | hasFocus = true; 321 | if (!hadFocus && focusGained != null) 322 | focusGained(); 323 | } 324 | else 325 | { 326 | hasFocus = false; 327 | if (hadFocus && focusLost != null) 328 | focusLost(); 329 | } 330 | } 331 | #end 332 | } 333 | 334 | /** 335 | * Handles keypresses generated on the stage. 336 | */ 337 | private function onKeyDown(e:KeyboardEvent):Void 338 | { 339 | var key:Int = e.keyCode; 340 | 341 | if (hasFocus) 342 | { 343 | // Do nothing for Shift, Ctrl, Esc, and flixel console hotkey 344 | 345 | if (key == 16 || key == 17 || key == 220 || key == 27) 346 | { 347 | return; 348 | } 349 | // Left arrow 350 | else if (key == 37) 351 | { 352 | if (caretIndex > 0) 353 | { 354 | caretIndex--; 355 | text = text; // forces scroll update 356 | } 357 | } 358 | // Right arrow 359 | else if (key == 39) 360 | { 361 | if (caretIndex < text.length) 362 | { 363 | caretIndex++; 364 | text = text; // forces scroll update 365 | } 366 | } 367 | // End key 368 | else if (key == 35) 369 | { 370 | caretIndex = text.length; 371 | text = text; // forces scroll update 372 | } 373 | // Home key 374 | else if (key == 36) 375 | { 376 | caretIndex = 0; 377 | text = text; 378 | } 379 | // Backspace 380 | else if (key == 8) 381 | { 382 | if (caretIndex > 0) 383 | { 384 | caretIndex--; 385 | text = text.substring(0, caretIndex) + text.substring(caretIndex + 1); 386 | onChange(BACKSPACE_ACTION); 387 | } 388 | } 389 | // Delete 390 | else if (key == 46) 391 | { 392 | if ((text.length > 0) && (caretIndex < text.length)) 393 | { 394 | text = text.substring(0, caretIndex) + text.substring(caretIndex + 1); 395 | onChange(DELETE_ACTION); 396 | } 397 | } 398 | // Enter 399 | else if (key == 13) 400 | { 401 | onChange(ENTER_ACTION); 402 | } 403 | // Actually add some text 404 | else 405 | { 406 | if (e.charCode == 0) // non-printable characters crash String.fromCharCode 407 | 408 | { 409 | return; 410 | } 411 | var newText:String = filter(String.fromCharCode(e.charCode)); 412 | 413 | if (newText.length > 0 && (maxLength == 0 || (text.length + newText.length) < maxLength)) 414 | { 415 | text = insertSubstring(text, newText, caretIndex); 416 | caretIndex++; 417 | onChange(INPUT_ACTION); 418 | } 419 | } 420 | } 421 | } 422 | 423 | private function onChange(action:String):Void 424 | { 425 | if (callback != null) 426 | { 427 | callback(text, action); 428 | } 429 | } 430 | 431 | /** 432 | * Inserts a substring into a string at a specific index 433 | * 434 | * @param Original The string to have something inserted into 435 | * @param Insert The string to insert 436 | * @param Index The index to insert at 437 | * @return Returns the joined string for chaining. 438 | */ 439 | private function insertSubstring(Original:String, Insert:String, Index:Int):String 440 | { 441 | if (Index != Original.length) 442 | { 443 | Original = Original.substring(0, Index) + (Insert) + (Original.substring(Index)); 444 | } 445 | else 446 | { 447 | Original = Original + (Insert); 448 | } 449 | return Original; 450 | } 451 | 452 | /** 453 | * Gets the index of the character in this box under the mouse cursor 454 | * @return The index of the character. 455 | * between 0 and the length of the text 456 | */ 457 | private function getCaretIndex():Int 458 | { 459 | #if FLX_MOUSE 460 | var hit = FlxPoint.get(FlxG.mouse.x - x, FlxG.mouse.y - y); 461 | return getCharIndexAtPoint(hit.x, hit.y); 462 | #else 463 | return 0; 464 | #end 465 | } 466 | 467 | // ------------------- 468 | // HAS BEEN CHANGED 469 | // ---------------------- 470 | 471 | public function getCharBoundaries(charIndex:Int):Rectangle 472 | { 473 | if (_charBoundaries == null || charIndex < 0 || _charBoundaries.length <= 0) 474 | return new Rectangle(2, 2); 475 | 476 | var charBoundaries:Rectangle = new Rectangle(), 477 | actualIndex = charIndex; 478 | 479 | if (textField.getCharBoundaries(charIndex) != null) 480 | { 481 | charBoundaries = textField.getCharBoundaries(charIndex); 482 | } 483 | else if (text.charAt(charIndex) == "\n") 484 | { 485 | var diff = 1; // this is going to be used when a user presses enter twice to display the caret at the correct height 486 | 487 | while (text.charAt(charIndex - 1) == "\n") 488 | { 489 | charIndex--; 490 | diff++; 491 | } 492 | // if this is a spacebar, we cant use textField.getCharBoundaries() since itll return null 493 | 494 | charBoundaries = getCharBoundaries(charIndex - 1); 495 | charBoundaries.y += diff * charBoundaries.height; 496 | if (alignment == RIGHT) 497 | charBoundaries.x = x + width - 2 498 | else 499 | charBoundaries.x = 2; 500 | charBoundaries.width = 0; 501 | } 502 | else if (text.charAt(charIndex) == " ") 503 | { 504 | // we know that it doesnt matter how many spacebars are pressed, 505 | 506 | // the first one after a char/at the start of the text 507 | 508 | // is always defined and has the correct boundaries 509 | 510 | var widthDiff = 0, originalIndex = charIndex; 511 | while (text.charAt(charIndex - 1) == " " && charIndex != 0) 512 | { 513 | charIndex--; 514 | widthDiff++; 515 | } 516 | charBoundaries = textField.getCharBoundaries(charIndex); 517 | // removing this makes pressing between word-wrapped lines crash 518 | 519 | if (charBoundaries == null) 520 | charBoundaries = textField.getCharBoundaries(charIndex - 1); 521 | charBoundaries.x += widthDiff * charBoundaries.width 522 | - (width - 4) * (textField.getLineIndexOfChar(originalIndex) - textField.getLineIndexOfChar(charIndex)); 523 | // guessing line height differences when lots of spacebars are pressed and are being wordwrapped 524 | 525 | charBoundaries.y = textField.getLineIndexOfChar(originalIndex) * charBoundaries.height; 526 | } 527 | return charBoundaries; 528 | } 529 | 530 | // ---------------------------------- 531 | // HAS BEEN CHANGED 532 | // ---------------------------------- 533 | 534 | private override function set_text(Text:String):String 535 | { 536 | if (Text == "") 537 | Text = "​"; 538 | #if !js 539 | if (textField != null) 540 | { 541 | lastScroll = textField.scrollH; 542 | } 543 | #end 544 | var return_text:String = super.set_text(Text); 545 | 546 | if (textField == null) 547 | { 548 | return return_text; 549 | } 550 | 551 | var numChars:Int = Text.length; 552 | prepareCharBoundaries(numChars); 553 | textField.text = Text; 554 | onSetTextCheck(); 555 | return return_text; 556 | } 557 | 558 | // ---------------------------------- 559 | // HAS BEEN CHANGED 560 | // ---------------------------------- 561 | 562 | public function getCharIndexAtPoint(X:Float, Y:Float):Int 563 | { 564 | var i:Int = 0; 565 | #if !js 566 | X += textField.scrollH + 2; 567 | #end 568 | 569 | // place caret at matching char position 570 | 571 | if (text.length > 0) 572 | { 573 | for (i in 0...text.length) 574 | { 575 | var r = getCharBoundaries(i); 576 | if (X >= r.x && X <= r.right && Y >= r.y && Y <= r.bottom) // <----------------- CHANGE HERE 577 | 578 | { 579 | return i; 580 | } 581 | } 582 | // the mouse might have been pressed between the lines 583 | 584 | var i = 0; 585 | while (i < text.length) 586 | { 587 | var r = getCharBoundaries(i), 588 | line = textField.getLineIndexOfChar(i + 1); 589 | if (r == null) 590 | return 0; 591 | if (Y >= r.y && Y <= r.bottom) 592 | { 593 | if (i == 0) 594 | i--; 595 | if (i != -1 && !text.contains("\n")) 596 | i -= 2; 597 | if (i + 1 + textField.getLineText(line).replace("\n", "").length == text.length - 1) 598 | i++; 599 | return i + 1 + textField.getLineText(line).replace("\n", "").length; 600 | } 601 | i++; 602 | } 603 | return text.length; 604 | } 605 | // place caret at leftmost position 606 | 607 | return 0; 608 | } 609 | 610 | private function prepareCharBoundaries(numChars:Int):Void 611 | { 612 | if (_charBoundaries == null) 613 | { 614 | _charBoundaries = []; 615 | } 616 | 617 | if (_charBoundaries.length > numChars) 618 | { 619 | var diff:Int = _charBoundaries.length - numChars; 620 | for (i in 0...diff) 621 | { 622 | _charBoundaries.pop(); 623 | } 624 | } 625 | 626 | for (i in 0...numChars) 627 | { 628 | if (_charBoundaries.length - 1 < i) 629 | { 630 | _charBoundaries.push(FlxRect.get(0, 0, 0, 0)); 631 | } 632 | } 633 | } 634 | 635 | /** 636 | * Called every time the text is changed (for both flash/cpp) to update scrolling, etc 637 | */ 638 | private function onSetTextCheck():Void 639 | { 640 | var boundary:Rectangle = null; 641 | if (caretIndex == -1) 642 | { 643 | boundary = getCharBoundaries(text.length - 1); 644 | } 645 | else 646 | { 647 | boundary = getCharBoundaries(caretIndex); 648 | } 649 | 650 | if (boundary != null) 651 | { 652 | // Checks if caret is out of textfield bounds 653 | 654 | // if it is update scroll, otherwise maintain the same scroll as last check. 655 | 656 | var diffW:Int = 0; 657 | if (boundary.right > lastScroll + textField.width - 2) 658 | { 659 | diffW = -Std.int((textField.width - 2) - boundary.right); // caret to the right of textfield. 660 | } 661 | else if (boundary.left < lastScroll) 662 | { 663 | diffW = Std.int(boundary.left) - 2; // caret to the left of textfield 664 | } 665 | else 666 | { 667 | diffW = lastScroll; // no scroll change 668 | } 669 | textField.scrollH = diffW; 670 | calcFrame(); 671 | } 672 | } 673 | 674 | // ---------------------------------- 675 | // HAS BEEN CHANGED 676 | // ---------------------------------- 677 | 678 | /** 679 | * Draws the frame of animation for the input text. 680 | * 681 | * @param RunOnCpp Whether the frame should also be recalculated if we're on a non-flash target 682 | */ 683 | private override function calcFrame(RunOnCpp:Bool = false):Void 684 | { 685 | super.calcFrame(RunOnCpp); 686 | 687 | var h = if (text.length > 0) getCharBoundaries(text.length).height else height; 688 | var h = if (text.length > 0) getCharBoundaries(text.length).height else height; 689 | #if js if (caret != null && text == "") 690 | text = "​"; #end 691 | if (fieldBorderSprite != null) 692 | { 693 | if (fieldBorderThickness > 0) 694 | { 695 | fieldBorderSprite.makeGraphic(Std.int(width + fieldBorderThickness * 2), Std.int(h + fieldBorderThickness * 2), fieldBorderColor); 696 | fieldBorderSprite.x = x - fieldBorderThickness; 697 | fieldBorderSprite.y = y - fieldBorderThickness; 698 | } 699 | else if (fieldBorderThickness == 0) 700 | { 701 | fieldBorderSprite.visible = false; 702 | } 703 | } 704 | 705 | if (backgroundSprite != null) 706 | { 707 | if (background) 708 | { 709 | backgroundSprite.makeGraphic(Std.int(width), Std.int(height), backgroundColor); 710 | backgroundSprite.x = x; 711 | backgroundSprite.y = y; 712 | } 713 | else 714 | { 715 | backgroundSprite.visible = false; 716 | } 717 | } 718 | 719 | if (caret != null) 720 | { 721 | // Generate the properly sized caret and also draw a border that matches that of the textfield (if a border style is set) 722 | 723 | // borderQuality can be safely ignored since the caret is always a rectangle 724 | 725 | var cw:Int = caretWidth; // Basic size of the caret 726 | 727 | var ch:Int = Std.int(size + 2); 728 | 729 | // Make sure alpha channels are correctly set 730 | 731 | var borderC:Int = (0xff000000 | (borderColor & 0x00ffffff)); 732 | var caretC:Int = (0xff000000 | (caretColor & 0x00ffffff)); 733 | 734 | // Generate unique key for the caret so we don't cause weird bugs if someone makes some random flxsprite of this size and color 735 | 736 | var caretKey:String = "caret" + cw + "x" + ch + "c:" + caretC + "b:" + borderStyle + "," + borderSize + "," + borderC; 737 | switch (borderStyle) 738 | { 739 | case NONE: 740 | // No border, just make the caret 741 | 742 | caret.makeGraphic(cw, ch, caretC, false, caretKey); 743 | caret.offset.x = caret.offset.y = 0; 744 | 745 | case SHADOW: 746 | // Shadow offset to the lower-right 747 | 748 | cw += Std.int(borderSize); 749 | ch += Std.int(borderSize); // expand canvas on one side for shadow 750 | 751 | caret.makeGraphic(cw, ch, FlxColor.TRANSPARENT, false, caretKey); // start with transparent canvas 752 | 753 | var r:Rectangle = new Rectangle(borderSize, borderSize, caretWidth, Std.int(size + 2)); 754 | caret.pixels.fillRect(r, borderC); // draw shadow 755 | 756 | r.x = r.y = 0; 757 | caret.pixels.fillRect(r, caretC); // draw caret 758 | 759 | caret.offset.x = caret.offset.y = 0; 760 | 761 | case OUTLINE_FAST, OUTLINE: 762 | // Border all around it 763 | 764 | cw += Std.int(borderSize * 2); 765 | ch += Std.int(borderSize * 2); // expand canvas on both sides 766 | 767 | caret.makeGraphic(cw, ch, borderC, false, caretKey); // start with borderColor canvas 768 | 769 | var r = new Rectangle(borderSize, borderSize, caretWidth, Std.int(size + 2)); 770 | caret.pixels.fillRect(r, caretC); // draw caret 771 | 772 | // we need to offset caret's drawing position since the caret is now larger than normal 773 | 774 | caret.offset.x = caret.offset.y = borderSize; 775 | } 776 | // Update width/height so caret's dimensions match its pixels 777 | 778 | caret.width = cw; 779 | caret.height = ch; 780 | 781 | caretIndex = caretIndex; // force this to update 782 | } 783 | } 784 | 785 | /** 786 | * Turns the caret on/off for the caret flashing animation. 787 | */ 788 | private function toggleCaret(timer:FlxTimer):Void 789 | { 790 | caret.visible = !caret.visible; 791 | } 792 | 793 | /** 794 | * Checks an input string against the current 795 | * filter and returns a filtered string 796 | */ 797 | private function filter(text:String):String 798 | { 799 | if (forceCase == UPPER_CASE) 800 | { 801 | text = text.toUpperCase(); 802 | } 803 | else if (forceCase == LOWER_CASE) 804 | { 805 | text = text.toLowerCase(); 806 | } 807 | 808 | if (filterMode != NO_FILTER) 809 | { 810 | var pattern:EReg; 811 | switch (filterMode) 812 | { 813 | case ONLY_ALPHA: 814 | pattern = ~/[^a-zA-Z]*/g; 815 | case ONLY_NUMERIC: 816 | pattern = ~/[^0-9]*/g; 817 | case ONLY_ALPHANUMERIC: 818 | pattern = ~/[^a-zA-Z0-9]*/g; 819 | case CUSTOM_FILTER: 820 | pattern = customFilterPattern; 821 | default: 822 | throw new Error("FlxInputText: Unknown filterMode (" + filterMode + ")"); 823 | } 824 | text = pattern.replace(text, ""); 825 | } 826 | return text; 827 | } 828 | 829 | private override function set_x(X:Float):Float 830 | { 831 | if ((fieldBorderSprite != null) && fieldBorderThickness > 0) 832 | { 833 | fieldBorderSprite.x = X - fieldBorderThickness; 834 | } 835 | if ((backgroundSprite != null) && background) 836 | { 837 | backgroundSprite.x = X; 838 | } 839 | return super.set_x(X); 840 | } 841 | 842 | private override function set_y(Y:Float):Float 843 | { 844 | if ((fieldBorderSprite != null) && fieldBorderThickness > 0) 845 | { 846 | fieldBorderSprite.y = Y - fieldBorderThickness; 847 | } 848 | if ((backgroundSprite != null) && background) 849 | { 850 | backgroundSprite.y = Y; 851 | } 852 | return super.set_y(Y); 853 | } 854 | 855 | private function set_hasFocus(newFocus:Bool):Bool 856 | { 857 | if (newFocus) 858 | { 859 | if (hasFocus != newFocus) 860 | { 861 | _caretTimer = new FlxTimer().start(0.5, toggleCaret, 0); 862 | caret.visible = true; 863 | caretIndex = text.length; 864 | } 865 | } 866 | else 867 | { 868 | // Graphics 869 | 870 | caret.visible = false; 871 | if (_caretTimer != null) 872 | { 873 | _caretTimer.cancel(); 874 | } 875 | } 876 | 877 | if (newFocus != hasFocus) 878 | { 879 | calcFrame(); 880 | } 881 | return hasFocus = newFocus; 882 | } 883 | 884 | private function getAlignStr():FlxTextAlign 885 | { 886 | var alignStr:FlxTextAlign = LEFT; 887 | if (_defaultFormat != null && _defaultFormat.align != null) 888 | { 889 | alignStr = alignment; 890 | } 891 | return alignStr; 892 | } 893 | 894 | // ---------------------------------- 895 | // HAS BEEN CHANGED 896 | // ---------------------------------- 897 | 898 | private function set_caretIndex(newCaretIndex:Int):Int 899 | { 900 | var offx:Float = 0; 901 | 902 | var alignStr:FlxTextAlign = getAlignStr(); 903 | 904 | switch (alignStr) 905 | { 906 | case RIGHT: 907 | offx = textField.width - 2 - textField.textWidth - 2; 908 | if (offx < 0) 909 | offx = 0; // hack, fix negative offset. 910 | 911 | case CENTER: 912 | offx = (textField.width - 2 - textField.textWidth) / 2 + textField.scrollH / 2; 913 | if (offx <= 1) 914 | offx = 0; // hack, fix offset rounding alignment. 915 | 916 | default: 917 | offx = 0; 918 | } 919 | 920 | caretIndex = newCaretIndex; 921 | 922 | // If caret is too far to the right something is wrong 923 | 924 | if (caretIndex > (text.length + 1)) 925 | { 926 | caretIndex = -1; 927 | } 928 | 929 | // Caret is OK, proceed to position 930 | 931 | if (caretIndex != -1) 932 | { 933 | var boundaries:Rectangle = null; 934 | 935 | // Caret is not to the right of text 936 | 937 | if (caretIndex < text.length) 938 | { 939 | boundaries = getCharBoundaries(caretIndex - 1); 940 | if (boundaries != null) 941 | { 942 | caret.x = boundaries.right + x; 943 | caret.y = boundaries.top + y; 944 | } 945 | } 946 | // Caret is to the right of text 947 | else 948 | { 949 | boundaries = getCharBoundaries(caretIndex - 1); 950 | if (boundaries != null) 951 | { 952 | caret.x = boundaries.right + x; 953 | caret.y = boundaries.top + y; 954 | } 955 | else if (text.length == 0) 956 | { 957 | // 2 px gutters 958 | 959 | caret.x = x + 2; 960 | caret.y = y + 2; 961 | } 962 | } 963 | } 964 | caret.x -= textField.scrollH; 965 | 966 | // Make sure the caret doesn't leave the textfield on single-line input texts 967 | 968 | if ((lines == 1) && ((caret.x + caret.width) > (x + width))) 969 | { 970 | caret.x = x + width - 2; 971 | } 972 | 973 | return caretIndex; 974 | } 975 | 976 | private function set_forceCase(Value:Int):Int 977 | { 978 | forceCase = Value; 979 | text = filter(text); 980 | return forceCase; 981 | } 982 | 983 | override private function set_size(Value:Int):Int 984 | { 985 | super.size = Value; 986 | caret.makeGraphic(1, Std.int(size + 2)); 987 | return Value; 988 | } 989 | 990 | private function set_maxLength(Value:Int):Int 991 | { 992 | maxLength = Value; 993 | if (text.length > maxLength) 994 | { 995 | text = text.substring(0, maxLength); 996 | } 997 | return maxLength; 998 | } 999 | 1000 | private function set_lines(Value:Int):Int 1001 | { 1002 | if (Value == 0) 1003 | return 0; 1004 | 1005 | if (Value > 1) 1006 | { 1007 | textField.wordWrap = true; 1008 | textField.multiline = true; 1009 | } 1010 | else 1011 | { 1012 | textField.wordWrap = false; 1013 | textField.multiline = false; 1014 | } 1015 | 1016 | lines = Value; 1017 | calcFrame(); 1018 | return lines; 1019 | } 1020 | 1021 | private function get_passwordMode():Bool 1022 | { 1023 | return textField.displayAsPassword; 1024 | } 1025 | 1026 | private function set_passwordMode(value:Bool):Bool 1027 | { 1028 | textField.displayAsPassword = value; 1029 | calcFrame(); 1030 | return value; 1031 | } 1032 | 1033 | private function set_filterMode(Value:Int):Int 1034 | { 1035 | filterMode = Value; 1036 | text = filter(text); 1037 | return filterMode; 1038 | } 1039 | 1040 | private function set_fieldBorderColor(Value:Int):Int 1041 | { 1042 | fieldBorderColor = Value; 1043 | calcFrame(); 1044 | return fieldBorderColor; 1045 | } 1046 | 1047 | private function set_fieldBorderThickness(Value:Int):Int 1048 | { 1049 | fieldBorderThickness = Value; 1050 | calcFrame(); 1051 | return fieldBorderThickness; 1052 | } 1053 | 1054 | private function set_backgroundColor(Value:Int):Int 1055 | { 1056 | backgroundColor = Value; 1057 | calcFrame(); 1058 | return backgroundColor; 1059 | } 1060 | } 1061 | #end 1062 | -------------------------------------------------------------------------------- /src/texter/general/Char.hx: -------------------------------------------------------------------------------- 1 | package texter.general; 2 | 3 | import texter.general.CharTools.*; 4 | 5 | /** 6 | * An abstract that loosely represents a `char`. 7 | * 8 | * The reason i say "loosely" is because its not actually 9 | * a real `char`, but an abstraction over `String` to 10 | * help you manipulate/get information about characters 11 | * 12 | * When used with `CharTools`, it provides some great tooling to work with characters & string 13 | */ 14 | abstract Char(String) 15 | { 16 | /** 17 | * Every single character has a code that associates with it. 18 | * 19 | * The `charCode` property is the code corresponding to this `Char`. 20 | * 21 | * Changing this value will also change the char. 22 | */ 23 | public var charCode(get, set):Null; 24 | 25 | @:noCompletion inline function get_charCode() 26 | return toInt(); 27 | 28 | @:noCompletion inline function set_charCode(i:Null):Null 29 | { 30 | this = charFromValue[i].toString(); 31 | return charFromValue[i]; 32 | } 33 | 34 | /** 35 | * A `String` representation of this `Char`. 36 | * 37 | * `Char`s are at their core just a fancy, one-character `String`, 38 | * so you should be able to use the actual char to represent a string too. 39 | */ 40 | public var character(get, set):String; 41 | 42 | @:noCompletion inline function get_character() 43 | return toString(); 44 | 45 | @:noCompletion inline function set_character(string:String) 46 | return this = string; 47 | 48 | /** 49 | * Creates a new `Char`, from either a `String`, or 50 | * an `Int`. If you supply both values, the char 51 | * will use the `String` property 52 | * 53 | * **Notice** - Creating a `Char` with an `Int` might be slow 54 | * 55 | * Usage: 56 | * 57 | * ```haxe 58 | * var char = new Char("n"); 59 | * //or maybe 60 | * var char2 = new Char(110); 61 | * ``` 62 | * 63 | * **Notice** - you can also just do: 64 | * ```haxe 65 | * var char = "n"; 66 | * var char2 = 110; 67 | * ``` 68 | */ 69 | public function new(?string:String, ?int:Int) 70 | { 71 | if (string != null && string.length != 0) 72 | { 73 | this = string.charAt(0); 74 | return; 75 | } 76 | else if (int != null) 77 | { 78 | this = charFromValue[int]; 79 | return; 80 | } 81 | this = ""; 82 | } 83 | 84 | @:from public static inline function fromInt(int:Int):Char 85 | { 86 | return charFromValue[int]; 87 | } 88 | 89 | @:from public static inline function fromString(string:String):Char 90 | { 91 | return new Char(string); 92 | } 93 | 94 | @:to public inline function toString():String 95 | { 96 | return this; 97 | } 98 | 99 | @:to public inline function toInt():Int 100 | { 101 | return charToValue[this]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/texter/general/TextTools.hx: -------------------------------------------------------------------------------- 1 | package texter.general; 2 | 3 | typedef TextTools = std.TextTools; 4 | 5 | enum TextDirection 6 | { 7 | RTL; 8 | LTR; 9 | UNDETERMINED; 10 | } 11 | -------------------------------------------------------------------------------- /src/texter/general/bidi/Bidi.hx: -------------------------------------------------------------------------------- 1 | package texter.general.bidi; 2 | 3 | import texter.general.CharTools; 4 | 5 | using TextTools; 6 | 7 | class Bidi 8 | { 9 | public static function getTextAttributes(text:String, convCheck = true) 10 | { 11 | var attributes:Array = [Bidified]; 12 | text.remove("\r"); 13 | if (text.contains(CharTools.RLM) && convCheck) 14 | { 15 | // get the first RLM 16 | var rlmIndex = text.indexOf(CharTools.RLM); 17 | // get the last RLM 18 | var rlmEndIndex = text.lastIndexOf(CharTools.RLM); 19 | // get the text between the two RLM 20 | var rlmText = text.substring(rlmIndex + 1, rlmEndIndex); 21 | // get the text before the first RLM 22 | var rlmPreText = text.substring(0, rlmIndex); 23 | // get the text after the last RLM 24 | var rlmPostText = text.substring(rlmEndIndex + 1); 25 | // combine with unbifiy 26 | text = rlmPreText + unbidify(rlmText) + rlmPostText; 27 | } 28 | text = text.replace(CharTools.RLM, ""); 29 | var rtlString = ""; 30 | var ltrString = ""; 31 | var processedNewLine = true; 32 | var currentLineDir = UNDETERMINED; 33 | 34 | function endOfStringCheck(i:Int) 35 | { 36 | if (i == text.length - 1) 37 | { 38 | if (rtlString.length > 0) 39 | { 40 | attributes.push(Rtl(rtlString)); 41 | rtlString = ""; 42 | } 43 | else if (ltrString.length > 0) 44 | { 45 | attributes.push(Ltr(ltrString)); 46 | ltrString = ""; 47 | } 48 | attributes.push(LineEnd); 49 | } 50 | } 51 | for (i in 0...text.length) 52 | { 53 | var char = text.charAt(i); 54 | if (CharTools.softChars.contains(char)) 55 | { 56 | if (i != text.length - 1) 57 | { 58 | var ti = i; 59 | var nextChar = if (convCheck) text.charAt(++ti) else text.charAt(--ti); 60 | while (CharTools.softChars.contains(nextChar)) 61 | { 62 | if (ti == if (convCheck) text.length - 1 else 0) 63 | { 64 | if (rtlString.length > 0) 65 | { 66 | rtlString += char; 67 | } 68 | else if (ltrString.length > 0) 69 | { 70 | ltrString += char; 71 | } 72 | if (convCheck) 73 | endOfStringCheck(i); 74 | break; 75 | } 76 | nextChar = if (convCheck) text.charAt(++ti) else text.charAt(--ti); 77 | } 78 | if (char == CharTools.NEWLINE) 79 | { 80 | if (rtlString.length > 0) 81 | { 82 | attributes.push(Rtl(rtlString)); 83 | rtlString = ""; 84 | } 85 | else if (ltrString.length > 0) 86 | { 87 | attributes.push(Ltr(ltrString)); 88 | ltrString = ""; 89 | } 90 | attributes.push(LineEnd); 91 | processedNewLine = true; 92 | } 93 | else if (CharTools.numbers.contains(nextChar)) 94 | { 95 | if (rtlString.length > 0) 96 | { 97 | rtlString += char; 98 | } 99 | else if (ltrString.length > 0) 100 | { 101 | ltrString += char; 102 | } 103 | } 104 | else if (CharTools.isRTL(nextChar)) 105 | { 106 | // if the direction is RTL, spacebars should be added to the RTL string 107 | if (currentLineDir == RTL) 108 | { 109 | if (ltrString.length > 0) 110 | { 111 | attributes.push(Ltr(ltrString)); 112 | ltrString = ""; 113 | } 114 | rtlString += char; 115 | } 116 | // the direction is LTR, spacebats should be added the the ltr strings 117 | else 118 | { 119 | if (ltrString.length > 0) 120 | { 121 | ltrString += char; 122 | attributes.push(Ltr(ltrString)); 123 | ltrString = ""; 124 | } 125 | else 126 | { 127 | rtlString += char; 128 | } 129 | } 130 | } 131 | else 132 | { 133 | // if the direction is RTL, spacebars should be added to the RTL string 134 | if (currentLineDir == RTL) 135 | { 136 | if (rtlString.length > 0) 137 | { 138 | rtlString += char; 139 | attributes.push(Rtl(rtlString)); 140 | rtlString = ""; 141 | } 142 | else 143 | { 144 | ltrString += char; 145 | } 146 | } 147 | // the direction is LTR, spacebats should be added the the ltr strings 148 | else 149 | { 150 | if (rtlString.length > 0) 151 | { 152 | attributes.push(Rtl(rtlString)); 153 | rtlString = ""; 154 | } 155 | ltrString += char; 156 | } 157 | } 158 | endOfStringCheck(i); 159 | continue; 160 | } 161 | if (ltrString.length > 0) 162 | { 163 | ltrString += char; 164 | endOfStringCheck(i); 165 | continue; 166 | } 167 | else if (rtlString.length > 0) 168 | { 169 | rtlString += char; 170 | endOfStringCheck(i); 171 | continue; 172 | } 173 | else 174 | { 175 | attributes.push(SoftChar(char, UNDETERMINED)); 176 | endOfStringCheck(i); 177 | continue; 178 | } 179 | } 180 | 181 | if (char == CharTools.NEWLINE) 182 | { 183 | if (rtlString.length > 0) 184 | { 185 | attributes.push(Rtl(rtlString)); 186 | rtlString = ""; 187 | } 188 | else if (ltrString.length > 0) 189 | { 190 | attributes.push(Ltr(ltrString)); 191 | ltrString = ""; 192 | } 193 | attributes.push(LineEnd); 194 | processedNewLine = true; 195 | currentLineDir = UNDETERMINED; 196 | 197 | endOfStringCheck(i); 198 | 199 | continue; 200 | } 201 | 202 | if (CharTools.numbers.contains(char)) 203 | { 204 | if (ltrString.length > 0) 205 | { 206 | ltrString += char; 207 | endOfStringCheck(i); 208 | continue; 209 | } 210 | else 211 | { 212 | rtlString += char; 213 | endOfStringCheck(i); 214 | continue; 215 | } 216 | } 217 | 218 | if (CharTools.isRTL(char)) 219 | { 220 | if (processedNewLine) 221 | { 222 | attributes.push(LineDirection(RTL)); 223 | currentLineDir = RTL; 224 | processedNewLine = false; 225 | } 226 | if (ltrString.length > 0) 227 | { 228 | attributes.push(Ltr(ltrString)); 229 | ltrString = ""; 230 | } 231 | rtlString += char; 232 | 233 | endOfStringCheck(i); 234 | 235 | continue; 236 | } 237 | else 238 | { 239 | if (rtlString.length > 0) 240 | { 241 | attributes.push(Rtl(rtlString)); 242 | rtlString = ""; 243 | } 244 | } 245 | 246 | // we have an LTR char 247 | if (processedNewLine) 248 | { 249 | attributes.push(LineDirection(LTR)); 250 | currentLineDir = LTR; 251 | processedNewLine = false; 252 | } 253 | if (rtlString.length > 0) 254 | { 255 | attributes.push(Rtl(rtlString)); 256 | rtlString = ""; 257 | } 258 | ltrString += char; 259 | 260 | endOfStringCheck(i); 261 | } 262 | attributes.push(Bidified); 263 | return attributes; 264 | } 265 | 266 | public static function unbidify(text:String) 267 | { 268 | return process(text, false); 269 | } 270 | 271 | public static function process(text:String, convCheck:Bool = true):String 272 | { 273 | var attributes:Array = getTextAttributes(text, convCheck); 274 | return processTextAttributes(attributes); 275 | } 276 | 277 | public static function processTextAttributes(attributes:Array):String 278 | { 279 | var result = ""; 280 | 281 | var currentLineDirection = UNDETERMINED; 282 | for (a in attributes) 283 | { 284 | switch a 285 | { 286 | case Bidified: 287 | result += CharTools.RLM; 288 | case LineDirection(letterType): 289 | currentLineDirection = letterType; 290 | case Rtl(string): 291 | { 292 | // we want to find all number groups in the string, reverse them, and then reverse the whole string 293 | var numberEreg = ~/\d+/; 294 | var groups = string.indexesFromEReg(numberEreg); 295 | for (i in 0...groups.length) 296 | { 297 | var group = groups[i]; 298 | var groupStr = string.substring(group.startIndex, group.endIndex); 299 | var reversed = groupStr.reverse(); 300 | string = string.replace(groupStr, reversed); 301 | } 302 | switch currentLineDirection 303 | { 304 | case RTL: { 305 | // find the previous line break 306 | var index = result.length - 1; 307 | while (result.charAt(index) != "\n" && index > 0) 308 | index--; 309 | result = result.substring(0, index + 1) + string.reverse() + result.substring(index + 1); 310 | } 311 | case LTR | UNDETERMINED: result += string.reverse(); 312 | } 313 | } 314 | case Ltr(string): 315 | { 316 | switch currentLineDirection 317 | { 318 | case RTL: { 319 | // find the previous line break 320 | var index = result.length - 1; 321 | while (result.charAt(index) != "\n" && index > 0) 322 | index--; 323 | result = result.substring(0, index + 1) + string + result.substring(index + 1); 324 | } 325 | case LTR | UNDETERMINED: result += string; 326 | } 327 | } 328 | case SoftChar(string, generalDirection): 329 | result += string; 330 | case LineEnd: 331 | result += "\n"; 332 | } 333 | if (result.charAt(result.length - 1) == "\n") 334 | result = result.replaceLast("\n", ""); 335 | } 336 | return result; 337 | } 338 | 339 | public static function stringityAttributes(array:Array):String 340 | { 341 | var result = ""; 342 | for (a in array) 343 | { 344 | result += Std.string(a) + "\n"; 345 | } 346 | return result; 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /src/texter/general/bidi/TextAttribute.hx: -------------------------------------------------------------------------------- 1 | package texter.general.bidi; 2 | 3 | import TextTools.TextDirection; 4 | 5 | enum TextAttribute { 6 | Bidified; 7 | LineDirection(letterType:TextDirection); 8 | Rtl(string:String); 9 | Ltr(string:String); 10 | SoftChar(string:String, generalDirection:TextDirection); 11 | LineEnd(); 12 | } -------------------------------------------------------------------------------- /src/texter/general/markdown/Markdown.hx: -------------------------------------------------------------------------------- 1 | package texter.general.markdown; 2 | 3 | import texter.general.markdown.MarkdownEffect; 4 | import texter.general.markdown.MarkdownPatterns; 5 | import texter.general.markdown.MarkdownBlocks; 6 | import texter.general.markdown.MarkdownVisualizer; 7 | import texter.general.Emoji; 8 | 9 | using StringTools; 10 | using texter.general.TextTools; 11 | 12 | /** 13 | * The `Markdown` class provides tools to handle markdown texts in a **cross-framework** fashion. 14 | * 15 | * **In its base resides the `interpret()` function.** 16 | * 17 | * Everything in this class is based around the interpreter. everything from the markdown rules, 18 | * to the patterns & visualization methods. The interpreter uses a pre-parser & lots of `regex`s 19 | * (Regular Expresions) to parse the markdown text. after parsing, it returns the text witout all 20 | * of the "ugly" markdown syntax, and an array of the effects that are needed to be appied, with 21 | * `startIndex` and `endIndex`. You don't have to utilize all of the information the interpreter 22 | * gives you - it doesnt enforce anything - it just gives you the data you need to start working 23 | * 24 | * `interpret()` is mostly used internally to get information about the markdown text for visualization methods, 25 | * but you can also use it yourself to make your own markdown styling 26 | * 27 | */ 28 | class Markdown 29 | { 30 | /** 31 | * The `patterns` field contains all of the patterns used to parse markdown text. 32 | * 33 | * If you want to access them individually, you can do it using this field 34 | */ 35 | public static inline var patterns = MarkdownPatterns; 36 | 37 | /** 38 | * `syntaxBlocks` is a field representing a class that contains many syntax coloring 39 | * methods for codeblocks. 40 | */ 41 | public static inline var syntaxBlocks = MarkdownBlocks; 42 | 43 | /** 44 | * If you want to modify certain visual aspects of the markdown text, you can 45 | * gain access to those via the `visualizer` field. 46 | */ 47 | public static inline var visualizer = MarkdownVisualizer; 48 | 49 | static var markdownRules(default, null):Array = [ 50 | patterns.doubleSpaceNewlineEReg, // Done. 51 | patterns.backslashNewlineEReg, // Done. 52 | patterns.alignmentEReg, // Done. 53 | patterns.indentEReg, // Done. 54 | patterns.hRuledTitleEReg, // Done. 55 | patterns.titleEReg, // Done. 56 | patterns.codeblockEReg, // Done. 57 | patterns.tildeCodeblockEReg, // Done. 58 | patterns.tabCodeblockEReg, // Done. 59 | patterns.emojiEReg, // Done. 60 | patterns.boldEReg, // Done. 61 | patterns.astBoldEReg, // Done. 62 | patterns.strikeThroughEReg, // Done. 63 | patterns.italicEReg, // Done. 64 | patterns.astItalicEReg, // Done. 65 | patterns.mathEReg, // Done. 66 | patterns.codeEReg, // Done. 67 | patterns.linkEReg, // Done. 68 | patterns.listItemEReg, // Done. 69 | patterns.hRuleEReg, // Done. 70 | patterns.parSepEReg // Done. 71 | ]; 72 | 73 | /** 74 | * Mostly for internal use, but can also be useful for creating your own 75 | * Markdown styling. 76 | * 77 | * This function takes in a string formatted in Markdown, and each time it encounteres 78 | * a Markdown "special effect" (headings, charts, points, etc.), it pushes 79 | * a style corresponding to the found effect. 80 | * 81 | * for some effects, it also includes a built-in visual effect: 82 | * - Unordered Lists 83 | * - Emojis 84 | * - Tables (coming soon) 85 | * 86 | * after finding the effects, it calls: 87 | * 88 | * ### onComplete: 89 | * 90 | * The `onComplete()` will get called after the text has been processed: 91 | * - **First Argument - The Actual Text**: to keep the text clean, after proccessing the text, a markdown- 92 | * free version of the text is returned (altho some marks do remain, such as the list items and hrules) 93 | * - **Second Argument - The Effects** - this array contains lots of ADTs (algebric data types). Those 94 | * contain the actual data - most of them contain the start & end index of the effect, and some contain more 95 | * data (things like list numbers, indentation...) 96 | * 97 | * ### Things to notice: 98 | 99 | * - The markdown text contains zero-width spaces (\u200B) in the text in order to keep track of effect positions. 100 | * - The effect's range is from startIndex up to, but not including endIndex. 101 | * - certine effects will already be rendered by the interpreter, so no need to mess with those. 102 | * - The interpreter doesnt support everything markdown has to offer (yet). supported markups: 103 | * - **Headings**: #, ##, ###, ####, #####, ######, ####### 104 | * - **Lists (also nested)**: -, *, +, 1., 2. 105 | * - **CodeBlocks**: ``````, ~~~~~~, four spaces 106 | * - **Inline Code**: `` 107 | * - **Italics**: _, * 108 | * - **Bolds**: **, __ 109 | * - **Strikethrough**: ~~~~ 110 | * - **Links**: `[]()` 111 | * - **Math**: $$ 112 | * - **Emojis**: :emojiNameHere: 113 | * - **HRules**: ---, ***, ___, ===, +++ 114 | * - **HRuled Headings**: H1 - title\n===,+++,***, H2 - title\n---,___ 115 | * - **Paragraph Gaps** (two or more newlines) 116 | * - **NewLines** \ or double-whitespace at the end of the line 117 | * - **Cancel Symbol**: \\{SYMBOL HERE} 118 | * 119 | * There are also some extra additions: 120 | * 121 | * - **Alignment**: , , , -> 122 | * - **Tabs**: \t 123 | * 124 | * 125 | * @param markdownText Just a plain string with markdown formatting. If you want to make sure 126 | * the formatting is correct, just write the markdown text in a `.md` file and do `File.getContent("path/to/file.md")` 127 | */ 128 | public static function interpret(markdownText:String, onComplete:(String, Array) -> Void) 129 | { 130 | var lineTexts = StringTools.replace(markdownText, "\r", ""); 131 | var effects:Array = []; 132 | 133 | // now, we should handle \SYMBOL 134 | 135 | for (rule in markdownRules) 136 | { 137 | while (rule.match(lineTexts)) 138 | { 139 | if (rule == patterns.indentEReg) 140 | { 141 | lineTexts = rule.replace(lineTexts, "​".multiply(rule.matched(1).length) + rule.matched(2)); 142 | final info = rule.matchedPos(); 143 | final pos = info.pos - 1 < 0 ? info.pos : info.pos - 1; 144 | effects.push(Indent(rule.matched(1).length, pos, info.pos + info.len)); 145 | } 146 | if (rule == patterns.italicEReg || rule == patterns.mathEReg || rule == patterns.codeEReg || rule == patterns.astItalicEReg) 147 | { 148 | lineTexts = rule.replace(lineTexts, "​$1​"); 149 | final info = rule.matchedPos(); 150 | effects.push(if (rule == patterns.mathEReg) Math(info.pos, 151 | info.pos + info.len) else if (rule == patterns.codeEReg) Code(info.pos, info.pos + info.len) else 152 | Italic(info.pos, info.pos + info.len)); 153 | } 154 | else if (rule == patterns.boldEReg || rule == patterns.strikeThroughEReg || rule == patterns.astBoldEReg) 155 | { 156 | lineTexts = rule.replace(lineTexts, "​​$1​​"); 157 | final info = rule.matchedPos(); 158 | effects.push(if (rule == patterns.boldEReg || rule == patterns.astBoldEReg) Bold(info.pos, 159 | info.pos + info.len) else StrikeThrough(info.pos, info.pos + info.len)); 160 | } 161 | else if (rule == patterns.hRuleEReg) 162 | { 163 | lineTexts = rule.replace(lineTexts, if (rule == patterns.parSepEReg) "\n\n" else "—".multiply(rule.matched(1).length)); 164 | final info = rule.matchedPos(); 165 | effects.push(HorizontalRule(rule.matched(1).charAt(0), info.pos, info.pos + info.len)); 166 | } 167 | else if (rule == patterns.hRuledTitleEReg) 168 | { 169 | lineTexts = rule.replace(lineTexts, rule.matched(1) + "\n" + "—".multiply(rule.matched(2).length)); 170 | final info = rule.matchedPos(); 171 | var type = rule.matched(2).charAt(0); 172 | if (rule.matched(1).charAt(0) == "#") 173 | continue; // this is inteded to be a regular heading 174 | effects.push(Heading(if (type == "*" || type == "+" || type == "=") 1 else 2, if (info.pos == 0) info.pos else info.pos - 1, 175 | info.pos + rule.matched(1).length)); 176 | effects.push(HorizontalRule(type, info.pos + rule.matched(1).length + 1, info.pos + info.len + 1)); 177 | } 178 | else if (rule == patterns.linkEReg) 179 | { 180 | var linkLength = "​".multiply(rule.matched(2).length); 181 | lineTexts = rule.replace(lineTexts, "​$1​​​" + linkLength); 182 | final info = rule.matchedPos(); 183 | effects.push(Link(rule.matched(2), info.pos, info.pos + info.len)); 184 | } 185 | else if (rule == patterns.listItemEReg) 186 | { 187 | if (!rule.matched(2).contains(".")) 188 | { 189 | var n = rule.matched(1).length; 190 | final info = rule.matchedPos(); 191 | var start = info.pos - 2; 192 | if (start < 0) 193 | { 194 | lineTexts = rule.replace(lineTexts, "$1• $3"); 195 | effects.push(UnorderedListItem(n, info.pos, info.pos + info.len - 1)); 196 | continue; 197 | } 198 | while (lineTexts.charAt(start--) != "\n" && start != 0) {} // now, start contains the previous line's start 199 | if (start != 0) 200 | start += 2; 201 | var prevLine = lineTexts.substring(start, info.pos); 202 | if (prevLine.trim().charAt(0) == "•" || prevLine.trim().charAt(0) == "◦") 203 | { 204 | var len = 0; 205 | while (prevLine.charAt(len) != "•" && prevLine.charAt(len) != "◦") 206 | len++; 207 | if (n < len) 208 | { 209 | lineTexts = rule.replace(lineTexts, "$1• $3"); 210 | } 211 | else if (len == n) 212 | { 213 | if (prevLine.trim().charAt(0) == "•") 214 | { 215 | lineTexts = rule.replace(lineTexts, "$1• $3"); 216 | } 217 | else 218 | { 219 | lineTexts = rule.replace(lineTexts, "$1◦ $3"); 220 | } 221 | } 222 | else 223 | { 224 | lineTexts = rule.replace(lineTexts, "$1◦ $3"); 225 | } 226 | effects.push(UnorderedListItem(n, info.pos, info.pos + info.len - 1)); 227 | continue; 228 | } 229 | lineTexts = rule.replace(lineTexts, "$1• $3"); 230 | effects.push(UnorderedListItem(n, info.pos, info.pos + info.len - 1)); 231 | } 232 | else 233 | { 234 | lineTexts = rule.replace(lineTexts, rule.matched(1) + rule.matched(2).replace(".", "") + "․ $3"); 235 | final info = rule.matchedPos(); 236 | effects.push(OrderedListItem(Std.parseInt(rule.matched(2)), rule.matched(1).length, info.pos, info.pos + info.len - 1)); 237 | } 238 | } 239 | else if (rule == patterns.titleEReg) 240 | { 241 | lineTexts = rule.replace(lineTexts, rule.matched(1).replace("#", "​") + "$2"); 242 | final info = rule.matchedPos(); 243 | effects.push(Heading(rule.matched(1).length, info.pos, info.pos + info.len)); 244 | } 245 | else if (rule == patterns.codeblockEReg || rule == patterns.tildeCodeblockEReg) 246 | { 247 | var langLength = ""; 248 | while (langLength.length < rule.matched(1).length) 249 | langLength += "​"; 250 | lineTexts = rule.replace(lineTexts, langLength + "​​​\r$2​​​"); 251 | final info = rule.matchedPos(); 252 | effects.push(CodeBlock(rule.matched(1), info.pos, info.pos + info.len)); 253 | } 254 | else if (rule == patterns.tabCodeblockEReg) 255 | { 256 | lineTexts = rule.replace(lineTexts, "​​​​" + "​​​$1​​​"); 257 | final info = rule.matchedPos(); 258 | effects.push(TabCodeBlock(info.pos, info.pos + info.len + 4)); 259 | } 260 | else if (rule == patterns.emojiEReg) 261 | { 262 | var emoji = '​${texter.general.Emoji.emojiFromString[rule.matched(1)]}${"​".multiply(rule.matched(1).length - 2)}'; 263 | if (emoji.contains("undefined")) 264 | emoji = rule.matched(1).replace(":", "​"); 265 | lineTexts = rule.replace(lineTexts, emoji); 266 | final info = rule.matchedPos(); 267 | effects.push(Emoji(emoji, info.pos, info.pos + info.len)); 268 | } 269 | else if (rule == patterns.parSepEReg) 270 | { 271 | lineTexts = rule.replace(lineTexts, "\r\r"); 272 | final info = rule.matchedPos(); 273 | effects.push(ParagraphGap(info.pos, info.pos + 2)); 274 | } 275 | else if (rule == patterns.alignmentEReg) 276 | { 277 | final align = rule.matched(1); 278 | var placeholder = "​".multiply(align.length); 279 | lineTexts = rule.replace(lineTexts, "​".multiply(10) + placeholder + rule.matched(2) + "​".multiply(8)); 280 | final info = rule.matchedPos(); 281 | effects.push(Alignment(align, info.pos, info.pos + info.len)); 282 | } 283 | else if (rule == patterns.backslashNewlineEReg) 284 | lineTexts = rule.replace(lineTexts, "\n"); 285 | else if (rule == patterns.doubleSpaceNewlineEReg) 286 | lineTexts = rule.replace(lineTexts, "\n" + "​"); 287 | } 288 | } 289 | onComplete(lineTexts.replace("\r", "\n").replace("\\t", " ") + "\n", effects); 290 | } 291 | 292 | #if openfl 293 | /** 294 | Generates the default visual theme from the markdown interpreter's information. 295 | 296 | If you want to edit the default visual theme, you can go to 297 | `Markdown.visualizer.markdownTextFormat`/`MarkdownVisualizer.markdownTextFormat` and change things there. 298 | 299 | examples (with/without static extension): 300 | ```haxe 301 | var visuals = new TextField(); 302 | visuals.text = "# hey everyone\n this is *so cool*" 303 | Markdown.visualizeMarkdown(visuals); 304 | //OR 305 | visuals.visualizeMarkdown(); 306 | ``` 307 | 308 | **/ 309 | public static overload extern inline function visualizeMarkdown(textField:openfl.text.TextField):openfl.text.TextField 310 | { 311 | return visualizer.generateVisuals(textField); 312 | } 313 | #end 314 | } 315 | -------------------------------------------------------------------------------- /src/texter/general/markdown/MarkdownBlocks.hx: -------------------------------------------------------------------------------- 1 | package texter.general.markdown; 2 | 3 | using texter.general.TextTools; 4 | 5 | /** 6 | The `MarkdownBlocks` class is a collection of static methods that can be used to get 7 | syntax highlighting for Markdown code-blocks. 8 | 9 | You might notice this class is a bit different from the other `Markdown` classes, as its 10 | methods are fully editable. This is because i cant just make syntax highlighters for every single language, 11 | but you can help with some that i dont knw and you are :) 12 | 13 | ### How to implement a syntax parser. 14 | **For this example, Ill use Haxe:** 15 | 16 | Lets say you wanted to give haxe syntax highlighting, 17 | but you discover its not supported and the text remains in a single color. how do you fix that? 18 | 19 | first, make sure the language exists on the class. you can look at the file/try autocompletion for that 20 | 21 | second, you make a syntax parser of your own. i recommand working with this template: 22 | ```haxe 23 | using texter.general.TextTools; 24 | 25 | function parseLanguage(text) { 26 | var interp:Array<{color:Int, start:Int, end:Int}> = []; 27 | var indexOfKeyColor1 = text.indexesFromEReg(~/(?: |\n|^)(keywords|that|need|to|be|colored|blue)/m), 28 | indexOfKeyColor2 = text.indexesOfSubs([ "You", "can", "also", "add", "words", "with", "an", "array"]), 29 | indexOfFunctionName = text.indexesFromEReg(~/([a-zA-Z_]+)\(/m), //detects function syntax, camal/snake/Title case 30 | indexOfClassName = text.indexesFromEReg(~/(?:\(| |\n|^)[A-Z]+[a-z]+/m), // detects Title Case 31 | indexOfString = text.indexesFromEReg(~/"[^"]*"|'[^']*'/), // detects "" and '' 32 | indexOfNumbers = text.indexesOfSubs(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]), 33 | indexOfComment = text.indexesFromEReg(~/\/\/[^\n]*|\/\*[^]*?\*\//m); // detects // 34 | 35 | //add the indexes to the interp array 36 | for (i in indexOfFunctionName) interp.push({color: customColor1, start: i.startIndex, end: i.endIndex - 1}); //dont coint the last ( 37 | for (i in indexOfKeyColor1) interp.push({color: customColor2, start: i.startIndex + 1, end: i.endIndex}); //dont count the first char, its not related. 38 | for (i in indexOfClassName) interp.push({color: customColor3, start: i.startIndex, end: i.endIndex}); 39 | for (i in indexOfKeyColor2) interp.push({color: customColor4, start: i.startIndex + 1, end: i.endIndex}); //dont count the first char, its not related. 40 | for (i in indexOfString) interp.push({color: customColor5, start: i.startIndex, end: i.endIndex}); 41 | for (i in indexOfNumbers) interp.push({color: customColor6, start: i.startIndex, end: i.endIndex}); 42 | for (i in indexOfComments) interp.push({color: customColor7, start: i.startIndex, end: i.endIndex}); 43 | for (i in indexOfConditionals) interp.push({color: customColor8, start: i.startIndex, end: i.endIndex}); 44 | return interp; 45 | } 46 | ``` 47 | What do i do here: 48 | 49 | - Im getting all of the special syntax - functions, classes, keywords, strings, numbers, comments, etc. 50 | - I differentiate between the different types of syntax, so i can color them differently. 51 | - I add the indexes to the `interp` array, each one with a unique start & end index. 52 | - I return the `interp` array. 53 | 54 | After that, i just assign this function to the correct language, in our case its haxe: 55 | 56 | 57 | MarkdownBlocks.parseHaxe = parseLanguage; 58 | 59 | **/ 60 | class MarkdownBlocks 61 | { 62 | public static var blockSyntaxMap(default, null):MapArray<{color:Int, start:Int, end:Int}>> = [ 63 | "json" => parseJSON, "haxe" => parseHaxe, "hx" => parseHaxe, "c" => parseC, "cpp" => parseCPP, "csharp" => parseCSharp, "cs" => parseCSharp, 64 | "java" => parseJava, "js" => parseJS, "php" => parsePHP, "python" => parsePython, "ruby" => parseRuby, "sql" => parseSQL, "xml" => parseXML, 65 | "yaml" => parseYAML, "html" => parseHTML, "css" => parseCSS, "ocaml" => parseOCaml, "ts" => parseTS, "go" => parseGo, "kotlin" => parseKotlin, 66 | "rust" => parseRust, "scala" => parseScala, "swift" => parseSwift, "typescript" => parseTS, "lua" => parseLua, "haskell" => parseHaskell, 67 | "erlang" => parseErlang, "elixir" => parseElixir, "elm" => parseElm, "clojure" => parseClojure, "crystal" => parseCrystal, "dart" => parseDart, 68 | "golang" => parseGo, "assembly" => parseAssembly, "vb" => parseVB, "basic" => parseBasic, "vhdl" => parseVHDL, "wasm" => parseWASM, 69 | "solidity" => parseSolidity, "cmake" => parseCMake, "default" => parseDefault 70 | ]; 71 | 72 | public static dynamic function parseJSON(text:String):Array<{color:Int, start:Int, end:Int}> 73 | { 74 | var interp:Array<{color:Int, start:Int, end:Int}> = []; 75 | var indexOfBool = text.indexesOfSubs(["true", "false", "null"]), 76 | indexOfCB = text.indexesOfSubs(["{", "}"]), 77 | indexOfEnd = text.indexesOfSubs(['"\n', '",']), 78 | indexOfKeyEnd = text.indexesOf('":'), 79 | indexOfNumbers = text.indexesOfSubs(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]); 80 | 81 | for (i in indexOfBool) 82 | interp.push({color: 0x4169E1, start: i.startIndex, end: i.endIndex}); 83 | for (i in indexOfNumbers) 84 | interp.push({color: 0x90EE90, start: i.startIndex, end: i.endIndex}); 85 | for (i in indexOfCB) 86 | interp.push({color: 0xDB7093, start: i.startIndex, end: i.endIndex}); 87 | 88 | for (i in indexOfKeyEnd) 89 | { 90 | var colorSetup:{color:Int, start:Int, end:Int} = {start: i.startIndex - 1, end: i.endIndex - 1, color: 0x00BBFF}; 91 | while (text.charAt(colorSetup.start) != '"' && colorSetup.start > 0) 92 | colorSetup.start--; 93 | interp.push(colorSetup); 94 | } 95 | 96 | for (i in indexOfEnd) 97 | { 98 | var colorSetup:{color:Int, start:Int, end:Int} = {start: i.startIndex - 1, end: i.endIndex - 1, color: 0xFF7F50}; 99 | while (text.charAt(colorSetup.start) != '"' && colorSetup.start > 0) 100 | colorSetup.start--; 101 | interp.push(colorSetup); 102 | } 103 | 104 | return interp; 105 | } 106 | 107 | public static dynamic function parseHaxe(text:String):Array<{color:Int, start:Int, end:Int}> 108 | { 109 | var interp:Array<{color:Int, start:Int, end:Int}> = []; 110 | var indexOfBlue = text.indexesFromEReg(~/(?:\(| |\n|^)(overload|true|false|null|public|static|dynamic|extern|inline|override|macro|abstract|final|var|function|package|enum|typedef|in|is|trace|new|this|class|super|extends|implements|interface|->)/m), 111 | indexOfPurple = text.indexesOfSubs([ 112 | "if", "else", "for", "while", "do", "switch", "case", "default", "break", "continue", "try", "catch", "throw", "import" 113 | ]), 114 | indexOfFunctionName = text.indexesFromEReg(~/([a-zA-Z_]+)\(/m), 115 | indexOfClassName = text.indexesFromEReg(~/(?::|\(| |\n|^)[A-Z][a-zA-Z]+/m), 116 | indexOfString = text.indexesFromEReg(~/"[^"]*"|'[^']*'/), 117 | indexOfNumbers = text.indexesOfSubs(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]), 118 | indexOfComments = text.indexesFromEReg(~/\/\/.*/m), 119 | indexOfConditionals = text.indexesFromEReg(~/#(?:if|end|elseif) [^\n]*/m); 120 | 121 | for (i in indexOfFunctionName) 122 | interp.push({color: 0xFF8700, start: i.startIndex, end: i.endIndex - 1}); 123 | for (i in indexOfBlue) 124 | interp.push({color: 0x4169E1, start: i.startIndex + 1, end: i.endIndex}); 125 | for (i in indexOfClassName) 126 | interp.push({color: 0x42B473, start: i.startIndex + 1, end: i.endIndex}); 127 | for (i in indexOfPurple) 128 | interp.push({color: 0xDC52BF, start: i.startIndex, end: i.endIndex}); 129 | for (i in indexOfString) 130 | interp.push({color: 0xFF7F50, start: i.startIndex, end: i.endIndex}); 131 | for (i in indexOfNumbers) 132 | interp.push({color: 0x62D493, start: i.startIndex, end: i.endIndex}); 133 | for (i in indexOfComments) 134 | interp.push({color: 0x556B2F, start: i.startIndex, end: i.endIndex}); 135 | for (i in indexOfConditionals) 136 | interp.push({color: 0x888888, start: i.startIndex, end: i.endIndex}); 137 | return interp; 138 | } 139 | 140 | public static dynamic function parseCSharp(text:String):Array<{color:Int, start:Int, end:Int}> 141 | { 142 | var interp:Array<{color:Int, start:Int, end:Int}> = []; 143 | var indexOfBlue = text.indexesFromEReg(~/(?:\(| |\n|^)(virtual|true|false|null|public|static|as|base|bool|byte|abstract|char|var|checked|class|enum|const|int|is|decimal|new|this|delegate|super|double|extern|float|in|inerface|internal|long|namespace|object|override|private|protected|readonly|short|sizeof|string|struct|typeof|uint|ulong|ushort|using|void|volatile|dynamic|where|yield|to|partial)/m), 144 | indexOfPurple = text.indexesFromEReg(~/(?:\(| |\n|^)(foreach|if|else|for|while|do|switch|case|default|break|continue|try|catch|throw|return)/m), 145 | indexOfFunctionName = text.indexesFromEReg(~/([a-zA-Z_]+)\(/m), 146 | indexOfClassName = text.indexesFromEReg(~/(?:\(| |\n|^)[A-Z]+[a-z]+/m), 147 | indexOfString = text.indexesFromEReg(~/"[^"]*"|'[^']*'|\$".*?"/), 148 | indexOfNumbers = text.indexesOfSubs(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]), 149 | indexOfComments = text.indexesFromEReg(~/\/\/.*/m); 150 | trace("endParse"); 151 | 152 | for (i in indexOfBlue) 153 | interp.push({color: 0x4169E1, start: i.startIndex + 1, end: i.endIndex}); 154 | for (i in indexOfClassName) 155 | interp.push({color: 0x42B473, start: i.startIndex + 1, end: i.endIndex}); 156 | for (i in indexOfFunctionName) 157 | interp.push({color: 0xFF8700, start: i.startIndex, end: i.endIndex - 1}); 158 | for (i in indexOfPurple) 159 | interp.push({color: 0xDC52BF, start: i.startIndex + 1, end: i.endIndex}); 160 | for (i in indexOfString) 161 | interp.push({color: 0xFF7F50, start: i.startIndex, end: i.endIndex}); 162 | for (i in indexOfNumbers) 163 | interp.push({color: 0x62D493, start: i.startIndex, end: i.endIndex}); 164 | for (i in indexOfComments) 165 | interp.push({color: 0x556B2F, start: i.startIndex, end: i.endIndex}); 166 | return interp; 167 | } 168 | 169 | public static dynamic function parseC(text:String):Array<{color:Int, start:Int, end:Int}> 170 | { 171 | var interp:Array<{color:Int, start:Int, end:Int}> = []; 172 | var indexOfBlue = text.indexesFromEReg(~/(?:\(| |\n|^)(auto|char|const|double|enum|extern|float|goto|inline|int|long|register|restrict|short|signed|sizeof|static|struct|typedef|union|unsigned|void|volatile)/m), 173 | indexOfPurple = text.indexesFromEReg(~/(?:\(| |\n|^)(break|case|continue|default|do|else|for|if|return|switch|while)/m), 174 | indexOfFunctionName = text.indexesFromEReg(~/(?:\(| |\n|^)([a-zA-Z_]+)\(/m), 175 | indexOfString = text.indexesFromEReg(~/"[^"]*"|'[^']*'/), 176 | indexOfNumbers = text.indexesOfSubs(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]), 177 | indexOfComments = text.indexesFromEReg(~/\/\/.*/m), 178 | indexOfPink = text.indexesFromEReg(~/^#[^\n]*/m); 179 | 180 | for (i in indexOfBlue) 181 | interp.push({color: 0x4169E1, start: i.startIndex + 1, end: i.endIndex}); 182 | for (i in indexOfFunctionName) 183 | interp.push({color: 0xFF8700, start: i.startIndex + 1, end: i.endIndex - 1}); 184 | for (i in indexOfPurple) 185 | interp.push({color: 0xDC52BF, start: i.startIndex + 1, end: i.endIndex}); 186 | for (i in indexOfString) 187 | interp.push({color: 0xFF7F50, start: i.startIndex, end: i.endIndex}); 188 | for (i in indexOfNumbers) 189 | interp.push({color: 0x62D493, start: i.startIndex, end: i.endIndex}); 190 | for (i in indexOfComments) 191 | interp.push({color: 0x556B2F, start: i.startIndex, end: i.endIndex}); 192 | for (i in indexOfPink) 193 | interp.push({color: 0xFF00FF, start: i.startIndex, end: i.endIndex}); 194 | return interp; 195 | } 196 | 197 | public static dynamic function parseR(text:String):Array<{color:Int, start:Int, end:Int}> 198 | return []; 199 | 200 | public static dynamic function parseCPP(text:String):Array<{color:Int, start:Int, end:Int}> 201 | return []; 202 | 203 | public static dynamic function parseFlash(text:String):Array<{color:Int, start:Int, end:Int}> 204 | return []; 205 | 206 | public static dynamic function parseXML(text:String):Array<{color:Int, start:Int, end:Int}> 207 | return []; 208 | 209 | public static dynamic function parseXAML(text:String):Array<{color:Int, start:Int, end:Int}> 210 | return []; 211 | 212 | public static dynamic function parseJava(text:String):Array<{color:Int, start:Int, end:Int}> 213 | return []; 214 | 215 | public static dynamic function parseKotlin(text:String):Array<{color:Int, start:Int, end:Int}> 216 | return []; 217 | 218 | public static dynamic function parseGo(text:String):Array<{color:Int, start:Int, end:Int}> 219 | return []; 220 | 221 | public static dynamic function parseHTML(text:String):Array<{color:Int, start:Int, end:Int}> 222 | return []; 223 | 224 | public static dynamic function parseCSS(text:String):Array<{color:Int, start:Int, end:Int}> 225 | return []; 226 | 227 | public static dynamic function parseSCSS(text:String):Array<{color:Int, start:Int, end:Int}> 228 | return []; 229 | 230 | public static dynamic function parseVue(text:String):Array<{color:Int, start:Int, end:Int}> 231 | return []; 232 | 233 | public static dynamic function parseJS(text:String):Array<{color:Int, start:Int, end:Int}> 234 | return []; 235 | 236 | public static dynamic function parseTS(text:String):Array<{color:Int, start:Int, end:Int}> 237 | return []; 238 | 239 | public static dynamic function parseLua(text:String):Array<{color:Int, start:Int, end:Int}> 240 | return []; 241 | 242 | public static dynamic function parseDart(text:String):Array<{color:Int, start:Int, end:Int}> 243 | return []; 244 | 245 | public static dynamic function parsePython(text:String):Array<{color:Int, start:Int, end:Int}> 246 | return []; 247 | 248 | public static dynamic function parsePHP(text:String):Array<{color:Int, start:Int, end:Int}> 249 | return []; 250 | 251 | public static dynamic function parseRuby(text:String):Array<{color:Int, start:Int, end:Int}> 252 | return []; 253 | 254 | public static dynamic function parseSQL(text:String):Array<{color:Int, start:Int, end:Int}> 255 | return []; 256 | 257 | public static dynamic function parseRust(text:String):Array<{color:Int, start:Int, end:Int}> 258 | return []; 259 | 260 | public static dynamic function parsePerl(text:String):Array<{color:Int, start:Int, end:Int}> 261 | return []; 262 | 263 | public static dynamic function parseOCaml(text:String):Array<{color:Int, start:Int, end:Int}> 264 | return []; 265 | 266 | public static dynamic function parseYAML(text:String):Array<{color:Int, start:Int, end:Int}> 267 | return []; 268 | 269 | public static dynamic function parseHaskell(text:String):Array<{color:Int, start:Int, end:Int}> 270 | return []; 271 | 272 | public static dynamic function parseCrystal(text:String):Array<{color:Int, start:Int, end:Int}> 273 | return []; 274 | 275 | public static dynamic function parseClojure(text:String):Array<{color:Int, start:Int, end:Int}> 276 | return []; 277 | 278 | public static dynamic function parseScala(text:String):Array<{color:Int, start:Int, end:Int}> 279 | return []; 280 | 281 | public static dynamic function parseLisp(text:String):Array<{color:Int, start:Int, end:Int}> 282 | return []; 283 | 284 | public static dynamic function parseSwift(text:String):Array<{color:Int, start:Int, end:Int}> 285 | return []; 286 | 287 | public static dynamic function parseElixir(text:String):Array<{color:Int, start:Int, end:Int}> 288 | return []; 289 | 290 | public static dynamic function parseErlang(text:String):Array<{color:Int, start:Int, end:Int}> 291 | return []; 292 | 293 | public static dynamic function parseElm(text:String):Array<{color:Int, start:Int, end:Int}> 294 | return []; 295 | 296 | public static dynamic function parseAssembly(text:String):Array<{color:Int, start:Int, end:Int}> 297 | return []; 298 | 299 | public static dynamic function parseVB(text:String):Array<{color:Int, start:Int, end:Int}> 300 | return []; 301 | 302 | public static dynamic function parseBasic(text:String):Array<{color:Int, start:Int, end:Int}> 303 | return []; 304 | 305 | public static dynamic function parseVHDL(text:String):Array<{color:Int, start:Int, end:Int}> 306 | return []; 307 | 308 | public static dynamic function parseWASM(text:String):Array<{color:Int, start:Int, end:Int}> 309 | return []; 310 | 311 | public static dynamic function parseSolidity(text:String):Array<{color:Int, start:Int, end:Int}> 312 | return []; 313 | 314 | public static dynamic function parseCMake(text:String):Array<{color:Int, start:Int, end:Int}> 315 | return []; 316 | 317 | public static dynamic function parseDefault(text:String):Array<{color:Int, start:Int, end:Int}> 318 | return []; 319 | } 320 | -------------------------------------------------------------------------------- /src/texter/general/markdown/MarkdownEffect.hx: -------------------------------------------------------------------------------- 1 | package texter.general.markdown; 2 | 3 | enum MarkdownEffect 4 | { 5 | Indent(level:Int, start:Int, end:Int); 6 | Bold(start:Int, end:Int); 7 | Italic(start:Int, end:Int); 8 | StrikeThrough(start:Int, end:Int); 9 | Code(start:Int, end:Int); 10 | Math(start:Int, end:Int); 11 | HorizontalRule(type:String, start:Int, end:Int); 12 | ParagraphGap(start:Int, end:Int); 13 | CodeBlock(language:String, start:Int, end:Int); 14 | TabCodeBlock(start:Int, end:Int); 15 | Link(link:String, start:Int, end:Int); 16 | Image(altText:String, imageSource:String, start:Int, end:Int); 17 | Emoji(type:String, start:Int, end:Int); 18 | Heading(level:Int, start:Int, end:Int); 19 | UnorderedListItem(nestingLevel:Int, start:Int, end:Int); 20 | OrderedListItem(number:Int, nestingLevel:Int, start:Int, end:Int); 21 | Alignment(alignment:String, start:Int, end:Int); 22 | } 23 | -------------------------------------------------------------------------------- /src/texter/general/markdown/MarkdownPatterns.hx: -------------------------------------------------------------------------------- 1 | package texter.general.markdown; 2 | 3 | class MarkdownPatterns 4 | { 5 | public static var hRuledTitleEReg(default, null):EReg = ~/([^\n]+)\n^(-{3,}|\+{3,}|_{3,}|\*{3,}|={3,})$/m; 6 | public static var linkEReg(default, null):EReg = ~/\[([^\]]+)\]\(([^)]+)\)/m; 7 | public static var codeEReg(default, null):EReg = ~/`([^`\n]+?)`/; 8 | public static var codeblockEReg(default, null):EReg = ~/```([^\n]*)\n(..+?)```/s; 9 | public static var tildeCodeblockEReg(default, null):EReg = ~/~~~([^\n]*)\n*(..+?)~~~/s; 10 | public static var tabCodeblockEReg(default, null):EReg = ~/ {4}(.+)/m; 11 | public static var imageEReg(default, null):EReg = ~/!\[([^\]]+)\]\(([^)]+)\s"([^")]+)"\)/m; 12 | public static var listItemEReg(default, null):EReg = ~/^( *)([0-9]+\.|[+\-*]) ([^\n]*)/m; 13 | public static var unorderedListItemEReg(default, null):EReg = ~/^( *)([+\-*]) ([^\n]*)/m; 14 | public static var titleEReg(default, null):EReg = ~/^(#{1,6}) ([^\n]+)/m; 15 | public static var hRuleEReg(default, null):EReg = ~/^(-{3,}|\+{3,}|_{3,}|\*{3,}|={3,})$/m; 16 | public static var astBoldEReg(default, null):EReg = ~/\*\*([^\n]+)\*\*/m; 17 | public static var boldEReg(default, null):EReg = ~/__([^\n]+)__/m; 18 | public static var strikeThroughEReg(default, null):EReg = ~/~~([^~{2}]+?)~~/m; 19 | public static var italicEReg(default, null):EReg = ~/_([^\n]+)_/m; 20 | public static var astItalicEReg(default, null):EReg = ~/\*([^\n]+)\*/m; 21 | public static var mathEReg(default, null):EReg = ~/\$([^\$]+?)\$/m; 22 | public static var parSepEReg(default, null):EReg = ~/\n\n/m; 23 | public static var emojiEReg(default, null):EReg = ~/(:[^: ]+:)/m; 24 | public static var indentEReg(default, null):EReg = ~/^(>+)(.+)/m; 25 | public static var doubleSpaceNewlineEReg(default, null):EReg = ~/ $/m; 26 | public static var backslashNewlineEReg(default, null):EReg = ~/\\$/m; 27 | public static var alignmentEReg(default, null):EReg = ~/^([^\r]+?)<\/align>/m; 28 | } 29 | -------------------------------------------------------------------------------- /src/texter/general/markdown/MarkdownVisualizer.hx: -------------------------------------------------------------------------------- 1 | package texter.general.markdown; 2 | 3 | using texter.general.TextTools; 4 | 5 | #if openfl 6 | import openfl.text.TextFormat; 7 | import openfl.text.TextField; 8 | #end 9 | 10 | /** 11 | * The `MarkdownVisualizer` class is a containing all framework-specific markdown visualization methods. 12 | * 13 | * For now, visualization is only supported for these frameworks: 14 | * 15 | * - OpenFL (via `TextField`, `TextFieldRTL`) 16 | * 17 | * If you'd like for more frameworks to be added you can do a couple of things: 18 | * 19 | * 1. Take an existing visualization method and make it work for your framework. If it works as intended, contact me and i'll add it. 20 | * 2. If you cant make existing solutions work well, add a new visualization method. again - if it works as intended, contact me and i'll add it. 21 | * 3. contact me and ask me to make a visualization method. this one will take the longest since ill need to download and learn how to make things with that framework. 22 | * 23 | * ### How To Implement 24 | * 25 | * If you want to make markdown visualization work for your framework, its actually pretty easy. 26 | * The interpreter already sends all of the data and even does some nice modifictaions, so its as easy as can be: 27 | * 28 | * **First, make a function, containing `Markdown.interpret` that recives a text field and retruns it** 29 | * **Dont forget to reset the text's "styling" each time to avoid artifacts!** 30 | * **** 31 | * ```haxe 32 | * function displayMarkdown(textField:Text):Text { 33 | * textField.textStyle = defaultTextStyle; 34 | * Markdown.interpret(textField.text, function(markdownText:String, effects:Array) { 35 | * 36 | * }); 37 | * return textField; 38 | * } 39 | * ``` 40 | * 41 | * **Second, in the body of the anonymus function, you implement this *giant* 42 | * switch-case to handle all of the effects you want to handle, as well as make your text "markdown-artifact-free".** 43 | * - **start** contains the starting index of the effect. 44 | * - **end** contains the ending index of the effect, but not included! 45 | * - **any extra argument** this is for effects that require extra information to be rendered correctly - language for codeblocks, level for headings... 46 | * 47 | * ```haxe 48 | * function displayMarkdown(textField:Text):Text { 49 | * textField.textStyle = defaultTextStyle; 50 | * Markdown.interpret(textField.text, function(markdownText:String, effects:Array) { 51 | * field.text = markdownText; 52 | for (e in effects) 53 | { 54 | switch e 55 | { 56 | case Emoji(type, start, end): 57 | case Bold(start, end): 58 | case Italic(start, end): 59 | case Code(start, end): 60 | case Math(start, end): 61 | case Link(link, start, end): 62 | case Heading(level, start, end): 63 | case UnorderedListItem(nestingLevel, start, end): 64 | case OrderedListItem(number, nestingLevel, start, end): 65 | case HorizontalRule(type, start, end): 66 | case CodeBlock(language, start, end): 67 | case StrikeThrough(start, end): 68 | case Image(altText, imageSource, start, end): 69 | case ParagraphGap(start, end): 70 | 71 | default: continue; 72 | } 73 | } 74 | }); 75 | return field; 76 | * } 77 | * ``` 78 | * 79 | * **And Finally, you can add your desired effect in each of the cases.** 80 | * ```haxe 81 | * case Bold: textField.setBold(start, end); 82 | * case Italic: textField.setItalic(start, end); 83 | * case Math: textField.setFont("path/to/mathFont.ttf", start, end); //ETC. 84 | * ``` 85 | * 86 | * ### For a working example you can look at this file's source code. 87 | * 88 | * 89 | * contact info (for submitting a visualization method): 90 | * - ShaharMS#8195 (discord) 91 | * - https://github.com/ShaharMS/texter (this haxelib's repo to make pull requests) 92 | */ 93 | class MarkdownVisualizer 94 | { 95 | /** 96 | * `visualConfig` is a "dictionary" containing all of the configuration options for the visualizer. 97 | * **NOTE** - because its a cross-framework field, and not every framework supports the same options, 98 | * You cant exect everything to work in every framework. 99 | */ 100 | public static var visualConfig:VisualConfig = @:privateAccess new VisualConfig(); 101 | 102 | #if openfl 103 | /** 104 | * When visualizing a given Markdown string, this `TextFormat` will be used. 105 | * You can modify the `TextFormat` to change the style of the text via `visualConfig`; 106 | */ 107 | public static var markdownTextFormat(get, never):openfl.text.TextFormat; 108 | 109 | @:noCompletion static function get_markdownTextFormat():TextFormat 110 | { 111 | return new openfl.text.TextFormat(visualConfig.font, visualConfig.size, visualConfig.color, false, false, false, "", "", visualConfig.alignment, 112 | visualConfig.leftMargin, visualConfig.rightMargin, visualConfig.indent, visualConfig.leading); 113 | } 114 | 115 | /** 116 | Generates the default visual theme from the markdown interpreter's information. 117 | 118 | examples (with/without static extension): 119 | ```haxe 120 | var visuals = new TextField(); 121 | visuals.text = "# hey everyone\n this is *so cool*" 122 | MarkdownVisualizer.generateTextFieldVisuals(visuals); 123 | //OR 124 | visuals.generateVisuals(); 125 | ``` 126 | 127 | **/ 128 | public static overload extern inline function generateVisuals(field:openfl.text.TextField):openfl.text.TextField 129 | { 130 | field.defaultTextFormat = markdownTextFormat; 131 | Markdown.interpret(field.text, (markdownText, effects) -> 132 | { 133 | field.text = markdownText; 134 | for (e in effects) 135 | { 136 | switch e 137 | { 138 | case Emoji(type, start, end): 139 | case Alignment(alignment, start, 140 | end): field.setTextFormat(new openfl.text.TextFormat(null, null, null, null, null, null, null, null, alignment), start, end); 141 | case Indent(level, start, 142 | end): field.setTextFormat(new openfl.text.TextFormat(null, null, null, null, null, null, null, null, null, null, null, 143 | level * markdownTextFormat.size), 144 | start, end); 145 | case Bold(start, end): field.setTextFormat(new openfl.text.TextFormat(null, null, null, true, null), start, end); 146 | case Italic(start, end): field.setTextFormat(new openfl.text.TextFormat(null, null, null, null, true), start, end); 147 | case Code(start, 148 | end): field.setTextFormat(new openfl.text.TextFormat("_typewriter", field.getTextFormat(start, end).size + 2), start, end); 149 | case Math(start, end): field.setTextFormat(new openfl.text.TextFormat("_serif"), start, end); 150 | case Link(link, start, end): field.setTextFormat(new openfl.text.TextFormat(null, null, 0x008080, null, null, true, link, ""), start, end); 151 | case Heading(level, start, 152 | end): field.setTextFormat(new openfl.text.TextFormat(null, markdownTextFormat.size * 3 - Std.int(level * 10), null, true), start, end); 153 | case UnorderedListItem(nestingLevel, start, 154 | end): field.setTextFormat(new openfl.text.TextFormat(null, markdownTextFormat.size, null, true), start + nestingLevel, 155 | start + nestingLevel + 1); 156 | case OrderedListItem(number, nestingLevel, start, end): continue; 157 | case HorizontalRule(type, start, end): continue; 158 | case CodeBlock(language, start, end): { 159 | field.setTextFormat(new openfl.text.TextFormat("_typewriter", markdownTextFormat.size + 2, markdownTextFormat.color, null, null, 160 | null, null, null, null, field.getTextFormat(start, end).leftMargin + markdownTextFormat.size, markdownTextFormat.size), 161 | start, end); 162 | try 163 | { 164 | var coloring:Array<{color:Int, start:Int, end:Int}> = Markdown.syntaxBlocks.blockSyntaxMap[language](field.text.substring(start, 165 | end)); 166 | for (i in coloring) 167 | { 168 | field.setTextFormat(new openfl.text.TextFormat("_typewriter", null, i.color), start + i.start, start + i.end); 169 | } 170 | } 171 | catch (e) 172 | trace(e); 173 | } 174 | case TabCodeBlock(start, end): { 175 | field.setTextFormat(new openfl.text.TextFormat("_typewriter", markdownTextFormat.size + 2, markdownTextFormat.color, null, null, 176 | null, null, null, null, field.getTextFormat(start, end).leftMargin + markdownTextFormat.size, markdownTextFormat.size), 177 | start, end); 178 | try 179 | { 180 | var coloring:Array<{color:Int, start:Int, end:Int}> = Markdown.syntaxBlocks.blockSyntaxMap["default"](field.text.substring(start, 181 | end)); 182 | for (i in coloring) 183 | { 184 | field.setTextFormat(new openfl.text.TextFormat("_typewriter", null, i.color), start + i.start, start + i.end); 185 | } 186 | } 187 | catch (e) 188 | trace(e); 189 | } 190 | case StrikeThrough(start, end): continue; 191 | case Image(altText, imageSource, start, end): continue; 192 | case ParagraphGap(start, end): continue; // default behaviour 193 | 194 | default: continue; 195 | } 196 | } 197 | }); 198 | return field; 199 | } 200 | #end 201 | } 202 | 203 | private class VisualConfig 204 | { 205 | @:noCompletion private function new() 206 | return; 207 | 208 | public var size:Int = 18; 209 | public var color:Int = 0x000000; 210 | public var font:String = "_sans"; 211 | public var leftMargin:Int = 0; 212 | public var rightMargin:Int = 0; 213 | public var indent:Int = 0; 214 | public var leading:Int; 215 | public var blockIndent:Int = 18; 216 | public var alignment:String = "left"; 217 | public var border:Bool = true; 218 | public var borderColor:Int = 0x000000; 219 | public var borderSize:Int = 1; 220 | public var background:Bool = false; 221 | public var backgroundColor:Int = 0xEEEEEE; 222 | public var codeblockBackgroundColor:Int = 0xCCCCCC; 223 | public var darkMode(default, set):Bool = false; 224 | 225 | /** 226 | * Set all the properties of the default visual configuration at once. 227 | * to skip values, set them to `null`. 228 | * @param size the size of the text 229 | * @param color the color of the text 230 | * @param font specify the font of the text, use the path to your font: `path/to/font.ttf` 231 | * @param leftMargin the left margin of the text (how far will the text be away from the left border, in pixels) 232 | * @param rightMargin the right margin of the text (how far will the text be away from the right border, in pixels) 233 | * @param indent the indent of the text (how far will the text be away from the left margin, in pixels) 234 | * @param leading the spacing between lines, in pixels 235 | * @param blockIndent the right & left margin used when displaying a code block 236 | * characters the contain the symbols; the first character is the symbol for the 237 | * first-level bullet point, the second character is the symbol for the second-level bullet point. 238 | * @param alignment the alignment of the text, can be `left`, `right`, `center` or `justify` 239 | * @param border whether to draw a border around the text 240 | * @param borderColor the color of the border 241 | * @param borderSize the size of the border (the thickness of the border, in pixels) 242 | * @param background whether to draw a background behind the text 243 | * @param backgroundColor the color of the background 244 | * @param codeblockBackgroundColor the color of the background behind code blocks 245 | */ 246 | public function setAll(?size:Int, ?color:Int, ?font:String, ?leftMargin:Int, ?rightMargin:Int, ?indent:Int, ?leading:Int, ?blockIndent:Int, 247 | ?alignment:String, ?border:Bool, ?borderColor:Int, ?borderSize:Int, ?background:Bool, ?backgroundColor:Int, 248 | ?codeblockBackgroundColor:Int):VisualConfig 249 | { 250 | this.size = size != null ? size : this.size; 251 | this.color = color != null ? color : this.color; 252 | this.font = font != null ? font : this.font; 253 | this.leftMargin = leftMargin != null ? leftMargin : this.leftMargin; 254 | this.rightMargin = rightMargin != null ? rightMargin : this.rightMargin; 255 | this.indent = indent != null ? indent : this.indent; 256 | this.leading = leading != null ? leading : this.leading; 257 | this.blockIndent = blockIndent != null ? blockIndent : this.blockIndent; 258 | this.alignment = alignment != null ? alignment : this.alignment; 259 | this.border = border != null ? border : this.border; 260 | this.borderColor = borderColor != null ? borderColor : this.borderColor; 261 | this.borderSize = borderSize != null ? borderSize : this.borderSize; 262 | this.background = background != null ? background : this.background; 263 | this.backgroundColor = backgroundColor != null ? backgroundColor : this.backgroundColor; 264 | this.codeblockBackgroundColor = codeblockBackgroundColor != null ? codeblockBackgroundColor : this.codeblockBackgroundColor; 265 | return this; 266 | } 267 | 268 | function set_darkMode(mode:Bool) 269 | { 270 | backgroundColor = mode ? 0x222222 : 0xEEEEEE; 271 | color = mode ? 0xEEFFFFFF : 0x000000; 272 | return mode; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/texter/general/math/MathAttribute.hx: -------------------------------------------------------------------------------- 1 | package texter.general.math; 2 | 3 | enum MathAttribute 4 | { 5 | FunctionDefinition(index:Int, letter:String); 6 | Division(index:Int, upperHandSide:MathAttribute, lowerHandSide:MathAttribute); 7 | Variable(index:Int, letter:String); 8 | Number(index:Int, letter:String); 9 | Sign(index:Int, letter:String); 10 | StartClosure(index:Int, letter:String); 11 | EndClosure(index:Int, letter:String); 12 | Closure(index:Int, letter:String, content:Array); 13 | Null(index:Int); // fill in removed characters with this 14 | } 15 | -------------------------------------------------------------------------------- /src/texter/general/math/MathLexer.hx: -------------------------------------------------------------------------------- 1 | package texter.general.math; 2 | 3 | using TextTools; 4 | using StringTools; 5 | using texter.general.CharTools; 6 | 7 | class MathLexer 8 | { 9 | public static var numbers:String = '0123456789'; 10 | 11 | public static var startClosures:String = '{[('; 12 | 13 | public static var endClosures:String = '}])'; 14 | 15 | /** 16 | * Returns a simplified array of mathematical attributes. to further 17 | * process this array, you can use the rest of the functions in this class. 18 | * @param text the text to be lexed and "trransformed" into an attribute array 19 | * @return Array 20 | */ 21 | public static function getMathAttributes(text:String):Array 22 | { 23 | var attributes:Array = []; 24 | text = text.trim().remove(" "); 25 | 26 | for (i in 0...text.length) 27 | { 28 | var char = text.charAt(i); 29 | 30 | if (numbers.contains(char)) 31 | { 32 | attributes.push(Number(i, char)); 33 | } 34 | else if (startClosures.contains(char)) 35 | { 36 | attributes.push(StartClosure(i, char)); 37 | } 38 | else if (endClosures.contains(char)) 39 | { 40 | attributes.push(EndClosure(i, char)); 41 | } 42 | else if ('${CharTools.generalMarks.join("")}√⩵'.contains(char)) 43 | { 44 | attributes.push(Sign(i, char)); 45 | } 46 | else if (char.isRTL() || char.isLTR()) 47 | { 48 | attributes.push(Variable(i, char)); 49 | } 50 | } 51 | 52 | return attributes; 53 | } 54 | 55 | /** 56 | * Fixes the order of elements in the array to match the one in the string representation. 57 | * @param attributes an array of maybe incorreectly ordered attributes 58 | * @return Array 59 | */ 60 | public static function reorderAttributes(attributes:Array):Array 61 | { 62 | var at = attributes.copy(); 63 | 64 | for (item in attributes) 65 | { 66 | switch item 67 | { 68 | case Sign(index, _) | Variable(index, _) | Number(index, _) | StartClosure(index, _) | EndClosure(index, _) | Null(index) | FunctionDefinition(index, _): 69 | { 70 | at[index] = item; 71 | } 72 | default: 73 | } 74 | } 75 | return at; 76 | } 77 | 78 | /** 79 | * Fixes the output of `splitBlocks` to correctly print to a string. 80 | * 81 | * takes in an array of attributes and resets their order to actually make sense: 82 | * 83 | * this 84 | * ``` 85 | * [Variable(6, "x"), Closure(")", 19, [Number(3, "123")])] 86 | * ``` 87 | * becomes this: 88 | * ``` 89 | * [Variable(0, "x"), Closure(")", 1, [Number(0, "123")])] 90 | * ``` 91 | * 92 | * `Null`s will always have an index of -1. 93 | */ 94 | public static function resetAttributesOrder(attributes:Array):Array 95 | { 96 | var copy = attributes.copy(); 97 | for (i in 0...copy.length) 98 | { 99 | var element = attributes[i]; 100 | switch element 101 | { 102 | case FunctionDefinition(_, letter): 103 | copy[i] = FunctionDefinition(i, letter); 104 | case Variable(index, letter): 105 | copy[i] = Variable(i, letter); 106 | case Number(index, letter): 107 | copy[i] = Number(i, letter); 108 | case Sign(index, letter): 109 | copy[i] = Sign(i, letter); 110 | case StartClosure(index, letter): 111 | copy[i] = StartClosure(i, letter); 112 | case EndClosure(index, letter): 113 | copy[i] = EndClosure(i, letter); 114 | case Closure(index, letter, content): 115 | { 116 | var contentCopy = resetAttributesOrder(content); 117 | copy[i] = Closure(i, letter, contentCopy); 118 | } 119 | case Division(index, upperHandSide, lowerHandSide): 120 | { 121 | var uhs = resetAttributesOrder([upperHandSide]); 122 | var lhs = resetAttributesOrder([lowerHandSide]); 123 | copy[i] = Division(i, uhs[0], lhs[0]); 124 | } 125 | case Null(index): 126 | copy[i] = Null(-1); 127 | } 128 | } 129 | return copy; 130 | } 131 | 132 | /** 133 | * Removes completely duplicate elements (elements with the same arguments & type). The first element of the similar 134 | * ones will be the only one "saved" 135 | * @param attributes the array that mioght contain duplicates 136 | * @return Array 137 | */ 138 | public static function removeDuplicates(attributes:Array):Array 139 | { 140 | var copy = []; 141 | for (a in attributes) 142 | if (!copy.contains(a)) 143 | copy.push(a); 144 | 145 | return copy; 146 | } 147 | 148 | /** 149 | * Removes whitespaces from a geven array of mathematical attributes. Whitespaces shouldnt appear in the first place. 150 | * if you need to call this, you might have done something wrong, but this function can still save you from yourself :) 151 | * @param attributes the attributes that may ot may not contain whitespace ones 152 | * @return an array empties of whitespace elements 153 | */ 154 | public static function condenseWhitespaces(attributes:Array):Array 155 | { 156 | var whitespaced:Array = reorderAttributes(attributes); 157 | 158 | // first, remove all whitespaces 159 | for (a in whitespaced) 160 | { 161 | if (a == null) 162 | continue; 163 | switch a 164 | { 165 | case Variable(index, " ") | Number(index, " ") | Sign(index, " "): 166 | whitespaced[index] = Null(index); 167 | default: 168 | } 169 | } 170 | trace(whitespaced); 171 | return whitespaced; 172 | } 173 | 174 | /** 175 | * Gets an array of `MathAttribute`s and returns their text representation. `Null` elements will be ignored, correctly. 176 | * @param attributes the mathematical attributes 177 | * @return The attributes' text representation 178 | */ 179 | public static function extractTextFromAttributes(attributes:Array):String 180 | { 181 | var a = []; 182 | for (item in attributes) 183 | { 184 | switch item 185 | { 186 | case FunctionDefinition(index, letter) | Variable(index, letter) | Number(index, letter) | Sign(index, letter) | StartClosure(index, letter) | EndClosure(index, letter): 187 | a[index] = letter; 188 | case Division(index, upperHandSide, lowerHandSide): 189 | a[index] = '${extractTextFromAttributes([upperHandSide])}/${extractTextFromAttributes([lowerHandSide])}'; 190 | case Closure(index, letter, content): 191 | final start = if (letter.contains("{") || letter.contains("}")) "{" else if (letter.contains("[") || letter.contains("]")) "[" else "("; 192 | final end = switch start 193 | { 194 | case "{": "}"; 195 | case "[": "]"; 196 | default: ")"; 197 | }; 198 | a[index] = '${start}${extractTextFromAttributes(content)}${end}'; 199 | case Null(index): 200 | } 201 | } 202 | return a.join("").replace("null", ""); 203 | } 204 | 205 | /** 206 | Gets an array of Mathematical Atributes and groups related elements to be more "useful": 207 | 208 | - Multiple numbers grouped together will become one element 209 | - StartClosure and EndClosure should be merged into one `Closure` 210 | - Division hand sides will be merged into one `Division` element 211 | - The resulting array will be in order, but element indices will get messed up. before extracting the text, use `resetAttributesOrder()` 212 | **/ 213 | public static function splitBlocks(attributes:Array):Array 214 | { 215 | // first, merge numbers 216 | var numbersMerged = attributes.copy(); 217 | attributes.push(Null(-1)); 218 | var currentNum = ""; 219 | var startIndex = -1; 220 | for (a in attributes) 221 | { 222 | switch a 223 | { 224 | case Number(index, letter): 225 | { 226 | if (startIndex == -1) 227 | startIndex = index; 228 | currentNum += letter; 229 | } 230 | default: 231 | { 232 | if (currentNum.length != 0) 233 | { 234 | // set the other items to `Null(-1)` to be removed later. 235 | for (i in startIndex...startIndex + currentNum.length) 236 | { 237 | numbersMerged[i] = Null(-1); 238 | } 239 | numbersMerged[startIndex] = Number(startIndex, currentNum); 240 | currentNum = ""; 241 | startIndex = -1; 242 | } 243 | } 244 | } 245 | } 246 | numbersMerged = numbersMerged.filter(a -> !Type.enumEq(a, Null(-1))); 247 | 248 | // TODO: #8 More efficient implementation of parenthesis grouping in SplitBlocks 249 | // Closure grouping - iterative scan from the begining of the array for Start & End Closure elements 250 | var closuresMerged = numbersMerged.copy(); 251 | var i = 0; 252 | var start:Int = 0, end:Int = -1; 253 | var elements:Array = []; 254 | while (i < numbersMerged.length) 255 | { 256 | var element = closuresMerged[i]; 257 | switch element 258 | { 259 | case StartClosure(index, letter): 260 | { 261 | elements = []; 262 | start = i; 263 | } 264 | case EndClosure(index, letter): 265 | { 266 | end = i; 267 | for (id in start...end) 268 | { 269 | closuresMerged[id] = Null(-1); 270 | } 271 | closuresMerged[i] = Closure(end, letter, elements); 272 | start = end = -1; 273 | i = 0; 274 | continue; 275 | } 276 | case Null(index): // dont push nulls 277 | default: 278 | { 279 | if (start != -1) 280 | { 281 | elements.push(element); 282 | } 283 | } 284 | } 285 | i++; 286 | } 287 | closuresMerged = closuresMerged.filter(a -> !Type.enumEq(a, Null(-1))); 288 | 289 | var divisionsCondensed = closuresMerged.copy(); 290 | // if we go end -> start, we would be able to scan everything, and still nest correctly 291 | var i = divisionsCondensed.length; 292 | while (--i > 0) 293 | { 294 | var element = divisionsCondensed[i]; 295 | switch element 296 | { 297 | case Sign(index, "/") | Sign(index, "\\") | Sign(index, "÷"): 298 | var uhs = divisionsCondensed[i - 1]; 299 | var lhs = divisionsCondensed[i + 1]; 300 | divisionsCondensed[i] = Division(i, uhs, lhs); 301 | divisionsCondensed[i - 1] = divisionsCondensed[i + 1] = Null(-1); 302 | default: 303 | } 304 | } 305 | 306 | return divisionsCondensed.filter(a -> !Type.enumEq(a, Null(-1))); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/texter/openfl/MathTextField.hx: -------------------------------------------------------------------------------- 1 | package texter.openfl; 2 | 3 | import openfl.display.Bitmap; 4 | import openfl.display.BitmapData; 5 | import openfl.text.TextFormat; 6 | import texter.general.math.MathAttribute; 7 | import texter.general.math.MathLexer; 8 | import openfl.events.Event; 9 | import openfl.display.Sprite; 10 | import openfl.text.TextField; 11 | import texter.openfl._internal.TextFieldCompatibility; 12 | import texter.openfl._internal.DrawableTextField; 13 | 14 | /** 15 | * A TextField that specializes in displaying mathematical 16 | * forms of string. 17 | * 18 | * For example, this equation: `f(x) = 2x + 5` 19 | * will display as: `$f(x) = 2x + 5$` 20 | */ 21 | class MathTextField extends TextFieldCompatibility 22 | { 23 | var textFields:Array = []; 24 | 25 | var baseBackground:Bitmap; 26 | 27 | public function new() 28 | { 29 | super(); 30 | textField = new TextField(); 31 | textField.addEventListener(Event.CHANGE, render); 32 | textField.x = textField.y = -100; 33 | textField.defaultTextFormat.size = 16; 34 | addEventListener(Event.ADDED_TO_STAGE, e -> 35 | { 36 | stage.addChild(textField); 37 | }); 38 | baseBackground = new Bitmap(new BitmapData(100, 100, true)); 39 | addChild(baseBackground); 40 | } 41 | 42 | public function render(e:Event) 43 | { 44 | trace("called with", text); 45 | var currentText = text; 46 | var mathProps:Array = MathLexer.resetAttributesOrder(MathLexer.splitBlocks(MathLexer.getMathAttributes(text))); 47 | 48 | // TODO: #9 Better Implementation for rendering in MathTextField 49 | trace("before removal"); 50 | for (t in textFields) 51 | if (t != null) 52 | removeChild(t); 53 | trace("after removal"); 54 | textFields = []; 55 | var smallClosure = false; 56 | for (element in mathProps) 57 | { 58 | trace(element); 59 | switch element 60 | { 61 | case FunctionDefinition(index, letter): 62 | var t = new DrawableTextField(); 63 | t.defaultTextFormat = new TextFormat(null, textField.defaultTextFormat.size + 4); 64 | t.text = letter; 65 | textFields.push(t); 66 | smallClosure = true; 67 | continue; 68 | case Division(index, upperHandSide, lowerHandSide): 69 | var uhsText = MathLexer.extractTextFromAttributes([upperHandSide]); 70 | var lhsText = MathLexer.extractTextFromAttributes([lowerHandSide]); 71 | var t = new DrawableTextField(); 72 | t.defaultTextFormat = new TextFormat(null, textField.defaultTextFormat.size, null, null, null, null, null, null, "center"); 73 | t.text = '$uhsText\n$lhsText'; 74 | textFields.push(t); 75 | case Variable(index, letter) | Number(index, letter) | Sign(index, letter) | StartClosure(index, letter) | EndClosure(index, letter): 76 | var t = new DrawableTextField(); 77 | t.defaultTextFormat = new TextFormat(null, textField.defaultTextFormat.size); 78 | t.text = letter; 79 | textFields.push(t); 80 | case Closure(index, letter, content): 81 | var t = new DrawableTextField(); 82 | t.defaultTextFormat = new TextFormat(null, 83 | smallClosure ? Std.int(textField.defaultTextFormat.size / 2) : textField.defaultTextFormat.size); 84 | var prefix = if (letter == ")") "(" else if (letter == "]") "[" else "{"; 85 | var postfix = letter; 86 | var c = MathLexer.extractTextFromAttributes(content); 87 | t.text = prefix + c + postfix; 88 | textFields.push(t); 89 | case Null(index): 90 | } 91 | smallClosure = false; 92 | } 93 | trace("gets to line 93"); 94 | var xPos = 0.; 95 | trace(textFields); 96 | for (t in textFields) 97 | { 98 | addChild(t); 99 | if (defaultTextFormat.bold) 100 | t.defaultTextFormat = new TextFormat("assets/texter/MathTextField/math-bold.ttf", 40, 0x000000); 101 | else 102 | t.defaultTextFormat = new TextFormat("assets/texter/MathTextField/math-regular.ttf", 40, 0x000000); 103 | t.width = t.textWidth + 4; 104 | t.height = t.textHeight + 4; 105 | t.x = xPos; 106 | xPos += t.width; 107 | t.y = t.height > textField.height ? 0 : textField.height / 2 - t.height / 2; 108 | } 109 | trace(x, y, textField.width, textField.height, textFields); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/texter/openfl/TextFieldRTL.hx: -------------------------------------------------------------------------------- 1 | package texter.openfl; 2 | 3 | #if openfl 4 | import openfl.text.TextField; 5 | 6 | /** 7 | * `TextFieldRTL` is an "extention" of `TextField` that adds support for right-to-left text. 8 | */ 9 | class TextFieldRTL extends TextField 10 | { 11 | /** 12 | Creates a new TextField instance. After you create the TextField instance, 13 | call the `addChild()` or `addChildAt()` method of 14 | the parent DisplayObjectContainer object to add the TextField instance to 15 | the display list. 16 | 17 | The default size for a text field is 100 x 100 pixels. 18 | **/ 19 | public function new() { 20 | super(); 21 | BidiTools.attachBidifier(this); 22 | } 23 | } 24 | #end 25 | -------------------------------------------------------------------------------- /src/texter/openfl/_internal/DrawableTextField.hx: -------------------------------------------------------------------------------- 1 | package texter.openfl._internal; 2 | 3 | import openfl.text.TextField; 4 | import openfl.display.Shape; 5 | import openfl.events.Event; 6 | import openfl.display.Sprite; 7 | 8 | /** 9 | * A regular textfield with a sprite baclground. will get released in the future when more feature-rich 10 | */ 11 | class DrawableTextField extends TextFieldCompatibility 12 | { 13 | public var backgroundSprite = new Sprite(); 14 | 15 | var m = new Shape(); 16 | 17 | public function new() 18 | { 19 | super(); 20 | textField = new TextField(); 21 | addChild(textField); 22 | m.graphics.drawRect(0, 0, textField.width, textField.height); 23 | addChild(m); 24 | backgroundSprite.mask = m; 25 | addChild(backgroundSprite); 26 | addEventListener(Event.ENTER_FRAME, render); 27 | } 28 | 29 | function render(e:Event) 30 | { 31 | m.graphics.drawRect(0, 0, textField.width, textField.height); 32 | backgroundSprite.x = textField.scrollH; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/texter/openfl/_internal/JointGraphic.hx: -------------------------------------------------------------------------------- 1 | package texter.openfl._internal; 2 | 3 | #if openfl 4 | import openfl.display.Bitmap; 5 | import openfl.display.Sprite; 6 | import openfl.display.Shape; 7 | import openfl.display.BitmapData; 8 | import openfl.geom.Point; 9 | 10 | class JointGraphic 11 | { 12 | var d:DynamicTextField; 13 | 14 | public function new(d:DynamicTextField) 15 | { 16 | this.d = d; 17 | } 18 | 19 | /** 20 | * This joint will be used when no other joint is available. 21 | * This can be used as a fallback graphic, or as a default for all joints. 22 | */ 23 | public var defaultGraphic(default, set):BitmapData = new DefaultJoint(); 24 | 25 | function set_defaultGraphic(g:BitmapData):BitmapData 26 | { 27 | if (g == null) 28 | return g; 29 | 30 | var ref = new Sprite(); 31 | for (i in [top, left, right, bottom]) 32 | { 33 | if (defaultEdgeGraphic != null) 34 | break; 35 | if (i != null) 36 | continue; 37 | if (i == top) 38 | ref = d.joints.middleTop; 39 | if (i == left) 40 | ref = d.joints.middleLeft; 41 | if (i == right) 42 | ref = d.joints.middleRight; 43 | if (i == bottom) 44 | ref = d.joints.middleBottom; 45 | 46 | ref.removeChildren(); 47 | ref.addChild(new Bitmap(g)); 48 | } 49 | 50 | for (i in [topRight, topLeft, bottomRight, bottomLeft]) 51 | { 52 | if (defaultCornerGraphic != null) 53 | break; 54 | if (i != null) 55 | continue; 56 | if (i == topRight) 57 | ref = d.joints.topRight; 58 | if (i == topLeft) 59 | ref = d.joints.topLeft; 60 | if (i == bottomRight) 61 | ref = d.joints.bottomRight; 62 | if (i == bottomLeft) 63 | ref = d.joints.bottomLeft; 64 | ref.removeChildren(); 65 | ref.addChild(new Bitmap(g)); 66 | } 67 | // update positions - the supplied graphic may be larger than the previous one 68 | d.width = d.width; 69 | d.width = d.height; 70 | return g; 71 | } 72 | 73 | /** 74 | * This joint will be used when no other corner joint is available. 75 | * This can be used as a fallback graphic, or as a default for all corner joints. 76 | * 77 | * Notice - this will only apply to corner joints. 78 | */ 79 | public var defaultCornerGraphic(default, set):BitmapData; 80 | 81 | function set_defaultCornerGraphic(g:BitmapData):BitmapData 82 | { 83 | if (g == null) 84 | return g; 85 | var ref = new Sprite(); 86 | for (i in [topRight, topLeft, bottomRight, bottomLeft]) 87 | { 88 | if (i != null) 89 | continue; 90 | if (i == topRight) 91 | ref = d.joints.topRight; 92 | if (i == topLeft) 93 | ref = d.joints.topLeft; 94 | if (i == bottomRight) 95 | ref = d.joints.bottomRight; 96 | if (i == bottomLeft) 97 | ref = d.joints.bottomLeft; 98 | ref.removeChildren(); 99 | ref.addChild(new Bitmap(g)); 100 | } 101 | // update positions - the supplied graphic may be larger than the previous one 102 | d.width = d.width; 103 | d.width = d.height; 104 | return g; 105 | } 106 | 107 | /** 108 | * This joint will be used when no other edge joint is available. 109 | * This can be used as a fallback graphic, or as a default for all edge joints. 110 | * 111 | * Notice - this will only apply to edge joints. 112 | */ 113 | public var defaultEdgeGraphic(default, set):BitmapData; 114 | 115 | function set_defaultEdgeGraphic(g:BitmapData):BitmapData 116 | { 117 | if (g == null) 118 | return g; 119 | var ref = new Sprite(); 120 | for (i in [top, left, right, bottom]) 121 | { 122 | if (i != null) 123 | continue; 124 | if (i == top) 125 | ref = d.joints.middleTop; 126 | if (i == left) 127 | ref = d.joints.middleLeft; 128 | if (i == right) 129 | ref = d.joints.middleRight; 130 | if (i == bottom) 131 | ref = d.joints.middleBottom; 132 | ref.removeChildren(); 133 | ref.addChild(new Bitmap(g)); 134 | } 135 | // update positions - the supplied graphic may be larger than the previous one 136 | d.width = d.width; 137 | d.width = d.height; 138 | return g; 139 | } 140 | 141 | /** 142 | This joint will be used for the rotation joint. 143 | 144 | When unset, will use the default graphic. 145 | **/ 146 | public var rotationHandle(default, set):BitmapData = new DefaultJoint(true); 147 | 148 | function set_rotationHandle(g:BitmapData):BitmapData 149 | { 150 | if (g == null) 151 | return g; 152 | d.joints.rotation.removeChildren(); 153 | d.joints.rotation.addChild(new Bitmap(g)); 154 | // update positions - the supplied graphic may be larger than the previous one 155 | d.joints.rotation.x = d.textField.width / 2 - d.joints.rotation.width / 2; 156 | d.joints.rotation.y = -ROTATION_JOINT_GUTTER; 157 | 158 | return g; 159 | } 160 | 161 | /** 162 | * When set, will be used for the top left corner joint. 163 | */ 164 | public var topLeft(default, set):BitmapData; 165 | 166 | function set_topLeft(g:BitmapData):BitmapData 167 | { 168 | if (g == null) 169 | return g; 170 | d.joints.topLeft.removeChildren(); 171 | d.joints.topLeft.addChild(new Bitmap(g)); 172 | // update positions - the supplied graphic may be larger than the previous one 173 | d.joints.topLeft.x = -JOINT_GUTTER; 174 | d.joints.topLeft.y = -JOINT_GUTTER; 175 | return g; 176 | } 177 | 178 | /** 179 | * When set, will be used for the top right corner joint. 180 | */ 181 | public var topRight(default, set):BitmapData; 182 | 183 | function set_topRight(g:BitmapData):BitmapData 184 | { 185 | if (g == null) 186 | return g; 187 | d.joints.topRight.removeChildren(); 188 | d.joints.topRight.addChild(new Bitmap(g)); 189 | // update positions - the supplied graphic may be larger than the previous one 190 | d.joints.topRight.x = d.textField.width - JOINT_GUTTER; 191 | d.joints.topRight.y = -JOINT_GUTTER; 192 | return g; 193 | } 194 | 195 | /** 196 | * When set, will be used for the bottom left corner joint. 197 | */ 198 | public var bottomLeft(default, set):BitmapData; 199 | 200 | function set_bottomLeft(g:BitmapData):BitmapData 201 | { 202 | if (g == null) 203 | return g; 204 | d.joints.bottomLeft.removeChildren(); 205 | d.joints.bottomLeft.addChild(new Bitmap(g)); 206 | // update positions - the supplied graphic may be larger than the previous one 207 | d.joints.bottomLeft.x = -JOINT_GUTTER; 208 | d.joints.bottomLeft.y = d.textField.height - JOINT_GUTTER; 209 | return g; 210 | } 211 | 212 | /** 213 | * When set, will be used for the bottom right corner joint. 214 | */ 215 | public var bottomRight(default, set):BitmapData; 216 | 217 | function set_bottomRight(g:BitmapData):BitmapData 218 | { 219 | if (g == null) 220 | return g; 221 | d.joints.bottomRight.removeChildren(); 222 | d.joints.bottomRight.addChild(new Bitmap(g)); 223 | // update positions - the supplied graphic may be larger than the previous one 224 | d.joints.bottomRight.x = d.textField.width - JOINT_GUTTER; 225 | d.joints.bottomRight.y = d.textField.height - JOINT_GUTTER; 226 | return g; 227 | } 228 | 229 | /** 230 | * When set, will be used for the left edge joint. 231 | */ 232 | public var left(default, set):BitmapData; 233 | 234 | function set_left(g:BitmapData):BitmapData 235 | { 236 | if (g == null) 237 | return g; 238 | d.joints.middleLeft.removeChildren(); 239 | d.joints.middleLeft.addChild(new Bitmap(g)); 240 | // update positions - the supplied graphic may be larger than the previous one 241 | d.joints.middleLeft.x = -JOINT_GUTTER; 242 | d.joints.middleLeft.y = d.textField.height / 2 - d.joints.middleLeft.height / 2; 243 | return g; 244 | } 245 | 246 | /** 247 | * When set, will be used for the right edge joint. 248 | */ 249 | public var right(default, set):BitmapData; 250 | 251 | function set_right(g:BitmapData):BitmapData 252 | { 253 | if (g == null) 254 | return g; 255 | d.joints.middleRight.removeChildren(); 256 | d.joints.middleRight.addChild(new Bitmap(g)); 257 | // update positions - the supplied graphic may be larger than the previous one 258 | d.joints.middleRight.x = d.textField.width - JOINT_GUTTER; 259 | d.joints.middleRight.y = d.textField.height / 2 - d.joints.middleRight.height / 2; 260 | return g; 261 | } 262 | 263 | /** 264 | * When set, will be used for the top edge joint. 265 | */ 266 | public var top(default, set):BitmapData; 267 | 268 | function set_top(g:BitmapData):BitmapData 269 | { 270 | if (g == null) 271 | return g; 272 | d.joints.middleTop.removeChildren(); 273 | d.joints.middleTop.addChild(new Bitmap(g)); 274 | // update positions - the supplied graphic may be larger than the previous one 275 | d.joints.middleTop.x = d.textField.width / 2 - d.joints.middleTop.width / 2; 276 | d.joints.middleTop.y = -JOINT_GUTTER; 277 | return g; 278 | } 279 | 280 | /** 281 | * When set, will be used for the bottom edge joint. 282 | */ 283 | public var bottom(default, set):BitmapData; 284 | 285 | function set_bottom(g:BitmapData):BitmapData 286 | { 287 | if (g == null) 288 | return g; 289 | d.joints.middleBottom.removeChildren(); 290 | d.joints.middleBottom.addChild(new Bitmap(g)); 291 | // update positions - the supplied graphic may be larger than the previous one 292 | d.joints.middleBottom.x = d.textField.width / 2 - d.joints.middleBottom.width / 2; 293 | d.joints.middleBottom.y = d.textField.height - JOINT_GUTTER; 294 | return g; 295 | } 296 | 297 | public static final JOINT_GUTTER = new DefaultJoint().width / 2 - 0.5; 298 | public static final ROTATION_JOINT_GUTTER = 33 + JOINT_GUTTER; 299 | } 300 | 301 | private class DefaultJoint extends BitmapData 302 | { 303 | public function new(?rotation = false) 304 | { 305 | super(11, 11, true, 0x00000000); 306 | 307 | if (rotation) 308 | { 309 | BitmapData.loadFromFile("assets/texter/DynamicTextField/RotationJoint.png").onComplete(function(bmp:BitmapData) 310 | { 311 | copyPixels(bmp, bmp.rect, new Point(0, 0)); 312 | }).onError(function(e) 313 | { 314 | trace(e); 315 | }); 316 | return; 317 | } 318 | var s = new Shape(); 319 | s.graphics.beginFill(0xffffff); 320 | s.graphics.lineStyle(1, 0x000000); 321 | s.graphics.drawCircle(5.5, 5.5, 4.5); 322 | s.graphics.endFill(); 323 | draw(s); 324 | } 325 | } 326 | #end 327 | -------------------------------------------------------------------------------- /src/texter/openfl/_internal/JointManager.hx: -------------------------------------------------------------------------------- 1 | package texter.openfl._internal; 2 | 3 | #if openfl 4 | import openfl.geom.Rectangle; 5 | import openfl.geom.Matrix; 6 | import texter.openfl.DynamicTextField; 7 | import openfl.geom.Point; 8 | import openfl.events.MouseEvent; 9 | import texter.openfl._internal.JointGraphic.*; 10 | 11 | using Math; 12 | 13 | class JointManager 14 | { 15 | public var tf:DynamicTextField; 16 | 17 | public function new(tf:DynamicTextField) 18 | { 19 | this.tf = tf; 20 | } 21 | 22 | var tX:Float; 23 | var tY:Float; 24 | var tWidth:Float; 25 | var tHeight:Float; 26 | 27 | function setPrevStats() 28 | { 29 | tX = tf.x; 30 | tY = tf.y; 31 | tWidth = tf.textFieldWidth; 32 | tHeight = tf.textFieldHeight; 33 | } 34 | 35 | public function startResizeTopLeft(e:MouseEvent) 36 | { 37 | if (tf.hideControlsWhenUnfocused) 38 | tf.showControls(); 39 | var p = { 40 | x: e.stageX, 41 | y: e.stageY, 42 | w: tf.textFieldWidth, // gutter 43 | h: tf.textFieldHeight // gutter 44 | }; 45 | setPrevStats(); 46 | 47 | function res(e:MouseEvent) 48 | { 49 | if (!e.buttonDown) 50 | { 51 | tf.stage.removeEventListener(MouseEvent.MOUSE_MOVE, res); 52 | 53 | tf.onResized(tf.x, tf.y, tf.textFieldWidth, tf.textFieldHeight, tX, tY, tWidth, tHeight); 54 | return; 55 | } 56 | tf.x = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).x; 57 | tf.y = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).y; 58 | final width = p.w + (p.x - e.stageX); 59 | final height = p.h + (p.y - e.stageY); 60 | tf.textFieldWidth = width; 61 | tf.textFieldHeight = height; 62 | if (width < 0) 63 | { 64 | tf.x = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).x + width; 65 | tf.textFieldWidth = -width; 66 | } 67 | if (height < 0) 68 | { 69 | tf.y = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).y + height; 70 | tf.textFieldHeight = -height; 71 | } 72 | } 73 | 74 | tf.stage.addEventListener(MouseEvent.MOUSE_MOVE, res); 75 | } 76 | 77 | public function startResizeTopRight(e:MouseEvent) 78 | { 79 | if (tf.hideControlsWhenUnfocused) 80 | tf.showControls(); 81 | var p = { 82 | x: e.stageX, 83 | y: e.stageY, 84 | w: tf.textFieldWidth, // gutter 85 | h: tf.textFieldHeight // gutter 86 | }; 87 | setPrevStats(); 88 | 89 | function res(e:MouseEvent) 90 | { 91 | if (!e.buttonDown) 92 | { 93 | tf.stage.removeEventListener(MouseEvent.MOUSE_MOVE, res); 94 | 95 | tf.onResized(tf.x, tf.y, tf.textFieldWidth, tf.textFieldHeight, tX, tY, tWidth, tHeight); 96 | return; 97 | } 98 | 99 | final width = p.w - (p.x - e.stageX); 100 | final height = p.h + (p.y - e.stageY); 101 | trace("width: " + width + " height: " + height); 102 | 103 | tf.textFieldWidth = width; 104 | tf.textFieldHeight = height; 105 | 106 | tf.y = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).y; 107 | 108 | if (width < 0) 109 | { 110 | tf.x = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).x; 111 | trace(width); 112 | tf.textFieldWidth = -width; 113 | } 114 | if (height < 0) 115 | { 116 | tf.y = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).y + height; 117 | tf.textFieldHeight = -height; 118 | } 119 | } 120 | 121 | tf.stage.addEventListener(MouseEvent.MOUSE_MOVE, res); 122 | } 123 | 124 | public function startResizeBottomLeft(e:MouseEvent) 125 | { 126 | if (tf.hideControlsWhenUnfocused) 127 | tf.showControls(); 128 | var p = { 129 | x: e.stageX, 130 | y: e.stageY, 131 | w: tf.textFieldWidth, // gutter 132 | h: tf.textFieldHeight // gutter 133 | }; 134 | setPrevStats(); 135 | 136 | function res(e:MouseEvent) 137 | { 138 | if (!e.buttonDown) 139 | { 140 | tf.stage.removeEventListener(MouseEvent.MOUSE_MOVE, res); 141 | 142 | tf.onResized(tf.x, tf.y, tf.textFieldWidth, tf.textFieldHeight, tX, tY, tWidth, tHeight); 143 | return; 144 | } 145 | tf.x = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).x; 146 | final width = p.w + (p.x - e.stageX); 147 | final height = p.h - (p.y - e.stageY); 148 | tf.textFieldWidth = width; 149 | tf.textFieldHeight = height; 150 | if (width < 0) 151 | { 152 | tf.x = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).x + width; 153 | tf.textFieldWidth = -width; 154 | } 155 | if (height < 0) 156 | { 157 | tf.y = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).y; 158 | tf.textFieldHeight = -height; 159 | } 160 | } 161 | 162 | tf.stage.addEventListener(MouseEvent.MOUSE_MOVE, res); 163 | } 164 | 165 | public function startResizeBottomRight(e:MouseEvent) 166 | { 167 | if (tf.hideControlsWhenUnfocused) 168 | tf.showControls(); 169 | var p = { 170 | x: e.stageX, 171 | y: e.stageY, 172 | w: tf.textFieldWidth, // gutter 173 | h: tf.textFieldHeight // gutter 174 | }; 175 | setPrevStats(); 176 | 177 | function res(e:MouseEvent) 178 | { 179 | if (!e.buttonDown) 180 | { 181 | tf.stage.removeEventListener(MouseEvent.MOUSE_MOVE, res); 182 | 183 | tf.onResized(tf.x, tf.y, tf.textFieldWidth, tf.textFieldHeight, tX, tY, tWidth, tHeight); 184 | return; 185 | } 186 | final width = p.w - (p.x - e.stageX); 187 | final height = p.h - (p.y - e.stageY); 188 | tf.textFieldWidth = width; 189 | tf.textFieldHeight = height; 190 | if (width < 0) 191 | { 192 | tf.x = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).x; 193 | tf.textFieldWidth = -width; 194 | } 195 | if (height < 0) 196 | { 197 | tf.y = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).y; 198 | tf.textFieldHeight = -height; 199 | } 200 | } 201 | 202 | tf.stage.addEventListener(MouseEvent.MOUSE_MOVE, res); 203 | } 204 | 205 | public function startResizeLeft(e:MouseEvent) 206 | { 207 | if (tf.hideControlsWhenUnfocused) 208 | tf.showControls(); 209 | var p = { 210 | x: e.stageX, 211 | y: e.stageY, 212 | w: tf.textFieldWidth, // gutter 213 | h: tf.textFieldHeight // gutter 214 | }; 215 | setPrevStats(); 216 | 217 | function res(e:MouseEvent) 218 | { 219 | if (!e.buttonDown) 220 | { 221 | tf.stage.removeEventListener(MouseEvent.MOUSE_MOVE, res); 222 | 223 | tf.onResized(tf.x, tf.y, tf.textFieldWidth, tf.textFieldHeight, tX, tY, tWidth, tHeight); 224 | return; 225 | } 226 | tf.x = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).x; 227 | final width = p.w + (p.x - e.stageX); 228 | tf.textFieldWidth = width; 229 | if (width < 0) 230 | { 231 | tf.x = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).x + width; 232 | tf.textFieldWidth = -width; 233 | } 234 | if (tf.matchTextSize) 235 | tf.textFieldHeight = tf.textHeight + 4 236 | else 237 | tf.textFieldHeight = p.h; 238 | } 239 | 240 | tf.stage.addEventListener(MouseEvent.MOUSE_MOVE, res); 241 | } 242 | 243 | public function startResizeRight(e:MouseEvent) 244 | { 245 | if (tf.hideControlsWhenUnfocused) 246 | tf.showControls(); 247 | var p = { 248 | x: e.stageX, 249 | y: e.stageY, 250 | w: tf.textFieldWidth, // gutter 251 | h: tf.textFieldHeight // gutter 252 | }; 253 | setPrevStats(); 254 | 255 | function res(e:MouseEvent) 256 | { 257 | if (!e.buttonDown) 258 | { 259 | tf.stage.removeEventListener(MouseEvent.MOUSE_MOVE, res); 260 | 261 | tf.onResized(tf.x, tf.y, tf.textFieldWidth, tf.textFieldHeight, tX, tY, tWidth, tHeight); 262 | return; 263 | } 264 | final width = p.w - (p.x - e.stageX); 265 | tf.textFieldWidth = width; 266 | if (width < 0) 267 | { 268 | tf.x = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).x; 269 | tf.textFieldWidth = -width; 270 | } 271 | if (tf.matchTextSize) 272 | tf.textFieldHeight = tf.textHeight + 4 273 | else 274 | tf.textFieldHeight = p.h; 275 | } 276 | 277 | tf.stage.addEventListener(MouseEvent.MOUSE_MOVE, res); 278 | } 279 | 280 | public function startResizeTop(e:MouseEvent) 281 | { 282 | if (tf.hideControlsWhenUnfocused) 283 | tf.showControls(); 284 | var p = { 285 | x: e.stageX, 286 | y: e.stageY, 287 | w: tf.textFieldWidth, // gutter 288 | h: tf.textFieldHeight // gutter 289 | }; 290 | setPrevStats(); 291 | 292 | function res(e:MouseEvent) 293 | { 294 | if (!e.buttonDown) 295 | { 296 | tf.stage.removeEventListener(MouseEvent.MOUSE_MOVE, res); 297 | 298 | tf.onResized(tf.x, tf.y, tf.textFieldWidth, tf.textFieldHeight, tX, tY, tWidth, tHeight); 299 | return; 300 | } 301 | tf.y = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).y; 302 | final height = p.h + (p.y - e.stageY); 303 | tf.textFieldHeight = height; 304 | if (height < 0) 305 | { 306 | tf.y = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).y + height; 307 | tf.textFieldHeight = -height; 308 | } 309 | tf.textFieldWidth = p.w; 310 | } 311 | 312 | tf.stage.addEventListener(MouseEvent.MOUSE_MOVE, res); 313 | } 314 | 315 | public function startResizeBottom(e:MouseEvent) 316 | { 317 | if (tf.hideControlsWhenUnfocused) 318 | tf.showControls(); 319 | var p = { 320 | x: e.stageX, 321 | y: e.stageY, 322 | w: tf.textFieldWidth, // gutter 323 | h: tf.textFieldHeight // gutter 324 | }; 325 | setPrevStats(); 326 | 327 | function res(e:MouseEvent) 328 | { 329 | if (!e.buttonDown) 330 | { 331 | tf.stage.removeEventListener(MouseEvent.MOUSE_MOVE, res); 332 | 333 | tf.onResized(tf.x, tf.y, tf.textFieldWidth, tf.textFieldHeight, tX, tY, tWidth, tHeight); 334 | return; 335 | } 336 | final height = p.h - (p.y - e.stageY); 337 | tf.textFieldHeight = height; 338 | if (height < 0) 339 | { 340 | tf.y = tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).y; 341 | tf.textFieldHeight = -height; 342 | } 343 | tf.textFieldWidth = p.w; 344 | } 345 | 346 | tf.stage.addEventListener(MouseEvent.MOUSE_MOVE, res); 347 | } 348 | 349 | public function startRotation(e:MouseEvent) 350 | { 351 | if (tf.hideControlsWhenUnfocused) 352 | tf.showControls(); 353 | var rect:Rectangle = tf.getBounds(tf.parent); 354 | var centerX = rect.left + (rect.width / 2); 355 | var centerY = rect.top + (rect.height / 2); 356 | 357 | var centerPoint = { 358 | x: centerX - 2.5, 359 | y: centerY - 11 360 | }; 361 | var prevRotation = tf.rotation; 362 | 363 | function rot(e:MouseEvent) 364 | { 365 | if (!e.buttonDown) 366 | { 367 | tf.stage.removeEventListener(MouseEvent.MOUSE_MOVE, rot); 368 | 369 | tf.onRotated(tf.rotation, prevRotation); 370 | return; 371 | } 372 | rotateAroundCenter(tf, 373 | angleFromPointToPoint(tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).x, tf.parent.globalToLocal(new Point(e.stageX, e.stageY)).y, 374 | centerPoint)); 375 | } 376 | tf.stage.addEventListener(MouseEvent.MOUSE_MOVE, rot); 377 | } 378 | 379 | // create a function that calculates the angle between a point and the center of the object 380 | 381 | function angleFromPointToPoint(x:Float, y:Float, p:{x:Float, y:Float}) 382 | { 383 | final angle = Math.atan2(y - p.y, x - p.x) * 180 / Math.PI; 384 | trace(angle); 385 | return angle; 386 | } 387 | 388 | public function rotateAroundCenter(object:DynamicTextField, angleDegrees:Float) 389 | { 390 | if (object.rotation == angleDegrees) 391 | { 392 | return; 393 | } 394 | 395 | var matrix:Matrix = object.transform.matrix; 396 | var rect:Rectangle = object.getBounds(object.parent); 397 | var centerX = rect.left + (rect.width / 2); 398 | var centerY = rect.top + (rect.height / 2); 399 | matrix.translate(-centerX, -centerY); 400 | matrix.rotate(((angleDegrees - object.rotation) / 180 + 0.5) * Math.PI); 401 | matrix.translate(centerX, centerY); 402 | object.transform.matrix = matrix; 403 | 404 | object.rotation = object.rotation; 405 | } 406 | } 407 | #end 408 | --------------------------------------------------------------------------------