├── .gitignore ├── Documentation ├── README.md └── assets │ ├── ask_number.png │ ├── ask_number_no_parameter.png │ ├── ask_text.png │ ├── ask_text_no_parameter.png │ ├── attribution.png │ ├── ceil.png │ ├── exit.png │ ├── floor.png │ ├── get_battery_level.png │ ├── get_name.png │ ├── get_type.png │ ├── hello_world.png │ ├── interpolated.png │ ├── multi_line_string.png │ ├── numbers.png │ ├── resizer.py │ ├── round.png │ ├── view_content_graph.png │ ├── wait.png │ └── wait_to_return.png ├── LICENSE ├── Makefile ├── README.md ├── compiler ├── action.c ├── action.h ├── interpolated.c ├── interpolated.h ├── main.c ├── output.c ├── output.h ├── scope.c ├── scope.h ├── splash-Bridging-Header.h ├── splash.lm ├── splash.ym ├── splash_compiler.h ├── splash_helper.c ├── splash_helper.h ├── structures │ ├── htable.c │ ├── htable.h │ ├── list.c │ ├── list.h │ ├── refcnt.c │ ├── refcnt.h │ ├── serializable.c │ ├── serializable.h │ ├── str.c │ ├── str.h │ └── structures.h ├── utils.c └── utils.h ├── examples ├── Leap Year.splash ├── Quadratic Solver.splash ├── age.mov ├── age.shortcut ├── age.splash ├── leap_year.mov ├── leap_year.shortcut ├── quadratic.mov └── quadratic.shortcut ├── imgs ├── RoundedIcon.png └── quadratic.gif ├── privacy.md ├── splash.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings ├── splashTests └── Info.plist ├── splashUITests └── Info.plist └── splash_app ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ ├── 167.png │ ├── 20.png │ ├── 20@2x-1.png │ ├── 20@2x.png │ ├── 20@3x.png │ ├── 29.png │ ├── 29@2x-1.png │ ├── 29@2x.png │ ├── 29@3x.png │ ├── 40.png │ ├── 40@2x-1.png │ ├── 40@2x.png │ ├── 40@3x.png │ ├── 60@2x.png │ ├── 60@3x.png │ ├── 76.png │ ├── 76@2x.png │ ├── Contents.json │ └── splash_icon.png ├── Contents.json └── gear.imageset │ ├── BackgroundTask_settings_16x21_@3x.png │ └── Contents.json ├── Base.lproj └── LaunchScreen.storyboard ├── Helpers ├── ActionView.swift ├── BrowserDelegate.swift ├── Bundle+Resources.swift ├── FileManager+Utils.swift ├── NSLayoutConstraint+Utils.swift ├── NSRange+Utils.swift ├── Notification+Constants.swift ├── PageViewController.swift ├── String+Utils.swift ├── ThemeControl.swift ├── ThemeManager.swift ├── UIAlertController+Error.swift ├── UIColor+Utils.swift ├── UIScrollView+Utils.swift ├── UIView+Utils.swift ├── UserDefaults+Constants.swift └── UserDefaults+Key.swift ├── Info.plist ├── Model └── SplashDocument.swift ├── UI ├── Editor │ ├── CodeAccessoryView.swift │ ├── EditorView.swift │ ├── EditorViewController.swift │ └── SyntaxColorizer.swift ├── Onboard │ ├── OnboardViewController+Github.swift │ ├── OnboardViewController+Intro.swift │ ├── OnboardViewController+PageVC.swift │ ├── OnboardViewController+Theme.swift │ ├── OnboardViewController+View.swift │ └── OnboardViewController.swift ├── Settings │ ├── SettingsViewController+Default.swift │ ├── SettingsViewController+FontSizeCell.swift │ ├── SettingsViewController+ThemeCell.swift │ └── SettingsViewController.swift └── ViewController.swift └── examples ├── Age.splash ├── Leap Year.splash └── Quadratic Solver.splash /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | # 51 | # Add this line if you want to avoid checking in source code from the Xcode workspace 52 | # *.xcworkspace 53 | 54 | # Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | # Carthage/Checkouts 58 | 59 | Carthage/Build 60 | 61 | # fastlane 62 | # 63 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 64 | # screenshots whenever they are needed. 65 | # For more information about the recommended setup visit: 66 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 67 | 68 | fastlane/report.xml 69 | fastlane/Preview.html 70 | fastlane/screenshots/**/*.png 71 | fastlane/test_output 72 | 73 | # Code Injection 74 | # 75 | # After new code Injection tools there's a generated folder /iOSInjectionProject 76 | # https://github.com/johnno1962/injectionforxcode 77 | 78 | iOSInjectionProject/ 79 | # Xcode 80 | # 81 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 82 | 83 | ## User settings 84 | xcuserdata/ 85 | 86 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 87 | *.xcscmblueprint 88 | *.xccheckout 89 | 90 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 91 | build/ 92 | DerivedData/ 93 | *.moved-aside 94 | *.pbxuser 95 | !default.pbxuser 96 | *.mode1v3 97 | !default.mode1v3 98 | *.mode2v3 99 | !default.mode2v3 100 | *.perspectivev3 101 | !default.perspectivev3 102 | # General 103 | .DS_Store 104 | .AppleDouble 105 | .LSOverride 106 | 107 | # Icon must end with two \r 108 | Icon 109 | 110 | # Thumbnails 111 | ._* 112 | 113 | # Files that might appear in the root of a volume 114 | .DocumentRevisions-V100 115 | .fseventsd 116 | .Spotlight-V100 117 | .TemporaryItems 118 | .Trashes 119 | .VolumeIcon.icns 120 | .com.apple.timemachine.donotpresent 121 | 122 | # Directories potentially created on remote AFP share 123 | .AppleDB 124 | .AppleDesktop 125 | Network Trash Folder 126 | Temporary Items 127 | .apdisk 128 | 129 | compiler/lex.yy.c 130 | compiler/splash.tab.c 131 | compiler/splash.tab.h 132 | splash 133 | *.shortcut 134 | !examples/*.shortcut 135 | *.splash 136 | !examples/*.splash 137 | -------------------------------------------------------------------------------- /Documentation/README.md: -------------------------------------------------------------------------------- 1 | # Splash Documentation and Reference 2 | 3 | Jump to the [actions reference](#actions-reference) 4 | 5 | ## Getting started 6 | 7 | Splash is in an early stage of development, so it support just a few actions. 8 | 9 | To give us some luck, let's begin with a classic Hello World. 10 | 11 | ```python 12 | ShowResult("Hello World") # strings can be surrounded by " or ' 13 | 14 | # This will result in the following shortcut 15 | ``` 16 | 17 | ![Show Result action with hello world text](assets/hello_world.png) 18 | 19 | As you can see, comments are preeceeded with the hash `#` character. 20 | 21 | Now let's dive in to data types 22 | 23 | ## Strings 24 | 25 | Strings are a sequence of characters that represent text. You can insert them in the middle of your code by puting the characters inside single or double quotes. 26 | 27 | 28 | ```python 29 | "This is a string" 30 | 'This is also a string' 31 | ``` 32 | 33 | But both quotes must be of the same type. This can be useful if you want to have a quote character inside your string. 34 | 35 | ```python 36 | "String with a single ' quote inside it" 37 | ``` 38 | 39 | The other option you have is to [escape](https://en.wikipedia.org/wiki/Escape_character) the special symbols. 40 | 41 | ```python 42 | 'String with a single \' quote inside it' 43 | ``` 44 | 45 | Strings can also be multiline 46 | 47 | ```python 48 | 'This is the first line 49 | This is the second 50 | This is the third' # Syntax coloring will not work here, but will work fine in the splash editor. 51 | ``` 52 | 53 | This will result in the following shortcut 54 | 55 | ![Multi line string](assets/multi_line_string.png) 56 | 57 | ### Ask text from the user 58 | 59 | To ask from the user you can use the `AskText()` function 60 | 61 | ```python 62 | AskText("What's your name?") 63 | ``` 64 | 65 | produces: 66 | 67 | ![Ask Text action](assets/ask_text.png) 68 | 69 | ## Numbers 70 | 71 | To input numbers you just type them. 72 | Here are some examples. 73 | 74 | ```python 75 | 3.1415 # always use the . character as decimal separator 76 | 2 77 | -3 78 | 0 79 | ``` 80 | 81 | ![numbers](assets/numbers.png) 82 | 83 | You can also perform math operations with numbers 84 | 85 | The available operators are: 86 | 87 | ```python 88 | 1 + 2 # sum 89 | 3 - 4 # subtraction 90 | 5 * 6 # multiplication 91 | 7 / 8 # division 92 | 9 % 10 # modulus operation 93 | 11 ^ 12 # 11 to the 12th power 94 | ``` 95 | 96 | And you can combine them: 97 | 98 | ```python 99 | 1 + 2 - 3 * 4 / 5 ^ 6 # will result in 2.999232 100 | ``` 101 | 102 | As you can see, operator precedence is respected as usual. 103 | 104 | ### Ask numbers from the user 105 | 106 | To ask a number you can use the `AskNumber()` function 107 | 108 | ```python 109 | AskNumber("How old are you?") 110 | ``` 111 | 112 | produces: 113 | 114 | ![Ask number action](assets/ask_number.png) 115 | 116 | 117 | ## Variables 118 | 119 | Variable names must start with a letter or a underscore `_` and can only contain `_`, letters and numbers. 120 | 121 | ```python 122 | # Valid variable names 123 | _foo 124 | foo 125 | bar1 126 | fooBar_123 127 | foo_bar 128 | 129 | # Invalid variable names 130 | 131 | 1foo 132 | foo-bar 133 | ``` 134 | 135 | To assign a value to a variable you can use `:=` 136 | 137 | ``` 138 | name := AskText("What's your name?") 139 | age := AskNumber("How old are you?") 140 | ``` 141 | 142 | produces: 143 | 144 | ![variable attribution](assets/attribution.png) 145 | 146 | ## Interpolated strings 147 | 148 | You can put variables inside strings and their content will be embeded in the string. 149 | 150 | ```python 151 | age := AskNumber("How old are you?") 152 | text := "You are {age} years old" 153 | ``` 154 | 155 | produces: 156 | 157 | ![interpolated string](assets/interpolated.png) 158 | 159 | 160 | ### Conditionals and flow control 161 | 162 | Splash `if else` statements behave like in the most of programming languages. 163 | 164 | ``` 165 | age := AskNumber("How old are you?") 166 | 167 | if age < 12 { 168 | ShowResult("Child") 169 | } else if age < 18 { 170 | ShowResult("Teen") 171 | } else if age < 60 { 172 | ShowResult("Adult") 173 | } else { 174 | ShowResult("Elder") 175 | } 176 | ``` 177 | 178 | You can also have expressions in the middle of your comparisons 179 | 180 | ```python 181 | year := AskNumber("Enter the year") 182 | 183 | if year % 4 > 0 { 184 | leap := 0 185 | } else if year % 100 > 0 { 186 | leap := 1 187 | } else if year % 400 > 0 { 188 | leap := 0 189 | } else { 190 | leap := 1 191 | } 192 | ``` 193 | 194 | To check for equality you use the `==` symbol 195 | 196 | ```python 197 | a := AskNumber("a =") 198 | b := AskNumber("b =") 199 | c := AskNumber("c =") 200 | 201 | delta := b^2 - 4 * a * c 202 | 203 | if delta == 0 { 204 | ShowResult("Both roots are equal") 205 | } else if delta < 0 { 206 | ShowResult("No real roots") 207 | } else { 208 | ShowResult("Equation have two distinct roots") 209 | } 210 | ``` 211 | 212 | At the moment, splash supports `<`, `>` and `==` comparison operators only. 213 | 214 | ## That's all folks 215 | 216 | That covers all the basics of the splash language. 217 | To know more you can read the [Actions Reference](#actions-reference) 218 | 219 | 220 | # Actions Reference 221 | 222 | ### `AskText([question])` 223 | 224 | You can pass an optional `string` parameter that will be promped to the user. 225 | 226 | ```python 227 | AskText("What's your name?") 228 | ``` 229 | 230 | produces: 231 | 232 | ![Ask Text action](assets/ask_text.png) 233 | 234 | Or you can pass no parameter 235 | 236 | ```python 237 | AskText() 238 | ``` 239 | 240 | produces: 241 | 242 | ![Ask text without parameter](assets/ask_text_no_parameter.png) 243 | 244 | 245 | ### `AskNumber([question])` 246 | 247 | You can pass an optional `string` parameter that will be promped to the user. 248 | 249 | ```python 250 | AskNumber("How old are you?") 251 | ``` 252 | 253 | produces: 254 | 255 | ![Ask number action](assets/ask_number.png) 256 | 257 | Or you can pass no parameter 258 | 259 | ```python 260 | AskNumber() 261 | ``` 262 | 263 | produces: 264 | 265 | ![Ask number without parameter](assets/ask_number_no_parameter.png) 266 | 267 | ### `ShowResult(result)` 268 | 269 | Shows in an alert the content of result 270 | 271 | ```python 272 | ShowResult("Hello World") 273 | ``` 274 | 275 | produces: 276 | 277 | ![Show Result action with hello world text](assets/hello_world.png) 278 | 279 | ### `Floor(value)` 280 | 281 | Rounds the number down 282 | 283 | ```python 284 | Floor(3.14) 285 | ``` 286 | 287 | produces: 288 | 289 | ![floor action](assets/floor.png) 290 | 291 | 292 | ### `Ceil(value)` 293 | 294 | Rounds the number up 295 | 296 | ```python 297 | Ceil(3.14) 298 | ``` 299 | 300 | produces: 301 | 302 | ![floor action](assets/ceil.png) 303 | 304 | ### `Round(value)` 305 | 306 | Round to the closest possible return value; when caught halfway between two positive numbers, round up; when caught between two negative numbers, round down. 307 | 308 | ```python 309 | Round(-3.5) 310 | ``` 311 | 312 | produces: 313 | 314 | ![floor action](assets/round.png) 315 | 316 | 317 | ### `GetName(item)` 318 | 319 | ```python 320 | GetName("Hello World") 321 | ``` 322 | 323 | produces: 324 | 325 | ![get name action](assets/get_name.png) 326 | 327 | ### `GetType(item)` 328 | 329 | ```python 330 | GetType("Hello World") 331 | ``` 332 | 333 | produces: 334 | 335 | ![get name action](assets/get_type.png) 336 | 337 | ### `ViewContentGraph(item)` 338 | 339 | ```python 340 | ViewContentGraph("Hello World") 341 | ``` 342 | 343 | produces: 344 | 345 | ![get name action](assets/view_content_graph.png) 346 | 347 | ### `Wait(seconds)` 348 | 349 | Waits for the specified number of seconds before continuing. You can pass a fractional value to the function, the Shortcuts UI will show the value rounded down, but it will wait the exact time you passed. 350 | 351 | ```python 352 | Wait(3.9) 353 | ``` 354 | 355 | produces: 356 | 357 | Waits for 3.9 seconds but UI shows this: 358 | 359 | ![get name action](assets/wait.png) 360 | 361 | ### `Exit()` 362 | 363 | Stops execution of current shortcut and dismisses the shortcut on screen. 364 | 365 | ```python 366 | Exit() 367 | ``` 368 | 369 | produces: 370 | 371 | ![get name action](assets/exit.png) 372 | 373 | ### `WaitToReturn()` 374 | 375 | Pauses execution until you leave the Shortcuts app and return to it. 376 | 377 | ```python 378 | WaitToReturn() 379 | ``` 380 | 381 | produces: 382 | 383 | ![get name action](assets/wait_to_return.png) 384 | 385 | ### `GetBatteryLevel()` 386 | 387 | Returns the percentage of battery remaining as a number from 0 to 100. 388 | 389 | ```python 390 | GetBatteryLevel() 391 | ``` 392 | 393 | produces: 394 | 395 | ![get name action](assets/get_battery_level.png) 396 | -------------------------------------------------------------------------------- /Documentation/assets/ask_number.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/ask_number.png -------------------------------------------------------------------------------- /Documentation/assets/ask_number_no_parameter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/ask_number_no_parameter.png -------------------------------------------------------------------------------- /Documentation/assets/ask_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/ask_text.png -------------------------------------------------------------------------------- /Documentation/assets/ask_text_no_parameter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/ask_text_no_parameter.png -------------------------------------------------------------------------------- /Documentation/assets/attribution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/attribution.png -------------------------------------------------------------------------------- /Documentation/assets/ceil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/ceil.png -------------------------------------------------------------------------------- /Documentation/assets/exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/exit.png -------------------------------------------------------------------------------- /Documentation/assets/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/floor.png -------------------------------------------------------------------------------- /Documentation/assets/get_battery_level.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/get_battery_level.png -------------------------------------------------------------------------------- /Documentation/assets/get_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/get_name.png -------------------------------------------------------------------------------- /Documentation/assets/get_type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/get_type.png -------------------------------------------------------------------------------- /Documentation/assets/hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/hello_world.png -------------------------------------------------------------------------------- /Documentation/assets/interpolated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/interpolated.png -------------------------------------------------------------------------------- /Documentation/assets/multi_line_string.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/multi_line_string.png -------------------------------------------------------------------------------- /Documentation/assets/numbers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/numbers.png -------------------------------------------------------------------------------- /Documentation/assets/resizer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from PIL import Image 4 | import os 5 | 6 | files = os.listdir() 7 | images = [f for f in files if f.endswith('.png')] 8 | 9 | target_width = 750//2 10 | 11 | for file_name in images: 12 | image = Image.open(file_name) 13 | size = image.size 14 | original_aspect_ratio = size[0]/size[1] 15 | size = target_width, int(target_width / original_aspect_ratio) 16 | image = image.resize(size, Image.ANTIALIAS) 17 | image.save(file_name) 18 | 19 | # a = w/h 20 | # h = w/a 21 | -------------------------------------------------------------------------------- /Documentation/assets/round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/round.png -------------------------------------------------------------------------------- /Documentation/assets/view_content_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/view_content_graph.png -------------------------------------------------------------------------------- /Documentation/assets/wait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/wait.png -------------------------------------------------------------------------------- /Documentation/assets/wait_to_return.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/Documentation/assets/wait_to_return.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | splash: lexycal syntax compile 2 | @echo DONE 3 | 4 | lexycal: 5 | flex -o compiler/lex.yy.c compiler/splash.lm 6 | syntax: 7 | bison -d compiler/splash.ym -o compiler/splash.tab.c 8 | 9 | compile: 10 | cc compiler/structures/*.c compiler/scope.c compiler/action.c compiler/output.c compiler/utils.c compiler/splash_helper.c compiler/interpolated.c compiler/lex.yy.c compiler/splash.tab.c compiler/main.c -ly -ll -lm -o splash 11 | 12 | .PHONY : clean 13 | clean: 14 | @rm \ 15 | compiler/splash.tab.c \ 16 | compiler/lex.yy.c \ 17 | compiler/y.tab.c \ 18 | splash \ 19 | 2> /dev/null || : 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SPLASH : Simple Programming LAnguage for SHortcuts 2 | 3 | ## Project being archived 4 | 5 | Unfortunately, apple made some modifications to the Shortcuts app that make it very difficult to develop new features in this project. The fact that it is no longer possible to directly import .shortcut files unbearably delays the tests needed to add functionality to this language. Therefore I'm stopping my activities in this repo. 6 | 7 | ![splash icon](https://raw.githubusercontent.com/gonzula/splash/master/imgs/RoundedIcon.png) 8 | [![AppStore badge](https://linkmaker.itunes.apple.com/en-us/badge-lrg.svg?releaseDate=2019-03-11&kind=iossoftware&bubble=ios_apps)](https://itunes.apple.com/us/app/splash-programming-language/id1455793030?mt=8) 9 | ##### The first real programming language that compiles to Apple's `Shortcuts.app` 10 | 11 | ## What it is 12 | 13 | Although the Shortcuts app is designed for non programmers/beginners, it's programming interface is similar to assembly in the meaning that very simple expressions need dozens of blocks. 14 | 15 | To solve this problem, `SPLASH` is being developed as a programming language designed for non programmers/beginners that compiles directly to shortcuts. 16 | 17 | ![Example GIF](https://raw.githubusercontent.com/gonzula/splash/master/imgs/quadratic.gif) 18 | 19 | ## For what it's worth 20 | 21 | Splash is meant to reduce substantially the manual labor, improve readability and maintainability of shortcuts. It's still under development but with a few fully working features. Between them: 22 | 23 | * Complex mathematical expressions 24 | * Flow control (ifs and elses) 25 | * String interpolation (variables inside a string) 26 | 27 | And those are some of the features in the backlog: 28 | 29 | * Loops 30 | * Functions declarations 31 | 32 | ## How it works 33 | 34 | ### The programming language 35 | 36 | The best way to learn is with some examples 37 | 38 | Here's an example splash program that given an age tells the person's stage of life. 39 | 40 | [shortcut file](https://github.com/gonzula/splash/blob/master/examples/age.shortcut) 41 | 42 | [video of the shortcut](https://github.com/gonzula/splash/blob/master/examples/age.mov) 43 | 44 | 45 | ``` BASH 46 | age := AskNumber() # The ':=' stores the right side expression 47 | # on the left side variable 48 | 49 | # And AskNumber() asks the user for a number input 50 | # when the shortcut is running 51 | 52 | if age < 12 { 53 | ShowResult("Child") 54 | } else if age < 18 { # Blocks of code are surrounded by '{' and '}' 55 | ShowResult("Teen") 56 | } else if age < 60 { 57 | ShowResult("Adult") 58 | } else { 59 | ShowResult("Elder") 60 | } 61 | 62 | # And comments are preceded by '#' 63 | ``` 64 | 65 | Here's an example with more advanced expressions that solves any quadratic expression in the form ax² + bx + c = 0 66 | 67 | [shortcut file](https://github.com/gonzula/splash/blob/master/examples/quadratic.shortcut) 68 | 69 | [video of the shortcut](https://github.com/gonzula/splash/blob/master/examples/quadratic.mov) 70 | 71 | ``` BASH 72 | a := AskNumber() 73 | b := AskNumber() 74 | c := AskNumber() 75 | 76 | delta := b^2 - 4 * a * c # a^b is a to the b power 77 | 78 | if a == 0 { 79 | x := -c/b 80 | 81 | answer := "x = {x}" # This is a string interpolation 82 | # It resolves to "x = (value of variable x)" 83 | } else if delta == 0 { # '==' tests for equality 84 | x := -b / (2 * a) 85 | 86 | answer := "x1 = x2 = {x}" 87 | } else if delta > 0 { 88 | x1 := (-b + delta^(1/2))/(2 * a) 89 | x2 := (-b + -delta^(1/2))/(2 * a) 90 | 91 | answer := "x1 = {x1}\nx2 = {x2}" 92 | } else { 93 | xr := -b / (2 * a) 94 | xi := (-delta)^(1/2) / (2 * a) 95 | nxi := -xi 96 | 97 | answer := "x1 = {xr} + {xi}i\nx2 = {xr} + {nxi}i" 98 | } 99 | 100 | ShowResult(answer) # ShowResult shows an alert with the 101 | # value passed inside the parenthesis 102 | ``` 103 | 104 | And a last example that tells if an year is a leap year: 105 | 106 | [shortcut file](https://github.com/gonzula/splash/blob/master/examples/leap_year.shortcut) 107 | 108 | [video of the shortcut](https://github.com/gonzula/splash/blob/master/examples/leap_year.mov) 109 | 110 | ``` BASH 111 | year := AskNumber() 112 | 113 | if year % 4 > 0 { # The % symbol performs the modulo operation 114 | leap := 0 115 | } else if year % 100 > 0 { # And, diffently from shortcuts, 116 | # you can have math expressions in 117 | # the comparison 118 | leap := 1 119 | } else if year % 400 > 0 { # So this line checks if year is divisible by 400 120 | leap := 0 121 | } else { 122 | leap := 1 123 | } 124 | 125 | if leap == 0 { 126 | type := "common" 127 | } else { 128 | type := "leap" 129 | } 130 | 131 | ShowResult("{year} is a {type} year") 132 | ``` 133 | 134 | ## How to get started 135 | 136 | You can use this language on your iOS device by [downloading the app from the App Store](https://itunes.apple.com/us/app/splash-programming-language/id1455793030?mt=8) or cloning this repo and compiling it on your Xcode. (You will need an Apple Developer account) 137 | 138 | Or you can compile the compiler on your computer. It's pure C code, without any dependencies, so it works on any operating system. 139 | 140 | ## Installing the app via Xcode 141 | 142 | You will need to have installed `bison` installed. 143 | ``` 144 | brew install bison 145 | ln -s /usr/local/opt/bison/bin/bison /usr/local/bin/bison 146 | ``` 147 | And then it's just a matter of building the Xcode project 148 | 149 | ## Compiling the Compiler 150 | 151 | ### The easy way 152 | 153 | Download the compiled version from the latest release 154 | 155 | [linux version](https://github.com/gonzula/splash/releases/download/v0.1.1/splash.linux) 156 | 157 | [macOS version](https://github.com/gonzula/splash/releases/download/v0.1.1/splash.macOS) 158 | 159 | and run 160 | 161 | ``` BASH 162 | chmod +x splash 163 | ``` 164 | 165 | 166 | ### Compiling from source 167 | 168 | You can compile the splash compiler from source, cloning this repo and running `make`. You will need `bison`, `flex` and a C compiler 169 | 170 | On macOS you can install the dependencies with homebrew: 171 | 172 | ``` 173 | brew install bison 174 | ln -s /usr/local/opt/bison/bin/bison /usr/local/bin/bison 175 | ``` 176 | 177 | On ubuntu: 178 | 179 | ``` 180 | sudo apt install bison flex gcc 181 | ``` 182 | 183 | ## How to run 184 | 185 | On a terminal window located at the folder you installed `splash` 186 | 187 | ``` 188 | ./splash input_file output_file 189 | ``` 190 | 191 | The splash compiler adds a `.shortcut` extension to output_file that is required by the shortcuts app. 192 | 193 | Also, the name of that output file is the display name of your shortcut. 194 | 195 | ## How to import the shortcuts 196 | 197 | The easiest way is to airdrop the `.shortcut` file to your device. 198 | -------------------------------------------------------------------------------- /compiler/action.h: -------------------------------------------------------------------------------- 1 | #ifndef ACTION_H 2 | #define ACTION_H 3 | 4 | #include 5 | #include "splash_helper.h" 6 | 7 | Action *action_create(ActionID id); 8 | Action *action_create_number(Operand op); 9 | Action *action_create_text(Operand op); 10 | Action *action_create_nothing(void); 11 | Action *action_create_get_variable(Operand op); 12 | Action *action_create_get_magic_variable(Operand op); 13 | Action *action_create_math_operation(char operator, Operand op2); 14 | Action *action_create_set_variable(char100 var_name); 15 | Action *action_create_comp(Comparison comp); 16 | 17 | Action *action_create_ask_input(Operand op, char *input_type); 18 | Action *action_create_show_result(Operand op); 19 | Action *action_create_round_number(const char *round_mode, const char *round_type); 20 | Action *action_create_get_item_name(void); 21 | Action *action_create_get_item_type(void); 22 | Action *action_create_view_content_graph(void); 23 | Action *action_create_wait(Operand op); 24 | Action *action_create_exit(void); 25 | Action *action_create_wait_to_return(void); 26 | Action *action_create_get_battery_level(void); 27 | Action *action_create_date(Operand op); 28 | Action *action_create_extract_archive(void); 29 | Action *action_create_get_current_location(void); 30 | 31 | 32 | List * action_create_cond_control(int value, int control_count); 33 | 34 | void action_output(Action *action, FILE *output); 35 | 36 | #endif /* ACTION_H */ 37 | -------------------------------------------------------------------------------- /compiler/interpolated.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "interpolated.h" 3 | 4 | void _token_release(void *obj); 5 | void _interpolated_release(void *obj); 6 | 7 | StringToken * 8 | token_init() { 9 | StringToken *token = (StringToken *)alloc(sizeof(StringToken), _token_release); 10 | token->name = str_init(); 11 | *(token->uuid) = 0; 12 | 13 | return token; 14 | } 15 | 16 | Interpolated * 17 | interpolated_init() { 18 | Interpolated *interpolated = (Interpolated *)alloc(sizeof(Interpolated), _interpolated_release); 19 | interpolated->str = str_init(); 20 | interpolated->tokens = list_init(); 21 | 22 | return interpolated; 23 | } 24 | 25 | char 26 | _unescape_char(char c) { 27 | switch (c) { 28 | case 'b': return '\b'; 29 | case 'f': return '\f'; 30 | case 'n': return '\n'; 31 | case 'r': return '\r'; 32 | case 't': return '\t'; 33 | default: return c; 34 | } 35 | } 36 | 37 | Interpolated * 38 | interpolated_create(char100 source) { 39 | Interpolated *interpolated = interpolated_init(); 40 | char *s = source.value; 41 | 42 | bool is_escaped = false; 43 | bool is_inside_interpolation = false; 44 | StringToken *current_token = NULL; 45 | size_t len = strlen(s); 46 | for (int i = 0; i < len; i++) { 47 | unsigned char c = s[i]; 48 | bool is_first_or_last_char = i == 0 || i == len - 1; 49 | bool is_string_delimiter = c == '\'' || c == '\"'; 50 | if (is_first_or_last_char && is_string_delimiter) { 51 | continue; 52 | } 53 | if (is_escaped) { 54 | str_append_char(interpolated->str, _unescape_char(c)); 55 | is_escaped = false; 56 | continue; 57 | } else if (c == '\\') { 58 | is_escaped = true; 59 | continue; 60 | } 61 | 62 | if (is_inside_interpolation) { 63 | if (c == '}') { 64 | list_append(interpolated->tokens, current_token); 65 | release(current_token); 66 | current_token = NULL; 67 | is_inside_interpolation = false; 68 | } else { 69 | str_append_char(current_token->name, c); 70 | } 71 | continue; 72 | } else if (c == '{') { 73 | current_token = token_init(); 74 | current_token->position = str_unicode_len(interpolated->str); 75 | //EFBFBC 76 | str_append_char(interpolated->str, 0xEF); 77 | str_append_char(interpolated->str, 0xBF); 78 | str_append_char(interpolated->str, 0xBC); 79 | is_inside_interpolation = true; 80 | continue; 81 | } 82 | 83 | str_append_char(interpolated->str, s[i]); 84 | } 85 | 86 | return interpolated; 87 | } 88 | 89 | Interpolated * 90 | interpolated_create_from_null() { 91 | char100 null; 92 | *(null.value) = 0; 93 | 94 | Interpolated *interpolated = interpolated_create(null); 95 | return interpolated; 96 | } 97 | 98 | Interpolated * 99 | interpolated_create_from_variable(Operand op) { 100 | char100 text; 101 | sprintf(text.value, "\"{%s}\"", op.name.value); 102 | 103 | Interpolated *interpolated = interpolated_create(text); 104 | return interpolated; 105 | } 106 | 107 | Interpolated * 108 | interpolated_create_from_magic_variable(Operand op) { 109 | Interpolated *interpolated = interpolated_init(); 110 | 111 | StringToken *token = token_init(); 112 | str_append(token->name, op.name.value); 113 | strcpy(token->uuid, op.uuid); 114 | 115 | token->position = str_unicode_len(interpolated->str); 116 | //EFBFBC 117 | str_append_char(interpolated->str, 0xEF); 118 | str_append_char(interpolated->str, 0xBF); 119 | str_append_char(interpolated->str, 0xBC); 120 | 121 | list_append(interpolated->tokens, token); 122 | 123 | release(token); 124 | 125 | return interpolated; 126 | } 127 | 128 | Interpolated * 129 | interpolated_create_from_operand(Operand op) { 130 | Interpolated *interpolated; 131 | switch (op.type) { 132 | case op_number: interpolated = interpolated_create(op.value); break; 133 | case op_magic_variable: interpolated = interpolated_create_from_magic_variable(op); break; 134 | case op_variable: interpolated = interpolated_create_from_variable(op); break; 135 | case op_string: interpolated = interpolated_create(op.value); break; 136 | case op_null: interpolated = interpolated_create_from_null(); break; 137 | } 138 | 139 | return interpolated; 140 | } 141 | 142 | Serializable * 143 | interpolated_parameters(Interpolated *interpolated) { 144 | HashTable *dict = htable_init(); 145 | 146 | HashTable *value = htable_init(); 147 | Serializable *s2 = serializable_create_ht(value); 148 | htable_set(dict, "Value", s2); 149 | 150 | HashTable *attachments = htable_init(); 151 | Serializable *s3 = serializable_create_ht(attachments); 152 | htable_set(value, "attachmentsByRange", s3); 153 | 154 | LIST_LOOP(interpolated->tokens) { 155 | StringToken *token = (StringToken *)node->object; 156 | 157 | HashTable *dict = htable_init(); 158 | Serializable *s1 = serializable_create_ht(dict); 159 | char range[100]; 160 | sprintf(range, "{%d, 1}", token->position); 161 | htable_set(attachments, range, s1); 162 | 163 | if (*token->uuid) { 164 | String *type = str_create("ActionOutput"); 165 | Serializable *s2 = serializable_create_str(type); 166 | htable_set(dict, "Type", s2); 167 | 168 | Serializable *s3 = serializable_create_str(token->name); 169 | htable_set(dict, "OutputName", s3); 170 | 171 | String *uuid = str_create(token->uuid); 172 | Serializable *s4 = serializable_create_str(uuid); 173 | htable_set(dict, "OutputUUID", s4); 174 | 175 | release(type); 176 | release(dict); 177 | release(s2); 178 | release(s3); 179 | release(uuid); 180 | release(s4); 181 | } else { 182 | String *type = str_create("Variable"); 183 | Serializable *s2 = serializable_create_str(type); 184 | htable_set(dict, "Type", s2); 185 | 186 | Serializable * s3 = serializable_create_str(token->name); 187 | htable_set(dict, "VariableName", s3); 188 | release(type); 189 | release(dict); 190 | release(s2); 191 | release(s3); 192 | } 193 | release(s1); 194 | } 195 | 196 | Serializable *s4 = serializable_create_str(interpolated->str); 197 | htable_set(value, "string", s4); 198 | 199 | String *serialization_type = str_create("WFTextTokenString"); 200 | Serializable *s5 = serializable_create_str(serialization_type); 201 | htable_set(dict, "WFSerializationType", s5); 202 | 203 | release(attachments); 204 | release(s3); 205 | release(value); 206 | release(s2); 207 | release(s4); 208 | release(serialization_type); 209 | release(s5); 210 | 211 | Serializable *s = serializable_create_ht(dict); 212 | release(dict); 213 | return s; 214 | } 215 | 216 | void 217 | _token_release(void *obj) { 218 | StringToken *token = (StringToken *)obj; 219 | release(token->name); 220 | } 221 | 222 | void 223 | _interpolated_release(void *obj) { 224 | Interpolated *interpolated = (Interpolated *)obj; 225 | release(interpolated->str); 226 | release(interpolated->tokens); 227 | } 228 | -------------------------------------------------------------------------------- /compiler/interpolated.h: -------------------------------------------------------------------------------- 1 | #ifndef INTERPOLATED_H 2 | #define INTERPOLATED_H 3 | 4 | #include "splash_helper.h" 5 | 6 | Interpolated *interpolated_init(void); 7 | StringToken *token_init(void); 8 | 9 | Interpolated *interpolated_create(char100 source); 10 | Interpolated *interpolated_create_from_operand(Operand op); 11 | 12 | Serializable *interpolated_parameters(Interpolated *interpolated); 13 | 14 | #endif /* INTERPOLATED_H */ 15 | -------------------------------------------------------------------------------- /compiler/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "splash_compiler.h" 4 | 5 | int 6 | main(int argc, char *argv[]) { 7 | if (argc != 3) { 8 | fprintf(stderr, "usage: %s \n", argv[0]); 9 | return EXIT_FAILURE; 10 | } 11 | return parse(argv[1], argv[2]); 12 | } 13 | -------------------------------------------------------------------------------- /compiler/output.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "output.h" 5 | 6 | void 7 | output_header(FILE *output) { 8 | fprintf(output, ""); 9 | fprintf(output, ""); 10 | fprintf(output, ""); 11 | fprintf(output, ""); 12 | fprintf(output, "WFWorkflowActions"); 13 | fprintf(output, ""); 14 | } 15 | 16 | void 17 | output_footer(FILE *output) { 18 | fprintf(output, ""); 19 | fprintf(output, "WFWorkflowClientRelease"); 20 | fprintf(output, "2.1.2"); 21 | fprintf(output, "WFWorkflowClientVersion"); 22 | fprintf(output, "754"); 23 | fprintf(output, "WFWorkflowIcon"); 24 | fprintf(output, ""); 25 | fprintf(output, "WFWorkflowIconGlyphNumber"); 26 | fprintf(output, "59511"); 27 | fprintf(output, "WFWorkflowIconImageData"); 28 | fprintf(output, ""); 29 | fprintf(output, ""); 30 | fprintf(output, "WFWorkflowIconStartColor"); 31 | fprintf(output, "463140863"); 32 | fprintf(output, ""); 33 | fprintf(output, "WFWorkflowImportQuestions"); 34 | fprintf(output, ""); 35 | fprintf(output, "WFWorkflowInputContentItemClasses"); 36 | fprintf(output, ""); 37 | fprintf(output, "WFAppStoreAppContentItem"); 38 | fprintf(output, "WFArticleContentItem"); 39 | fprintf(output, "WFContactContentItem"); 40 | fprintf(output, "WFDateContentItem"); 41 | fprintf(output, "WFEmailAddressContentItem"); 42 | fprintf(output, "WFGenericFileContentItem"); 43 | fprintf(output, "WFImageContentItem"); 44 | fprintf(output, "WFiTunesProductContentItem"); 45 | fprintf(output, "WFLocationContentItem"); 46 | fprintf(output, "WFDCMapsLinkContentItem"); 47 | fprintf(output, "WFAVAssetContentItem"); 48 | fprintf(output, "WFPDFContentItem"); 49 | fprintf(output, "WFPhoneNumberContentItem"); 50 | fprintf(output, "WFRichTextContentItem"); 51 | fprintf(output, "WFSafariWebPageContentItem"); 52 | fprintf(output, "WFStringContentItem"); 53 | fprintf(output, "WFURLContentItem"); 54 | fprintf(output, ""); 55 | fprintf(output, "WFWorkflowTypes"); 56 | fprintf(output, ""); 57 | fprintf(output, "NCWidget"); 58 | fprintf(output, "WatchKit"); 59 | fprintf(output, ""); 60 | fprintf(output, ""); 61 | fprintf(output, ""); 62 | fprintf(output, "\n"); 63 | } 64 | 65 | void 66 | output_entry(Entry *entry, int i, size_t count, bool *stop, void *context) { 67 | FILE *output = (FILE *)context; 68 | 69 | char *key = entry->key; 70 | Serializable *value = entry->obj; 71 | 72 | fprintf(output, "%s", key); 73 | 74 | switch (value->type) { 75 | case st_ht: output_htable(output, value->ht); break; 76 | case st_list: DEBUGPRINT("NOT IMPLEMENTED\n"); break; 77 | case st_str: fprintf(output, "%s", value->str->string); break; 78 | case st_int: fprintf(output, "%d", value->i); break; 79 | case st_float: fprintf(output, "%f", value->f); break; 80 | case st_bool: DEBUGPRINT("NOT IMPLEMENTED\n"); break; 81 | case st_null: DEBUGPRINT("NOT IMPLEMENTED\n"); break; 82 | } 83 | } 84 | 85 | void 86 | output_htable(FILE *output, HashTable *htable) { 87 | fprintf(output, ""); 88 | 89 | htable_iterate(htable, output, output_entry); 90 | 91 | fprintf(output, ""); 92 | } 93 | -------------------------------------------------------------------------------- /compiler/output.h: -------------------------------------------------------------------------------- 1 | #ifndef OUTPUT_H 2 | #define OUTPUT_H 3 | 4 | #include 5 | #include "structures/serializable.h" 6 | #include "utils.h" 7 | #include "splash_helper.h" 8 | 9 | void output_header(FILE *output); 10 | void output_footer(FILE *output); 11 | 12 | void output_htable(FILE *output, HashTable *htable); 13 | 14 | #endif /* OUTPUT_H */ 15 | -------------------------------------------------------------------------------- /compiler/scope.c: -------------------------------------------------------------------------------- 1 | #include "scope.h" 2 | #include "action.h" 3 | #include "utils.h" 4 | #include "structures/structures.h" 5 | 6 | #include 7 | 8 | void _scope_release(void *obj); 9 | 10 | Scope * 11 | scope_create() { 12 | Scope *scope = (Scope *)alloc(sizeof(Scope), _scope_release); 13 | char uuid[37]; 14 | uuid_gen(uuid); 15 | scope->name = str_create(uuid); 16 | scope->actions = list_init(); 17 | scope->parent_name = NULL; 18 | 19 | htable_set(scopes, uuid, scope); 20 | 21 | scope_clear_last_uuid(scope); 22 | 23 | return scope; 24 | } 25 | 26 | void 27 | scope_output(Scope *scope, FILE *output) { 28 | char last_uuid_output[37]; 29 | *last_uuid_output = 0; 30 | LIST_LOOP(scope->actions) { 31 | Action *action = (Action *)node->object; 32 | if (strcmp(last_uuid_output, action->uuid) == 0) { 33 | DEBUGPRINT("Skipping action\n"); 34 | continue; 35 | } 36 | 37 | action_output(action, output); 38 | strcpy(last_uuid_output, action->uuid); 39 | } 40 | } 41 | 42 | void 43 | scope_clear_last_uuid(Scope *scope) { 44 | *(scope->last_uuid) = 0; 45 | } 46 | 47 | void 48 | scope_add_action(Scope *scope, Action *action) { 49 | list_append(scope->actions, action); 50 | strcpy(scope->last_uuid, action->uuid); 51 | } 52 | 53 | void 54 | scope_add_actions(Scope *scope, List *actions) { 55 | LIST_LOOP(actions) { 56 | Action *action = (Action *)node->object; 57 | scope_add_action(scope, action); 58 | } 59 | } 60 | 61 | void 62 | _scope_release(void *obj) { 63 | Scope *scope = (Scope *)obj; 64 | 65 | if (scope->name) { 66 | release(scope->name); 67 | } 68 | release(scope->actions); 69 | } 70 | -------------------------------------------------------------------------------- /compiler/scope.h: -------------------------------------------------------------------------------- 1 | #ifndef SCOPE_H 2 | #define SCOPE_H 3 | 4 | #include 5 | #include "splash_helper.h" 6 | 7 | Scope *scope_create(void); 8 | void scope_add_action(Scope *scope, Action *action); 9 | void scope_add_actions(Scope *scope, List *actions); 10 | void scope_output(Scope *scope, FILE *output); 11 | void scope_clear_last_uuid(Scope *scope); 12 | 13 | #endif /* SCOPE_H */ 14 | -------------------------------------------------------------------------------- /compiler/splash-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "splash_compiler.h" 6 | -------------------------------------------------------------------------------- /compiler/splash.lm: -------------------------------------------------------------------------------- 1 | 2 | %{ 3 | #include 4 | #include 5 | #include "splash_helper.h" 6 | #include "splash.tab.h" 7 | 8 | #pragma clang diagnostic push 9 | #pragma clang diagnostic ignored "-Wdocumentation" 10 | #pragma clang diagnostic ignored "-Wunused-function" 11 | #pragma clang diagnostic ignored "-Wunneeded-internal-declaration" 12 | 13 | %} 14 | %option noyywrap 15 | %% 16 | 17 | [ ] { } 18 | 19 | #.*?\n {} /* comment */ 20 | 21 | \"([^\"\\]|\\.)*\"|\'([^\'\\]|\\.)*\' { strcpy(yylval.STR.value, yytext); return STR; } 22 | [0-9]+(\.[0-9]+)? { strcpy(yylval.NUM.value, yytext); return NUM; } 23 | \:\= { return ATT; } 24 | 25 | if { return IF; } 26 | otherwise|else { return ELSE; } 27 | 28 | and|&& { return AND; } 29 | or|\|\| { return OR; } 30 | 31 | equals|== { return EQ; } 32 | less_than|< { return LT; } 33 | less_or_equal_than|<= { return LE; } 34 | greater_than|> { return GT; } 35 | greater_or_equal_than|>= { return GE; } 36 | 37 | [a-zA-Z_][a-zA-Z_0-9]* { strcpy(yylval.ID.value, yytext); return ID; } 38 | 39 | \n|\r|. { return yytext[0]; } 40 | 41 | <> { static int once = 0; return once++ ? 0 : '\n'; } /*https://stackoverflow.com/a/1779949*/ 42 | 43 | %% 44 | 45 | 46 | #pragma clang diagnostic pop 47 | -------------------------------------------------------------------------------- /compiler/splash.ym: -------------------------------------------------------------------------------- 1 | %{ 2 | #include 3 | #include 4 | #include 5 | #include "output.h" 6 | #include "splash_helper.h" 7 | void yyerror(const char* msg) { 8 | fprintf(stderr, "%s\n", msg); 9 | helper_error = 1; 10 | error_message = malloc(sizeof(char) * (strlen(msg) + 1)); 11 | strcpy(error_message, msg); 12 | } 13 | extern FILE *yyin; 14 | int yylex(void); 15 | //void yyerror(void); 16 | %} 17 | 18 | %define api.value.type union 19 | 20 | 21 | %type expr_ 22 | %type expr 23 | %type param 24 | %type comp 25 | 26 | %token STR 27 | %token NUM 28 | %token ATT 29 | %token IF 30 | %token ELSE 31 | %token AND 32 | %token OR 33 | %token EQ 34 | %token LT 35 | %token LE 36 | %token GT 37 | %token GE 38 | %token ID 39 | 40 | %left LT LE EQ 41 | %right ATT 42 | %left '+' '-' 43 | %right UMINUS 44 | %left '*' '/' '%' 45 | %right '^' 46 | 47 | %% 48 | 49 | prog : stat_list { YYACCEPT; } 50 | | error new_line { yyerror("Invalid Syntax"); YYABORT; } 51 | ; 52 | 53 | stat_list : stat_list stat new_line {} 54 | | stat_list new_line {} 55 | | %empty 56 | ; 57 | 58 | new_line : '\n' 59 | | '\r' 60 | ; 61 | 62 | stat : expr { DEBUGPRINT("\n"); } 63 | | attrib { DEBUGPRINT("\n"); } 64 | | cond { DEBUGPRINT("\n"); } 65 | ; 66 | 67 | attrib : ID ATT expr { place_set_variable($1); } 68 | ; 69 | 70 | cond : IF comp { increment_if_count(); append_cond_control(); append_conditional($2); } 71 | '{' 72 | stat_list 73 | '}' { close_scope(); } 74 | opt_else_if 75 | ; 76 | 77 | opt_else_if : ELSE IF comp { append_else(); append_conditional($3); } 78 | '{' 79 | stat_list 80 | '}' { close_scope(); close_scope(); } 81 | opt_else_if 82 | | opt_else 83 | ; 84 | 85 | opt_else : ELSE { append_else(); } 86 | '{' 87 | stat_list 88 | '}' { close_scope(); } 89 | | %empty 90 | ; 91 | 92 | comp : expr_ EQ expr_ { append_comparison(&$$, comp_op_eq, $1, $3); } 93 | | expr_ LT expr_ { append_comparison(&$$, comp_op_lt, $1, $3); } 94 | | expr_ GT expr_ { append_comparison(&$$, comp_op_gt, $1, $3); } 95 | | '(' comp ')' { $$ = $2; } 96 | ; 97 | 98 | expr : expr_ { $$ = $1; place_operand($1, false); } 99 | ; 100 | 101 | expr_ : expr_[left] '+' expr_[right] { append_operation(&$$, '+', $[left], $[right]); } 102 | | expr_[left] '-' expr_[right] { append_operation(&$$, '-', $[left], $[right]); } 103 | | expr_[left] '*' expr_[right] { append_operation(&$$, '*', $[left], $[right]); } 104 | | expr_[left] '/' expr_[right] { append_operation(&$$, '/', $[left], $[right]); } 105 | | expr_[left] '%' expr_[right] { append_operation(&$$, '%', $[left], $[right]); } 106 | | expr_[left] '^' expr_[right] { append_operation(&$$, '^', $[left], $[right]); } 107 | | '(' expr_ ')' { $$ = $2; } 108 | | '-' expr_ %prec UMINUS { append_minus_op(&$$, $2); } 109 | | NUM { append_operand(&$$, op_number, $1); } 110 | | ID { append_operand(&$$, op_variable, $1); } 111 | | ID '(' param ')' { if (append_func_call(&$$, $1, $3)) {YYABORT;} } 112 | | STR { append_operand(&$$, op_string, $1); } 113 | ; 114 | 115 | param : expr_ 116 | | %empty { append_null_operand(&$$); } 117 | ; 118 | 119 | %% 120 | 121 | int 122 | parse(const char *in_file_name, const char *out_file_name, char **error) { 123 | FILE *fp = init_parse(in_file_name, out_file_name); 124 | if (!fp) { 125 | return EXIT_FAILURE; 126 | } 127 | yyin = fp; 128 | int ret = yyparse(); 129 | if (ret == 0 && helper_error == 0) { 130 | end_parse(); 131 | } 132 | 133 | *error = error_message; 134 | return ret || helper_error; 135 | } 136 | -------------------------------------------------------------------------------- /compiler/splash_compiler.h: -------------------------------------------------------------------------------- 1 | // 2 | // splash_compiler.h 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 03/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | #ifndef splash_compiler_h 10 | #define splash_compiler_h 11 | 12 | int parse(const char *in_file_name, const char *out_file_name, char **error); 13 | 14 | #endif /* splash_compiler_h */ 15 | -------------------------------------------------------------------------------- /compiler/splash_helper.h: -------------------------------------------------------------------------------- 1 | #ifndef SPLASH_HELPER_H 2 | #define SPLASH_HELPER_H 3 | 4 | #include 5 | #include 6 | #include "structures/structures.h" 7 | 8 | typedef struct { 9 | String *name; 10 | char uuid[37]; 11 | int position; 12 | } StringToken; 13 | 14 | typedef struct { 15 | String *str; 16 | List *tokens; 17 | } Interpolated; // Interpolated String 18 | 19 | typedef union { 20 | char value[100]; 21 | } char100; 22 | 23 | typedef enum { 24 | op_number, 25 | op_magic_variable, // referencied by uuid 26 | op_variable, // referencied by name 27 | op_string, 28 | op_null 29 | } OpType; 30 | 31 | typedef struct { 32 | OpType type; 33 | char100 value; 34 | char100 name; 35 | char uuid[37]; 36 | } Operand; 37 | 38 | typedef enum { 39 | comp_op_eq, 40 | comp_op_lt, 41 | /* comp_op_le, not implemented */ 42 | comp_op_gt 43 | /* comp_op_ge not implemented */ 44 | } CompOp; 45 | 46 | typedef struct { 47 | Operand op1; 48 | Operand op2; 49 | CompOp operator; 50 | } Comparison; 51 | 52 | typedef struct { 53 | String *name; 54 | List *actions; 55 | String *parent_name; 56 | char last_uuid[37]; 57 | } Scope; 58 | 59 | typedef enum { 60 | WF_conditional, 61 | WF_get_variable, 62 | WF_math, 63 | WF_number, 64 | WF_text, 65 | WF_nothing, 66 | WF_set_variable, 67 | 68 | // functions 69 | WF_ask, 70 | WF_show_result, 71 | WF_round_number, 72 | WF_get_item_name, 73 | WF_get_item_type, 74 | WF_view_content_graph, 75 | WF_wait, 76 | WF_exit, 77 | WF_wait_to_return, 78 | WF_get_battery_level, 79 | WF_date, 80 | WF_extract_archive, 81 | WF_get_current_location 82 | } ActionID; 83 | 84 | typedef struct { 85 | ActionID id; 86 | HashTable *parameters; 87 | char uuid[37]; /* In case of groups, should be the same for all the actions in the group */ 88 | Scope *sub_scope; /* for groups, like if and loop */ 89 | int cond_control_count; 90 | bool cond_should_close_control; 91 | } Action; 92 | 93 | FILE *input_file; 94 | FILE *output_file; 95 | Scope *current_scope; 96 | HashTable *scopes; 97 | int if_count; 98 | int helper_error; 99 | char *error_message; 100 | 101 | FILE *init_parse(const char *in_file_name, const char *out_file_name); /* Must be called before starting parse */ 102 | void end_parse(void); /* Must be called after ending parse */ 103 | 104 | void increment_if_count(void); 105 | 106 | void action_add_subaction(Action *this, Action *other); 107 | 108 | void append_operand(Operand *, OpType, char100); 109 | void append_null_operand(Operand *); 110 | int append_func_call(Operand *, char100, Operand parameter); 111 | void append_operation(Operand *, char, Operand, Operand); 112 | void append_minus_op(Operand *, Operand); 113 | void set_variable(char100, Operand); 114 | void place_set_variable(char100 var_name); 115 | void place_operand(Operand op, bool force_null); 116 | 117 | void append_comparison(Comparison *, CompOp, Operand, Operand); 118 | void append_cond_control(void); 119 | void append_conditional(Comparison); 120 | void append_else(void); 121 | void close_scope(void); 122 | 123 | 124 | #endif /* SPLASH_HELPER_H */ 125 | -------------------------------------------------------------------------------- /compiler/structures/htable.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "htable.h" 4 | 5 | #define TABLE_SIZE (1 << 10) 6 | #define streq(a, b) (strcmp(a,b)==0) 7 | 8 | Entry * entry_create(const char *key, void *obj); 9 | void entry_free(Entry *e); 10 | Hash string_hash(const char * string); 11 | 12 | void 13 | _htable_release(void *t); 14 | 15 | HashTable * 16 | htable_init() { 17 | HashTable * t = (HashTable *)alloc(sizeof(HashTable), _htable_release); 18 | t->table = (TableBucket**)calloc(TABLE_SIZE, sizeof(TableBucket*)); 19 | t->count = 0; 20 | return t; 21 | } 22 | 23 | 24 | HashTable * 25 | htable_set(HashTable *t, const char *key, void *obj) { 26 | Entry *e = entry_create(key, obj); 27 | Hash hash = string_hash(key); 28 | TableBucket * bucket = *(t->table + (hash & (TABLE_SIZE - 1))); 29 | TableBucket * previous_bucket = NULL; 30 | 31 | /* check if we have a collision or the key already existed in the table */ 32 | while (bucket && !streq(bucket->entry->key, key)) { 33 | previous_bucket = bucket; 34 | bucket = bucket->next; 35 | } 36 | if (!bucket) { /* new Entry */ 37 | bucket = (TableBucket*)malloc(sizeof(TableBucket)); 38 | bucket->next = NULL; 39 | bucket->entry = NULL; 40 | t->count++; 41 | } else { /* updating Entry */ 42 | entry_free(bucket->entry); 43 | bucket->entry = NULL; 44 | } 45 | 46 | if (previous_bucket) { 47 | previous_bucket->next = bucket; 48 | } 49 | 50 | bucket->entry = e; 51 | 52 | if (!t->table[hash & (TABLE_SIZE - 1)]) { 53 | t->table[hash & (TABLE_SIZE - 1)] = bucket; 54 | } 55 | 56 | return t; 57 | } 58 | 59 | void * 60 | htable_retrieve(HashTable *t, const char * key, bool remove) { 61 | Hash hash = string_hash(key); 62 | TableBucket **top_bucket = &(t->table[hash & (TABLE_SIZE - 1)]); 63 | TableBucket *bucket = *top_bucket; 64 | TableBucket *previous_bucket = NULL; 65 | while (bucket && !streq(bucket->entry->key, key)) { 66 | previous_bucket = bucket; 67 | bucket = bucket->next; 68 | } 69 | if (bucket) { /* Found */ 70 | Entry *entry = bucket->entry; 71 | void *obj = entry->obj; 72 | if (remove) { 73 | if (previous_bucket) { 74 | previous_bucket->next = bucket->next; 75 | } 76 | else { 77 | *top_bucket = bucket->next; 78 | } 79 | retain(entry->obj); 80 | entry_free(entry); 81 | free(bucket); 82 | t->count--; 83 | } 84 | return obj; 85 | } 86 | return NULL; /* Not found */ 87 | } 88 | 89 | void 90 | htable_iterate(HashTable *t, void *context, void (*obj_iteration)(Entry *entry, int i, size_t count, bool *stop, void *context)) { 91 | bool stop = 0; 92 | for (int i = 0, j = 0; j < t->count && !stop; i++) { 93 | if (t->table[i]) { 94 | TableBucket * bucket = t->table[i]; 95 | while (bucket && !stop) { 96 | obj_iteration(bucket->entry, j, t->count, &stop, context); 97 | j++; 98 | bucket = bucket->next; 99 | } 100 | } 101 | } 102 | } 103 | 104 | void 105 | htable_release(HashTable *t) { 106 | for (int i = 0; t->count; i++) { 107 | if (t->table[i]) { 108 | TableBucket * bucket = t->table[i]; 109 | while (bucket) { 110 | TableBucket * bucketToFree = bucket; 111 | entry_free(bucket->entry); 112 | bucket = bucket->next; 113 | free(bucketToFree); 114 | t->count--; 115 | } 116 | } 117 | } 118 | free(t->table); 119 | } 120 | 121 | void 122 | _htable_release(void *t) { 123 | htable_release(t); 124 | } 125 | 126 | Entry * 127 | entry_create(const char *key, void *obj) { 128 | Entry * e = (Entry *)malloc(sizeof(Entry)); 129 | e->key = malloc(sizeof(char) * (strlen(key) + 1)); 130 | strcpy(e->key, key); 131 | e->obj = obj; 132 | retain(obj); 133 | return e; 134 | } 135 | 136 | void 137 | entry_free(Entry *e) { 138 | release(e->obj); 139 | free(e->key); 140 | free(e); 141 | } 142 | 143 | Hash 144 | string_hash(const char * string) { 145 | unsigned i = 0; 146 | unsigned long x = string[i] << 7; 147 | while (string[i]) 148 | x = (1000003 * x) ^ string[i++]; 149 | x ^= i; 150 | return x; 151 | } 152 | -------------------------------------------------------------------------------- /compiler/structures/htable.h: -------------------------------------------------------------------------------- 1 | #ifndef structures_htable_h 2 | #define structures_htable_h 3 | 4 | #include "refcnt.h" 5 | #include 6 | 7 | typedef unsigned long Hash; 8 | 9 | typedef struct { 10 | char *key; 11 | void *obj; 12 | } Entry; 13 | 14 | typedef struct _tableBucket { 15 | Entry *entry; 16 | struct _tableBucket *next; 17 | 18 | } TableBucket; 19 | 20 | typedef struct { 21 | TableBucket **table; 22 | size_t count; 23 | } HashTable; 24 | 25 | 26 | HashTable *htable_init(void); 27 | HashTable *htable_set(HashTable *t, const char *key, void *obj); 28 | void *htable_retrieve(HashTable *t, const char *key, bool remove); 29 | void htable_iterate(HashTable *t, void *context, void (*obj_iteration)(Entry *entry, int i, size_t count, bool *stop, void *context)); 30 | Hash string_hash(const char *string); 31 | 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /compiler/structures/list.c: -------------------------------------------------------------------------------- 1 | /* 2 | // List.c 3 | // DataStructures 4 | // 5 | // Created by Gonzo Fialho on 5/16/14. 6 | // Copyright (c) 2014 CEAFDC. All rights reserved. 7 | */ 8 | 9 | #include 10 | #include "list.h" 11 | #include "refcnt.h" 12 | 13 | 14 | void _list_release(void *aList); 15 | 16 | List * 17 | list_init() { 18 | List * aList = (List *)alloc(sizeof(List), _list_release); 19 | aList->count = 0; 20 | aList->firstNode = NULL; 21 | aList->lastNode = NULL; 22 | return aList; 23 | } 24 | 25 | List * 26 | list_append(List *aList, void *object) { 27 | Node *newNode = (Node *)malloc(sizeof(Node)); 28 | retain(object); 29 | newNode->object = object; 30 | newNode->next = NULL; 31 | newNode->prev = aList->lastNode; 32 | 33 | if (!aList->count) { 34 | aList->firstNode = newNode; 35 | } else { 36 | aList->lastNode->next = newNode; 37 | } 38 | aList->lastNode = newNode; 39 | aList->count++; 40 | return aList; 41 | } 42 | 43 | List * 44 | list_push(List *aList, void *object) { 45 | Node *newNode = (Node *)malloc(sizeof(Node)); 46 | retain(object); 47 | newNode->object = object; 48 | newNode->next = aList->firstNode; 49 | newNode->prev = NULL; 50 | 51 | if (!aList->count) { 52 | aList->lastNode = newNode; 53 | } else { 54 | aList->firstNode->prev = newNode; 55 | } 56 | aList->firstNode = newNode; 57 | aList->count++; 58 | 59 | return aList; 60 | } 61 | 62 | void * 63 | list_pop(List *aList) { 64 | Node *node; 65 | void *object; 66 | if (!aList->count) { 67 | return NULL; 68 | } 69 | node = aList->firstNode; 70 | object = node->object; 71 | node->next->prev = NULL; 72 | aList->firstNode = node->next; 73 | free(node); 74 | aList->count--; 75 | if (!aList->count) { 76 | aList->lastNode = NULL; 77 | } 78 | return object; 79 | } 80 | 81 | void * 82 | list_dequeue(List *aList) { 83 | Node *node; 84 | void *object; 85 | if (!aList->count) { 86 | return NULL; 87 | } 88 | node = aList->lastNode; 89 | object = node->object; 90 | node->prev->next = NULL; 91 | aList->lastNode = node->prev; 92 | free(node); 93 | aList->count--; 94 | if (!aList->count) { 95 | aList->firstNode = NULL; 96 | } 97 | return object; 98 | } 99 | 100 | void 101 | list_iterate(List *aList, void (*obj_iteration)(void *obj, int i, int count, bool *stop)) { 102 | Node *node; 103 | int i; 104 | bool stop = false; 105 | for (node = aList->firstNode, i = 0; node && !stop; node = node->next, i++) { 106 | obj_iteration(node->object, i, aList->count, &stop); 107 | } 108 | } 109 | 110 | void 111 | list_release(List *aList) { 112 | Node *node; 113 | for (node = aList->firstNode; node;) { 114 | Node *nodeToFree = node; 115 | release(nodeToFree->object); 116 | node = node->next; 117 | free(nodeToFree); 118 | } 119 | } 120 | 121 | void 122 | _list_release(void *aList) { 123 | list_release(aList); 124 | } 125 | -------------------------------------------------------------------------------- /compiler/structures/list.h: -------------------------------------------------------------------------------- 1 | #ifndef structures_list_h 2 | #define structures_list_h 3 | 4 | #define LIST_LOOP(_list) for (Node *node = _list->firstNode; node; node = node->next) 5 | #include 6 | 7 | typedef struct _node { 8 | void * object; 9 | struct _node *prev; 10 | struct _node *next; 11 | } Node; 12 | 13 | typedef struct { 14 | Node * firstNode; 15 | Node * lastNode; 16 | int count; 17 | } List; 18 | 19 | List * list_init(void); 20 | 21 | //Insert at the end 22 | List * list_append(List *aList, void *object); 23 | 24 | // Insert at the begining 25 | List * list_push(List *aList, void *object); 26 | 27 | // Remove the first 28 | void * list_pop(List *aList); 29 | 30 | // Remove the last 31 | void * list_dequeue(List *aList); 32 | 33 | void list_iterate(List *aList, void (*obj_iteration)(void *obj, int i, int count, bool *stop)); 34 | 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /compiler/structures/refcnt.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "refcnt.h" 3 | 4 | typedef struct { 5 | unsigned int retainCnt; 6 | refcnt_obj_free freeFunc; 7 | void *data; 8 | } _refcnt_objwrapper; 9 | 10 | 11 | void * 12 | alloc(size_t size, refcnt_obj_free freeFunc) { 13 | _refcnt_objwrapper *ow; 14 | void *ptr; 15 | 16 | ow = (_refcnt_objwrapper *)calloc(1, sizeof(_refcnt_objwrapper) + size); 17 | ptr = ow; 18 | ptr += sizeof(_refcnt_objwrapper); 19 | ow->retainCnt = 1; 20 | ow->data = ptr; 21 | ow->freeFunc = freeFunc; 22 | 23 | return ( void * )ptr; 24 | } 25 | 26 | void 27 | retain(void *ptr) { 28 | if (!ptr) return; 29 | _refcnt_objwrapper * ow; 30 | void *cptr; 31 | cptr = ptr; 32 | cptr -= sizeof(_refcnt_objwrapper); 33 | ow = (_refcnt_objwrapper *)cptr; 34 | ow->retainCnt++; 35 | } 36 | 37 | void 38 | release(void *ptr) { 39 | if (!ptr) return; 40 | _refcnt_objwrapper * ow; 41 | char *cptr; 42 | cptr = (char *)ptr; 43 | cptr -= sizeof(_refcnt_objwrapper); 44 | ow = (_refcnt_objwrapper *)cptr; 45 | ow->retainCnt--; 46 | if(ow->retainCnt == 0) { 47 | if (ow->freeFunc) 48 | ow->freeFunc(ptr); 49 | free(ow); 50 | } 51 | } 52 | 53 | int 54 | refcnt_getcnt(void *ptr) { 55 | _refcnt_objwrapper * ow; 56 | char *cptr; 57 | cptr = (char *)ptr; 58 | cptr -= sizeof(_refcnt_objwrapper); 59 | ow = (_refcnt_objwrapper *)cptr; 60 | return ow->retainCnt; 61 | } 62 | -------------------------------------------------------------------------------- /compiler/structures/refcnt.h: -------------------------------------------------------------------------------- 1 | #ifndef structures_refcnt_h 2 | #define structures_refcnt_h 3 | 4 | #include 5 | 6 | typedef void (*refcnt_obj_free)(void *a_data); 7 | 8 | void * alloc(size_t size, refcnt_obj_free freeFunc); 9 | void retain(void *ptr); 10 | void release(void *ptr); 11 | int refcnt_getcnt(void *ptr); 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /compiler/structures/serializable.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | 5 | #include "serializable.h" 6 | #include "refcnt.h" 7 | 8 | #define isblank(c) (strchr(" \n\r", c) != NULL) 9 | #define isprimitive(c) (strchr("-0123456789eEfalsetruenull.", c) != NULL) 10 | #define ishex(c) (strchr("0123456789abcdefABCDEF", c) != NULL) 11 | #define streq(a, b) (strcmp(a,b)==0) 12 | 13 | void serializable_free(void *sobj); 14 | 15 | Serializable * 16 | serializable_init() { 17 | Serializable *s = (Serializable *)alloc(sizeof(Serializable), serializable_free); 18 | s->obj = NULL; 19 | s->type = 0; 20 | return s; 21 | } 22 | 23 | Serializable * 24 | serializable_create(void *obj, SerializableType type) { 25 | Serializable *s = (Serializable *)alloc(sizeof(Serializable), serializable_free); 26 | retain(obj); 27 | switch (type) { 28 | case st_ht: 29 | case st_list: 30 | case st_str: 31 | s->obj = obj; 32 | break; 33 | default: 34 | break; 35 | } 36 | s->type = type; 37 | return s; 38 | } 39 | 40 | Serializable * 41 | serializable_create_ht(HashTable *ht) { 42 | return serializable_create(ht, st_ht); 43 | } 44 | 45 | Serializable * 46 | serializable_create_list(List *list) { 47 | return serializable_create(list, st_list); 48 | } 49 | 50 | Serializable * 51 | serializable_create_str(String *str) { 52 | return serializable_create(str, st_str); 53 | } 54 | 55 | Serializable * 56 | serializable_create_int(int i) { 57 | Serializable *s = serializable_init(); 58 | s->type = st_int; 59 | s->i = i; 60 | 61 | return s; 62 | } 63 | 64 | Serializable * 65 | serializable_create_float(float f) { 66 | Serializable *s = serializable_init(); 67 | s->type = st_float; 68 | s->f = f; 69 | 70 | return s; 71 | } 72 | 73 | Serializable * 74 | serializable_create_bool(int i) { 75 | Serializable *s = serializable_init(); 76 | s->type = st_bool; 77 | s->i = i; 78 | 79 | return s; 80 | } 81 | 82 | Serializable * 83 | serializable_create_null() { 84 | Serializable *s = serializable_init(); 85 | s->type = st_null; 86 | s->i = 0; 87 | 88 | return s; 89 | } 90 | 91 | void 92 | serializable_free(void *sobj) { 93 | Serializable *s = (Serializable *)sobj; 94 | switch (s->type) { 95 | case st_ht: 96 | case st_list: 97 | case st_str: 98 | release(s->obj); 99 | break; 100 | default: 101 | break; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /compiler/structures/serializable.h: -------------------------------------------------------------------------------- 1 | #ifndef __serializable_H_ 2 | #define __serializable_H_ 3 | 4 | #include "structures.h" 5 | 6 | typedef enum { 7 | st_ht, 8 | st_list, 9 | st_str, 10 | st_int, 11 | st_float, 12 | st_bool, 13 | st_null 14 | } SerializableType; 15 | 16 | typedef struct { 17 | union { 18 | HashTable *ht; 19 | List *list; 20 | String *str; 21 | int i; 22 | float f; 23 | void *obj; 24 | }; 25 | SerializableType type; 26 | } Serializable; 27 | 28 | Serializable *serializable_create_ht(HashTable *ht); 29 | Serializable *serializable_create_list(List *list); 30 | Serializable *serializable_create_str(String *str); 31 | Serializable *serializable_create_int(int i); 32 | Serializable *serializable_create_float(float f); 33 | Serializable *serializable_create_bool(int i); 34 | Serializable *serializable_create_null(void); 35 | 36 | #endif //__serializable_H_ 37 | -------------------------------------------------------------------------------- /compiler/structures/str.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | #include "str.h" 7 | #include "refcnt.h" 8 | #include "structures.h" 9 | 10 | 11 | #define BUFFER_SIZE (1 << 8) 12 | 13 | 14 | void str_free(void *obj); 15 | 16 | String * 17 | str_init() { 18 | String *str = alloc(sizeof(String), str_free); 19 | str->string = malloc(sizeof(char) * BUFFER_SIZE); 20 | str->string[0] = 0; 21 | str->bufferSize = BUFFER_SIZE; 22 | str->len = 0; 23 | return str; 24 | } 25 | 26 | 27 | String * 28 | str_create(const char *original) { 29 | String *str = alloc(sizeof(String), str_free); 30 | str->len = strlen(original); 31 | str->bufferSize = ((str->len + 1)/BUFFER_SIZE + 1) * BUFFER_SIZE; 32 | str->string = malloc(sizeof(char) * str->bufferSize); 33 | memcpy(str->string, original, sizeof(char) * (str->len + 1)); 34 | 35 | return str; 36 | } 37 | 38 | /// " asd " 39 | void 40 | str_center(String *str, int size) { 41 | int unicodeLen = str_unicode_len(str); 42 | int space_amnt = size - unicodeLen; 43 | if (space_amnt <= 0) return; 44 | 45 | size_t newSize = space_amnt + str->len; 46 | char *newString = malloc(sizeof(char) * (newSize + 1)); 47 | int firstGap = space_amnt/2; 48 | 49 | 50 | for (int i = 0; i < space_amnt + str->len; i++) { 51 | if (i < firstGap) { 52 | newString[i] = ' '; 53 | continue; 54 | } 55 | int originalPos = i - firstGap; 56 | if (originalPos < str->len) 57 | newString[i] = str->string[originalPos]; 58 | else 59 | newString[i] = ' '; 60 | } 61 | free(str->string); 62 | str->string = newString; 63 | str->len = newSize; 64 | str->bufferSize = newSize + 1; 65 | 66 | newString[newSize] = 0; 67 | } 68 | 69 | int 70 | str_unicode_len(String *str) { 71 | int unicodeLen = 0; 72 | int toFinishSequence = 0; 73 | for (int i = 0; i < str->len; i++) { 74 | unsigned char c = str->ustring[i]; 75 | if (toFinishSequence) { 76 | toFinishSequence--; 77 | continue; 78 | } 79 | unicodeLen++; 80 | if (c > 128) { 81 | bin8 b; 82 | b.uc = c; 83 | toFinishSequence = (b.b7?1:0) + (b.b6?1:0) + (b.b5?1:0); 84 | toFinishSequence -= 1; 85 | } 86 | } 87 | return unicodeLen; 88 | } 89 | 90 | String * 91 | str_escape_cstring(char * string) { 92 | String *esc = str_init(); 93 | unsigned char utfSeq[4]; 94 | utfSeq[3] = 0; 95 | int currentSeqSize = 0; 96 | int seqSize = 0; 97 | for (int i = 0; string[i]; i++) { 98 | unsigned char c = string[i]; 99 | if (c <= '~') { 100 | switch (c) { 101 | case '\"': str_append(esc, "\\\""); break; 102 | case '\n': str_append(esc, "\\n"); break; 103 | case '\r': str_append(esc, "\\r"); break; 104 | case '\t': str_append(esc, "\\t"); break; 105 | case '\b': str_append(esc, "\\b"); break; 106 | case '\f': str_append(esc, "\\f"); break; 107 | case '\\': str_append(esc, "\\\\"); break; 108 | case '/': str_append(esc, "\\/"); break; 109 | default: str_append_char(esc, c); break; 110 | } 111 | } else { 112 | if (currentSeqSize == 0) { 113 | bin8 b; 114 | b.c = c; 115 | seqSize = (b.b7?1:0) + (b.b6?1:0) + (b.b5?1:0); 116 | utfSeq[0] = 117 | utfSeq[1] = 118 | utfSeq[2] = 119 | utfSeq[3] = 0; 120 | } 121 | utfSeq[currentSeqSize] = c; 122 | currentSeqSize += 1; 123 | if (currentSeqSize >= seqSize) { 124 | bin32 b; 125 | b.u = 0; 126 | if (seqSize == 1) { 127 | bin8 c1; 128 | c1.uc = utfSeq[0]; 129 | b.b0 = c1.b0; 130 | b.b1 = c1.b1; 131 | b.b2 = c1.b2; 132 | b.b3 = c1.b3; 133 | b.b4 = c1.b4; 134 | b.b5 = c1.b5; 135 | b.b6 = c1.b6; 136 | } 137 | else 138 | if(seqSize == 2) { 139 | bin8 c1, c2; 140 | c1.uc = utfSeq[0]; 141 | c2.uc = utfSeq[1]; 142 | 143 | b.b0 = c2.b0; 144 | b.b1 = c2.b1; 145 | b.b2 = c2.b2; 146 | b.b3 = c2.b3; 147 | b.b4 = c2.b4; 148 | b.b5 = c2.b5; 149 | b.b6 = c1.b0; 150 | b.b7 = c1.b1; 151 | 152 | b.b8 = c1.b2; 153 | b.b9 = c1.b3; 154 | b.b10 = c1.b4; 155 | } 156 | else if(seqSize == 3) { 157 | bin8 c0, c1, c2; 158 | c2.uc = utfSeq[0]; 159 | c1.uc = utfSeq[1]; 160 | c0.uc = utfSeq[2]; 161 | 162 | b.b0 = c0.b0; 163 | b.b1 = c0.b1; 164 | b.b2 = c0.b2; 165 | b.b3 = c0.b3; 166 | b.b4 = c0.b4; 167 | b.b5 = c0.b5; 168 | b.b6 = c1.b0; 169 | b.b7 = c1.b1; 170 | 171 | b.b8 = c1.b2; 172 | b.b9 = c1.b3; 173 | b.b10 = c1.b4; 174 | b.b11 = c1.b5; 175 | b.b12 = c2.b0; 176 | b.b13 = c2.b1; 177 | b.b14 = c2.b2; 178 | b.b15 = c2.b3; 179 | } 180 | char unicode[20]; 181 | sprintf(unicode, "\\u%04x", b.u); 182 | str_append(esc, unicode); 183 | currentSeqSize = 0; 184 | } 185 | } 186 | } 187 | return esc; 188 | } 189 | 190 | String * 191 | str_escape(String *str) { 192 | return str_escape_cstring(str->string); 193 | } 194 | 195 | 196 | void 197 | str_append(String *str, const char * toAppend) { 198 | size_t appLen = strlen(toAppend); 199 | if (str->len + appLen + 1 > str->bufferSize) { 200 | str->bufferSize = ((str->len + appLen + 1)/ BUFFER_SIZE + 1) * BUFFER_SIZE; 201 | str->string = realloc(str->string, sizeof(char) * str->bufferSize); 202 | } 203 | memcpy(str->string+str->len, toAppend, (appLen + 1) * sizeof(char)); 204 | str->len += appLen; 205 | } 206 | 207 | void 208 | str_append_char(String *str, const unsigned char c) { 209 | size_t appLen = 1; 210 | if (str->len + appLen + 1 > str->bufferSize) { 211 | str->bufferSize = ((str->len + appLen + 1)/ BUFFER_SIZE + 1) * BUFFER_SIZE; 212 | str->string = realloc(str->string, sizeof(char) * str->bufferSize); 213 | } 214 | *(str->string+str->len) = c; 215 | *(str->string+str->len+1) = 0; 216 | str->len += appLen; 217 | } 218 | 219 | void 220 | str_free(void *obj) { 221 | String *str = (String *) obj; 222 | free(str->string); 223 | } 224 | -------------------------------------------------------------------------------- /compiler/structures/str.h: -------------------------------------------------------------------------------- 1 | #ifndef __str_H_ 2 | #define __str_H_ 3 | 4 | typedef struct { 5 | union { 6 | char *string; 7 | unsigned char *ustring; 8 | }; 9 | size_t len; 10 | size_t bufferSize; 11 | } String; 12 | 13 | String *str_init(void); 14 | String *str_create(const char * original); 15 | String *str_escape_cstring(char * string); 16 | String *str_escape(String *str); 17 | void str_append(String *str, const char * toAppend); 18 | void str_append_char(String *str, const unsigned char c); 19 | void str_center(String *str, int size); 20 | int str_unicode_len(String *str); 21 | 22 | #endif //__str_H_ 23 | -------------------------------------------------------------------------------- /compiler/structures/structures.h: -------------------------------------------------------------------------------- 1 | #ifndef structures_combined_h 2 | #define structures_combined_h 3 | 4 | #include "refcnt.h" 5 | 6 | #include "list.h" 7 | #include "htable.h" 8 | #include "str.h" 9 | #include "serializable.h" 10 | 11 | #include 12 | 13 | typedef union { 14 | unsigned char uc; 15 | char c; 16 | struct { 17 | int b0:1; 18 | int b1:1; 19 | int b2:1; 20 | int b3:1; 21 | int b4:1; 22 | int b5:1; 23 | int b6:1; 24 | int b7:1; 25 | }; 26 | } bin8; 27 | 28 | typedef union { 29 | unsigned int u; 30 | int i; 31 | struct { 32 | int b0:1; 33 | int b1:1; 34 | int b2:1; 35 | int b3:1; 36 | int b4:1; 37 | int b5:1; 38 | int b6:1; 39 | int b7:1; 40 | int b8:1; 41 | int b9:1; 42 | int b10:1; 43 | int b11:1; 44 | int b12:1; 45 | int b13:1; 46 | int b14:1; 47 | int b15:1; 48 | int b16:1; 49 | int b17:1; 50 | int b18:1; 51 | int b19:1; 52 | int b20:1; 53 | int b21:1; 54 | int b22:1; 55 | int b23:1; 56 | int b24:1; 57 | int b25:1; 58 | int b26:1; 59 | int b27:1; 60 | int b28:1; 61 | int b29:1; 62 | int b30:1; 63 | int b31:1; 64 | }; 65 | } bin32; 66 | 67 | #endif 68 | -------------------------------------------------------------------------------- /compiler/utils.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "utils.h" 6 | 7 | #define CHARS "0123456789ABCDEF" 8 | 9 | void 10 | uuid_gen(char out[]) { 11 | static char seeded = 0; 12 | if (!seeded) { 13 | srand((unsigned int)time(NULL)); 14 | seeded = 1; 15 | } 16 | 17 | for (int i = 0; i < 37; i++) { 18 | char c; 19 | if (i == 8 || i == 13 || i == 18 || i == 23) { 20 | c = '-'; 21 | } 22 | else if (i == 14) { 23 | c = '4'; 24 | } else if (i == 36) { 25 | c = 0; 26 | } else { 27 | c = CHARS[rand() % 16]; 28 | } 29 | 30 | out[i] = c; 31 | } 32 | } 33 | 34 | char * 35 | xml_escape(char *str) { 36 | /* https://stackoverflow.com/a/1091953/1186116 */ 37 | /* " " */ 38 | /* ' ' */ 39 | /* < < */ 40 | /* > > */ 41 | /* & & */ 42 | char *escaped = (char *)calloc(strlen(str) * 6 + 1, sizeof(char)); // 6 is the longest escape sequence's length 43 | int j = 0; 44 | for (int i = 0; str[i]; i++) { 45 | char to_replace[10]; 46 | to_replace[0] = 0; 47 | if (str[i] == '\"') { 48 | strcpy(to_replace, """); 49 | } else if (str[i] == '\'') { 50 | strcpy(to_replace, "'"); 51 | } else if (str[i] == '<') { 52 | strcpy(to_replace, "<"); 53 | } else if (str[i] == '>') { 54 | strcpy(to_replace, ">"); 55 | } else if (str[i] == '&') { 56 | strcpy(to_replace, "&"); 57 | } 58 | 59 | if (to_replace[0]) { 60 | strcpy(escaped + j, to_replace); 61 | j += strlen(to_replace); 62 | } else { 63 | escaped[j] = str[i]; 64 | j++; 65 | } 66 | } 67 | escaped[j] = 0; 68 | 69 | return escaped; 70 | } 71 | -------------------------------------------------------------------------------- /compiler/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_H 2 | #define UTILS_H 3 | 4 | #ifdef DEBUG 5 | # define DEBUGPRINT(...) fprintf(stderr, __VA_ARGS__) 6 | #else 7 | # define DEBUGPRINT(...) 8 | #endif 9 | 10 | void uuid_gen(char out[]); 11 | char *xml_escape(char *str); 12 | 13 | #endif /* UTILS_H */ 14 | -------------------------------------------------------------------------------- /examples/Leap Year.splash: -------------------------------------------------------------------------------- 1 | year := AskNumber("Enter the year") 2 | 3 | if year % 4 > 0 { 4 | leap := 0 5 | } else if year % 100 > 0 { 6 | leap := 1 7 | } else if year % 400 > 0 { 8 | leap := 0 9 | } else { 10 | leap := 1 11 | } 12 | 13 | if leap == 0 { 14 | type := "common" 15 | } else { 16 | type := "leap" 17 | } 18 | 19 | ShowResult("{year} is a {type} year") 20 | -------------------------------------------------------------------------------- /examples/Quadratic Solver.splash: -------------------------------------------------------------------------------- 1 | a := AskNumber("a =") 2 | b := AskNumber("b =") 3 | c := AskNumber("c =") 4 | 5 | delta := b^2 - 4 * a * c 6 | 7 | if a == 0 { 8 | x := -c/b 9 | 10 | answer := "x = {x}" 11 | } else if delta == 0 { 12 | x := -b / (2 * a) 13 | 14 | answer := "x1 = x2 = {x}" 15 | } else if delta > 0 { 16 | x1 := (-b + delta^(1/2))/(2 * a) 17 | x2 := (-b + -delta^(1/2))/(2 * a) 18 | 19 | answer := "x1 = {x1}\nx2 = {x2}" 20 | } else { 21 | xr := -b / (2 * a) 22 | xi := (-delta)^(1/2) / (2 * a) 23 | nxi := -xi 24 | 25 | answer := "x1 = {xr} + {xi}i\nx2 = {xr} + {nxi}i" 26 | } 27 | 28 | ShowResult(answer) 29 | -------------------------------------------------------------------------------- /examples/age.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/examples/age.mov -------------------------------------------------------------------------------- /examples/age.splash: -------------------------------------------------------------------------------- 1 | age := AskNumber("How old are you?") 2 | 3 | if age < 12 { 4 | ShowResult("Child") 5 | } else if age < 18 { 6 | ShowResult("Teen") 7 | } else if age < 60 { 8 | ShowResult("Adult") 9 | } else { 10 | ShowResult("Elder") 11 | } 12 | -------------------------------------------------------------------------------- /examples/leap_year.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/examples/leap_year.mov -------------------------------------------------------------------------------- /examples/quadratic.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/examples/quadratic.mov -------------------------------------------------------------------------------- /imgs/RoundedIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/imgs/RoundedIcon.png -------------------------------------------------------------------------------- /imgs/quadratic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/imgs/quadratic.gif -------------------------------------------------------------------------------- /privacy.md: -------------------------------------------------------------------------------- 1 | # SPLASH Privacy Policy 2 | 3 | The splash app doesn't collect any user data whatsoever. In fact, the app doesn't make any Internet connection. 4 | -------------------------------------------------------------------------------- /splash.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /splash.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /splash.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Original 7 | 8 | 9 | -------------------------------------------------------------------------------- /splashTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /splashUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /splash_app/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 02/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | var mainViewController = ViewController() 17 | 18 | func application(_ application: UIApplication, 19 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 20 | 21 | if UserDefaults.standard.alreadyInstalledExamples1 == false { 22 | FileManager.createExamplesDirectory() 23 | UserDefaults.standard.alreadyInstalledExamples1 = true 24 | } 25 | 26 | window = UIWindow(frame: UIScreen.main.bounds) 27 | window!.rootViewController = mainViewController 28 | window!.makeKeyAndVisible() 29 | 30 | window!.tintColor = ThemeManager.shared.theme.tintColor 31 | 32 | return true 33 | } 34 | 35 | func application(_ app: UIApplication, 36 | open url: URL, 37 | options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { 38 | guard url.isFileURL else { return false } 39 | self.mainViewController.revealDocument( 40 | at: url, 41 | importIfNeeded: true, 42 | completion: {[unowned self] (url, error) in 43 | if let url = url { 44 | self.mainViewController.presentEditor(withURL: url) 45 | } else if let error = error { 46 | let alertController = UIAlertController(title: "Error", 47 | message: error.localizedDescription, 48 | preferredStyle: .alert) 49 | self.mainViewController.present(alertController, animated: true, completion: nil) 50 | return 51 | } 52 | }) 53 | 54 | return true 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/20@2x-1.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/20@2x.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/20@3x.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/29@2x-1.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/29@2x.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/29@3x.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/40@2x-1.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/40@2x.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/40@3x.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/60@2x.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/60@3x.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/76@2x.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "29@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "29@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "40@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "40@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "60@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "60@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "20.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "20@2x-1.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "29.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "29@2x-1.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "40.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "40@2x-1.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "76.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "76@2x.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "167.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "splash_icon.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/AppIcon.appiconset/splash_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/AppIcon.appiconset/splash_icon.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/gear.imageset/BackgroundTask_settings_16x21_@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonzula/splash/8a6272d0314c3da1c949bfff56993ffa5379dade/splash_app/Assets.xcassets/gear.imageset/BackgroundTask_settings_16x21_@3x.png -------------------------------------------------------------------------------- /splash_app/Assets.xcassets/gear.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "BackgroundTask_settings_16x21_@3x.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /splash_app/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /splash_app/Helpers/ActionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionView.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 16/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ActionView: UIView { 12 | 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | 16 | isOpaque = false 17 | layer.shadowColor = UIColor.black.cgColor 18 | layer.shadowRadius = 40 19 | layer.shadowOpacity = 0.2 20 | } 21 | 22 | required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")} 23 | 24 | override var intrinsicContentSize: CGSize {return CGSize(width: 300, height: 335/2)} 25 | 26 | override func draw(_ rect: CGRect) { 27 | drawTopRect() 28 | drawBottomRect() 29 | drawLine() 30 | 31 | drawTitle() 32 | drawText() 33 | 34 | drawGear() 35 | } 36 | 37 | private func drawTopRect() { 38 | let topRect = UIBezierPath(roundedRect: CGRect(origin: .zero, 39 | size: CGSize(width: bounds.width, height: 45)), 40 | byRoundingCorners: [.topLeft, .topRight], 41 | cornerRadii: CGSize(width: 13, height: 13)) 42 | UIColor(red: 242/255, green: 244/255, blue: 246/255, alpha: 1.0).setFill() 43 | topRect.fill() 44 | } 45 | 46 | private func drawBottomRect() { 47 | let bottomRect = UIBezierPath(roundedRect: CGRect(x: 0, 48 | y: 45, 49 | width: bounds.width, 50 | height: bounds.height - 45), 51 | byRoundingCorners: [.bottomLeft, .bottomRight], 52 | cornerRadii: CGSize(width: 13, height: 13)) 53 | UIColor.white.setFill() 54 | bottomRect.fill() 55 | } 56 | 57 | private func drawLine() { 58 | let line = UIBezierPath() 59 | line.lineWidth = 0.5 60 | line.move(to: CGPoint(x: 0, y: 45)) 61 | line.addLine(to: CGPoint(x: bounds.width, y: 45)) 62 | UIColor(white: 210/255, alpha: 1.0).setStroke() 63 | line.stroke() 64 | } 65 | 66 | private func drawTitle() { 67 | let text = NSAttributedString(string: "Show Result", attributes: [ 68 | .font: UIFont.systemFont(ofSize: 16, weight: .regular), 69 | .foregroundColor: UIColor.black 70 | ]) 71 | text.draw(at: CGPoint(x: 44, y: 14)) 72 | } 73 | 74 | private func drawText() { 75 | let text = NSAttributedString(string: "Hello World", attributes: [ 76 | .font: UIFont.systemFont(ofSize: 16, weight: .light), 77 | .foregroundColor: UIColor.black 78 | ]) 79 | text.draw(at: CGPoint(x: 16, y: 57)) 80 | } 81 | 82 | private func drawGear() { 83 | var rect = CGRect(x: 8, 84 | y: 8, 85 | width: 29, 86 | height: 29) 87 | let path = UIBezierPath(roundedRect: rect, cornerRadius: 7) 88 | UIColor(white: 141/255, alpha: 1.0).setFill() 89 | path.fill() 90 | 91 | rect = rect.insetBy(dx: 3, dy: 3) 92 | let gear = #imageLiteral(resourceName: "gear") 93 | gear.draw(in: rect) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /splash_app/Helpers/BrowserDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowserDelegate.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 17/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BrowserDelegate: NSObject, UIDocumentBrowserViewControllerDelegate { 12 | func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentURLs documentURLs: [URL]) { 13 | guard let url = documentURLs.first else {return} 14 | guard let viewController = controller as? ViewController else {return} 15 | 16 | viewController.presentEditor(withURL: url) 17 | } 18 | 19 | func documentBrowser(_ controller: UIDocumentBrowserViewController, 20 | didRequestDocumentCreationWithHandler importHandler: 21 | @escaping (URL?, UIDocumentBrowserViewController.ImportMode) -> Void) { 22 | guard let viewController = controller as? ViewController else {return} 23 | 24 | let alertController = UIAlertController(title: "New Script", 25 | message: nil, 26 | preferredStyle: .alert) 27 | alertController.addTextField { textField in 28 | textField.autocapitalizationType = .none 29 | textField.returnKeyType = .done 30 | textField.autocorrectionType = .no 31 | textField.spellCheckingType = .no 32 | textField.smartDashesType = .no 33 | textField.smartQuotesType = .no 34 | textField.placeholder = "File name" 35 | } 36 | alertController.addAction( 37 | UIAlertAction(title: "Create", 38 | style: .default, 39 | handler: { _ in 40 | let url = self.createNewFile( 41 | named: alertController.textFields?.first?.text ?? "") 42 | if let url = url { 43 | importHandler(url, .move) 44 | } else { 45 | importHandler(nil, .none) 46 | } 47 | })) 48 | alertController.addAction(UIAlertAction(title: "Cancel", 49 | style: .cancel, 50 | handler: { _ in 51 | importHandler(nil, .none) 52 | })) 53 | viewController.present(alertController, animated: true, completion: nil) 54 | } 55 | 56 | func documentBrowser(_ controller: UIDocumentBrowserViewController, 57 | didImportDocumentAt sourceURL: URL, 58 | toDestinationURL destinationURL: URL) { 59 | guard let viewController = controller as? ViewController else {return} 60 | viewController.presentEditor(withURL: destinationURL) 61 | } 62 | 63 | func documentBrowser(_ controller: UIDocumentBrowserViewController, 64 | failedToImportDocumentAt documentURL: URL, 65 | error: Error?) { 66 | guard let viewController = controller as? ViewController else {return} 67 | let alertController: UIAlertController 68 | if let error = error { 69 | alertController = UIAlertController(error: error) 70 | } else { 71 | alertController = UIAlertController(title: "Error", 72 | message: "Unknown error occurred.", 73 | preferredStyle: .alert) 74 | } 75 | viewController.present(alertController, 76 | animated: true, 77 | completion: nil) 78 | } 79 | 80 | fileprivate func createNewFile(named name: String) -> URL? { 81 | let tempDirectoryPath = NSTemporaryDirectory() 82 | var name = name.trimmingCharacters(in: .whitespacesAndNewlines) 83 | guard !name.isEmpty else {return nil} 84 | if !name.lowercased().hasSuffix(".splash") { 85 | name += ".splash" 86 | } 87 | let filePath = (tempDirectoryPath as NSString).appendingPathComponent("\(name)") 88 | let url = URL(fileURLWithPath: filePath) 89 | do { 90 | try Data().write(to: url) 91 | return url 92 | } catch { 93 | return nil 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /splash_app/Helpers/Bundle+Resources.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+Resources.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 10/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Bundle { 12 | 13 | var shortVersion: String { 14 | return infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown" 15 | } 16 | 17 | var longVersion: String { 18 | return infoDictionary?["CFBundleVersion"] as? String ?? "unknown" 19 | } 20 | 21 | var fullVersion: String {return "\(shortVersion) (\(longVersion))"} 22 | 23 | func dataFromResource(fileName: String) -> Data { 24 | // swiftlint:disable:next force_try 25 | return try! Data(contentsOf: url(forResource: fileName, withExtension: nil)!) 26 | } 27 | 28 | func helpHtmlContent() -> String { 29 | let htmlData = dataFromResource(fileName: "Help.html") 30 | var htmlString = String(data: htmlData, encoding: .utf8)! 31 | htmlString = htmlString.replacingOccurrences(of: "<+APP_VERSION+>", with: fullVersion) 32 | return htmlString 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /splash_app/Helpers/FileManager+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManager+Utils.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 10/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension FileManager { 12 | static var documentsDirectory: URL { 13 | let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) 14 | let documentsDirectory = paths[0] 15 | return documentsDirectory 16 | } 17 | 18 | static func createExamplesDirectory() { 19 | let documentsPath = FileManager.documentsDirectory.path 20 | let examplesPath = (documentsPath as NSString).appendingPathComponent("Examples") 21 | let examplesURL = URL(fileURLWithPath: examplesPath) 22 | try? FileManager.default.createDirectory(atPath: examplesPath, 23 | withIntermediateDirectories: true, 24 | attributes: nil) 25 | 26 | var resourceValues = URLResourceValues() 27 | resourceValues.isExcludedFromBackup = true 28 | try? (examplesURL as NSURL).setResourceValue(true, forKey: .isExcludedFromBackupKey) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /splash_app/Helpers/NSLayoutConstraint+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSLayoutConstraint+Utils.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 15/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension NSLayoutConstraint { 12 | @discardableResult 13 | func setPriority(_ value: Float) -> NSLayoutConstraint { 14 | self.priority = UILayoutPriority(rawValue: value) 15 | return self 16 | } 17 | 18 | @discardableResult 19 | func activate() -> NSLayoutConstraint { 20 | self.isActive = true 21 | return self 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /splash_app/Helpers/NSRange+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSRange+Utils.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 09/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSRange { 12 | var isAtBegining: Bool {return location == 0} 13 | } 14 | -------------------------------------------------------------------------------- /splash_app/Helpers/Notification+Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification+Constants.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 12/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Notification.Name { 12 | static let themeChanged = Notification.Name("theme changed") 13 | } 14 | -------------------------------------------------------------------------------- /splash_app/Helpers/PageViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageViewController.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 14/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol PageIndicator: class { 12 | func set(currentPage: Int, animated: Bool) 13 | var numberOfPages: Int {get set} 14 | } 15 | 16 | open class PageViewController: UIViewController { 17 | 18 | var isAnimatingViewChange: Bool = false 19 | open var updatesPageIndicatorAutomatically = true 20 | open weak var pageIndicatorDelegate: PageIndicator? 21 | 22 | private var currentViewController: UIViewController? 23 | private var currentIndex: Int? 24 | 25 | let contentView = UIView() 26 | 27 | open var totalPageCount: Int = 0 { 28 | didSet { 29 | guard let currentIndex = currentIndex, updatesPageIndicatorAutomatically else {return} 30 | setPageControl(page: currentIndex, animated: view.window != nil) 31 | } 32 | } 33 | 34 | override open var shouldAutomaticallyForwardAppearanceMethods: Bool { 35 | return false 36 | } 37 | 38 | open override func loadView() { 39 | view = UIView() 40 | view.backgroundColor = .white 41 | 42 | contentView.setupForAutoLayout(in: view) 43 | 44 | contentView.backgroundColor = .clear 45 | 46 | contentView.pinToSuperview() 47 | } 48 | 49 | fileprivate func setPageControl(page: Int, animated: Bool) { 50 | pageIndicatorDelegate?.set(currentPage: page, animated: animated) 51 | pageIndicatorDelegate?.numberOfPages = totalPageCount 52 | } 53 | 54 | open override func viewDidLayoutSubviews() { 55 | super.viewDidLayoutSubviews() 56 | 57 | guard let currentViewController = currentViewController, let currentIndex = currentIndex else {return} 58 | 59 | currentViewController.view.frame = CGRect(x: offset(for: currentIndex), 60 | y: 0, 61 | width: view.bounds.width, 62 | height: view.bounds.height) 63 | } 64 | 65 | private func add(_ viewController: UIViewController, at index: Int) { 66 | addChild(viewController) 67 | viewController.view.frame = CGRect(x: offset(for: index), 68 | y: 0, 69 | width: view.bounds.width, 70 | height: view.bounds.height) 71 | contentView.addSubview(viewController.view) 72 | viewController.didMove(toParent: self) 73 | } 74 | 75 | fileprivate func remove(_ viewController: UIViewController?) { 76 | guard let viewController = viewController else {return} 77 | guard !viewController.isModal else {return} 78 | 79 | viewController.willMove(toParent: nil) 80 | viewController.view.removeFromSuperview() 81 | viewController.removeFromParent() 82 | } 83 | 84 | private func set(_ viewController: UIViewController, 85 | at index: Int, 86 | animated: Bool, 87 | completion: (() -> Void)? = nil) { 88 | add(viewController, at: index) 89 | var viewControllerToDismiss: UIViewController? 90 | if !(currentViewController?.isModal ?? true) { 91 | viewControllerToDismiss = currentViewController 92 | } 93 | 94 | viewController.beginAppearanceTransition(true, animated: animated) 95 | viewControllerToDismiss?.beginAppearanceTransition(false, animated: animated) 96 | 97 | if animated { 98 | isAnimatingViewChange = true 99 | UIView.animate(withDuration: 0.3, animations: { 100 | self.contentView.bounds.origin.x = self.offset(for: index) 101 | }, completion: {(_) in 102 | self.isAnimatingViewChange = false 103 | viewController.endAppearanceTransition() 104 | viewControllerToDismiss?.endAppearanceTransition() 105 | completion?() 106 | }) 107 | } else { 108 | contentView.bounds.origin.x = offset(for: index) 109 | viewController.endAppearanceTransition() 110 | viewControllerToDismiss?.endAppearanceTransition() 111 | completion?() 112 | } 113 | } 114 | 115 | open func present(_ viewController: UIViewController, at index: Int, animated: Bool) { 116 | if updatesPageIndicatorAutomatically { 117 | setPageControl(page: index, animated: animated) 118 | } 119 | view.endEditing(true) 120 | let viewControllerToRemove = currentViewController 121 | if viewControllerToRemove?.isModal ?? false { 122 | viewControllerToRemove!.dismiss(animated: animated, completion: nil) 123 | self.set(viewController, at: index, animated: false) 124 | self.currentViewController = viewController 125 | self.currentIndex = index 126 | } else { 127 | self.set(viewController, at: index, animated: animated) { 128 | self.remove(viewControllerToRemove) 129 | self.currentViewController = viewController 130 | self.currentIndex = index 131 | } 132 | } 133 | } 134 | 135 | open func presentModally(_ viewController: UIViewController) { 136 | view.endEditing(true) 137 | 138 | let viewControllerToRemove = currentViewController 139 | if viewControllerToRemove?.isModal ?? false { 140 | viewControllerToRemove!.dismiss(animated: true) { 141 | self.present(viewController, animated: true) { 142 | self.currentViewController = viewController 143 | self.currentIndex = nil 144 | } 145 | } 146 | } else { 147 | present(viewController, animated: true) { 148 | self.remove(viewControllerToRemove) 149 | self.currentViewController = viewController 150 | self.currentIndex = nil 151 | } 152 | } 153 | } 154 | 155 | private func offset(`for` index: Int) -> CGFloat { 156 | return CGFloat(index) * view.frame.width 157 | } 158 | 159 | // MARK: - User Interaction 160 | } 161 | 162 | fileprivate extension UIViewController { 163 | var isModal: Bool { 164 | return self.presentingViewController?.presentedViewController == self 165 | } 166 | } 167 | 168 | extension UIPageControl: PageIndicator { 169 | public func set(currentPage: Int, animated: Bool) { 170 | self.currentPage = currentPage 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /splash_app/Helpers/String+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Utils.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 16/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension StringProtocol { 12 | func capitalizingFirstLetter() -> String { 13 | return prefix(1).uppercased() + dropFirst() 14 | } 15 | } 16 | 17 | public extension String { 18 | mutating func capitalizeFirstLetter() { 19 | self = self.capitalizingFirstLetter() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /splash_app/Helpers/ThemeControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemeControl.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 17/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ThemeControl: UISegmentedControl { 12 | 13 | var observers = [Any]() 14 | 15 | init() { 16 | let themes = ThemeManager.Theme.allCases 17 | super.init(items: themes.map {$0.humanName}) 18 | setValue() 19 | 20 | addTarget(self, 21 | action: #selector(changeTheme), 22 | for: .valueChanged) 23 | 24 | observers.append( // swiftlint:disable:next discarded_notification_center_observer 25 | NotificationCenter.default.addObserver(forName: .themeChanged, 26 | object: nil, 27 | queue: nil, 28 | using: { [weak self] _ in 29 | self?.setValue() 30 | })) 31 | } 32 | 33 | override init(frame: CGRect) { 34 | super.init(frame: frame) 35 | } 36 | 37 | deinit { 38 | observers.forEach(NotificationCenter.default.removeObserver) 39 | } 40 | 41 | required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")} 42 | 43 | func setValue() { 44 | let themes = ThemeManager.Theme.allCases 45 | let selectedTheme = UserDefaults.standard.lastUsedTheme 46 | let index = themes.firstIndex(of: selectedTheme) 47 | guard self.selectedSegmentIndex != index else {return} 48 | selectedSegmentIndex = index ?? -1 49 | } 50 | 51 | // MARK: - User Interaction 52 | 53 | @objc 54 | func changeTheme() { 55 | let themes = ThemeManager.Theme.allCases 56 | let themesRange = themes.startIndex..> 0x10 ) / 0xff, 15 | green: CGFloat((hex & 0x00ff00 ) >> 0x08 ) / 0xff, 16 | blue: CGFloat((hex & 0x0000ff ) >> 0x00 ) / 0xff, 17 | alpha: alpha) 18 | } 19 | // swiftlint:enable colon 20 | var components: [CGFloat] { 21 | var fRed: CGFloat = 0 22 | var fGreen: CGFloat = 0 23 | var fBlue: CGFloat = 0 24 | var fAlpha: CGFloat = 0 25 | self.getRed(&fRed, 26 | green: &fGreen, 27 | blue: &fBlue, 28 | alpha: &fAlpha) 29 | return [fRed, fGreen, fBlue, fAlpha] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /splash_app/Helpers/UIScrollView+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+Utils.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 04/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIScrollView { 12 | func setupForVerticalScrollOnly() { 13 | let view = UIView() 14 | view.setupForAutoLayout(in: self) 15 | view.backgroundColor = .clear 16 | view.topAnchor.constraint(equalTo: topAnchor).activate() 17 | view.leftAnchor.constraint(equalTo: leftAnchor).activate() 18 | view.rightAnchor.constraint(equalTo: rightAnchor).activate() 19 | view.heightAnchor.constraint(equalToConstant: 1).activate() 20 | view.widthAnchor.constraint(equalTo: widthAnchor).activate() 21 | view.setRequiredSize() 22 | } 23 | 24 | func setupForHorizontalScrollOnly() { 25 | let view = UIView() 26 | view.setupForAutoLayout(in: self) 27 | view.backgroundColor = .clear 28 | view.topAnchor.constraint(equalTo: topAnchor).activate() 29 | view.leftAnchor.constraint(equalTo: leftAnchor).activate() 30 | view.bottomAnchor.constraint(equalTo: bottomAnchor).activate() 31 | view.widthAnchor.constraint(equalToConstant: 1).activate() 32 | view.heightAnchor.constraint(equalTo: heightAnchor).activate() 33 | view.setRequiredSize() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /splash_app/Helpers/UIView+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Utils.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 04/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | func pin(to other: UIView, withInset inset: UIEdgeInsets = .zero) { 13 | topAnchor.constraint(equalTo: other.topAnchor, constant: inset.top).activate() 14 | leftAnchor.constraint(equalTo: other.leftAnchor, constant: inset.left).activate() 15 | rightAnchor.constraint(equalTo: other.rightAnchor, constant: -inset.right).activate() 16 | bottomAnchor.constraint(equalTo: other.bottomAnchor, constant: -inset.bottom).activate() 17 | } 18 | 19 | func pinToSuperview(withInset inset: UIEdgeInsets = .zero) { 20 | pin(to: superview!, withInset: inset) 21 | } 22 | 23 | func placeAtCenter() { 24 | centerXAnchor.constraint(equalTo: superview!.centerXAnchor).activate() 25 | centerYAnchor.constraint(equalTo: superview!.centerYAnchor).activate() 26 | } 27 | 28 | func setupForAutoLayout(in superview: UIView) { 29 | self.translatesAutoresizingMaskIntoConstraints = false 30 | superview.addSubview(self) 31 | } 32 | 33 | func setRequiredSize() { 34 | setContentHuggingPriority(.required, for: .horizontal) 35 | setContentCompressionResistancePriority(.required, for: .horizontal) 36 | setContentHuggingPriority(.required, for: .vertical) 37 | setContentCompressionResistancePriority(.required, for: .vertical) 38 | } 39 | 40 | func toImage() -> UIImage? { 41 | UIGraphicsBeginImageContextWithOptions(bounds.size, true, 1.0) 42 | guard let context = UIGraphicsGetCurrentContext() else {return nil} 43 | defer { UIGraphicsEndImageContext() } 44 | layer.render(in: context) 45 | guard let image = UIGraphicsGetImageFromCurrentImageContext() else {return nil} 46 | return image 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /splash_app/Helpers/UserDefaults+Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+Constants.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 12/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UserDefaults.Domain { 12 | static let splashApp = UserDefaults.Domain(name: "splash app") 13 | } 14 | 15 | extension UserDefaults.Key { 16 | static let lastUsedTheme = UserDefaults.Key(domain: .splashApp, keyName: "last used theme") 17 | static let alreadyShowedOnboard1 = UserDefaults.Key(domain: .splashApp, keyName: "already showed onboard 1") 18 | static let alreadyInstalledExamples1 = UserDefaults.Key(domain: .splashApp, keyName: "already installed examples 1") 19 | static let fontSize = UserDefaults.Key(domain: .splashApp, keyName: "font size") 20 | } 21 | 22 | extension UserDefaults { 23 | 24 | var fontSize: CGFloat { 25 | get { 26 | let fontSize = (UserDefaults.standard.object(forKey: .fontSize) as? CGFloat) ?? UIFont.systemFontSize 27 | return fontSize 28 | } 29 | set { 30 | UserDefaults.standard.set(newValue, forKey: .fontSize) 31 | } 32 | } 33 | 34 | var alreadyInstalledExamples1: Bool { 35 | get { 36 | return UserDefaults.standard.bool(forKey: .alreadyInstalledExamples1) 37 | } 38 | set { 39 | UserDefaults.standard.set(newValue, forKey: .alreadyInstalledExamples1) 40 | } 41 | } 42 | 43 | var alreadyShowedOnboard1: Bool { 44 | get { 45 | return UserDefaults.standard.bool(forKey: .alreadyShowedOnboard1) 46 | } 47 | set { 48 | UserDefaults.standard.set(newValue, forKey: .alreadyShowedOnboard1) 49 | } 50 | } 51 | 52 | var lastUsedTheme: ThemeManager.Theme { 53 | get { 54 | guard let themeName = string(forKey: .lastUsedTheme), 55 | let theme = ThemeManager.Theme(rawValue: themeName) else { 56 | return .light // default theme 57 | } 58 | return theme 59 | } 60 | set { 61 | set(newValue.rawValue, forKey: .lastUsedTheme) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /splash_app/Helpers/UserDefaults+Key.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+Key.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 12/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension UserDefaults { 12 | struct Domain: Equatable { 13 | public let name: String 14 | 15 | public init(name: String) { 16 | self.name = name 17 | } 18 | } 19 | 20 | struct Key: Equatable { 21 | public let domain: Domain 22 | public let keyName: String 23 | 24 | public var rawValue: String { 25 | return "(\(domain.name)).(\(keyName))" 26 | } 27 | 28 | public init(domain: Domain, keyName: String) { 29 | self.domain = domain 30 | self.keyName = keyName 31 | } 32 | } 33 | 34 | func object(forKey key: Key) -> Any? { 35 | return object(forKey: key.rawValue) 36 | } 37 | 38 | func url(forKey key: Key) -> URL? { 39 | return url(forKey: key.rawValue) 40 | } 41 | 42 | func array(forKey key: Key) -> [Any]? { 43 | return array(forKey: key.rawValue) 44 | } 45 | 46 | func dictionary(forKey key: Key) -> [String: Any]? { 47 | return dictionary(forKey: key.rawValue) 48 | } 49 | 50 | func string(forKey key: Key) -> String? { 51 | return string(forKey: key.rawValue) 52 | } 53 | 54 | func stringArray(forKey key: Key) -> [String]? { 55 | return stringArray(forKey: key.rawValue) 56 | } 57 | 58 | func data(forKey key: Key) -> Data? { 59 | return data(forKey: key.rawValue) 60 | } 61 | 62 | func bool(forKey key: Key) -> Bool { 63 | return bool(forKey: key.rawValue) 64 | } 65 | 66 | func integer(forKey key: Key) -> Int { 67 | return integer(forKey: key.rawValue) 68 | } 69 | 70 | func float(forKey key: Key) -> Float { 71 | return float(forKey: key.rawValue) 72 | } 73 | 74 | func double(forKey key: Key) -> Double { 75 | return double(forKey: key.rawValue) 76 | } 77 | 78 | func set(_ object: Any?, forKey key: Key) { 79 | set(object, forKey: key.rawValue) 80 | } 81 | 82 | func set(_ object: Float, forKey key: Key) { 83 | set(object, forKey: key.rawValue) 84 | } 85 | 86 | func set(_ object: Double, forKey key: Key) { 87 | set(object, forKey: key.rawValue) 88 | } 89 | 90 | func set(_ object: Int, forKey key: Key) { 91 | set(object, forKey: key.rawValue) 92 | } 93 | 94 | func set(_ object: Bool, forKey key: Key) { 95 | set(object, forKey: key.rawValue) 96 | } 97 | 98 | func set(_ object: URL?, forKey key: Key) { 99 | set(object, forKey: key.rawValue) 100 | } 101 | 102 | func removeObject(forKey key: Key) { 103 | removeObject(forKey: key.rawValue) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /splash_app/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Splash 9 | CFBundleDocumentTypes 10 | 11 | 12 | CFBundleTypeIconFiles 13 | 14 | CFBundleTypeName 15 | splash 16 | CFBundleTypeRole 17 | Editor 18 | LSHandlerRank 19 | Owner 20 | LSItemContentTypes 21 | 22 | ninja.gonzo.splash.script 23 | 24 | 25 | 26 | CFBundleExecutable 27 | $(EXECUTABLE_NAME) 28 | CFBundleIdentifier 29 | $(PRODUCT_BUNDLE_IDENTIFIER) 30 | CFBundleInfoDictionaryVersion 31 | 6.0 32 | CFBundleName 33 | $(PRODUCT_NAME) 34 | CFBundlePackageType 35 | APPL 36 | CFBundleShortVersionString 37 | 1.0 38 | CFBundleVersion 39 | 20190310.1900 40 | LSApplicationQueriesSchemes 41 | 42 | shortcuts 43 | 44 | LSRequiresIPhoneOS 45 | 46 | UILaunchStoryboardName 47 | LaunchScreen 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | UISupportsDocumentBrowser 66 | 67 | UTExportedTypeDeclarations 68 | 69 | 70 | UTTypeConformsTo 71 | 72 | public.script 73 | 74 | UTTypeDescription 75 | splash source code 76 | UTTypeIdentifier 77 | ninja.gonzo.splash.script 78 | UTTypeReferenceURL 79 | https://github.com/gonzula/splash 80 | UTTypeTagSpecification 81 | 82 | public.filename-extension 83 | 84 | splash 85 | 86 | public.mime-type 87 | text/plain 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /splash_app/Model/SplashDocument.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplashDocument.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 03/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SplashDocument: UIDocument { 12 | enum ExecutionError: LocalizedError, Equatable { 13 | case saveError 14 | case compilationError(String) 15 | case shortcutsNotFound 16 | 17 | var errorDescription: String? { 18 | switch self { 19 | case .saveError: return "Unknown error when saving file" 20 | case .compilationError(let message): return "Compilation error: \(message)" 21 | case .shortcutsNotFound: return "Shortctus app not found." 22 | } 23 | } 24 | } 25 | 26 | /// Just the last path component with extension 27 | var fileName: String { 28 | return (self.fileURL.path as NSString) 29 | .lastPathComponent 30 | } 31 | 32 | /// File name without extension 33 | var fileTitle: String { 34 | return (fileName as NSString) 35 | .deletingPathExtension 36 | } 37 | 38 | var string = String() { 39 | didSet { 40 | string.formatForCode() 41 | } 42 | } 43 | 44 | override func contents(forType typeName: String) throws -> Any { 45 | string.formatForCode() 46 | let data = string.data(using: .utf8)! 47 | return data 48 | } 49 | 50 | override func load(fromContents contents: Any, ofType typeName: String?) throws { 51 | guard let data = contents as? Data else {return} 52 | self.string = String(data: data, encoding: .utf8)! 53 | } 54 | 55 | func compileAndRun(completion: @escaping (ExecutionError?) -> Void) { 56 | save(to: fileURL, 57 | for: .forOverwriting) { [unowned self] (completed) in 58 | guard completed else { 59 | completion(.saveError) 60 | return 61 | } 62 | 63 | let tempDirectoryPath = NSTemporaryDirectory() 64 | let shortcutPath = (tempDirectoryPath as NSString).appendingPathComponent("temp.shortcut") 65 | 66 | var errorMessage: UnsafeMutablePointer? 67 | 68 | let parseError = parse(self.fileURL.path, 69 | shortcutPath, 70 | &errorMessage) 71 | if parseError != 0 { 72 | print(errorMessage as Any) 73 | let message = String(cString: errorMessage!) 74 | completion(.compilationError(message)) 75 | free(errorMessage!) 76 | return 77 | } 78 | 79 | completion(self.run(fromPath: shortcutPath)) 80 | } 81 | } 82 | 83 | func run(fromPath path: String) -> ExecutionError? { 84 | let nsDictionary = NSDictionary(contentsOfFile: path)! 85 | // swiftlint:disable:next force_try 86 | let data = try! PropertyListSerialization.data(fromPropertyList: nsDictionary, 87 | format: .binary, 88 | options: 0) 89 | 90 | let b64 = "data:text/shortcut;base64," + data.base64EncodedString() 91 | var urlComponents = URLComponents(string: "shortcuts://import-shortcut")! 92 | let queryItems = [URLQueryItem(name: "name", value: fileTitle), 93 | URLQueryItem(name: "url", value: b64)] 94 | urlComponents.queryItems = queryItems 95 | let url = urlComponents.url! 96 | if UIApplication.shared.canOpenURL(url) { 97 | UIApplication.shared.open(url, 98 | options: [:], 99 | completionHandler: nil) 100 | } else { 101 | return .shortcutsNotFound 102 | } 103 | return nil 104 | } 105 | } 106 | 107 | private extension String { 108 | mutating func formatForCode() { 109 | convertToLF() 110 | appendTrailingNewLineIfNeeded() 111 | } 112 | 113 | private mutating func convertToLF() { 114 | self = replacingOccurrences(of: "\r\n", with: "\n") 115 | self = replacingOccurrences(of: "\r", with: "") 116 | } 117 | 118 | private mutating func appendTrailingNewLineIfNeeded() { 119 | if !hasSuffix("\n") { 120 | append("\n") 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /splash_app/UI/Editor/CodeAccessoryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeAccessoryView.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 04/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol CodeAccessoryViewDelegate: class { 12 | func insertAutoCode(ofKind kind: CodeAccessoryView.CodeButtonKind) 13 | } 14 | 15 | class CodeAccessoryView: UIView { 16 | enum CodeButtonKind: CaseIterable { 17 | case curlyBrackets 18 | case parenthesis 19 | case quotes 20 | 21 | case comment 22 | 23 | case attribution 24 | case lessThan 25 | case greaterThan 26 | case equal 27 | 28 | case keywordIf 29 | case keywordElse 30 | case keywordElseIf 31 | 32 | var buttonTitle: String { 33 | switch self { 34 | case .curlyBrackets: return "{ }" 35 | case .parenthesis: return "( )" 36 | case .quotes: return "\" \"" 37 | case .comment: return "#" 38 | case .attribution: return ":=" 39 | case .lessThan: return "<" 40 | case .greaterThan: return ">" 41 | case .equal: return "==" 42 | case .keywordIf: return "if" 43 | case .keywordElse: return "else" 44 | case .keywordElseIf: return "else if" 45 | } 46 | } 47 | 48 | var needsSpaceBefore: Bool { 49 | switch self { 50 | case .parenthesis: return false 51 | case .curlyBrackets, 52 | .quotes, 53 | .comment, 54 | .attribution, 55 | .lessThan, 56 | .greaterThan, 57 | .equal, 58 | .keywordIf, 59 | .keywordElse, 60 | .keywordElseIf: return true 61 | } 62 | } 63 | 64 | var needsSpaceAfter: Bool { 65 | switch self { 66 | case .parenthesis, 67 | .quotes, 68 | .curlyBrackets: return false 69 | case .attribution, 70 | .lessThan, 71 | .comment, 72 | .greaterThan, 73 | .equal, 74 | .keywordIf, 75 | .keywordElse, 76 | .keywordElseIf: return true 77 | } 78 | } 79 | 80 | var needsCurlyBraceAfter: Bool { 81 | switch self { 82 | case .keywordElse: return true 83 | case .curlyBrackets, 84 | .quotes, 85 | .parenthesis, 86 | .comment, 87 | .attribution, 88 | .lessThan, 89 | .greaterThan, 90 | .equal, 91 | .keywordIf, 92 | .keywordElseIf: return false 93 | } 94 | } 95 | 96 | var needsNewLineAfter: Bool { 97 | switch self { 98 | case .curlyBrackets: return true 99 | case .parenthesis, 100 | .quotes, 101 | .attribution, 102 | .lessThan, 103 | .comment, 104 | .greaterThan, 105 | .equal, 106 | .keywordIf, 107 | .keywordElse, 108 | .keywordElseIf: return false 109 | } 110 | } 111 | 112 | var textToAdd: String { 113 | switch self { 114 | case .quotes: return "\"\"" 115 | case .curlyBrackets: return "{" 116 | case .parenthesis: return "()" 117 | case .attribution, 118 | .lessThan, 119 | .greaterThan, 120 | .comment, 121 | .equal, 122 | .keywordIf, 123 | .keywordElse, 124 | .keywordElseIf: return buttonTitle 125 | } 126 | } 127 | 128 | var offsetAfterInsertion: Int { 129 | switch self { 130 | case .parenthesis, 131 | .quotes: return -1 132 | case .curlyBrackets, 133 | .attribution, 134 | .lessThan, 135 | .greaterThan, 136 | .comment, 137 | .equal, 138 | .keywordIf, 139 | .keywordElse, 140 | .keywordElseIf: return 0 141 | } 142 | } 143 | } 144 | 145 | let scrollView = UIScrollView() 146 | let stackView = UIStackView() 147 | 148 | unowned let delegate: CodeAccessoryViewDelegate 149 | 150 | init(delegate: CodeAccessoryViewDelegate) { 151 | self.delegate = delegate 152 | super.init(frame: CGRect(origin: .zero, size: CGSize(width: UIScreen.main.bounds.width, height: 44))) 153 | autoresizingMask = .flexibleWidth 154 | setup() 155 | } 156 | 157 | required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")} 158 | 159 | private func setup() { 160 | backgroundColor = UIColor(red: 209/255, green: 212/255, blue: 216/255, alpha: 1.0) 161 | scrollView.setupForAutoLayout(in: self) 162 | scrollView.pinToSuperview() 163 | scrollView.setupForHorizontalScrollOnly() 164 | 165 | stackView.setupForAutoLayout(in: scrollView) 166 | stackView.pinToSuperview(withInset: UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)) 167 | 168 | stackView.axis = .horizontal 169 | stackView.alignment = .center 170 | stackView.spacing = 20 171 | stackView.distribution = .equalSpacing 172 | 173 | CodeButtonKind.allCases 174 | .map(AutoCodeButton.init(codeButtonKind: )) 175 | .forEach(stackView.addArrangedSubview(_:)) 176 | 177 | stackView.arrangedSubviews.compactMap {$0 as? AutoCodeButton}.forEach { 178 | $0.addTarget(self, 179 | action: #selector(buttonTouched(sender:)), 180 | for: .touchUpInside) 181 | } 182 | 183 | let line = UIView() 184 | line.setupForAutoLayout(in: self) 185 | line.leftAnchor.constraint(equalTo: leftAnchor).activate() 186 | line.rightAnchor.constraint(equalTo: rightAnchor).activate() 187 | line.bottomAnchor.constraint(equalTo: bottomAnchor).activate() 188 | line.heightAnchor.constraint(equalToConstant: 1).activate() 189 | line.backgroundColor = UIColor(red: 180/255, green: 188/255, blue: 199/255, alpha: 1) 190 | } 191 | 192 | // MARK: - UserInteraction 193 | 194 | @objc func buttonTouched(sender: AutoCodeButton) { 195 | delegate.insertAutoCode(ofKind: sender.codeButtonKind) 196 | } 197 | } 198 | 199 | extension CodeAccessoryView { 200 | class AutoCodeButton: UIButton { 201 | let codeButtonKind: CodeButtonKind 202 | 203 | init(codeButtonKind: CodeButtonKind) { 204 | self.codeButtonKind = codeButtonKind 205 | 206 | super.init(frame: .zero) 207 | 208 | setup() 209 | } 210 | 211 | required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")} 212 | 213 | private func setup() { 214 | let attributedString = NSAttributedString(string: codeButtonKind.buttonTitle, attributes: [ 215 | .font: UIFont(name: "Menlo", size: UIFont.systemFontSize)!, 216 | .foregroundColor: UIColor.black 217 | ]) 218 | setAttributedTitle(attributedString, for: .normal) 219 | 220 | backgroundColor = .lightGray 221 | contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) 222 | layer.cornerRadius = 4 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /splash_app/UI/Editor/EditorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditorView.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 10/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class EditorView: UITextView { 12 | 13 | var forcedTheme: ThemeManager.Theme? { 14 | didSet { 15 | setupAppearance() 16 | } 17 | } 18 | 19 | private var theme: ThemeManager.Theme {return forcedTheme ?? ThemeManager.shared.theme} 20 | 21 | var observers = [Any]() 22 | 23 | override init(frame: CGRect, textContainer: NSTextContainer?) { 24 | super.init(frame: frame, textContainer: textContainer) 25 | 26 | setup() 27 | } 28 | 29 | required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")} 30 | 31 | deinit { 32 | observers.forEach(NotificationCenter.default.removeObserver) 33 | } 34 | 35 | private func setup() { 36 | observers.append( // swiftlint:disable:next discarded_notification_center_observer 37 | NotificationCenter.default.addObserver( 38 | forName: .themeChanged, object: nil, queue: nil) {[weak self] _ in 39 | self?.setupAppearance() 40 | }) 41 | 42 | setupAppearance() 43 | 44 | inputAccessoryView = CodeAccessoryView(delegate: self) 45 | font = UIFont(name: "Menlo", size: UserDefaults.standard.fontSize) 46 | autocapitalizationType = .none 47 | returnKeyType = .default 48 | autocorrectionType = .no 49 | spellCheckingType = .no 50 | smartDashesType = .no 51 | smartQuotesType = .no 52 | delegate = self 53 | keyboardType = .asciiCapable 54 | keyboardDismissMode = .interactive 55 | } 56 | 57 | private func setupAppearance() { 58 | self.colorizeText() 59 | self.backgroundColor = self.theme.backgroundColor 60 | self.keyboardAppearance = self.theme.keyboardAppearance 61 | self.tintColor = self.theme.tintColor 62 | } 63 | 64 | func colorizeText() { 65 | keepLocation { () -> Int? in 66 | attributedText = SyntaxColorizer.shared.colorize(text, theme: theme) 67 | return nil 68 | } 69 | } 70 | 71 | @discardableResult 72 | func insertNewLine(at range: NSRange) -> Int { 73 | let indentationLevel = self.text.indentationLevel(at: range.location) 74 | let indentationUnit = String(repeating: " ", count: 4) 75 | let indentedNewLine = "\n" + String(repeating: indentationUnit, count: indentationLevel) 76 | let newLocation = keepLocation { () -> Int? in 77 | text = (self.text as NSString).replacingCharacters(in: range, with: indentedNewLine) 78 | return indentedNewLine.count 79 | } 80 | return newLocation 81 | } 82 | 83 | func insertClosingBracket(at location: Int) { 84 | let indentationLevel = max(self.text.indentationLevel(at: location) - 1, 0) 85 | let indentationUnit = String(repeating: " ", count: 4) 86 | let indentedNewLine = "\n" + String(repeating: indentationUnit, count: indentationLevel) + "}" 87 | let range = NSRange(location: location, length: 0) 88 | keepLocation { () -> Int? in 89 | text = (self.text as NSString).replacingCharacters(in: range, with: indentedNewLine) 90 | return nil 91 | } 92 | } 93 | 94 | /// Perform changes in *changes* and places the caret at the same location offseted by `changes` return value 95 | @discardableResult 96 | func keepLocation(after changes: () -> Int?) -> Int { 97 | let location = selectedRange.location 98 | let offset = changes() ?? 0 99 | let newLocation = location + offset 100 | selectedRange = NSRange(location: newLocation, length: 0) 101 | return newLocation 102 | } 103 | } 104 | 105 | extension EditorView: UITextViewDelegate { 106 | func textViewDidChange(_ textView: UITextView) { 107 | colorizeText() 108 | } 109 | 110 | // https://stackoverflow.com/a/6113117 111 | func textView(_ textView: UITextView, 112 | shouldChangeTextIn range: NSRange, 113 | replacementText: String) -> Bool { 114 | if range.location > 0, !replacementText.isEmpty { 115 | let whitespace = CharacterSet.whitespaces 116 | 117 | let start = replacementText.unicodeScalars.startIndex 118 | let location = textView.text.unicodeScalars.index(textView.text.unicodeScalars.startIndex, 119 | offsetBy: range.location - 1) 120 | if whitespace.contains(replacementText.unicodeScalars[start]), 121 | whitespace.contains(textView.text.unicodeScalars[location]) { 122 | textView.text = (textView.text as NSString).replacingCharacters(in: range, with: " ") 123 | textView.selectedRange = NSRange(location: range.location + 1, length: 0) 124 | colorizeText() 125 | return false 126 | } 127 | } 128 | 129 | if replacementText == "\n" { 130 | insertNewLine(at: range) 131 | colorizeText() 132 | return false 133 | } 134 | 135 | return true 136 | } 137 | } 138 | 139 | extension EditorView: CodeAccessoryViewDelegate { 140 | func insertAutoCode(ofKind kind: CodeAccessoryView.CodeButtonKind) { 141 | var textToAdd = kind.textToAdd 142 | let range = selectedRange 143 | let location = range.location 144 | var text = self.text as NSString 145 | if kind.needsSpaceBefore, !range.isAtBegining { 146 | let previousChar = text.character(at: range.location - 1) 147 | var characterSet = CharacterSet.whitespacesAndNewlines 148 | characterSet.insert(charactersIn: "(") 149 | let previousCharWasSpace = characterSet.contains(Unicode.Scalar(previousChar)!) 150 | if !previousCharWasSpace { 151 | textToAdd = " " + textToAdd 152 | } 153 | } 154 | 155 | if kind.needsSpaceAfter { 156 | textToAdd += " " 157 | } 158 | 159 | text = text.replacingCharacters(in: range, with: textToAdd) as NSString 160 | self.text = text as String 161 | selectedRange = NSRange(location: location + textToAdd.count + kind.offsetAfterInsertion, length: 0) 162 | 163 | if kind.needsNewLineAfter { 164 | let newLocation = insertNewLine(at: selectedRange) 165 | if kind.textToAdd == "{" { 166 | insertClosingBracket(at: newLocation) 167 | } 168 | } 169 | 170 | if kind.needsCurlyBraceAfter { 171 | insertAutoCode(ofKind: .curlyBrackets) 172 | return 173 | } 174 | 175 | colorizeText() 176 | } 177 | } 178 | 179 | private extension String { 180 | func indentationLevel(at location: Int) -> Int { 181 | var indentationLevel = 0 182 | var currentStringDelimiter: Character? 183 | var isEscaped = false 184 | for (idx, char) in self.enumerated() { 185 | guard idx < location else {break} 186 | if let delimiter = currentStringDelimiter { 187 | if isEscaped { 188 | isEscaped = false 189 | continue 190 | } else { 191 | if char == delimiter { 192 | currentStringDelimiter = nil 193 | continue 194 | } 195 | } 196 | } else { 197 | switch char { 198 | case "{": indentationLevel += 1 199 | case "}": indentationLevel -= 1 200 | case "'", "\"": currentStringDelimiter = char 201 | default: break 202 | } 203 | continue 204 | } 205 | } 206 | return indentationLevel 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /splash_app/UI/Editor/EditorViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditorViewController.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 03/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StoreKit 11 | 12 | class EditorNavigationController: UINavigationController { 13 | 14 | var textView: UIView { 15 | guard let editorViewController = viewControllers.first as? EditorViewController else {fatalError()} 16 | return editorViewController.textView 17 | } 18 | 19 | init() { 20 | super.init(rootViewController: EditorViewController()) 21 | } 22 | 23 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 24 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 25 | } 26 | 27 | required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")} 28 | 29 | func set(_ document: SplashDocument, completion: @escaping () -> Void) { 30 | guard let editorViewController = viewControllers.first as? EditorViewController else {return} 31 | 32 | editorViewController.set(document, completion: completion) 33 | } 34 | 35 | func close(completion: (() -> Void)?) { 36 | guard let editorViewController = viewControllers.first as? EditorViewController else { 37 | completion?() 38 | return 39 | } 40 | 41 | editorViewController.closeEditor(completion: completion) 42 | } 43 | } 44 | 45 | class EditorViewController: UIViewController { 46 | 47 | var observers = [Any]() 48 | 49 | var splashDocument: SplashDocument? { 50 | didSet { 51 | title = splashDocument?.fileName 52 | } 53 | } 54 | 55 | let textView = EditorView() 56 | 57 | override func loadView() { 58 | view = textView 59 | 60 | setupNavigationBarItems() 61 | } 62 | 63 | override func viewDidLoad() { 64 | super.viewDidLoad() 65 | 66 | setupObservers() 67 | setupAppearance() 68 | } 69 | 70 | override func viewDidAppear(_ animated: Bool) { 71 | super.viewDidAppear(animated) 72 | 73 | textView.becomeFirstResponder() 74 | } 75 | 76 | deinit { 77 | observers.forEach(NotificationCenter.default.removeObserver) 78 | } 79 | 80 | private func setupAppearance() { 81 | let theme = ThemeManager.shared.theme 82 | navigationController?.navigationBar.barStyle = theme.navigationBarStyle 83 | } 84 | 85 | private func setupNavigationBarItems() { 86 | navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, 87 | target: self, 88 | action: #selector(doneTouched(sender:))) 89 | navigationItem.rightBarButtonItems = navigationItem.rightBarButtonItems ?? [] 90 | navigationItem.rightBarButtonItems?.append(UIBarButtonItem(barButtonSystemItem: .play, 91 | target: self, 92 | action: #selector(compileAndRun(sender:)))) 93 | } 94 | 95 | fileprivate func setupObservers() { 96 | NotificationCenter.default.addObserver(self, 97 | selector: #selector(adjustForKeyboard), 98 | name: UIResponder.keyboardWillHideNotification, object: nil) 99 | NotificationCenter.default.addObserver(self, 100 | selector: #selector(adjustForKeyboard), 101 | name: UIResponder.keyboardWillChangeFrameNotification, object: nil) 102 | 103 | observers.append( // swiftlint:disable:next discarded_notification_center_observer 104 | NotificationCenter.default.addObserver( 105 | forName: .themeChanged, 106 | object: nil, 107 | queue: nil) { [weak self] _ in 108 | self?.setupAppearance() 109 | }) 110 | } 111 | 112 | func set(_ document: SplashDocument, completion: @escaping () -> Void) { 113 | self.splashDocument = document 114 | document.open { success in 115 | guard success else {return} 116 | 117 | self.textView.text = document.string 118 | self.textView.colorizeText() 119 | 120 | completion() 121 | } 122 | } 123 | 124 | func presentShortcutsNotFoundAlert() { 125 | let alertController = UIAlertController(title: "Error", 126 | message: "Shortcuts app not found. Install it and try again.", 127 | preferredStyle: .alert) 128 | alertController.addAction(UIAlertAction(title: "Ok", 129 | style: .cancel, 130 | handler: nil)) 131 | alertController.addAction(UIAlertAction(title: "Install it", 132 | style: .default, 133 | handler: { [unowned self] _ in 134 | self.presentAppStore() 135 | })) 136 | self.present(alertController, animated: true, completion: nil) 137 | } 138 | 139 | func presentAppStore() { 140 | let controller = SKStoreProductViewController() 141 | controller.loadProduct(withParameters: [ 142 | SKStoreProductParameterITunesItemIdentifier: NSNumber(value: 915249334)]) { [weak self] (_, error) in 143 | guard let self = self else {return} 144 | if let error = error { 145 | self.present(UIAlertController(error: error), 146 | animated: true, 147 | completion: nil) 148 | } 149 | self.present(controller, animated: true, completion: nil) 150 | } 151 | controller.delegate = self 152 | } 153 | 154 | // MARK: - User Interaction 155 | 156 | @objc func doneTouched(sender: UIBarButtonItem) { 157 | closeEditor(completion: nil) 158 | } 159 | 160 | @objc func closeEditor(completion: (() -> Void)?) { 161 | guard let document = splashDocument else { 162 | dismiss(animated: true, completion: completion) 163 | return 164 | } 165 | document.string = (textView.text ?? "") 166 | document.save(to: document.fileURL, 167 | for: .forOverwriting) { (completed) in 168 | if completed { 169 | document.close { success in 170 | if success { 171 | self.dismiss(animated: true, completion: completion) 172 | } 173 | } 174 | } 175 | } 176 | } 177 | 178 | @objc func compileAndRun(sender: UIBarButtonItem?) { 179 | guard let document = splashDocument else {return} 180 | document.string = textView.text ?? "" 181 | document.compileAndRun { [unowned self] (error) in 182 | if let error = error { 183 | if error == .shortcutsNotFound { 184 | self.presentShortcutsNotFoundAlert() 185 | } else { 186 | self.present(UIAlertController(error: error), animated: true, completion: nil) 187 | } 188 | } 189 | } 190 | } 191 | } 192 | 193 | extension EditorViewController { 194 | @objc func adjustForKeyboard(notification: Notification) { 195 | let userInfo = notification.userInfo! 196 | 197 | guard let keyboardScreenEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)? 198 | .cgRectValue else { return } 199 | let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window) 200 | 201 | if notification.name == UIResponder.keyboardWillHideNotification { 202 | textView.contentInset.bottom = 0 203 | textView.scrollIndicatorInsets.bottom = textView.contentInset.bottom 204 | } else { 205 | let height = keyboardViewEndFrame.height 206 | textView.contentInset.bottom = height - view.safeAreaInsets.bottom 207 | textView.scrollIndicatorInsets.bottom = textView.contentInset.bottom 208 | } 209 | } 210 | } 211 | 212 | extension EditorViewController: SKStoreProductViewControllerDelegate { 213 | func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { 214 | viewController.dismiss(animated: true) { 215 | self.compileAndRun(sender: nil) 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /splash_app/UI/Editor/SyntaxColorizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyntaxColorizer.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 04/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SyntaxColorizer { 12 | enum TokenKind { 13 | case regular 14 | case comment 15 | case keyword 16 | case functionCall 17 | case identifier 18 | case number 19 | case string 20 | case error 21 | 22 | var foregroundLightColor: UIColor { 23 | switch self { 24 | case .regular: return UIColor(hex: 0x000000) 25 | case .comment: return UIColor(hex: 0x536579) 26 | case .keyword: return UIColor(hex: 0x9B2393) 27 | case .functionCall: return UIColor(hex: 0x3900A0) 28 | case .identifier: return UIColor(hex: 0x000000) 29 | case .number: return UIColor(hex: 0x1C00CF) 30 | case .string: return UIColor(hex: 0xC41A16) 31 | case .error: return UIColor(hex: 0x000000) 32 | } 33 | } 34 | 35 | var foregroundDarkColor: UIColor { 36 | switch self { 37 | case .regular: return UIColor(hex: 0xffffff) 38 | case .comment: return UIColor(hex: 0x6C7986) 39 | case .keyword: return UIColor(hex: 0xFC5FA3) 40 | case .functionCall: return UIColor(hex: 0x75B492) 41 | case .identifier: return UIColor(hex: 0xffffff) 42 | case .number: return UIColor(hex: 0x9686F5) 43 | case .string: return UIColor(hex: 0xFC6A5D) 44 | case .error: return UIColor(hex: 0xffffff) 45 | } 46 | } 47 | 48 | func foregroundColor(`for` theme: ThemeManager.Theme) -> UIColor { 49 | switch theme { 50 | case .light: return foregroundLightColor 51 | case .dark: return foregroundDarkColor 52 | } 53 | } 54 | 55 | func backgroundColor(`for` theme: ThemeManager.Theme) -> UIColor? { 56 | if self == .error { 57 | return UIColor.red 58 | } else { 59 | return nil 60 | } 61 | } 62 | 63 | func attributes(`for` theme: ThemeManager.Theme) -> [NSAttributedString.Key: Any] { 64 | var attributes = [NSAttributedString.Key: Any]() 65 | attributes[.foregroundColor] = self.foregroundColor(for: theme) 66 | attributes[.backgroundColor] = self.backgroundColor(for: theme) 67 | 68 | switch self { 69 | case .comment: attributes[.font] = UIFont(name: "Menlo-Italic", size: UserDefaults.standard.fontSize)! 70 | case .keyword: attributes[.font] = UIFont(name: "Menlo-Bold", size: UserDefaults.standard.fontSize)! 71 | case .functionCall, 72 | .identifier, 73 | .number, 74 | .regular, 75 | .error, 76 | .string: attributes[.font] = UIFont(name: "Menlo", size: UserDefaults.standard.fontSize)! 77 | } 78 | 79 | return attributes 80 | } 81 | } 82 | 83 | static let shared = SyntaxColorizer() 84 | 85 | lazy var knownFunctions: Set = [ 86 | "AskNumber", 87 | "AskText", 88 | "ShowResult", 89 | "Floor", 90 | "Ceil", 91 | "Round", 92 | "GetName", 93 | "GetType", 94 | "ViewContentGraph", 95 | "Wait", 96 | "Exit", 97 | "WaitToReturn", 98 | "GetBatteryLevel", 99 | "Date", 100 | "ExtractArchive", 101 | "GetCurrentLocation" 102 | ] 103 | 104 | lazy var patterns: [(String, TokenKind)] = [ 105 | ("#.*?(?:$|\\n)", .comment), 106 | ("[a-zA-Z_][a-zA-Z_0-9]*(?=\\s*\\()", .functionCall), 107 | ("[a-zA-Z_][a-zA-Z_0-9]*", .identifier), 108 | ("[0-9]+(?:\\.[0-9]+)?", .number), 109 | ("\"(?:[^\"\\\\]|\\\\.)*\"|\'(?:[^\'\\\\]|\\\\.)*\'", .string) 110 | ] 111 | 112 | lazy var pattern: String = patterns 113 | .map {"(\($0.0))"} 114 | .joined(separator: "|") 115 | 116 | func colorize(_ input: String, theme: ThemeManager.Theme) -> NSAttributedString { 117 | let attributedString = NSMutableAttributedString(string: input, 118 | attributes: TokenKind.regular.attributes(for: theme)) 119 | 120 | let regex = try! NSRegularExpression(pattern: pattern, options: []) // swiftlint:disable:this force_try 121 | let results = regex.matches(in: input, options: [], range: NSRange(location: 0, length: input.count)) 122 | 123 | for result in results { 124 | let kind = patterns[result.matchId].1 125 | 126 | let attributes: [NSAttributedString.Key: Any] 127 | let value = (input as NSString).substring(with: result.range) 128 | .trimmingCharacters(in: .whitespacesAndNewlines) 129 | if kind == .identifier { 130 | if isKeyword(identifier: value) { 131 | attributes = TokenKind.keyword.attributes(for: theme) 132 | } else { 133 | attributes = kind.attributes(for: theme) 134 | } 135 | } else if kind == .functionCall { 136 | if knownFunctions.contains(value) { 137 | attributes = kind.attributes(for: theme) 138 | } else { 139 | attributes = TokenKind.error.attributes(for: theme) 140 | } 141 | } else { 142 | attributes = kind.attributes(for: theme) 143 | } 144 | 145 | attributedString.addAttributes(attributes, range: result.range) 146 | } 147 | 148 | return attributedString 149 | } 150 | 151 | func isKeyword(identifier: String) -> Bool { 152 | let keywords = ["if", "else"] 153 | return keywords.contains(identifier) 154 | } 155 | } 156 | 157 | fileprivate extension NSTextCheckingResult { 158 | var matchId: Int { 159 | return (1.., with event: UIEvent?) { 25 | view.endEditing(true) 26 | super.touchesBegan(touches, with: event) 27 | } 28 | } 29 | } 30 | 31 | extension OnboardViewController { 32 | class ThemeView: UIView { 33 | let segmentedControl = ThemeControl() 34 | 35 | let lightEditor = EditorView() 36 | let darkEditor = EditorView() 37 | 38 | override init(frame: CGRect) { 39 | super.init(frame: frame) 40 | 41 | setup() 42 | } 43 | 44 | required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")} 45 | 46 | private func setup() { 47 | setupSegmentedControl() 48 | setupLightEditor() 49 | setupDarkEditor() 50 | } 51 | 52 | private func setupSegmentedControl() { 53 | segmentedControl.setupForAutoLayout(in: self) 54 | 55 | segmentedControl.centerXAnchor.constraint(equalTo: centerXAnchor).activate() 56 | segmentedControl.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, 57 | constant: 20).activate() 58 | } 59 | 60 | private func setupLightEditor() { 61 | lightEditor.setupForAutoLayout(in: self) 62 | lightEditor.forcedTheme = .light 63 | 64 | lightEditor.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor, constant: 20).activate() 65 | lightEditor.leftAnchor.constraint(equalTo: leftAnchor).activate() 66 | lightEditor.rightAnchor.constraint(equalTo: rightAnchor).activate() 67 | lightEditor.heightAnchor.constraint(equalToConstant: 44).activate() 68 | 69 | lightEditor.text = "ShowResult(\"This is the light theme\")" 70 | lightEditor.colorizeText() 71 | } 72 | 73 | private func setupDarkEditor() { 74 | darkEditor.setupForAutoLayout(in: self) 75 | darkEditor.forcedTheme = .dark 76 | 77 | darkEditor.topAnchor.constraint(equalTo: lightEditor.bottomAnchor, constant: 20).activate() 78 | darkEditor.leftAnchor.constraint(equalTo: leftAnchor).activate() 79 | darkEditor.rightAnchor.constraint(equalTo: rightAnchor).activate() 80 | darkEditor.heightAnchor.constraint(equalToConstant: 44).activate() 81 | 82 | darkEditor.text = "ShowResult(\"This is the dark theme\")" 83 | darkEditor.colorizeText() 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /splash_app/UI/Onboard/OnboardViewController+View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardViewController+FixedContent.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 14/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension OnboardViewController { 12 | 13 | enum ButtonState: String { 14 | case `continue` 15 | case finish 16 | 17 | var title: String { 18 | return rawValue.capitalizingFirstLetter() 19 | } 20 | } 21 | 22 | class FixedView: UIView { 23 | 24 | var buttonState: ButtonState = .continue { 25 | didSet { 26 | continueButton.setTitle(buttonState.title, for: .normal) 27 | } 28 | } 29 | 30 | let continueButton = UIButton(type: .system) 31 | let pageControl = UIPageControl() 32 | let bottomView = UIVisualEffectView(effect: UIBlurEffect(style: .extraLight)) 33 | 34 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 35 | return bottomView.hitTest(self.convert(point, to: bottomView), with: event) 36 | } 37 | 38 | override init(frame: CGRect) { 39 | super.init(frame: frame) 40 | 41 | setup() 42 | } 43 | 44 | required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")} 45 | 46 | private func setup() { 47 | setupBottomView() 48 | setupPageControl() 49 | setupContinueButton() 50 | } 51 | 52 | private func setupBottomView() { 53 | bottomView.setupForAutoLayout(in: self) 54 | 55 | bottomView.leftAnchor.constraint(equalTo: leftAnchor).activate() 56 | bottomView.rightAnchor.constraint(equalTo: rightAnchor).activate() 57 | bottomView.bottomAnchor.constraint(equalTo: bottomAnchor).activate() 58 | } 59 | 60 | private func setupPageControl() { 61 | pageControl.setupForAutoLayout(in: bottomView.contentView) 62 | pageControl.bottomAnchor.constraint(equalTo: bottomView.safeAreaLayoutGuide.bottomAnchor, 63 | constant: 0).activate() 64 | pageControl.centerXAnchor.constraint(equalTo: bottomView.safeAreaLayoutGuide.centerXAnchor).activate() 65 | 66 | pageControl.pageIndicatorTintColor = .lightGray 67 | pageControl.currentPageIndicatorTintColor = .black 68 | pageControl.addTarget(nil, 69 | action: #selector(OnboardViewController.pageControlChanged(sender:)), 70 | for: .valueChanged) 71 | } 72 | 73 | private func setupContinueButton() { 74 | continueButton.setupForAutoLayout(in: bottomView.contentView) 75 | 76 | continueButton.setTitle(buttonState.title, for: .normal) 77 | continueButton.addTarget(nil, 78 | action: #selector(OnboardViewController.advance), 79 | for: .touchUpInside) 80 | 81 | continueButton.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) 82 | continueButton.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor).activate() 83 | continueButton.bottomAnchor.constraint(equalTo: pageControl.topAnchor, constant: -4).activate() 84 | continueButton.topAnchor.constraint(equalTo: bottomView.contentView.topAnchor, 85 | constant: 8).activate() 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /splash_app/UI/Onboard/OnboardViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardViewController.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 14/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class OnboardViewController: PageViewController { 12 | 13 | let viewControllers: [UIViewController] = [ 14 | IntroViewController(), 15 | GitHubViewController(), 16 | ThemeViewController() 17 | ] 18 | 19 | override var isAnimatingViewChange: Bool { 20 | set { 21 | super.isAnimatingViewChange = newValue 22 | fixedView.pageControl.isEnabled = !newValue 23 | } 24 | get { 25 | return super.isAnimatingViewChange 26 | } 27 | } 28 | 29 | var canChangePage: Bool { 30 | return !isAnimatingViewChange 31 | } 32 | 33 | override var supportedInterfaceOrientations: UIInterfaceOrientationMask {return .portrait} 34 | override var preferredStatusBarStyle: UIStatusBarStyle { 35 | return ThemeManager.shared.theme.statusBarStyle 36 | } 37 | 38 | var currentViewController: UIViewController? 39 | var currentPageIndex: Int? { 40 | guard let currentViewController = currentViewController else {return nil} 41 | return viewControllers.firstIndex(of: currentViewController) 42 | } 43 | 44 | let fixedView = FixedView() 45 | 46 | var observers = [Any]() 47 | 48 | override func loadView() { 49 | super.loadView() 50 | 51 | fixedView.setupForAutoLayout(in: view) 52 | fixedView.pinToSuperview() 53 | 54 | totalPageCount = viewControllers.count 55 | self.pageIndicatorDelegate = fixedView.pageControl 56 | 57 | let firstViewController = viewControllers.first! 58 | currentViewController = firstViewController 59 | present(firstViewController, at: 0, animated: false) 60 | } 61 | 62 | override func viewDidLoad() { 63 | super.viewDidLoad() 64 | 65 | observers.append( // swiftlint:disable:next discarded_notification_center_observer 66 | NotificationCenter.default.addObserver(forName: .themeChanged, 67 | object: nil, 68 | queue: nil, 69 | using: { _ in 70 | self.setupAppearance() 71 | })) 72 | setupAppearance() 73 | } 74 | 75 | override func viewDidLayoutSubviews() { 76 | super.viewDidLayoutSubviews() 77 | 78 | let bottomViewHeight = fixedView.bottomView.frame.height - view.safeAreaInsets.bottom 79 | let safeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: bottomViewHeight, right: 0) 80 | viewControllers.forEach {$0.additionalSafeAreaInsets = safeAreaInsets} 81 | } 82 | 83 | deinit { 84 | observers.forEach(NotificationCenter.default.removeObserver) 85 | } 86 | 87 | func setupAppearance() { 88 | let theme = ThemeManager.shared.theme 89 | view.backgroundColor = theme.backgroundColor 90 | setNeedsStatusBarAppearanceUpdate() 91 | 92 | fixedView.bottomView.effect = theme.blurEffect 93 | fixedView.pageControl.currentPageIndicatorTintColor = theme.pageControlCurrentPageTintColor 94 | fixedView.pageControl.pageIndicatorTintColor = theme.pageControlTintColor 95 | } 96 | 97 | // MARK: - Page Control 98 | 99 | @objc 100 | func advance() { 101 | guard let currentPageIndex = currentPageIndex else {return} 102 | 103 | if currentPageIndex + 1 >= viewControllers.count { 104 | UserDefaults.standard.alreadyShowedOnboard1 = true 105 | dismiss(animated: true) 106 | } else { 107 | setPage(at: currentPageIndex + 1) 108 | } 109 | } 110 | 111 | @objc 112 | func pageControlChanged(sender: UIPageControl) { 113 | setPage(at: sender.currentPage) 114 | } 115 | 116 | func setPage(at index: Int) { 117 | guard canChangePage else {return} 118 | let viewControllersRange = viewControllers.startIndex.. ((SettingsViewController) -> Void)? { 25 | return { viewController in 26 | viewController.openURL(self.url) 27 | } 28 | } 29 | } 30 | } 31 | 32 | extension SettingsViewController { 33 | struct CustomAction: CellConfigurator { 34 | 35 | let name: String 36 | let action: (SettingsViewController) -> Void 37 | 38 | var cellIdentifier: String {return "default cell"} 39 | var cellClass: UITableViewCell.Type {return DefaultCell.self} 40 | 41 | func configure(_ cell: UITableViewCell, at indexPath: IndexPath, in viewController: SettingsViewController) { 42 | cell.textLabel?.text = name 43 | } 44 | 45 | func action(for indexPath: IndexPath) -> ((SettingsViewController) -> Void)? { 46 | return action 47 | } 48 | } 49 | } 50 | 51 | extension SettingsViewController { 52 | class DefaultCell: UITableViewCell { 53 | 54 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 55 | super.init(style: .default, reuseIdentifier: reuseIdentifier) 56 | } 57 | 58 | required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")} 59 | } 60 | } 61 | 62 | extension SettingsViewController.DefaultCell: AppearanceAdjustable { 63 | func setupAppearance() { 64 | let theme = ThemeManager.shared.theme 65 | backgroundColor = theme.tableViewCellBackgroundColor 66 | textLabel?.textColor = theme.textColor 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /splash_app/UI/Settings/SettingsViewController+FontSizeCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewController+FontSizeCell.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 17/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension SettingsViewController { 12 | struct FontSize: CellConfigurator { 13 | var cellIdentifier: String {return "font size cell"} 14 | var cellClass: UITableViewCell.Type {return FontSizeCell.self} 15 | 16 | func configure(_ cell: UITableViewCell, at indexPath: IndexPath, in viewController: SettingsViewController) { 17 | } 18 | 19 | func action(for indexPath: IndexPath) -> ((SettingsViewController) -> Void)? {return nil} 20 | } 21 | } 22 | 23 | extension SettingsViewController { 24 | class FontSizeCell: UITableViewCell { 25 | let label = UILabel() 26 | let valueLabel = UILabel() 27 | let stepper = UIStepper() 28 | 29 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 30 | super.init(style: style, reuseIdentifier: reuseIdentifier) 31 | 32 | setup() 33 | } 34 | 35 | required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")} 36 | 37 | private func setup() { 38 | selectionStyle = .none 39 | setupLabel() 40 | setupStepper() 41 | setupValueLabel() 42 | } 43 | 44 | private func setupLabel() { 45 | label.setupForAutoLayout(in: contentView) 46 | label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).activate() 47 | label.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20).activate() 48 | label.text = "Code font size:" 49 | } 50 | 51 | private func setupStepper() { 52 | stepper.setupForAutoLayout(in: contentView) 53 | 54 | stepper.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20).activate() 55 | stepper.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).activate() 56 | stepper.minimumValue = 7 57 | stepper.maximumValue = 28 58 | stepper.stepValue = 1 59 | stepper.value = Double(UserDefaults.standard.fontSize) 60 | stepper.addTarget(self, action: #selector(changeFontSize), for: .valueChanged) 61 | } 62 | 63 | private func setupValueLabel() { 64 | valueLabel.setupForAutoLayout(in: contentView) 65 | 66 | valueLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).activate() 67 | valueLabel.rightAnchor.constraint(equalTo: stepper.leftAnchor, constant: -8).activate() 68 | 69 | valueLabel.text = "\(Int(stepper.value))" 70 | } 71 | 72 | // MARK: - User Interaction 73 | 74 | @objc 75 | func changeFontSize(sender: UIStepper) { 76 | UserDefaults.standard.fontSize = CGFloat(sender.value) 77 | valueLabel.text = "\(Int(sender.value))" 78 | } 79 | } 80 | } 81 | 82 | extension SettingsViewController.FontSizeCell: AppearanceAdjustable { 83 | func setupAppearance() { 84 | let theme = ThemeManager.shared.theme 85 | backgroundColor = theme.tableViewCellBackgroundColor 86 | label.textColor = theme.textColor 87 | valueLabel.textColor = theme.textColor 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /splash_app/UI/Settings/SettingsViewController+ThemeCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewController+ThemeCell.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 17/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension SettingsViewController { 12 | struct Theme: CellConfigurator { 13 | var cellIdentifier: String {return "theme cell"} 14 | var cellClass: UITableViewCell.Type {return ThemeCell.self} 15 | 16 | func configure(_ cell: UITableViewCell, at indexPath: IndexPath, in viewController: SettingsViewController) { 17 | 18 | } 19 | 20 | func action(for indexPath: IndexPath) -> ((SettingsViewController) -> Void)? {return nil} 21 | } 22 | } 23 | 24 | extension SettingsViewController { 25 | class ThemeCell: UITableViewCell { 26 | let label = UILabel() 27 | let themeControl = ThemeControl() 28 | 29 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 30 | super.init(style: style, reuseIdentifier: reuseIdentifier) 31 | 32 | setup() 33 | } 34 | 35 | required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")} 36 | 37 | private func setup() { 38 | selectionStyle = .none 39 | setupLabel() 40 | setupThemeControl() 41 | } 42 | 43 | private func setupLabel() { 44 | label.setupForAutoLayout(in: contentView) 45 | label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).activate() 46 | label.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20).activate() 47 | label.text = "Theme:" 48 | } 49 | 50 | private func setupThemeControl() { 51 | themeControl.setupForAutoLayout(in: contentView) 52 | 53 | themeControl.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20).activate() 54 | themeControl.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).activate() 55 | } 56 | } 57 | } 58 | 59 | extension SettingsViewController.ThemeCell: AppearanceAdjustable { 60 | func setupAppearance() { 61 | let theme = ThemeManager.shared.theme 62 | backgroundColor = theme.tableViewCellBackgroundColor 63 | label.textColor = theme.textColor 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /splash_app/UI/Settings/SettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewController.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 17/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SafariServices 11 | 12 | class SettingsNavigationController: UINavigationController { 13 | 14 | init() { 15 | super.init(rootViewController: SettingsViewController()) 16 | } 17 | 18 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 19 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 20 | } 21 | 22 | required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")} 23 | } 24 | 25 | protocol CellConfigurator { 26 | var cellIdentifier: String {get} 27 | var cellClass: UITableViewCell.Type {get} 28 | 29 | func configure(_ cell: UITableViewCell, at indexPath: IndexPath, in viewController: SettingsViewController) 30 | func action(`for` indexPath: IndexPath) -> ((SettingsViewController) -> Void)? 31 | } 32 | 33 | protocol AppearanceAdjustable { 34 | func setupAppearance() 35 | } 36 | 37 | class SettingsViewController: UITableViewController { 38 | struct Section { 39 | let title: String? 40 | let rows: [CellConfigurator] 41 | } 42 | 43 | let items: [Section] = [ 44 | Section(title: "Appearance Settings", 45 | rows: [ 46 | Theme(), 47 | FontSize() 48 | ]), 49 | Section(title: "Reference", 50 | rows: [ 51 | Link(name: "GitHub Repository", 52 | url: URL(string: "https://github.com/gonzula/splash")!), 53 | CustomAction(name: "Restore Examples", 54 | action: {$0.askToRestoreExamples()}), 55 | CustomAction(name: "Show Welcome Message", 56 | action: {$0.presentOnboard()}), 57 | Link(name: "Documentation", 58 | url: URL(string: "https://github.com/gonzula/splash/blob/master/Documentation/README.md")!) 59 | ]), 60 | Section(title: nil, rows: [ 61 | Link(name: "Repor an issue", 62 | url: URL(string: "https://github.com/gonzula/splash/issues/new")!), 63 | CustomAction(name: "App Version: \(Bundle.main.fullVersion)", 64 | action: { 65 | UIPasteboard.general.string = Bundle.main.fullVersion 66 | $0.deselectAllRows(animated: true) 67 | }) 68 | ]) 69 | ] 70 | 71 | var observers = [Any]() 72 | 73 | init() { 74 | super.init(style: .grouped) 75 | tableView.rowHeight = 44 76 | 77 | self.items 78 | .flatMap {$0.rows} 79 | .reduce(into: [String: UITableViewCell.Type]()) {$0[$1.cellIdentifier] = $1.cellClass} 80 | .forEach {tableView.register($1, forCellReuseIdentifier: $0)} 81 | } 82 | 83 | required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")} 84 | 85 | override func viewDidLoad() { 86 | super.viewDidLoad() 87 | 88 | navigationItem.leftBarButtonItem = UIBarButtonItem( 89 | barButtonSystemItem: .done, 90 | target: self, 91 | action: #selector(self.doneTouched)) 92 | 93 | observers.append( // swiftlint:disable:next discarded_notification_center_observer 94 | NotificationCenter.default.addObserver(forName: .themeChanged, 95 | object: nil, 96 | queue: nil, 97 | using: { [weak self] _ in 98 | self?.setupAppearance() 99 | }) 100 | ) 101 | } 102 | 103 | override func viewWillAppear(_ animated: Bool) { 104 | super.viewWillAppear(animated) 105 | 106 | setupAppearance() 107 | } 108 | 109 | private func setupAppearance() { 110 | let theme = ThemeManager.shared.theme 111 | navigationController?.navigationBar.barStyle = theme.navigationBarStyle 112 | view.backgroundColor = theme.tableViewBackgroundColor 113 | tableView.reloadData() 114 | } 115 | 116 | private func section(at index: Int) -> Section { 117 | return items[index] 118 | } 119 | 120 | private func item(at indexPath: IndexPath) -> CellConfigurator { 121 | return section(at: indexPath.section).rows[indexPath.row] 122 | } 123 | 124 | override func numberOfSections(in tableView: UITableView) -> Int { 125 | return self.items.count 126 | } 127 | 128 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 129 | return self.section(at: section).rows.count 130 | } 131 | 132 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 133 | let item = self.item(at: indexPath) 134 | let cell = tableView.dequeueReusableCell(withIdentifier: item.cellIdentifier, for: indexPath) 135 | item.configure(cell, at: indexPath, in: self) 136 | 137 | return cell 138 | } 139 | 140 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 141 | return self.section(at: section).title 142 | } 143 | 144 | override func tableView(_ tableView: UITableView, 145 | willDisplay cell: UITableViewCell, 146 | forRowAt indexPath: IndexPath) { 147 | (cell as? AppearanceAdjustable)?.setupAppearance() 148 | } 149 | 150 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 151 | self.item(at: indexPath).action(for: indexPath)?(self) 152 | } 153 | 154 | func openURL(_ url: URL) { 155 | let viewController = SFSafariViewController(url: url) 156 | 157 | viewController.delegate = self 158 | viewController.modalTransitionStyle = .coverVertical 159 | 160 | (navigationController ?? self).present(viewController, animated: true, completion: nil) 161 | } 162 | 163 | // MARK: - User Interaction 164 | 165 | @objc 166 | func doneTouched() { 167 | dismiss(animated: true) 168 | } 169 | 170 | func askToRestoreExamples() { 171 | let alertController = UIAlertController(title: nil, 172 | message: "Are you sure you want to restore and override the examples?", 173 | preferredStyle: .alert) 174 | alertController.addAction(UIAlertAction(title: "Cancel", 175 | style: .cancel, 176 | handler: {[weak self] _ in self?.deselectAllRows(animated: true)})) 177 | alertController.addAction(UIAlertAction(title: "Restore", 178 | style: .destructive, 179 | handler: { [weak self] _ in 180 | self?.deselectAllRows(animated: true) 181 | self?.restoreExamples() 182 | })) 183 | 184 | (navigationController ?? self).present(alertController, animated: true) 185 | } 186 | 187 | func restoreExamples() { 188 | FileManager.createExamplesDirectory() 189 | (UIApplication.shared.delegate as? AppDelegate)?.mainViewController.showExamplesInRecents { 190 | self.dismiss(animated: true, completion: nil) 191 | } 192 | } 193 | 194 | func presentOnboard() { 195 | if UIDevice.current.userInterfaceIdiom == .pad { 196 | dismiss(animated: true) { 197 | (UIApplication.shared.delegate as? AppDelegate)?.mainViewController.present(OnboardViewController(), 198 | animated: true, 199 | completion: nil) 200 | } 201 | } else { 202 | (navigationController ?? self).present(OnboardViewController(), animated: true, completion: nil) 203 | } 204 | } 205 | 206 | func deselectAllRows(animated: Bool) { 207 | tableView.indexPathsForSelectedRows?.forEach {tableView.deselectRow(at: $0, animated: animated)} 208 | } 209 | } 210 | 211 | extension SettingsViewController: SFSafariViewControllerDelegate { 212 | func safariViewControllerDidFinish(_ controller: SFSafariViewController) { 213 | controller.dismiss(animated: true, completion: nil) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /splash_app/UI/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // splash 4 | // 5 | // Created by Gonzo Fialho on 02/03/19. 6 | // Copyright © 2019 Gonzo Fialho. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIDocumentBrowserViewController { 12 | 13 | let browserDelegate = BrowserDelegate() // swiftlint:disable:this weak_delegate 14 | var transitioningController: UIDocumentBrowserTransitionController? 15 | 16 | var observers = [Any]() 17 | 18 | init() { 19 | super.init(forOpeningFilesWithContentTypes: ["ninja.gonzo.splash.script"]) 20 | delegate = browserDelegate 21 | let button = UIBarButtonItem(title: "Settings", 22 | style: .plain, 23 | target: self, 24 | action: #selector(presentSettings(sender:))) 25 | additionalLeadingNavigationBarButtonItems.append(button) 26 | } 27 | 28 | required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")} 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | 33 | observers.append( // swiftlint:disable:next discarded_notification_center_observer 34 | NotificationCenter.default.addObserver(forName: .themeChanged, 35 | object: nil, 36 | queue: nil, 37 | using: { [weak self] _ in 38 | self?.setupAppearance() 39 | }) 40 | ) 41 | setupAppearance() 42 | } 43 | 44 | override func viewDidAppear(_ animated: Bool) { 45 | super.viewDidAppear(animated) 46 | 47 | if UserDefaults.standard.alreadyShowedOnboard1 == false { 48 | present(OnboardViewController(), animated: animated) 49 | perform(#selector(showExamplesDelayed), with: nil, afterDelay: 0.5) 50 | } 51 | } 52 | 53 | func setupAppearance() { 54 | let theme = ThemeManager.shared.theme 55 | 56 | browserUserInterfaceStyle = theme.browserUserInterfaceStyle 57 | } 58 | 59 | func presentEditor(withURL url: URL) { 60 | if let presentedViewController = presentedViewController { 61 | if let editor = presentedViewController as? EditorNavigationController { 62 | editor.close { 63 | self.presentEditor(withURL: url) 64 | } 65 | } else { 66 | presentedViewController.dismiss(animated: true) { 67 | self.presentEditor(withURL: url) 68 | } 69 | } 70 | return 71 | } 72 | 73 | let editorViewController = EditorNavigationController() 74 | let document = SplashDocument(fileURL: url) 75 | 76 | editorViewController.transitioningDelegate = self 77 | let transitioningController = transitionController(forDocumentAt: url) 78 | transitioningController.targetView = editorViewController.view 79 | 80 | self.transitioningController = transitioningController 81 | 82 | editorViewController.set(document) { 83 | self.present(editorViewController, animated: true, completion: nil) 84 | } 85 | 86 | print(Thread.isMainThread) 87 | } 88 | 89 | @objc 90 | func showExamplesDelayed() { 91 | showExamplesInRecents() 92 | } 93 | 94 | func showExamplesInRecents(_ fileNames: [String]? = nil, completion: (() -> Void)? = nil) { 95 | let fileNames = fileNames ?? ["Age", "Leap Year", "Quadratic Solver"].map {$0 + ".splash"} 96 | guard let fileName = fileNames.first else { 97 | completion?() 98 | return 99 | } 100 | let remainingFiles = Array(fileNames.dropFirst()) 101 | let documentsPath = FileManager.documentsDirectory.path 102 | let examplesPath = (documentsPath as NSString).appendingPathComponent("Examples") 103 | let fullFileName = (examplesPath as NSString).appendingPathComponent(fileName) 104 | let url = URL(fileURLWithPath: fullFileName) 105 | 106 | let data = Bundle.main.dataFromResource(fileName: fileName) 107 | try! data.write(to: URL(fileURLWithPath: fullFileName)) // swiftlint:disable:this force_try 108 | 109 | revealDocument(at: url, 110 | importIfNeeded: false) { (_, _) in 111 | DispatchQueue.main.async { 112 | self.showExamplesInRecents(remainingFiles, completion: completion) 113 | } 114 | } 115 | } 116 | 117 | // MARK: - User Interaction 118 | 119 | @objc func presentSettings(sender: UIBarButtonItem?) { 120 | let viewController = SettingsNavigationController() 121 | if UIDevice.current.userInterfaceIdiom == .pad { 122 | viewController.modalPresentationStyle = .popover 123 | viewController.popoverPresentationController?.barButtonItem = sender 124 | viewController.popoverPresentationController?.permittedArrowDirections = .any 125 | viewController.popoverPresentationController?.canOverlapSourceViewRect = false 126 | self.present(viewController, animated: true, completion: nil) 127 | } else { 128 | self.present(viewController, animated: true, completion: nil) 129 | } 130 | } 131 | } 132 | 133 | extension ViewController: UIViewControllerTransitioningDelegate { 134 | func animationController(forPresented presented: UIViewController, 135 | presenting: UIViewController, 136 | source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 137 | return transitioningController! 138 | } 139 | 140 | func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 141 | return transitioningController! 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /splash_app/examples/Age.splash: -------------------------------------------------------------------------------- 1 | age := AskNumber() 2 | 3 | if age < 12 { 4 | ShowResult("Child") 5 | } else if age < 18 { 6 | ShowResult("Teen") 7 | } else if age < 60 { 8 | ShowResult("Adult") 9 | } else { 10 | ShowResult("Elder") 11 | } 12 | -------------------------------------------------------------------------------- /splash_app/examples/Leap Year.splash: -------------------------------------------------------------------------------- 1 | year := AskNumber() 2 | 3 | if year % 4 > 0 { 4 | leap := 0 5 | } else if year % 100 > 0 { 6 | leap := 1 7 | } else if year % 400 > 0 { 8 | leap := 0 9 | } else { 10 | leap := 1 11 | } 12 | 13 | if leap == 0 { 14 | type := "common" 15 | } else { 16 | type := "leap" 17 | } 18 | 19 | ShowResult("{year} is a {type} year") 20 | -------------------------------------------------------------------------------- /splash_app/examples/Quadratic Solver.splash: -------------------------------------------------------------------------------- 1 | a := AskNumber() 2 | b := AskNumber() 3 | c := AskNumber() 4 | 5 | delta := b^2 - 4 * a * c 6 | 7 | if a == 0 { 8 | x := -c/b 9 | 10 | answer := "x = {x}" 11 | } else if delta == 0 { 12 | x := -b / (2 * a) 13 | 14 | answer := "x1 = x2 = {x}" 15 | } else if delta > 0 { 16 | x1 := (-b + delta^(1/2))/(2 * a) 17 | x2 := (-b + -delta^(1/2))/(2 * a) 18 | 19 | answer := "x1 = {x1}\nx2 = {x2}" 20 | } else { 21 | xr := -b / (2 * a) 22 | xi := (-delta)^(1/2) / (2 * a) 23 | nxi := -xi 24 | 25 | answer := "x1 = {xr} + {xi}i\nx2 = {xr} + {nxi}i" 26 | } 27 | 28 | ShowResult(answer) 29 | --------------------------------------------------------------------------------