├── .gitignore ├── BookOfShaders.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ └── BookOfShaders.xcscheme ├── BookOfShaders ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── App Icon.appiconset │ │ ├── Contents.json │ │ ├── Icon-1024.png │ │ └── Icon-256@2x.png │ ├── Contents.json │ └── Shader Icon.imageset │ │ ├── Contents.json │ │ └── Shader-Icon@2x.png ├── BookOfShaders.entitlements ├── BookOfShadersApp.swift ├── Bridging.h ├── ContentView.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── ShaderEditorView.swift ├── ShaderModels.swift ├── ShaderRenderer.swift ├── ShaderTextView.swift ├── ShaderTypes.h └── Shaders │ ├── 02-hello-world.metal │ ├── 03a-uniforms-time.metal │ ├── 03b-fragment-coord.metal │ ├── 05a-shape-line.metal │ ├── 05b-shape-quintic.metal │ ├── 05c-shape-step.metal │ ├── 05d-shape-smoothstep.metal │ ├── 06a-color-mix.metal │ └── vertex.metal ├── LICENSE ├── README.md ├── Splash ├── .gitignore ├── .swiftlint.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Examples │ └── sundellsColors.css ├── Images │ ├── Code.png │ └── Logo.png ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Sources │ ├── Splash │ │ ├── Extensions │ │ │ ├── CharacterSet │ │ │ │ └── CharacterSet+Contains.swift │ │ │ ├── Equatable │ │ │ │ └── Equatable+AnyOf.swift │ │ │ ├── Int │ │ │ │ └── Int+IsOdd.swift │ │ │ ├── Sequence │ │ │ │ ├── Sequence+AnyOf.swift │ │ │ │ └── Sequence+Occurrences.swift │ │ │ └── Strings │ │ │ │ ├── String+HTMLEntities.swift │ │ │ │ ├── String+IsNumber.swift │ │ │ │ ├── String+PrefixChecking.swift │ │ │ │ ├── String+Removing.swift │ │ │ │ └── Substring+HasSuffix.swift │ │ ├── Grammar │ │ │ ├── Grammar.swift │ │ │ ├── MetalGrammar.swift │ │ │ └── SwiftGrammar.swift │ │ ├── Output │ │ │ ├── AttributedStringOutputFormat.swift │ │ │ ├── HTMLOutputFormat.swift │ │ │ ├── MarkdownDecorator.swift │ │ │ ├── OutputBuilder.swift │ │ │ └── OutputFormat.swift │ │ ├── Syntax │ │ │ ├── SyntaxHighlighter.swift │ │ │ └── SyntaxRule.swift │ │ ├── Theming │ │ │ ├── Color.swift │ │ │ ├── Font.swift │ │ │ ├── Theme+Defaults.swift │ │ │ └── Theme.swift │ │ └── Tokenizing │ │ │ ├── Segment.swift │ │ │ ├── TokenType.swift │ │ │ └── Tokenizer.swift │ ├── SplashHTMLGen │ │ └── main.swift │ ├── SplashImageGen │ │ ├── Extensions │ │ │ ├── CGImage+WriteToURL.swift │ │ │ ├── CommandLine+Options.swift │ │ │ ├── NSGraphicsContext+Fill.swift │ │ │ └── NSGraphicsContext+InitWithSize.swift │ │ └── main.swift │ ├── SplashMarkdown │ │ └── main.swift │ └── SplashTokenizer │ │ ├── TokenizerOutputFormat.swift │ │ └── main.swift └── Tests │ └── SplashTests │ ├── Core │ └── SyntaxHighlighterTestCase.swift │ ├── Mocks │ ├── OutputBuilderMock.swift │ └── OutputFormatMock.swift │ └── Tests │ ├── ClosureTests.swift │ ├── CommentTests.swift │ ├── DeclarationTests.swift │ ├── EnumTests.swift │ ├── FunctionCallTests.swift │ ├── HTMLOutputFormatTests.swift │ ├── LiteralTests.swift │ ├── MarkdownTests.swift │ ├── OptionalTests.swift │ ├── PreprocessorTests.swift │ ├── StatementTests.swift │ └── TokenTypeTests.swift └── screenshots └── 1.png /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ 2 | *.xcscmblueprint 3 | *.xccheckout 4 | .DS_Store 5 | .swiftpm 6 | .build/ 7 | Carthage/Build/ 8 | -------------------------------------------------------------------------------- /BookOfShaders.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 837A1A842A3A7EB900FF761E /* ShaderEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837A1A832A3A7EB900FF761E /* ShaderEditorView.swift */; }; 11 | 837A1AA52A3A89B300FF761E /* 05a-shape-line.metal in CopyFiles */ = {isa = PBXBuildFile; fileRef = 837A1A8F2A3A898300FF761E /* 05a-shape-line.metal */; }; 12 | 837A1AA62A3A89B300FF761E /* 05b-shape-quintic.metal in CopyFiles */ = {isa = PBXBuildFile; fileRef = 837A1A932A3A898300FF761E /* 05b-shape-quintic.metal */; }; 13 | 837A1AA72A3A89B300FF761E /* 05c-shape-step.metal in CopyFiles */ = {isa = PBXBuildFile; fileRef = 837A1AA32A3A898300FF761E /* 05c-shape-step.metal */; }; 14 | 837A1AA82A3A89B300FF761E /* 05d-shape-smoothstep.metal in CopyFiles */ = {isa = PBXBuildFile; fileRef = 837A1A912A3A898300FF761E /* 05d-shape-smoothstep.metal */; }; 15 | 837A1AA92A3A89B300FF761E /* 06a-color-mix.metal in CopyFiles */ = {isa = PBXBuildFile; fileRef = 837A1AA22A3A898300FF761E /* 06a-color-mix.metal */; }; 16 | 83F388332A37AD9B008E7395 /* BookOfShadersApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F388322A37AD9B008E7395 /* BookOfShadersApp.swift */; }; 17 | 83F388352A37AD9B008E7395 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F388342A37AD9B008E7395 /* ContentView.swift */; }; 18 | 83F388372A37AD9C008E7395 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 83F388362A37AD9C008E7395 /* Assets.xcassets */; }; 19 | 83F3883A2A37AD9C008E7395 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 83F388392A37AD9C008E7395 /* Preview Assets.xcassets */; }; 20 | 83F388492A37B083008E7395 /* Splash in Frameworks */ = {isa = PBXBuildFile; productRef = 83F388482A37B083008E7395 /* Splash */; }; 21 | 83F388A92A37E818008E7395 /* 02-hello-world.metal in CopyFiles */ = {isa = PBXBuildFile; fileRef = 83F3884A2A37C53C008E7395 /* 02-hello-world.metal */; }; 22 | 83F388AA2A37EFF1008E7395 /* 03a-uniforms-time.metal in CopyFiles */ = {isa = PBXBuildFile; fileRef = 83F3884C2A37C5E1008E7395 /* 03a-uniforms-time.metal */; }; 23 | 83F388AB2A37F004008E7395 /* 03b-fragment-coord.metal in CopyFiles */ = {isa = PBXBuildFile; fileRef = 83F3884E2A37C6AE008E7395 /* 03b-fragment-coord.metal */; }; 24 | 83F388C82A37F506008E7395 /* ShaderModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F388C72A37F506008E7395 /* ShaderModels.swift */; }; 25 | 83F388CE2A38E41F008E7395 /* vertex.metal in Sources */ = {isa = PBXBuildFile; fileRef = 83F388CC2A38E41F008E7395 /* vertex.metal */; }; 26 | 83F388D02A38E84D008E7395 /* ShaderRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F388CF2A38E84D008E7395 /* ShaderRenderer.swift */; }; 27 | 83F388E62A393702008E7395 /* ShaderTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F388E52A393702008E7395 /* ShaderTextView.swift */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXCopyFilesBuildPhase section */ 31 | 83F388A82A37E805008E7395 /* CopyFiles */ = { 32 | isa = PBXCopyFilesBuildPhase; 33 | buildActionMask = 2147483647; 34 | dstPath = ""; 35 | dstSubfolderSpec = 7; 36 | files = ( 37 | 83F388A92A37E818008E7395 /* 02-hello-world.metal in CopyFiles */, 38 | 83F388AA2A37EFF1008E7395 /* 03a-uniforms-time.metal in CopyFiles */, 39 | 83F388AB2A37F004008E7395 /* 03b-fragment-coord.metal in CopyFiles */, 40 | 837A1AA52A3A89B300FF761E /* 05a-shape-line.metal in CopyFiles */, 41 | 837A1AA62A3A89B300FF761E /* 05b-shape-quintic.metal in CopyFiles */, 42 | 837A1AA72A3A89B300FF761E /* 05c-shape-step.metal in CopyFiles */, 43 | 837A1AA82A3A89B300FF761E /* 05d-shape-smoothstep.metal in CopyFiles */, 44 | 837A1AA92A3A89B300FF761E /* 06a-color-mix.metal in CopyFiles */, 45 | ); 46 | runOnlyForDeploymentPostprocessing = 0; 47 | }; 48 | /* End PBXCopyFilesBuildPhase section */ 49 | 50 | /* Begin PBXFileReference section */ 51 | 837A1A832A3A7EB900FF761E /* ShaderEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShaderEditorView.swift; sourceTree = ""; }; 52 | 837A1A8F2A3A898300FF761E /* 05a-shape-line.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = "05a-shape-line.metal"; sourceTree = ""; }; 53 | 837A1A912A3A898300FF761E /* 05d-shape-smoothstep.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = "05d-shape-smoothstep.metal"; sourceTree = ""; }; 54 | 837A1A932A3A898300FF761E /* 05b-shape-quintic.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = "05b-shape-quintic.metal"; sourceTree = ""; }; 55 | 837A1AA22A3A898300FF761E /* 06a-color-mix.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = "06a-color-mix.metal"; sourceTree = ""; }; 56 | 837A1AA32A3A898300FF761E /* 05c-shape-step.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = "05c-shape-step.metal"; sourceTree = ""; }; 57 | 83F3882F2A37AD9B008E7395 /* BookOfShaders.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BookOfShaders.app; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | 83F388322A37AD9B008E7395 /* BookOfShadersApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookOfShadersApp.swift; sourceTree = ""; }; 59 | 83F388342A37AD9B008E7395 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 60 | 83F388362A37AD9C008E7395 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 61 | 83F388392A37AD9C008E7395 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 62 | 83F3883B2A37AD9C008E7395 /* BookOfShaders.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BookOfShaders.entitlements; sourceTree = ""; }; 63 | 83F3884A2A37C53C008E7395 /* 02-hello-world.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = "02-hello-world.metal"; sourceTree = ""; }; 64 | 83F3884C2A37C5E1008E7395 /* 03a-uniforms-time.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = "03a-uniforms-time.metal"; sourceTree = ""; }; 65 | 83F3884E2A37C6AE008E7395 /* 03b-fragment-coord.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = "03b-fragment-coord.metal"; sourceTree = ""; }; 66 | 83F388C72A37F506008E7395 /* ShaderModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShaderModels.swift; sourceTree = ""; }; 67 | 83F388C92A38C4F5008E7395 /* Splash */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Splash; sourceTree = ""; }; 68 | 83F388CC2A38E41F008E7395 /* vertex.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = vertex.metal; sourceTree = ""; }; 69 | 83F388CF2A38E84D008E7395 /* ShaderRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShaderRenderer.swift; sourceTree = ""; }; 70 | 83F388D12A38F4C6008E7395 /* Bridging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Bridging.h; sourceTree = ""; }; 71 | 83F388D22A38F4C7008E7395 /* ShaderTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShaderTypes.h; sourceTree = ""; }; 72 | 83F388E52A393702008E7395 /* ShaderTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShaderTextView.swift; sourceTree = ""; }; 73 | /* End PBXFileReference section */ 74 | 75 | /* Begin PBXFrameworksBuildPhase section */ 76 | 83F3882C2A37AD9B008E7395 /* Frameworks */ = { 77 | isa = PBXFrameworksBuildPhase; 78 | buildActionMask = 2147483647; 79 | files = ( 80 | 83F388492A37B083008E7395 /* Splash in Frameworks */, 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | /* End PBXFrameworksBuildPhase section */ 85 | 86 | /* Begin PBXGroup section */ 87 | 83F388262A37AD9B008E7395 = { 88 | isa = PBXGroup; 89 | children = ( 90 | 83F388312A37AD9B008E7395 /* BookOfShaders */, 91 | 83F388C92A38C4F5008E7395 /* Splash */, 92 | 83F388302A37AD9B008E7395 /* Products */, 93 | ); 94 | sourceTree = ""; 95 | }; 96 | 83F388302A37AD9B008E7395 /* Products */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 83F3882F2A37AD9B008E7395 /* BookOfShaders.app */, 100 | ); 101 | name = Products; 102 | sourceTree = ""; 103 | }; 104 | 83F388312A37AD9B008E7395 /* BookOfShaders */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 83F388412A37ADC3008E7395 /* Shaders */, 108 | 83F388C72A37F506008E7395 /* ShaderModels.swift */, 109 | 837A1A832A3A7EB900FF761E /* ShaderEditorView.swift */, 110 | 83F388CF2A38E84D008E7395 /* ShaderRenderer.swift */, 111 | 83F388E52A393702008E7395 /* ShaderTextView.swift */, 112 | 83F388342A37AD9B008E7395 /* ContentView.swift */, 113 | 83F388322A37AD9B008E7395 /* BookOfShadersApp.swift */, 114 | 83F388D22A38F4C7008E7395 /* ShaderTypes.h */, 115 | 83F388D12A38F4C6008E7395 /* Bridging.h */, 116 | 83F388362A37AD9C008E7395 /* Assets.xcassets */, 117 | 83F3883B2A37AD9C008E7395 /* BookOfShaders.entitlements */, 118 | 83F388382A37AD9C008E7395 /* Preview Content */, 119 | ); 120 | path = BookOfShaders; 121 | sourceTree = ""; 122 | }; 123 | 83F388382A37AD9C008E7395 /* Preview Content */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 83F388392A37AD9C008E7395 /* Preview Assets.xcassets */, 127 | ); 128 | path = "Preview Content"; 129 | sourceTree = ""; 130 | }; 131 | 83F388412A37ADC3008E7395 /* Shaders */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 83F3884A2A37C53C008E7395 /* 02-hello-world.metal */, 135 | 83F3884C2A37C5E1008E7395 /* 03a-uniforms-time.metal */, 136 | 83F3884E2A37C6AE008E7395 /* 03b-fragment-coord.metal */, 137 | 837A1A8F2A3A898300FF761E /* 05a-shape-line.metal */, 138 | 837A1A932A3A898300FF761E /* 05b-shape-quintic.metal */, 139 | 837A1AA32A3A898300FF761E /* 05c-shape-step.metal */, 140 | 837A1A912A3A898300FF761E /* 05d-shape-smoothstep.metal */, 141 | 837A1AA22A3A898300FF761E /* 06a-color-mix.metal */, 142 | 83F388CC2A38E41F008E7395 /* vertex.metal */, 143 | ); 144 | path = Shaders; 145 | sourceTree = ""; 146 | }; 147 | /* End PBXGroup section */ 148 | 149 | /* Begin PBXNativeTarget section */ 150 | 83F3882E2A37AD9B008E7395 /* BookOfShaders */ = { 151 | isa = PBXNativeTarget; 152 | buildConfigurationList = 83F3883E2A37AD9C008E7395 /* Build configuration list for PBXNativeTarget "BookOfShaders" */; 153 | buildPhases = ( 154 | 83F3882B2A37AD9B008E7395 /* Sources */, 155 | 83F3882C2A37AD9B008E7395 /* Frameworks */, 156 | 83F3882D2A37AD9B008E7395 /* Resources */, 157 | 83F388A82A37E805008E7395 /* CopyFiles */, 158 | ); 159 | buildRules = ( 160 | ); 161 | dependencies = ( 162 | ); 163 | name = BookOfShaders; 164 | packageProductDependencies = ( 165 | 83F388482A37B083008E7395 /* Splash */, 166 | ); 167 | productName = BookOfShaders; 168 | productReference = 83F3882F2A37AD9B008E7395 /* BookOfShaders.app */; 169 | productType = "com.apple.product-type.application"; 170 | }; 171 | /* End PBXNativeTarget section */ 172 | 173 | /* Begin PBXProject section */ 174 | 83F388272A37AD9B008E7395 /* Project object */ = { 175 | isa = PBXProject; 176 | attributes = { 177 | BuildIndependentTargetsInParallel = 1; 178 | LastSwiftUpdateCheck = 1500; 179 | LastUpgradeCheck = 1500; 180 | TargetAttributes = { 181 | 83F3882E2A37AD9B008E7395 = { 182 | CreatedOnToolsVersion = 15.0; 183 | LastSwiftMigration = 1500; 184 | }; 185 | }; 186 | }; 187 | buildConfigurationList = 83F3882A2A37AD9B008E7395 /* Build configuration list for PBXProject "BookOfShaders" */; 188 | compatibilityVersion = "Xcode 13.0"; 189 | developmentRegion = en; 190 | hasScannedForEncodings = 0; 191 | knownRegions = ( 192 | en, 193 | Base, 194 | ); 195 | mainGroup = 83F388262A37AD9B008E7395; 196 | packageReferences = ( 197 | 83F388472A37B083008E7395 /* XCRemoteSwiftPackageReference "Splash" */, 198 | ); 199 | productRefGroup = 83F388302A37AD9B008E7395 /* Products */; 200 | projectDirPath = ""; 201 | projectRoot = ""; 202 | targets = ( 203 | 83F3882E2A37AD9B008E7395 /* BookOfShaders */, 204 | ); 205 | }; 206 | /* End PBXProject section */ 207 | 208 | /* Begin PBXResourcesBuildPhase section */ 209 | 83F3882D2A37AD9B008E7395 /* Resources */ = { 210 | isa = PBXResourcesBuildPhase; 211 | buildActionMask = 2147483647; 212 | files = ( 213 | 83F3883A2A37AD9C008E7395 /* Preview Assets.xcassets in Resources */, 214 | 83F388372A37AD9C008E7395 /* Assets.xcassets in Resources */, 215 | ); 216 | runOnlyForDeploymentPostprocessing = 0; 217 | }; 218 | /* End PBXResourcesBuildPhase section */ 219 | 220 | /* Begin PBXSourcesBuildPhase section */ 221 | 83F3882B2A37AD9B008E7395 /* Sources */ = { 222 | isa = PBXSourcesBuildPhase; 223 | buildActionMask = 2147483647; 224 | files = ( 225 | 83F388C82A37F506008E7395 /* ShaderModels.swift in Sources */, 226 | 837A1A842A3A7EB900FF761E /* ShaderEditorView.swift in Sources */, 227 | 83F388352A37AD9B008E7395 /* ContentView.swift in Sources */, 228 | 83F388332A37AD9B008E7395 /* BookOfShadersApp.swift in Sources */, 229 | 83F388D02A38E84D008E7395 /* ShaderRenderer.swift in Sources */, 230 | 83F388E62A393702008E7395 /* ShaderTextView.swift in Sources */, 231 | 83F388CE2A38E41F008E7395 /* vertex.metal in Sources */, 232 | ); 233 | runOnlyForDeploymentPostprocessing = 0; 234 | }; 235 | /* End PBXSourcesBuildPhase section */ 236 | 237 | /* Begin XCBuildConfiguration section */ 238 | 83F3883C2A37AD9C008E7395 /* Debug */ = { 239 | isa = XCBuildConfiguration; 240 | buildSettings = { 241 | ALWAYS_SEARCH_USER_PATHS = NO; 242 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 243 | CLANG_ANALYZER_NONNULL = YES; 244 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 245 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 246 | CLANG_ENABLE_MODULES = YES; 247 | CLANG_ENABLE_OBJC_ARC = YES; 248 | CLANG_ENABLE_OBJC_WEAK = YES; 249 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 250 | CLANG_WARN_BOOL_CONVERSION = YES; 251 | CLANG_WARN_COMMA = YES; 252 | CLANG_WARN_CONSTANT_CONVERSION = YES; 253 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 254 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 255 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 256 | CLANG_WARN_EMPTY_BODY = YES; 257 | CLANG_WARN_ENUM_CONVERSION = YES; 258 | CLANG_WARN_INFINITE_RECURSION = YES; 259 | CLANG_WARN_INT_CONVERSION = YES; 260 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 261 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 262 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 263 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 264 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 265 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 266 | CLANG_WARN_STRICT_PROTOTYPES = YES; 267 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 268 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 269 | CLANG_WARN_UNREACHABLE_CODE = YES; 270 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 271 | COPY_PHASE_STRIP = NO; 272 | DEBUG_INFORMATION_FORMAT = dwarf; 273 | ENABLE_STRICT_OBJC_MSGSEND = YES; 274 | ENABLE_TESTABILITY = YES; 275 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 276 | GCC_C_LANGUAGE_STANDARD = gnu17; 277 | GCC_DYNAMIC_NO_PIC = NO; 278 | GCC_NO_COMMON_BLOCKS = YES; 279 | GCC_OPTIMIZATION_LEVEL = 0; 280 | GCC_PREPROCESSOR_DEFINITIONS = ( 281 | "DEBUG=1", 282 | "$(inherited)", 283 | ); 284 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 285 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 286 | GCC_WARN_UNDECLARED_SELECTOR = YES; 287 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 288 | GCC_WARN_UNUSED_FUNCTION = YES; 289 | GCC_WARN_UNUSED_VARIABLE = YES; 290 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 291 | MACOSX_DEPLOYMENT_TARGET = 12.0; 292 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 293 | MTL_FAST_MATH = YES; 294 | ONLY_ACTIVE_ARCH = YES; 295 | SDKROOT = macosx; 296 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 297 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 298 | }; 299 | name = Debug; 300 | }; 301 | 83F3883D2A37AD9C008E7395 /* Release */ = { 302 | isa = XCBuildConfiguration; 303 | buildSettings = { 304 | ALWAYS_SEARCH_USER_PATHS = NO; 305 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 306 | CLANG_ANALYZER_NONNULL = YES; 307 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 308 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 309 | CLANG_ENABLE_MODULES = YES; 310 | CLANG_ENABLE_OBJC_ARC = YES; 311 | CLANG_ENABLE_OBJC_WEAK = YES; 312 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 313 | CLANG_WARN_BOOL_CONVERSION = YES; 314 | CLANG_WARN_COMMA = YES; 315 | CLANG_WARN_CONSTANT_CONVERSION = YES; 316 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 317 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 318 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 319 | CLANG_WARN_EMPTY_BODY = YES; 320 | CLANG_WARN_ENUM_CONVERSION = YES; 321 | CLANG_WARN_INFINITE_RECURSION = YES; 322 | CLANG_WARN_INT_CONVERSION = YES; 323 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 324 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 325 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 326 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 327 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 328 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 329 | CLANG_WARN_STRICT_PROTOTYPES = YES; 330 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 331 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 332 | CLANG_WARN_UNREACHABLE_CODE = YES; 333 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 334 | COPY_PHASE_STRIP = NO; 335 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 336 | ENABLE_NS_ASSERTIONS = NO; 337 | ENABLE_STRICT_OBJC_MSGSEND = YES; 338 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 339 | GCC_C_LANGUAGE_STANDARD = gnu17; 340 | GCC_NO_COMMON_BLOCKS = YES; 341 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 342 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 343 | GCC_WARN_UNDECLARED_SELECTOR = YES; 344 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 345 | GCC_WARN_UNUSED_FUNCTION = YES; 346 | GCC_WARN_UNUSED_VARIABLE = YES; 347 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 348 | MACOSX_DEPLOYMENT_TARGET = 12.0; 349 | MTL_ENABLE_DEBUG_INFO = NO; 350 | MTL_FAST_MATH = YES; 351 | SDKROOT = macosx; 352 | SWIFT_COMPILATION_MODE = wholemodule; 353 | }; 354 | name = Release; 355 | }; 356 | 83F3883F2A37AD9C008E7395 /* Debug */ = { 357 | isa = XCBuildConfiguration; 358 | buildSettings = { 359 | ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon"; 360 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 361 | CLANG_ENABLE_MODULES = YES; 362 | CODE_SIGN_ENTITLEMENTS = BookOfShaders/BookOfShaders.entitlements; 363 | CODE_SIGN_STYLE = Automatic; 364 | COMBINE_HIDPI_IMAGES = YES; 365 | CURRENT_PROJECT_VERSION = 1; 366 | DEVELOPMENT_ASSET_PATHS = "\"BookOfShaders/Preview Content\""; 367 | DEVELOPMENT_TEAM = RHRJ88BAB5; 368 | ENABLE_HARDENED_RUNTIME = YES; 369 | ENABLE_PREVIEWS = YES; 370 | GENERATE_INFOPLIST_FILE = YES; 371 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 372 | LD_RUNPATH_SEARCH_PATHS = ( 373 | "$(inherited)", 374 | "@executable_path/../Frameworks", 375 | ); 376 | MARKETING_VERSION = 1.0; 377 | PRODUCT_BUNDLE_IDENTIFIER = com.metalbyexample.BookOfShaders; 378 | PRODUCT_NAME = "$(TARGET_NAME)"; 379 | SWIFT_EMIT_LOC_STRINGS = YES; 380 | SWIFT_OBJC_BRIDGING_HEADER = BookOfShaders/Bridging.h; 381 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 382 | SWIFT_VERSION = 5.0; 383 | }; 384 | name = Debug; 385 | }; 386 | 83F388402A37AD9C008E7395 /* Release */ = { 387 | isa = XCBuildConfiguration; 388 | buildSettings = { 389 | ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon"; 390 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 391 | CLANG_ENABLE_MODULES = YES; 392 | CODE_SIGN_ENTITLEMENTS = BookOfShaders/BookOfShaders.entitlements; 393 | CODE_SIGN_STYLE = Automatic; 394 | COMBINE_HIDPI_IMAGES = YES; 395 | CURRENT_PROJECT_VERSION = 1; 396 | DEVELOPMENT_ASSET_PATHS = "\"BookOfShaders/Preview Content\""; 397 | DEVELOPMENT_TEAM = RHRJ88BAB5; 398 | ENABLE_HARDENED_RUNTIME = YES; 399 | ENABLE_PREVIEWS = YES; 400 | GENERATE_INFOPLIST_FILE = YES; 401 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 402 | LD_RUNPATH_SEARCH_PATHS = ( 403 | "$(inherited)", 404 | "@executable_path/../Frameworks", 405 | ); 406 | MARKETING_VERSION = 1.0; 407 | PRODUCT_BUNDLE_IDENTIFIER = com.metalbyexample.BookOfShaders; 408 | PRODUCT_NAME = "$(TARGET_NAME)"; 409 | SWIFT_EMIT_LOC_STRINGS = YES; 410 | SWIFT_OBJC_BRIDGING_HEADER = BookOfShaders/Bridging.h; 411 | SWIFT_VERSION = 5.0; 412 | }; 413 | name = Release; 414 | }; 415 | /* End XCBuildConfiguration section */ 416 | 417 | /* Begin XCConfigurationList section */ 418 | 83F3882A2A37AD9B008E7395 /* Build configuration list for PBXProject "BookOfShaders" */ = { 419 | isa = XCConfigurationList; 420 | buildConfigurations = ( 421 | 83F3883C2A37AD9C008E7395 /* Debug */, 422 | 83F3883D2A37AD9C008E7395 /* Release */, 423 | ); 424 | defaultConfigurationIsVisible = 0; 425 | defaultConfigurationName = Release; 426 | }; 427 | 83F3883E2A37AD9C008E7395 /* Build configuration list for PBXNativeTarget "BookOfShaders" */ = { 428 | isa = XCConfigurationList; 429 | buildConfigurations = ( 430 | 83F3883F2A37AD9C008E7395 /* Debug */, 431 | 83F388402A37AD9C008E7395 /* Release */, 432 | ); 433 | defaultConfigurationIsVisible = 0; 434 | defaultConfigurationName = Release; 435 | }; 436 | /* End XCConfigurationList section */ 437 | 438 | /* Begin XCRemoteSwiftPackageReference section */ 439 | 83F388472A37B083008E7395 /* XCRemoteSwiftPackageReference "Splash" */ = { 440 | isa = XCRemoteSwiftPackageReference; 441 | repositoryURL = "git@github.com:JohnSundell/Splash.git"; 442 | requirement = { 443 | kind = upToNextMajorVersion; 444 | minimumVersion = 0.16.0; 445 | }; 446 | }; 447 | /* End XCRemoteSwiftPackageReference section */ 448 | 449 | /* Begin XCSwiftPackageProductDependency section */ 450 | 83F388482A37B083008E7395 /* Splash */ = { 451 | isa = XCSwiftPackageProductDependency; 452 | package = 83F388472A37B083008E7395 /* XCRemoteSwiftPackageReference "Splash" */; 453 | productName = Splash; 454 | }; 455 | /* End XCSwiftPackageProductDependency section */ 456 | }; 457 | rootObject = 83F388272A37AD9B008E7395 /* Project object */; 458 | } 459 | -------------------------------------------------------------------------------- /BookOfShaders.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BookOfShaders.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BookOfShaders.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /BookOfShaders.xcodeproj/xcshareddata/xcschemes/BookOfShaders.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /BookOfShaders/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.891", 9 | "green" : "0.519", 10 | "red" : "0.849" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "light" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.759", 27 | "green" : "0.291", 28 | "red" : "0.707" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | }, 33 | { 34 | "appearances" : [ 35 | { 36 | "appearance" : "luminosity", 37 | "value" : "dark" 38 | } 39 | ], 40 | "color" : { 41 | "color-space" : "srgb", 42 | "components" : { 43 | "alpha" : "1.000", 44 | "blue" : "0.891", 45 | "green" : "0.519", 46 | "red" : "0.849" 47 | } 48 | }, 49 | "idiom" : "universal" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /BookOfShaders/Assets.xcassets/App Icon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "filename" : "Icon-256@2x.png", 40 | "idiom" : "mac", 41 | "scale" : "2x", 42 | "size" : "256x256" 43 | }, 44 | { 45 | "idiom" : "mac", 46 | "scale" : "1x", 47 | "size" : "512x512" 48 | }, 49 | { 50 | "filename" : "Icon-1024.png", 51 | "idiom" : "mac", 52 | "scale" : "2x", 53 | "size" : "512x512" 54 | } 55 | ], 56 | "info" : { 57 | "author" : "xcode", 58 | "version" : 1 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /BookOfShaders/Assets.xcassets/App Icon.appiconset/Icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metal-by-example/book-of-shaders-metal/12bb2366697cba9c5f660d54fead7bdcd73b6b8a/BookOfShaders/Assets.xcassets/App Icon.appiconset/Icon-1024.png -------------------------------------------------------------------------------- /BookOfShaders/Assets.xcassets/App Icon.appiconset/Icon-256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metal-by-example/book-of-shaders-metal/12bb2366697cba9c5f660d54fead7bdcd73b6b8a/BookOfShaders/Assets.xcassets/App Icon.appiconset/Icon-256@2x.png -------------------------------------------------------------------------------- /BookOfShaders/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /BookOfShaders/Assets.xcassets/Shader Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Shader-Icon@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /BookOfShaders/Assets.xcassets/Shader Icon.imageset/Shader-Icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metal-by-example/book-of-shaders-metal/12bb2366697cba9c5f660d54fead7bdcd73b6b8a/BookOfShaders/Assets.xcassets/Shader Icon.imageset/Shader-Icon@2x.png -------------------------------------------------------------------------------- /BookOfShaders/BookOfShaders.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /BookOfShaders/BookOfShadersApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct BookOfShadersApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | .environmentObject(ShaderEditorModel()) 9 | .navigationTitle("Book of Shaders") 10 | } 11 | .commands { 12 | SidebarCommands() 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BookOfShaders/Bridging.h: -------------------------------------------------------------------------------- 1 | #import "ShaderTypes.h" 2 | -------------------------------------------------------------------------------- /BookOfShaders/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContentView: View { 4 | @EnvironmentObject var editorModel: ShaderEditorModel 5 | 6 | var body: some View { 7 | NavigationView { 8 | List { 9 | ForEach(editorModel.exampleStore.sections) { section in 10 | Section(section.title) { 11 | ForEach(section.examples) { example in 12 | NavigationLink(example.title, 13 | destination: ShaderEditorView(sourceString: $editorModel.sourceString, 14 | editorModel: editorModel), 15 | tag: example.id, 16 | selection: $editorModel.selectedExampleID) 17 | } 18 | } 19 | } 20 | } 21 | .listStyle(.sidebar) 22 | .frame(idealWidth: 225) 23 | Text("Select a shader") 24 | } 25 | .toolbar { 26 | ToolbarItem(placement: .navigation) { 27 | Button(action: toggleSidebar, label: { 28 | Image(systemName: "sidebar.leading") 29 | }) 30 | } 31 | } 32 | } 33 | 34 | private func toggleSidebar() { 35 | NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), 36 | with: nil) 37 | } 38 | } 39 | 40 | struct ContentView_Previews: PreviewProvider { 41 | static var previews: some View { 42 | ContentView() 43 | .environmentObject(ShaderEditorModel()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /BookOfShaders/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /BookOfShaders/ShaderEditorView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | import MetalKit 4 | import Splash 5 | 6 | class ShaderEditorModel: ObservableObject { 7 | @Published var selectedExampleID: String? { 8 | didSet { 9 | if let selectedExampleID, 10 | let example = exampleStore.example(for: selectedExampleID), 11 | let source = example.fragmentShaderSource 12 | { 13 | renderer.example = example 14 | renderer.fragmentFunctionSource = example.fragmentShaderSource 15 | sourceString = sourceHighlighter.highlight(source) 16 | } 17 | } 18 | } 19 | 20 | @Published var sourceString = NSAttributedString(string: "") { 21 | didSet { 22 | renderer.fragmentFunctionSource = sourceString.string 23 | } 24 | } 25 | 26 | let theme: Theme 27 | let font = Splash.Font(name: "Monaco", size: 12.0) 28 | let grammar = MetalGrammar() 29 | let sourceHighlighter: SyntaxHighlighter 30 | let exampleStore = ShaderExampleStore() 31 | let device: MTLDevice 32 | let renderDelegate: MTKViewDelegate 33 | 34 | private let renderer: ShaderRenderer 35 | 36 | init() { 37 | device = MTLCreateSystemDefaultDevice()! 38 | renderer = ShaderRenderer(device: device) 39 | renderDelegate = renderer 40 | 41 | theme = Theme.wwdc18(withFont: font) 42 | sourceHighlighter = SyntaxHighlighter(format: AttributedStringOutputFormat(theme: theme), 43 | grammar: grammar) 44 | 45 | defer { 46 | // Auto-select the first available example 47 | selectedExampleID = exampleStore.sections.first?.examples.first?.id 48 | } 49 | } 50 | } 51 | 52 | struct MetalView : NSViewRepresentable { 53 | typealias NSViewType = MTKView 54 | 55 | let device: MTLDevice 56 | let delegate: MTKViewDelegate 57 | 58 | func makeNSView(context: Context) -> MTKView { 59 | let view = MTKView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 60 | view.device = device 61 | view.delegate = delegate 62 | return view 63 | } 64 | 65 | func updateNSView(_ nsView: MTKView, context: Context) {} 66 | } 67 | 68 | struct ShaderEditorView: View { 69 | @StateObject var context = ShaderTextEditorContext() 70 | @Binding var sourceString: NSAttributedString 71 | 72 | let device: MTLDevice 73 | let renderDelegate: MTKViewDelegate 74 | let theme: Splash.Theme 75 | let sourceHighlighter: SyntaxHighlighter 76 | 77 | init(sourceString: Binding, editorModel: ShaderEditorModel) { 78 | self._sourceString = sourceString 79 | self.device = editorModel.device 80 | self.renderDelegate = editorModel.renderDelegate 81 | self.theme = editorModel.theme 82 | self.sourceHighlighter = editorModel.sourceHighlighter 83 | } 84 | 85 | var body: some View { 86 | ZStack(alignment: .topTrailing) { 87 | ShaderTextEditor(text: $sourceString, context: context) { textView in 88 | textView.backgroundColor = theme.backgroundColor 89 | textView.insertionPointColor = NSColor.white 90 | } 91 | .onChange(of: context.attributedString, perform: { newContents in 92 | guard let newString = newContents?.string else { return } 93 | // Re-highlight text on every keystroke. This might look like 94 | // it leads to an infinite loop, but updates via the context 95 | // are designed not to cause changes to be published back to us 96 | context.attributedString = sourceHighlighter.highlight(newString) 97 | }) 98 | MetalView(device: device, delegate: renderDelegate) 99 | .frame(width: 200.0, height: 200.0) 100 | .cornerRadius(4.0) 101 | .overlay( 102 | RoundedRectangle(cornerRadius: 5.0) 103 | .inset(by: -1.0) 104 | .stroke(.white, lineWidth: 2.0) 105 | ) 106 | .padding(EdgeInsets(top: 5.0, leading: 0.0, bottom: 0.0, trailing: 20.0)) 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /BookOfShaders/ShaderModels.swift: -------------------------------------------------------------------------------- 1 | 2 | import SwiftUI 3 | 4 | struct ShaderExample : Identifiable { 5 | var id: String { return title } 6 | let title: String 7 | let fileName: String 8 | let entryPoint: String = "fragment_main" 9 | 10 | var fragmentShaderSource: String? { 11 | if let sourceURL = Bundle.main.url(forResource: fileName, withExtension: "metal") { 12 | return try? String(contentsOf: sourceURL, encoding: .utf8) 13 | } 14 | return nil 15 | } 16 | } 17 | 18 | struct ShaderExampleSection : Identifiable { 19 | var id: String { return title } 20 | let title: String 21 | let examples: [ShaderExample] 22 | } 23 | 24 | class ShaderExampleStore : ObservableObject { 25 | @Published var sections : [ShaderExampleSection] = [ 26 | ShaderExampleSection(title: "Hello World", examples: [ 27 | ShaderExample(title: "Solid Color", fileName: "02-hello-world") 28 | ]), 29 | ShaderExampleSection(title: "Uniforms", examples: [ 30 | ShaderExample(title: "Time", fileName: "03a-uniforms-time"), 31 | ShaderExample(title: "Fragment Coordinates", fileName: "03b-fragment-coord") 32 | ]), 33 | ShaderExampleSection(title: "Shaping Functions", examples: [ 34 | ShaderExample(title: "Line", fileName:"05a-shape-line"), 35 | ShaderExample(title: "Quintic Curve", fileName:"05b-shape-quintic"), 36 | ShaderExample(title: "Step", fileName:"05c-shape-step"), 37 | ShaderExample(title: "Smoothstep", fileName:"05d-shape-smoothstep") 38 | ]), 39 | ShaderExampleSection(title: "Colors", examples: [ 40 | ShaderExample(title: "Mixing Colors", fileName:"06a-color-mix"), 41 | //ShaderExample(title: "Color Gradients", fileName:"06b-color-gradient"), 42 | //ShaderExample(title: "HSB Color Space", fileName:"06c-color-hsb"), 43 | //ShaderExample(title: "HSB in Polar Coordinates", fileName:"06d-color-polar") 44 | ]), 45 | /* 46 | ShaderExampleSection(title: "Shapes", examples: [ 47 | ShaderExample(title: "Rectangle", fileName: "07a-shape-rectangle"), 48 | ShaderExample(title: "Circle", fileName: "07b-shape-circle"), 49 | ShaderExample(title: "Circle SDF", fileName: "07c-sdf-circle"), 50 | ShaderExample(title: "Round Rect", fileName: "07d-sdf-circles"), 51 | ShaderExample(title: "Polar Shapes", fileName: "07e-sdf-lobes"), 52 | ShaderExample(title: "Triangle", fileName: "07f-sdf-triangle") 53 | ]), 54 | ShaderExampleSection(title: "Matrices", examples: [ 55 | ShaderExample(title: "Translate", fileName: "08a-matrix-translate"), 56 | ShaderExample(title: "Rotate", fileName: "08b-matrix-rotate"), 57 | ShaderExample(title: "Scale", fileName: "08c-matrix-scale"), 58 | ShaderExample(title: "YUV Color Space", fileName: "08d-matrix-yuv") 59 | ]), 60 | ShaderExampleSection(title: "Patterns", examples: [ 61 | ShaderExample(title: "Spaces", fileName: "09a-pattern-spaces"), 62 | ShaderExample(title: "Squares", fileName: "09b-pattern-squares"), 63 | ShaderExample(title: "Bricks", fileName: "09c-pattern-bricks"), 64 | ShaderExample(title: "Tiles", fileName: "09d-pattern-tiles"), 65 | ]), 66 | ShaderExampleSection(title: "Random", examples: [ 67 | ShaderExample(title: "Random", fileName: "10a-random"), 68 | ShaderExample(title: "Random Grid", fileName: "10b-random-grid"), 69 | ShaderExample(title: "Random Truchet Tiles", fileName: "10c-random-truchet") 70 | ]), 71 | ShaderExampleSection(title: "Noise", examples: [ 72 | ShaderExample(title: "Noise", fileName: "11a-noise"), 73 | ShaderExample(title: "Simplex Noise", fileName: "11b-noise-simplex") 74 | ]), 75 | ShaderExampleSection(title: "Cellular Noise", examples: [ 76 | ShaderExample(title: "Point Distance", fileName: "12a-point-distance"), 77 | ShaderExample(title: "Cellular Noise", fileName: "12b-cellular-noise"), 78 | ShaderExample(title: "Voronoi", fileName: "12c-voronoi") 79 | ]), 80 | ShaderExampleSection(title: "Fractal Brownian Motion", examples: [ 81 | ShaderExample(title: "fBm", fileName: "13a-fbm"), 82 | ShaderExample(title: "Domain Warping", fileName: "13b-fbm-domain-warping") 83 | ]) 84 | */ 85 | ] 86 | 87 | func example(for id: String) -> ShaderExample? { 88 | // Linear scan isn't great, but given that we'll never have more than a few dozen examples, it's fine. 89 | for section in sections { 90 | for example in section.examples { 91 | if example.id == id { 92 | return example 93 | } 94 | } 95 | } 96 | return nil 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /BookOfShaders/ShaderRenderer.swift: -------------------------------------------------------------------------------- 1 | import MetalKit 2 | 3 | class ShaderRenderer : NSObject, MTKViewDelegate { 4 | let device: MTLDevice 5 | let commandQueue: MTLCommandQueue 6 | private var renderPipelineState: MTLRenderPipelineState? 7 | private let defaultLibrary: MTLLibrary 8 | 9 | var sceneTime: TimeInterval = 0.0 10 | var lastRenderTime: TimeInterval 11 | 12 | var example: ShaderExample? { 13 | didSet { 14 | renderPipelineState = nil 15 | } 16 | } 17 | 18 | var fragmentFunctionSource: String? { 19 | didSet { 20 | renderPipelineState = nil 21 | } 22 | } 23 | 24 | private var lastFragmentFunctionSource: String? = nil 25 | 26 | init(device: MTLDevice) { 27 | self.device = device 28 | self.commandQueue = device.makeCommandQueue()! 29 | lastRenderTime = CACurrentMediaTime() 30 | guard let defaultLibrary = device.makeDefaultLibrary() else { 31 | fatalError("Unable to create default Metal library") 32 | } 33 | self.defaultLibrary = defaultLibrary 34 | 35 | super.init() 36 | } 37 | 38 | func makePipeline(view: MTKView) { 39 | guard let fragmentShaderSource = fragmentFunctionSource else { return } 40 | guard let fragmentEntryPoint = example?.entryPoint else { return } 41 | 42 | // Whatever the outcome of the previous compilation, don't recompile if nothing's changed. 43 | if (lastFragmentFunctionSource == fragmentShaderSource) { 44 | return 45 | } 46 | 47 | var fragmentLibrary: MTLLibrary? = nil 48 | do { 49 | fragmentLibrary = try device.makeLibrary(source: fragmentShaderSource, options: nil) 50 | } catch { 51 | let nsError = error as NSError 52 | print("\(nsError.localizedDescription)") 53 | lastFragmentFunctionSource = fragmentShaderSource 54 | return 55 | } 56 | 57 | let renderPipelineDescriptor = MTLRenderPipelineDescriptor() 58 | renderPipelineDescriptor.colorAttachments[0].pixelFormat = view.colorPixelFormat 59 | 60 | guard let vertexFunction = defaultLibrary.makeFunction(name: "vertex_main") else { return } 61 | guard let fragmentFunction = fragmentLibrary?.makeFunction(name: fragmentEntryPoint) else { return } 62 | renderPipelineDescriptor.vertexFunction = vertexFunction 63 | renderPipelineDescriptor.fragmentFunction = fragmentFunction 64 | 65 | do { 66 | renderPipelineState = try device.makeRenderPipelineState(descriptor: renderPipelineDescriptor) 67 | } catch { 68 | print("Error while creating render pipeline state: \(error)") 69 | } 70 | } 71 | 72 | // MARK: - MTKViewDelegate 73 | 74 | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { 75 | } 76 | 77 | func draw(in view: MTKView) { 78 | if renderPipelineState == nil { 79 | makePipeline(view: view) 80 | } 81 | 82 | guard let renderPipelineState else { 83 | return 84 | } 85 | 86 | guard let renderPassDescriptor = view.currentRenderPassDescriptor else { return } 87 | 88 | guard let commandBuffer = commandQueue.makeCommandBuffer() else { return } 89 | 90 | var mouseX: Float = 0.0, mouseY: Float = 0.0 91 | if let currentMouseLocation = view.window?.mouseLocationOutsideOfEventStream { 92 | var mouseLocationInView = view.convert(currentMouseLocation, from: nil) 93 | mouseLocationInView = view.convertToBacking(mouseLocationInView) 94 | mouseX = Float(mouseLocationInView.x) 95 | mouseY = Float(mouseLocationInView.y) 96 | } 97 | 98 | let currentTime = CACurrentMediaTime() 99 | let timestep = currentTime - lastRenderTime 100 | 101 | var uniforms = Uniforms(resolution: SIMD2(Float(view.drawableSize.width), 102 | Float(view.drawableSize.height)), 103 | mouse: SIMD2(mouseX, mouseY), 104 | time: Float(sceneTime)) 105 | 106 | let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! 107 | renderCommandEncoder.setRenderPipelineState(renderPipelineState) 108 | renderCommandEncoder.setFragmentBytes(&uniforms, length: MemoryLayout.stride, index: 0) 109 | renderCommandEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3) 110 | renderCommandEncoder.endEncoding() 111 | 112 | commandBuffer.present(view.currentDrawable!) 113 | commandBuffer.commit() 114 | 115 | sceneTime += timestep 116 | lastRenderTime = currentTime 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /BookOfShaders/ShaderTextView.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import AppKit 4 | import Combine 5 | import SwiftUI 6 | 7 | class ShaderTextView: NSTextView { 8 | func setup(_ initialText: NSAttributedString) { 9 | attributedString = initialText 10 | allowsImageEditing = false 11 | allowsUndo = true 12 | backgroundColor = .clear 13 | layoutManager?.defaultAttachmentScaling = .scaleProportionallyDown 14 | setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 15 | } 16 | 17 | var attributedString: NSAttributedString { 18 | get { attributedString() } 19 | set { textStorage?.setAttributedString(newValue) } 20 | } 21 | 22 | var isFirstResponder: Bool { 23 | window?.firstResponder == self 24 | } 25 | } 26 | 27 | class ShaderTextEditorContext: ObservableObject { 28 | @Published var attributedString: NSAttributedString? 29 | } 30 | 31 | class ShaderTextViewDelegate: NSObject, NSTextViewDelegate { 32 | public init( 33 | text: Binding, 34 | textView: ShaderTextView, 35 | context: ShaderTextEditorContext 36 | ) { 37 | textView.attributedString = text.wrappedValue 38 | self.text = text 39 | self.textView = textView 40 | self.context = context 41 | super.init() 42 | self.textView.delegate = self 43 | subscribeToContextChanges() 44 | } 45 | 46 | public let context: ShaderTextEditorContext 47 | 48 | public var text: Binding 49 | 50 | var textView: ShaderTextView 51 | 52 | public var cancellables = Set() 53 | 54 | func subscribeToContextChanges() { 55 | context.$attributedString.sink(receiveCompletion: { _ in }, 56 | receiveValue: { [weak self] in 57 | let selection = self?.textView.selectedRange() 58 | self?.setAttributedString(to: $0) 59 | if let selection { 60 | self?.textView.selectedRange = selection 61 | } 62 | }).store(in: &cancellables) 63 | } 64 | 65 | func syncContextWithTextView() { 66 | // This is admittedly janky, but it cuts down on flicker 67 | DispatchQueue.main.async { 68 | self.syncContextWithTextViewImmediate() 69 | } 70 | } 71 | 72 | func syncContextWithTextViewImmediate() { 73 | context.attributedString = self.textView.attributedString 74 | } 75 | 76 | func setAttributedString(to newValue: NSAttributedString?) { 77 | guard let newValue else { return } 78 | textView.attributedString = newValue 79 | text.wrappedValue = newValue 80 | } 81 | 82 | func textDidChange(_ notification: Notification) { 83 | syncContextWithTextView() 84 | } 85 | } 86 | 87 | struct ShaderTextEditor : NSViewRepresentable { 88 | typealias ViewUserConfiguration = (NSTextView) -> Void 89 | 90 | public let scrollView = ShaderTextView.scrollableTextView() 91 | 92 | public var textView: ShaderTextView { 93 | scrollView.documentView as? ShaderTextView ?? ShaderTextView() 94 | } 95 | 96 | private var text: Binding 97 | 98 | @ObservedObject 99 | private var context: ShaderTextEditorContext 100 | 101 | private var userConfiguration: ViewUserConfiguration 102 | 103 | public init( 104 | text: Binding, 105 | context: ShaderTextEditorContext, 106 | userConfiguration: @escaping ViewUserConfiguration = { _ in } 107 | ) { 108 | self.text = text 109 | self._context = ObservedObject(wrappedValue: context) 110 | self.userConfiguration = userConfiguration 111 | } 112 | 113 | func makeNSView(context: Context) -> some NSView { 114 | textView.setup(text.wrappedValue) 115 | userConfiguration(textView) 116 | return scrollView 117 | } 118 | 119 | func updateNSView(_ nsView: NSViewType, context: Context) { 120 | } 121 | 122 | func makeCoordinator() -> ShaderTextViewDelegate { 123 | return ShaderTextViewDelegate(text: text, 124 | textView: textView, 125 | context: context) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /BookOfShaders/ShaderTypes.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | #include 4 | 5 | typedef struct Uniforms { 6 | simd_float2 resolution; 7 | simd_float2 mouse; 8 | float time; 9 | } Uniforms; 10 | -------------------------------------------------------------------------------- /BookOfShaders/Shaders/02-hello-world.metal: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace metal; 3 | 4 | [[fragment]] 5 | float4 fragment_main() { 6 | // Return a solid color for every pixel (magenta by default). 7 | // The fourth component of the color is alpha, representing 8 | // opacity. It has no effect because blending isn't enabled. 9 | // But try changing the other values to change the color! 10 | return float4(1.0f, 0.0f, 1.0f, 1.0f); 11 | } 12 | -------------------------------------------------------------------------------- /BookOfShaders/Shaders/03a-uniforms-time.metal: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace metal; 3 | 4 | struct Uniforms { 5 | float2 resolution; 6 | float2 mouse; 7 | float time; 8 | }; 9 | 10 | [[fragment]] 11 | float4 fragment_main(constant Uniforms &uniforms [[buffer(0)]]) 12 | { 13 | // The time value is populated by the CPU every frame, so 14 | // it can be used to animate. In this case, we take the 15 | // sine of the time value to get a value that oscillates 16 | // between -1 and 1, then take its absolute value to get 17 | // a value that "bounces" at 0 and peaks at 1. 18 | return float4(abs(sin(uniforms.time)), 0.0f, 0.0f, 1.0f); 19 | } 20 | -------------------------------------------------------------------------------- /BookOfShaders/Shaders/03b-fragment-coord.metal: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace metal; 3 | 4 | struct Uniforms { 5 | float2 resolution; 6 | float2 mouse; 7 | float time; 8 | }; 9 | 10 | struct FragmentIn { 11 | float4 position [[position]]; 12 | float2 st; 13 | }; 14 | 15 | [[fragment]] 16 | float4 fragment_main(FragmentIn in [[stage_in]], 17 | constant Uniforms &uniforms [[buffer(0)]]) 18 | { 19 | // We divide the fragment's position, which is in viewport 20 | // coordinates, by the viewport dimensions to get a set of 21 | // coordinates in the range (0, 1). We then invert the y 22 | // coordinate because in Metal, the origin of texture space 23 | // is in the upper left, while in OpenGL, the origin is 24 | // in the bottom left. The resulting value is the same as 25 | // the one we get from the vertex shader (in.st), but this 26 | // is another way of calculating it when you don't have 27 | // access to interpolated coordinates. 28 | float2 st = in.position.xy / uniforms.resolution; 29 | st.y = 1.0f - st.y; 30 | 31 | return float4(st, 0.0f, 1.0f); 32 | } 33 | -------------------------------------------------------------------------------- /BookOfShaders/Shaders/05a-shape-line.metal: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace metal; 3 | 4 | struct Uniforms { 5 | float2 resolution; 6 | float2 mouse; 7 | float time; 8 | }; 9 | 10 | struct FragmentIn { 11 | float4 position [[position]]; 12 | float2 st; 13 | }; 14 | 15 | float line(float2 st, float halfWidth) { 16 | // The expression st.y - st.x is exactly 0 on the line y = x. 17 | // Since a line is infinitely thin, we use smoothstep to blend 18 | // from 1 along the centerline to 0 a short distance away, 19 | // thus thickening the line. 20 | float v = st.y - st.x; 21 | return smoothstep(-halfWidth, 0.0f, v) - 22 | smoothstep(0.0f, halfWidth, v); 23 | } 24 | 25 | [[fragment]] 26 | float4 fragment_main(FragmentIn in [[stage_in]], 27 | constant Uniforms &uniforms [[buffer(0)]]) 28 | { 29 | float2 st = in.st; 30 | float y = st.x; 31 | 32 | // Start by shading the background with a horizontal gray gradient 33 | float3 color = float3(y); 34 | 35 | // Calculate the coverage of the line 36 | float lineCoverage = line(st, 0.01f); 37 | 38 | // Manually blend (interpolate) from the background gradient to 39 | // the line based on how close we are to the line. 40 | color = (1.0f - lineCoverage) * color + 41 | lineCoverage * float3(0.0f, 1.0f, 0.0f); 42 | 43 | return float4(color, 1.0f); 44 | } 45 | -------------------------------------------------------------------------------- /BookOfShaders/Shaders/05b-shape-quintic.metal: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace metal; 3 | 4 | struct Uniforms { 5 | float2 resolution; 6 | float2 mouse; 7 | float time; 8 | }; 9 | 10 | struct FragmentIn { 11 | float4 position [[position]]; 12 | float2 st; 13 | }; 14 | 15 | float plot(float2 st, float f, float halfWidth) { 16 | // We can plot arbitrary functions just like we did with our line. 17 | // First, we find the vertical distance from the function to the 18 | // current pixel's y coordinate; this is 0 along the function. 19 | float d = f - st.y; 20 | // The difference of two smoothstep calls is a smoothed square pulse, 21 | // which widens a curve of infinite thinness into a plot we can see. 22 | return smoothstep(-halfWidth, 0.0f, d) - 23 | smoothstep(0.0f, halfWidth, d); 24 | } 25 | 26 | [[fragment]] 27 | float4 fragment_main(FragmentIn in [[stage_in]], 28 | constant Uniforms &uniforms [[buffer(0)]]) 29 | { 30 | float2 st = in.st; 31 | st = st * 2.0f - 1.0f; // Transform x and y to span from -1 to 1 32 | 33 | // We perform exponentiation by using the powr() function. 34 | // A fifth-order polynomial like x^5 is called a quintic. 35 | float y = powr(st.x, 5.0f); 36 | 37 | // First we plot the background gradient which shows the 38 | // magnitude of x^5 for each x as a grayscale gradient. 39 | float3 color = float3(abs(y)); 40 | 41 | // Then we blend the function plot on top 42 | float coverage = plot(st, y, 0.025f); 43 | color = (1.0f - coverage) * color + coverage * float3(0.0f, 1.0f, 0.0f); 44 | 45 | return float4(color, 1.0f); 46 | } 47 | -------------------------------------------------------------------------------- /BookOfShaders/Shaders/05c-shape-step.metal: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace metal; 3 | 4 | struct Uniforms { 5 | float2 resolution; 6 | float2 mouse; 7 | float time; 8 | }; 9 | 10 | struct FragmentIn { 11 | float4 position [[position]]; 12 | float2 st; 13 | }; 14 | 15 | float plot(float2 st, float f, float halfWidth) { 16 | float d = f - st.y; 17 | return smoothstep(-halfWidth, 0.0f, d) - 18 | smoothstep(0.0f, halfWidth, d); 19 | } 20 | 21 | [[fragment]] 22 | float4 fragment_main(FragmentIn in [[stage_in]], 23 | constant Uniforms &uniforms [[buffer(0)]]) 24 | { 25 | float2 st = in.st; 26 | 27 | // The step() function returns 0 if its second argument is 28 | // less than its first argument, and 1 otherwise. 29 | float y = step(0.5f, st.x); 30 | 31 | float3 color = float3(y); 32 | 33 | float coverage = plot(st, y, 0.02f); 34 | color = (1.0f - coverage) * color + coverage * float3(0.0f, 1.0f, 0.0f); 35 | 36 | return float4(color, 1.0f); 37 | } 38 | -------------------------------------------------------------------------------- /BookOfShaders/Shaders/05d-shape-smoothstep.metal: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace metal; 3 | 4 | struct Uniforms { 5 | float2 resolution; 6 | float2 mouse; 7 | float time; 8 | }; 9 | 10 | struct FragmentIn { 11 | float4 position [[position]]; 12 | float2 st; 13 | }; 14 | 15 | float plot(float2 st, float f, float width) { 16 | float d = f - st.y; 17 | return smoothstep(-width * 0.5f, 0.0f, d) - 18 | smoothstep(0.0f, width * 0.5f, d); 19 | } 20 | 21 | [[fragment]] 22 | float4 fragment_main(FragmentIn in [[stage_in]], 23 | constant Uniforms &uniforms [[buffer(0)]]) 24 | { 25 | float2 st = in.st; 26 | 27 | // We've been using smoothstep to plot other functions, 28 | // now we use it to plot the smoothstep() function itself. 29 | // Smoothstep smoothly interpolates from 0 to 1 when its 30 | // third argument is between its first two arguments. 31 | float y = smoothstep(0.1f, 0.9f, st.x); 32 | 33 | float3 color = float3(y); 34 | 35 | float coverage = plot(st, y, 0.03f); 36 | color = (1.0f - coverage) * color + 37 | coverage * float3(0.0f, 1.0f, 0.0f); 38 | 39 | return float4(color, 1.0f); 40 | } 41 | -------------------------------------------------------------------------------- /BookOfShaders/Shaders/06a-color-mix.metal: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace metal; 3 | 4 | struct Uniforms { 5 | float2 resolution; 6 | float2 mouse; 7 | float time; 8 | }; 9 | 10 | struct FragmentIn { 11 | float4 position [[position]]; 12 | float2 st; 13 | }; 14 | 15 | constant float3 colorA { 0.000f, 0.129f, 0.647f }; 16 | constant float3 colorB { 0.980f, 0.275f, 0.090f }; 17 | 18 | [[fragment]] 19 | float4 fragment_main(FragmentIn in [[stage_in]], 20 | constant Uniforms &uniforms [[buffer(0)]]) 21 | { 22 | // Vary the proportion of colors as a function of time. 23 | // Sine oscillates between -1 and 1 so we scale and offset 24 | // to get a value ranging from 0 to 1. 25 | float fraction =sin(uniforms.time) * 0.5 + 0.5f; 26 | 27 | // The mix() function linearly interpolates between its 28 | // first two arguments based on its third argument (0-1). 29 | float3 color = mix(colorA, colorB, fraction); 30 | 31 | return float4(color,1.0); 32 | } 33 | -------------------------------------------------------------------------------- /BookOfShaders/Shaders/vertex.metal: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | using namespace metal; 4 | 5 | struct VertexOut { 6 | float4 position [[position]]; 7 | float2 st; 8 | }; 9 | 10 | vertex VertexOut vertex_main(uint vertexID [[vertex_id]]) 11 | { 12 | float2 positions[] = { float2(-1.0f, 1.0f), float2(-1.0f, -3.0f), float2(3.0f, 1.0f) }; 13 | float2 texCoords[] = { float2(0.0f, 0.0f), float2(0.0f, 2.0f), float2(2.0f, 0.0f) }; 14 | 15 | float4 clipPosition = float4(positions[vertexID], 0.0f, 1.0f); 16 | float2 texCoord = texCoords[vertexID]; 17 | texCoord.y = 1.0f - texCoord.y; // Flip to GL convention 18 | 19 | VertexOut out { 20 | .position = clipPosition, 21 | .st = texCoord, 22 | }; 23 | 24 | return out; 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Warren Moore 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Book of Shaders Metal Companion 2 | 3 | This project is a live-coding environment for Metal fragment shaders intended to accompany [The Book of Shaders](https://thebookofshaders.com). It includes a rudimentary editor and live Metal viewport that recompiles and renders your shaders as you type. 4 | 5 | ![Editor screenshot](screenshots/1.png) 6 | 7 | ## Project Status 8 | 9 | This project is not intended for serious shader work and should be regarded as a novelty. It contains no facilities for loading or exporting shaders, nor does the UI communicate shader compiler errors, so it is not especially useful for except for the most casual use. Furthermore, because of the copyright status of the original project, most shaders are not included (though they are referenced in the ShaderModels.swift file in case you want to add them yourself). 10 | 11 | No further development or support of this project should be expected. 12 | 13 | ## Metal Shader Porting Tips 14 | 15 | Broadly speaking, Metal Shading Language (MSL) is syntactically similar to GLSL, but there are a few differences. 16 | 17 | - GLSL matrix and vector types such as `mat3` and `vec3` should be converted to their respective Metal types (e.g., `float3x3` and `float3`) 18 | - Metal's `fmod` function differs subtly from GLSL's `mod` function and cannot be used as a direct substitute. Instead, use the following replacement: 19 | 20 | ```c++ 21 | float mod(float x, float y) { 22 | return x - y * floor(x / y); 23 | } 24 | ``` 25 | 26 | - Be careful when using C++ uniform initialization with Metal's matrix types. To specify a column-major matrix, place each column's elements in its own braces, e.g.: 27 | 28 | ```c++ 29 | float2x2 m = { { 1.0f, 0.0f }, { 0.0f, 1.0f } }; 30 | ``` 31 | 32 | - MSL's `powr` function is a closer match to GLSL's `pow` function than MSL's `pow` function and `powr` should be used whenever the first argument is known to be non-negative. 33 | -------------------------------------------------------------------------------- /Splash/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /.swiftpm 5 | /*.xcodeproj 6 | Package.resolved 7 | -------------------------------------------------------------------------------- /Splash/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - nesting 3 | line_length: 200 4 | function_body_length: 100 5 | type_body_length: 600 6 | file_length: 800 7 | cyclomatic_complexity: 15 8 | -------------------------------------------------------------------------------- /Splash/.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | language: generic 3 | sudo: required 4 | dist: trusty 5 | env: 6 | - SWIFT_VERSION=5.2 7 | install: 8 | - eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" 9 | script: 10 | - swift test 11 | -------------------------------------------------------------------------------- /Splash/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Splash Code of Conduct 2 | 3 | Below is the Code of Conduct that all contributors and participants in the Splash community are expected to adhere to. 4 | It's adopted from the [Contributor Covenant Code of Conduct][homepage]. 5 | 6 | ## Our Pledge 7 | 8 | In the interest of fostering an open and welcoming environment, we as 9 | contributors and maintainers pledge to making participation in our project and 10 | our community a harassment-free experience for everyone, regardless of age, body 11 | size, disability, ethnicity, gender identity and expression, level of experience, 12 | nationality, personal appearance, race, religion, or sexual identity and 13 | orientation. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to creating a positive environment 18 | include: 19 | 20 | * Using welcoming and inclusive language 21 | * Being respectful of differing viewpoints and experiences 22 | * Gracefully accepting constructive criticism 23 | * Focusing on what is best for the community 24 | * Showing empathy towards other community members 25 | 26 | Examples of unacceptable behavior by participants include: 27 | 28 | * The use of sexualized language or imagery and unwelcome sexual attention or 29 | advances 30 | * Trolling, insulting/derogatory comments, and personal or political attacks 31 | * Public or private harassment 32 | * Publishing others' private information, such as a physical or electronic 33 | address, without explicit permission 34 | * Other conduct which could reasonably be considered inappropriate in a 35 | professional setting 36 | 37 | ## Our Responsibilities 38 | 39 | Project maintainers are responsible for clarifying the standards of acceptable 40 | behavior and are expected to take appropriate and fair corrective action in 41 | response to any instances of unacceptable behavior. 42 | 43 | Project maintainers have the right and responsibility to remove, edit, or 44 | reject comments, commits, code, wiki edits, issues, and other contributions 45 | that are not aligned to this Code of Conduct, or to ban temporarily or 46 | permanently any contributor for other behaviors that they deem inappropriate, 47 | threatening, offensive, or harmful. 48 | 49 | ## Scope 50 | 51 | This Code of Conduct applies both within project spaces and in public spaces 52 | when an individual is representing the project or its community. Examples of 53 | representing a project or community include using an official project e-mail 54 | address, posting via an official social media account, or acting as an appointed 55 | representative at an online or offline event. Representation of a project may be 56 | further defined and clarified by project maintainers. 57 | 58 | ## Enforcement 59 | 60 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 61 | reported by contacting the project leader at john@sundell.co. All 62 | complaints will be reviewed and investigated and will result in a response that 63 | is deemed necessary and appropriate to the circumstances. The project team is 64 | obligated to maintain confidentiality with regard to the reporter of an incident. 65 | Further details of specific enforcement policies may be posted separately. 66 | 67 | Project maintainers who do not follow or enforce the Code of Conduct in good 68 | faith may face temporary or permanent repercussions as determined by other 69 | members of the project's leadership. 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 74 | available at [http://contributor-covenant.org/version/1/4][version] 75 | 76 | [homepage]: http://contributor-covenant.org 77 | [version]: http://contributor-covenant.org/version/1/4/ 78 | -------------------------------------------------------------------------------- /Splash/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Splash Contribution Guide 2 | 3 | Welcome to the *Splash Contribution Guide* - a document that aims to give you all the information you need to contribute to the Splash project. 4 | 5 | Before you continue, make sure that you've read & agree to [the Code of Conduct](https://github.com/JohnSundell/Splash/blob/master/CODE_OF_CONDUCT.md). 6 | 7 | ## Bugs, feature requests and support 8 | 9 | Splash doesn't use GitHub issues, so all form of support - whether that's asking a question, reporting a bug, or discussing a feature request - takes place in Pull Requests. 10 | 11 | The idea behind this workflow is to encourage more people using Splash to dive into the source code and familiarize themselves with how it works, in order to better be able to *self-service* on bugs and issues - hopefully leading to a better and more fluid experience for everyone involved 😊. 12 | 13 | *This workflow is still very much an experiment, so please be patient and try to keep an open mind as we work things out together* 😉 14 | 15 | **🐞 I found a bug, how do I report it?** 16 | 17 | If you find a bug, for example a piece of code that doesn't highlight correctly, here's the recommended workflow: 18 | 19 | 1. Come up with the simplest code possible that reproduces the issue (`SplashTokenizer` can be a great tool to use in order to quickly test how Splash tokenizes a string of code). 20 | 2. Write a test case using the code that reproduces the issue. [See Splash's existing tests for inspiration on how to get started](https://github.com/JohnSundell/Splash/tree/master/Tests/SplashTests/Tests). When writing a test, you essentially give `SyntaxHighlighter` a string to highlight, and then perform an `XCTAssertEqual` against an array of expected components. 21 | 3. Either fix the bug yourself, or simply submit your failing test as a Pull Request, and we can work on a solution together. 22 | 23 | While doing the above does require a bit of extra work for the person who found the bug, it gives us as a community a very nice starting point for fixing issues - hopefully leading to quicker fixes and a more constructive workflow. 24 | 25 | **💡 I have an idea for a feature request!** 26 | 27 | First of all, that's awesome! 👍 Your ideas on how to make Splash better and more powerful are super welcome. Here's the recommended workflow for feature requests: 28 | 29 | 1. Do some prototyping and come up with a sample implementation of your idea or feature request. Note that this doesn't have to be a fully working, complete implementation, just something that illustrates the feature and your idea on how it could be added to Splash. 30 | 2. Submit your sample implementation as a Pull Request. Use the description field to write down why you think the feature should be added and some initial discussion points. 31 | 3. Together we'll discuss the feature and your sample implementation, and either accept it as-is, use it as a starting point for a new implementation, or decide that the idea is not worth implementing as this time. 32 | 33 | **🤔 I have a question that the documentation doesn't yet answer** 34 | 35 | With Splash, the goal is to end up with state of the art documentation that answers most of the questions that both users and developers of the tool might have - and the only way to get there is through continued improvement, with your help. 36 | 37 | Here's the recommended workflow for getting your question answered: 38 | 39 | 1. Start by looking through the code. Splash is a normal Swift package that uses standard Swift conventions with a (hopefully 😅) well-defined structure. Chances are high that you'll be able to answer your own question by reading through the implementation, the tests, and the inline code documentation. 40 | 2. If you found out the answer to your question (congrats! 🎉) - then don't stop there. Other people will probably ask themselves the same question at some point - so let's improve the documentation! Find an appropriate place where your question could've been answered by clearer documentation or a better structure (for example this document, or inline in the code) - and add the documentation you wish would've been there. If you didn't manage to find an answer (no worries, we're all always learning 👍), write down your question as a comment - either in the code or in one of the Markdown documents. 41 | 3. Submit your new documentation or your comment as a Pull Request, and we'll work on improving the documentation together. 42 | 43 | ## Design and technical decisions 44 | 45 | Like most programs & frameworks, Splash could've been written in many different ways. Specifically for the task of Swift syntax highlighting, there were three main options to consider: 46 | 47 | 1. Apply regular expressions to the code in order to tokenize it. This is how most JavaScript-based syntax highlighters work. It's a common and proven approach, but it usually doesn't yield the most accurate results (writing really granular regular expressions is really hard), and can be a bit alienating for people who haven't used advanced regular expressions before. 48 | 2. Hook into Apple's SourceKit service. SourceKit is what powers Xcode's syntax highlighting, and works in tandem with the Swift compiler to tokenize and highlight code. SourceKit is awesome, but using it is quite complicated and requires cross-process communication. 49 | 3. Simply parse the code manually. Like all programming languages, Swift has a well-defined syntax and clear grammar that we can model in code, in order to parse and tokenize code by iterating through it. 50 | 51 | When I first started exploring the idea of a custom Swift syntax highlighter, I built quick prototypes using all of the above three techniques - and the one that I liked the most (by far) was option number 3. Writing Splash as a normal Swift package, using normal Swift code, with standard Swift conventions turned out (at least for me) to be the most easy to understand and easy to work with solution. 52 | 53 | The next challenge then became to decide exactly *how* to write such a Swift package. Swift's syntax changes over time, so Splash required a flexible setup in order to avoid becoming hard to maintain due to complicated logic and lots of different conditions scattered all over the code. 54 | 55 | ## Architectural overview 56 | 57 | Splash's architecture was designed to enable easy tweaking of how it parses and tokenizes code, and to make bugs and edge cases easier to debug - but also to hide all those implementation details from the API user. 58 | 59 | **SyntaxHighlighter** 60 | 61 | The most top level API that most API users will interact with is `SyntaxHighlighter`. It doesn't do much itself, but instead works as the *"middleman"* between the internal `Tokenizer` type and user-configurable implementations of `Grammar` and `OutputFormat`. 62 | 63 | So as an API user, all you have to know in order to use Splash is this: 64 | 65 | ```swift 66 | let highlighter = SyntaxHighlighter(format: HTMLOutputFormat()) 67 | let code = highlighter.highlight("func hello() -> Int") 68 | ``` 69 | 70 | **Tokenizer** 71 | 72 | The `Tokenizer` type enables `SyntaxHighlighter` to ask for a sequence of code segments for a given string, using a set of delimiters to use to split the code up. It then iterates through each character of the given string and uses the set of delimiters to check when each segment should begin and end. 73 | 74 | The reason `Tokenizer` doesn't simply *split* the string is to enable Splash to have as close to `O(N)` performance characteristics as possible. If we first had to split the string, *then* iterate through it, we would always make at least two passes through the code. With the current approach, only one full pass has to be made. 75 | 76 | **Grammar** 77 | 78 | What delimiters that `Tokenizer` should use to split the code up into segments is determined by the given language `Grammar`. The default implementation is called `SwiftGrammar`, which aims to mimic the behavior of the Swift compiler as close as possible without actually having to compile the code (which is what enables Splash to be so fast). 79 | 80 | The decision to not simply hardcode `SwiftGrammar` across the code base was to [decouple the code using a protocol](https://www.swiftbysundell.com/posts/separation-of-concerns-using-protocols-in-swift) (in this case `Grammar`) to achieve a much more flexible solution. If the Swift grammar changes a lot in the future, we can always add a second implementation while still maintaining backward compatibility, and it also opens up the possibility of using Splash with languages other than Swift - since it doesn't make many (if any) hard assumptions about Swift itself (Objective-C support, anyone? 😉). 81 | 82 | Apart from supplying `Tokenizer` with delimiters, the most important role of a `Grammar` implementation is to provide an array of `SyntaxRule` implementations. When `SyntaxHighlighter` iterates through the segments that its `Tokenizer` gave it, it applies the syntax rules from its `Grammar` to each one of them to figure out each token's type. Each rule is asked if it matches a given segment, and as soon as a match is found that rule's `TokenType` is used to determine the type of that token. 83 | 84 | Have a look at `SwiftGrammar` to see all of its `SyntaxRule` implementations and how they decide how to classify each token using code segments. 85 | 86 | **OutputFormat** 87 | 88 | The final piece of the puzzle is `OutputFormat`, which determines how the result of tokenizing a string of code should be transformed into its final form. Splash ships with two implementations of this protocol `HTMLOutputFormat` and `AttributedStringOutputFormat`, but the framework makes no assumptions about what output format that the API user may want, since the output format can be fully customized. 89 | 90 | An `OutputFormat` has two responsibilities. The first is to define what type that the output will actually be (through its `Output` associated type). The second is to construct an `OutputBuilder` to build up a value of that output format type. Splash uses the *[builder pattern](https://www.swiftbysundell.com/posts/using-the-builder-pattern-in-swift)* to be able to continuously build up the output as it iterates through each token, and at the end call `build()` on the builder to output the final result. 91 | 92 | **Conclusion** 93 | 94 | Hopefully this document has given you an introduction to how Splash works, both in terms of its recommended project workflow and its technical implementation. Feel free to submit Pull Requests to improve this document, and I look forward to working with you on Splash and seeing how you use it 😀 95 | -------------------------------------------------------------------------------- /Splash/Examples/sundellsColors.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Example CSS file that can be used to style Splash HTML output 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | pre { 8 | margin-bottom: 1.5em; 9 | background-color: #1a1a1a; 10 | padding: 16px 0; 11 | border-radius: 16px; 12 | } 13 | 14 | pre code { 15 | font-family: monospace; 16 | display: block; 17 | padding: 0 20px; 18 | color: #a9bcbc; 19 | line-height: 1.4em; 20 | font-size: 0.95em; 21 | overflow-x: auto; 22 | white-space: pre; 23 | -webkit-overflow-scrolling: touch; 24 | } 25 | 26 | pre code .keyword { 27 | color: #e73289; 28 | } 29 | 30 | pre code .type { 31 | color: #8281ca; 32 | } 33 | 34 | pre code .call { 35 | color: #348fe5; 36 | } 37 | 38 | pre code .property { 39 | color: #21ab9d; 40 | } 41 | 42 | pre code .number { 43 | color: #db6f57; 44 | } 45 | 46 | pre code .string { 47 | color: #fa641e; 48 | } 49 | 50 | pre code .comment { 51 | color: #6b8a94; 52 | } 53 | 54 | pre code .dotAccess { 55 | color: #92b300; 56 | } 57 | 58 | pre code .preprocessing { 59 | color: #b68a00; 60 | } 61 | -------------------------------------------------------------------------------- /Splash/Images/Code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metal-by-example/book-of-shaders-metal/12bb2366697cba9c5f660d54fead7bdcd73b6b8a/Splash/Images/Code.png -------------------------------------------------------------------------------- /Splash/Images/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metal-by-example/book-of-shaders-metal/12bb2366697cba9c5f660d54fead7bdcd73b6b8a/Splash/Images/Logo.png -------------------------------------------------------------------------------- /Splash/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 John Sundell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Splash/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | swift package update 3 | swift build -c release 4 | install .build/release/SplashHTMLGen /usr/local/bin/SplashHTMLGen 5 | install .build/release/SplashMarkdown /usr/local/bin/SplashMarkdown 6 | install .build/release/SplashImageGen /usr/local/bin/SplashImageGen 7 | install .build/release/SplashTokenizer /usr/local/bin/SplashTokenizer 8 | -------------------------------------------------------------------------------- /Splash/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.4 2 | 3 | /** 4 | * Splash 5 | * Copyright (c) John Sundell 2018 6 | * MIT license - see LICENSE.md 7 | */ 8 | 9 | import PackageDescription 10 | 11 | let package = Package( 12 | name: "Splash", 13 | products: [ 14 | .library(name: "Splash", targets: ["Splash"]), 15 | .executable(name: "SplashMarkdown", targets: ["SplashMarkdown"]), 16 | .executable(name: "SplashHTMLGen", targets: ["SplashHTMLGen"]), 17 | .executable(name: "SplashImageGen", targets: ["SplashImageGen"]), 18 | .executable(name: "SplashTokenizer", targets: ["SplashTokenizer"]), 19 | ], 20 | targets: [ 21 | .target(name: "Splash"), 22 | .executableTarget( 23 | name: "SplashMarkdown", 24 | dependencies: ["Splash"] 25 | ), 26 | .executableTarget( 27 | name: "SplashHTMLGen", 28 | dependencies: ["Splash"] 29 | ), 30 | .executableTarget( 31 | name: "SplashImageGen", 32 | dependencies: ["Splash"] 33 | ), 34 | .executableTarget( 35 | name: "SplashTokenizer", 36 | dependencies: ["Splash"] 37 | ), 38 | .testTarget( 39 | name: "SplashTests", 40 | dependencies: ["Splash"] 41 | ) 42 | ] 43 | ) 44 | -------------------------------------------------------------------------------- /Splash/README.md: -------------------------------------------------------------------------------- 1 |

2 | Splash 3 |

4 | 5 |

6 | 7 | 8 | Swift Package Manager 9 | 10 | Mac + Linux 11 | 12 | Twitter: @johnsundell 13 | 14 |

15 | 16 | Welcome to **Splash** - a fast, lightweight and flexible Swift syntax highlighter. It can be used to generate code sample HTML for a blog post, to turn a string of Swift code into a fully syntax highlighted image, or to build custom developer tools. 17 | 18 | It's used to highlight all articles on [swiftbysundell.com](https://swiftbysundell.com). 19 | 20 | ## Usage 21 | 22 | Splash can be used either as a library in your own Swift Package Manager-powered tool or script, or by using one of the four built-in command line tools that act as frontends for the Splash library. 23 | 24 | ### 🌍 On the web 25 | 26 | If you're using [Publish](https://github.com/JohnSundell/Publish), then there's an official plugin that makes it easy to integrate Splash into your website: 27 | 28 | 👉 [SplashPublishPlugin](https://github.com/JohnSundell/SplashPublishPlugin) 29 | 30 | If you're using Jekyll, there's also a custom ```{% splash %}``` tag available for the Liquid templating language. 31 | 32 | 👉 [splashtag](https://github.com/mannberg/splashtag) 33 | 34 | ### 🖥 On the command line 35 | 36 | The easiest way to get started building things with Splash is to use one of the four built-in command line tools that each enable you to use Splash in different ways. 37 | 38 | #### SplashHTMLGen 39 | 40 | `SplashHTMLGen` uses Splash's HTML output format to generate an HTML string from Swift code. You simply pass it the code you want to highlight as an argument and HTML is returned as standard output. 41 | 42 | For example, if you call it like this: 43 | 44 | ``` 45 | $ SplashHTMLGen "func hello(world: String) -> Int" 46 | ``` 47 | 48 | You'll get the following output back: 49 | 50 | ```html 51 | func hello(world: String) -> Int 52 | ``` 53 | 54 | To be as flexible as possible, Splash doesn't hardcode any colors or other CSS attributes in the HTML it generates. Instead it simply assigns a CSS class to each token. For an example of a CSS file that can be used to style Splash-generated HTML, see [Examples/sundellsColors.css](https://github.com/JohnSundell/Splash/blob/master/Examples/sundellsColors.css). 55 | 56 | When rendering your outputted html, make sure to wrap your output code in the `
` and `` tags and properly link to your `.css` file. Like this:
 57 | 
 58 | ```html
 59 | 
 60 | 
 61 |     Hello World
 62 |     
 63 | 
 64 | 
 65 | 
 66 |     
 67 |         func hello(world: String) -> Int
 68 |     
 69 | 
70 | ``` 71 | 72 | For more information about HTML generation with Splash and how to customize it, see `HTMLOutputFormat` [here](https://github.com/JohnSundell/Splash/blob/master/Sources/Splash/Output/HTMLOutputFormat.swift). 73 | 74 | #### SplashMarkdown 75 | 76 | `SplashMarkdown` builds on top of `SplashHTMLGen` to enable easy Splash decoration of any Markdown file. Pass it a path to a Markdown file, and it will iterate through all code blocks within that file and convert them into Splash-highlighted HTML. 77 | 78 | Just like the HTML generated by `SplashHTMLGen` itself, a CSS file should also be added to any page serving the processed Markdown, since Splash only adds CSS classes to tokens — rather than hardcoding styles inline. See the above `SplashHTMLGen` documentation for more information. 79 | 80 | Here’s an example call to decorate a Markdown file at the path `~/Documents/Article.md`: 81 | 82 | ``` 83 | $ SplashMarkdown ~/Documents/Article.md 84 | ``` 85 | 86 | The decorated Markdown will be returned as standard output. 87 | 88 | Highlighting can be skipped for any code block by adding `no-highlight` next to the block’s opening row of backticks — like this: *“```no-highlight”*. 89 | 90 | #### SplashImageGen 91 | 92 | `SplashImageGen` uses Splash to generate an `NSAttributedString` from Swift code, then draws that attributed string into a graphics context to turn it into an image, which is then written to disk. 93 | 94 | For example, if you call it like this: 95 | 96 | ``` 97 | $ SplashImageGen "func hello(world: String) -> Int" "MyImage.png" 98 | ``` 99 | 100 | The following image will be generated (and written to disk as `MyImage.png`): 101 | 102 | Code sample 103 | 104 | *`SplashImageGen` is currently only available on macOS.* 105 | 106 | #### SplashTokenizer 107 | 108 | The final built-in command line tool, `SplashTokenizer`, is mostly useful as a debugging tool when working on Splash - but can also be interesting to use in order to see how Splash breaks down code into tokens. Given a string of Swift code, it simply outputs all of its components (excluding whitespaces). 109 | 110 | So if you call it like this: 111 | 112 | ``` 113 | $ SplashTokenizer "func hello(world: String) -> Int" 114 | ``` 115 | 116 | You'll get the following standard output back: 117 | 118 | ``` 119 | Keyword token: func 120 | Plain text: hello(world: 121 | Type token: String 122 | Plain text: ) 123 | Plain text: -> 124 | Type token: Int 125 | ``` 126 | 127 | ### 📦 As a package 128 | 129 | To include Splash in your own script or Swift package, [add it as a dependency](#installation) and use the `SyntaxHighlighter` class combined with your output format of choice to highlight a string of code: 130 | 131 | ```swift 132 | import Splash 133 | 134 | let highlighter = SyntaxHighlighter(format: HTMLOutputFormat()) 135 | let html = highlighter.highlight("func hello() -> String") 136 | ``` 137 | 138 | Splash ships with two built-in output formats - HTML and `NSAttributedString`, but you can also easily add your own by implementing the `OutputFormat` protocol. 139 | 140 | ## Installation 141 | 142 | Splash is distributed as a Swift package, making it easy to install for use in scripts, developer tools, server-side applications, or to use its built-in command line tools. 143 | 144 | Splash supports both macOS and Linux. 145 | 146 | *Before you begin, make sure that you have a Swift 5.2-compatible toolchain installed (for example Xcode 11.5 or later if you're on a Mac).* 147 | 148 | ### 📦 As a package 149 | 150 | To install Splash for use in a Swift Package Manager-powered tool or server-side application, add Splash as a dependency to your `Package.swift` file. For more information, please see the [Swift Package Manager documentation](https://github.com/apple/swift-package-manager/tree/master/Documentation). 151 | 152 | ``` 153 | .package(url: "https://github.com/JohnSundell/Splash", from: "0.1.0") 154 | ``` 155 | 156 | ### 🛠 Command line tools 157 | 158 | If you want to use Splash through one of its built-in command line tools, start by cloning the repo to your local machine: 159 | 160 | ``` 161 | $ git clone https://github.com/johnsundell/splash.git 162 | $ cd splash 163 | ``` 164 | 165 | To run a tool without installing it, you can use the Swift Package Manager's `run` command, like this: 166 | 167 | ``` 168 | $ swift run SplashHTMLGen "func hello(world: String) -> Int" 169 | ``` 170 | 171 | To install all four command line tools globally on your system, use Make: 172 | 173 | ``` 174 | $ make install 175 | ``` 176 | 177 | That will install the following four tools in your `/usr/local/bin` folder: 178 | 179 | ``` 180 | SplashHTMLGen 181 | SplashMarkdown 182 | SplashImageGen 183 | SplashTokenizer 184 | ``` 185 | 186 | If you only wish to install one of these, compile it and then move it to `/usr/local/bin`, like this: 187 | 188 | ``` 189 | $ swift build -c release -Xswiftc -static-stdlib 190 | $ install .build/release/SplashHTMLGen /usr/local/bin/SplashHTMLGen 191 | ``` 192 | 193 | ## Contributions and support 194 | 195 | Splash is developed completely in the open, and your contributions are more than welcome. It's still a very new project, so I'm sure there are bugs to be found and improvements to be made - and hopefully we can work on those together as a community. 196 | 197 | This project does not come with GitHub Issues-based support, and users are instead encouraged to become active participants in its continued development — by fixing any bugs that they encounter, or by improving the documentation wherever it's found to be lacking. 198 | 199 | To read more about suggested workflows when contributing to Splash, how to report bugs and feature requests, as well as technical details and an architectural overview - check out the [Contributing Guide](https://github.com/JohnSundell/Splash/blob/master/CONTRIBUTING.md). 200 | 201 | ## Hope you enjoy using Splash! 202 | 203 | I had a lot of fun building Splash, and I'm looking forward to continue working on it in the open together with you! I hope you'll like it and that you'll find it useful. Let me know what you think on [Twitter](https://twitter.com/johnsundell) 😊 204 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Extensions/CharacterSet/CharacterSet+Contains.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | internal extension CharacterSet { 10 | func contains(_ character: Character) -> Bool { 11 | guard let scalar = character.unicodeScalars.first else { 12 | return false 13 | } 14 | 15 | return contains(scalar) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Extensions/Equatable/Equatable+AnyOf.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | extension Equatable { 10 | func isAny(of candidates: Self...) -> Bool { 11 | return candidates.contains(self) 12 | } 13 | 14 | func isAny(of candidates: S) -> Bool where S.Element == Self { 15 | return candidates.contains(self) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Extensions/Int/Int+IsOdd.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | internal extension Int { 10 | var isEven: Bool { 11 | return self % 2 == 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Extensions/Sequence/Sequence+AnyOf.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | internal extension Sequence where Element: Equatable { 10 | func contains(anyOf candidates: Element...) -> Bool { 11 | return contains(anyOf: candidates) 12 | } 13 | 14 | func contains(anyOf candidates: S) -> Bool where S.Element == Element { 15 | for candidate in candidates { 16 | if contains(candidate) { 17 | return true 18 | } 19 | } 20 | 21 | return false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Extensions/Sequence/Sequence+Occurrences.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | internal extension Sequence where Element: Equatable { 10 | func numberOfOccurrences(of target: Element) -> Int { 11 | return reduce(0) { count, element in 12 | return element == target ? count + 1 : count 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Extensions/Strings/String+HTMLEntities.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2019 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | internal extension StringProtocol { 10 | func escapingHTMLEntities() -> String { 11 | return String(flatMap { character -> String in 12 | switch character { 13 | case "&": 14 | return "&" 15 | case "<": 16 | return "<" 17 | case ">": 18 | return ">" 19 | default: 20 | return String(character) 21 | } 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Extensions/Strings/String+IsNumber.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | extension String { 10 | var isNumber: Bool { 11 | return Int(self) != nil 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Extensions/Strings/String+PrefixChecking.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | internal extension String { 10 | var isCapitalized: Bool { 11 | guard let firstCharacter = first.map(String.init) else { 12 | return false 13 | } 14 | 15 | return firstCharacter != firstCharacter.lowercased() 16 | } 17 | 18 | var startsWithLetter: Bool { 19 | guard let firstCharacter = first else { 20 | return false 21 | } 22 | 23 | return CharacterSet.letters.contains(firstCharacter) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Extensions/Strings/String+Removing.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | internal extension String { 10 | func removing(_ substring: String) -> String { 11 | return replacingOccurrences(of: substring, with: "") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Extensions/Strings/Substring+HasSuffix.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if os(Linux) 4 | 5 | internal extension Substring { 6 | func hasSuffix(_ suffix: String) -> Bool { 7 | guard count >= suffix.count else { 8 | return false 9 | } 10 | 11 | let startIndex = index(endIndex, offsetBy: -suffix.count) 12 | return self[startIndex...] == suffix 13 | } 14 | } 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Grammar/Grammar.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Protocol used to define the grammar of a language to use for 10 | /// syntax highlighting. See `SwiftGrammar` for a default implementation 11 | /// of the Swift language grammar. 12 | public protocol Grammar { 13 | /// The set of characters that make up the delimiters that separates 14 | /// tokens within the language, such as punctuation characters. You 15 | /// can control whether delimiters should be merged when forming 16 | /// tokens by implementing the `isDelimiter(mergableWith:)` method. 17 | var delimiters: CharacterSet { get } 18 | /// The rules that define the syntax of the language. When tokenizing, 19 | /// the rules will be iterated over in sequence, and the first rule 20 | /// that matches a given code segment will be used to determine that 21 | /// segment's token type. 22 | var syntaxRules: [SyntaxRule] { get } 23 | 24 | /// Return whether two delimiters should be merged into a single 25 | /// token, or whether they should be treated as separate ones. 26 | /// The delimiters are passed in the order in which they appear 27 | /// in the source code to be highlighted. 28 | /// - Parameter delimiterA: The first delimiter 29 | /// - Parameter delimiterB: The second delimiter 30 | func isDelimiter(_ delimiterA: Character, 31 | mergableWith delimiterB: Character) -> Bool 32 | } 33 | 34 | public extension Grammar { 35 | func isDelimiter(_ delimiterA: Character, 36 | mergableWith delimiterB: Character) -> Bool { 37 | return true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Output/AttributedStringOutputFormat.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | #if !os(Linux) 8 | 9 | import Foundation 10 | 11 | /// Output format to use to generate an NSAttributedString from the 12 | /// highlighted code. A `Theme` is used to determine what fonts and 13 | /// colors to use for the various tokens. 14 | public struct AttributedStringOutputFormat: OutputFormat { 15 | public var theme: Theme 16 | 17 | public init(theme: Theme) { 18 | self.theme = theme 19 | } 20 | 21 | public func makeBuilder() -> Builder { 22 | return Builder(theme: theme) 23 | } 24 | } 25 | 26 | public extension AttributedStringOutputFormat { 27 | struct Builder: OutputBuilder { 28 | private let theme: Theme 29 | private lazy var font = theme.font.load() 30 | private var string = NSMutableAttributedString() 31 | 32 | fileprivate init(theme: Theme) { 33 | self.theme = theme 34 | } 35 | 36 | public mutating func addToken(_ token: String, ofType type: TokenType) { 37 | let color = theme.tokenColors[type] ?? Color(red: 1, green: 1, blue: 1) 38 | string.append(token, font: font, color: color) 39 | } 40 | 41 | public mutating func addPlainText(_ text: String) { 42 | string.append(text, font: font, color: theme.plainTextColor) 43 | } 44 | 45 | public mutating func addWhitespace(_ whitespace: String) { 46 | let color = Color(red: 1, green: 1, blue: 1) 47 | string.append(whitespace, font: font, color: color) 48 | } 49 | 50 | public func build() -> NSAttributedString { 51 | return NSAttributedString(attributedString: string) 52 | } 53 | } 54 | } 55 | 56 | private extension NSMutableAttributedString { 57 | func append(_ string: String, font: Font.Loaded, color: Color) { 58 | let attributedString = NSAttributedString(string: string, attributes: [ 59 | .foregroundColor: color, 60 | .font: font 61 | ]) 62 | 63 | append(attributedString) 64 | } 65 | } 66 | 67 | #endif 68 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Output/HTMLOutputFormat.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Output format to use to generate an HTML string with a semantic 10 | /// representation of the highlighted code. Each token will be wrapped 11 | /// in a `span` element with a CSS class matching the token's type. 12 | /// Optionally, a `classPrefix` can be set to prefix each CSS class with 13 | /// a given string. 14 | public struct HTMLOutputFormat: OutputFormat { 15 | public var classPrefix: String 16 | 17 | public init(classPrefix: String = "") { 18 | self.classPrefix = classPrefix 19 | } 20 | 21 | public func makeBuilder() -> Builder { 22 | return Builder(classPrefix: classPrefix) 23 | } 24 | } 25 | 26 | public extension HTMLOutputFormat { 27 | struct Builder: OutputBuilder { 28 | private let classPrefix: String 29 | private var html = "" 30 | private var pendingToken: (string: String, type: TokenType)? 31 | private var pendingWhitespace: String? 32 | 33 | fileprivate init(classPrefix: String) { 34 | self.classPrefix = classPrefix 35 | } 36 | 37 | public mutating func addToken(_ token: String, ofType type: TokenType) { 38 | if var pending = pendingToken { 39 | guard pending.type != type else { 40 | pendingWhitespace.map { pending.string += $0 } 41 | pendingWhitespace = nil 42 | pending.string += token 43 | pendingToken = pending 44 | return 45 | } 46 | } 47 | 48 | appendPending() 49 | pendingToken = (token, type) 50 | } 51 | 52 | public mutating func addPlainText(_ text: String) { 53 | appendPending() 54 | html.append(text.escapingHTMLEntities()) 55 | } 56 | 57 | public mutating func addWhitespace(_ whitespace: String) { 58 | if pendingToken != nil { 59 | pendingWhitespace = (pendingWhitespace ?? "") + whitespace 60 | } else { 61 | html.append(whitespace) 62 | } 63 | } 64 | 65 | public mutating func build() -> String { 66 | appendPending() 67 | return html 68 | } 69 | 70 | private mutating func appendPending() { 71 | if let pending = pendingToken { 72 | html.append(""" 73 | \(pending.string.escapingHTMLEntities()) 74 | """) 75 | 76 | pendingToken = nil 77 | } 78 | 79 | if let whitespace = pendingWhitespace { 80 | html.append(whitespace) 81 | pendingWhitespace = nil 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Output/MarkdownDecorator.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2019 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Type used to decorate a Markdown file with Splash-highlighted code blocks 10 | public struct MarkdownDecorator { 11 | private let highlighter: SyntaxHighlighter 12 | private let skipHighlightingPrefix = "no-highlight" 13 | 14 | /// Create a Markdown decorator with a given prefix to apply to all CSS 15 | /// classes used when highlighting code blocks within a Markdown string. 16 | public init(classPrefix: String = "", grammar: Grammar = SwiftGrammar()) { 17 | highlighter = SyntaxHighlighter(format: HTMLOutputFormat(classPrefix: classPrefix), grammar: grammar) 18 | } 19 | 20 | /// Decorate all code blocks within a given Markdown string. This API assumes 21 | /// that the passed Markdown is valid. Each code block will be replaced by 22 | /// Splash-highlighted HTML for that block's code. To skip highlighting for 23 | /// any given code block, add "no-highlight" next to the opening row of 24 | /// backticks for that block. 25 | public func decorate(_ markdown: String) -> String { 26 | let components = markdown.components(separatedBy: "```") 27 | var output = "" 28 | 29 | for (index, component) in components.enumerated() { 30 | guard index % 2 != 0 else { 31 | output.append(component) 32 | continue 33 | } 34 | 35 | var code = component.trimmingCharacters(in: .whitespacesAndNewlines) 36 | 37 | if code.hasPrefix(skipHighlightingPrefix) { 38 | let charactersToDrop = skipHighlightingPrefix + "\n" 39 | code = code.dropFirst(charactersToDrop.count).escapingHTMLEntities() 40 | } else { 41 | code = highlighter.highlight(code) 42 | } 43 | 44 | output.append(""" 45 |
\(code)
46 | """) 47 | } 48 | 49 | return output 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Output/OutputBuilder.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Protocol used to define a builder for a highlighted string that's 10 | /// returned as output from `SyntaxHighlighter`. Each builder defines 11 | /// its own output type through the `Output` associated type, and can 12 | /// add the various tokens and other text found in the highlighted code 13 | /// in whichever fashion it wants. 14 | public protocol OutputBuilder { 15 | /// The type of output that this builder produces 16 | associatedtype Output 17 | /// Add a token with a given type to the builder 18 | mutating func addToken(_ token: String, ofType type: TokenType) 19 | /// Add some plain text, without any formatting, to the builder 20 | mutating func addPlainText(_ text: String) 21 | /// Add some whitespace to the builder 22 | mutating func addWhitespace(_ whitespace: String) 23 | /// Build the final output based on the builder's current state 24 | mutating func build() -> Output 25 | } 26 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Output/OutputFormat.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Protocol used to define an output format for a `SyntaxHighlighter`. 10 | /// Default implementations of this protocol are provided for HTML and 11 | /// NSAttributedString outputs, and custom ones can be defined by 12 | /// conforming to this protocol and passing the implementation to a 13 | /// syntax highlighter when it's created. 14 | public protocol OutputFormat { 15 | /// The type of builder that this output format uses. The builder's 16 | /// `Output` type determines the output type of the format. 17 | associatedtype Builder: OutputBuilder 18 | 19 | /// Make a new instance of the output format's builder. This will be 20 | /// called once per syntax highlighting session. The builder is expected 21 | /// to be a newly created, blank instance. 22 | func makeBuilder() -> Builder 23 | } 24 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Syntax/SyntaxHighlighter.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | /// This class acts as the main API entry point for Splash. Using it, 10 | /// any code string can be highlighted to any desired format, using 11 | /// any language grammar. Per default, Swift's langauge grammar is used. 12 | /// To initialize this class, pass the desired output format, such as 13 | /// `AttributedStringOutputFormat` or `HTMLOutputFormat`, or a custom 14 | /// implementation. One syntax highlighter may be reused multiple times. 15 | public struct SyntaxHighlighter { 16 | private let format: Format 17 | private let grammar: Grammar 18 | private let tokenizer = Tokenizer() 19 | 20 | /// Initialize an instance with the desired output format. 21 | /// If no grammar is passed, then Swift's grammar is used. 22 | public init(format: Format, grammar: Grammar = SwiftGrammar()) { 23 | self.format = format 24 | self.grammar = grammar 25 | } 26 | 27 | /// Highlight the given code, returning output as specified by the 28 | /// syntax highlighter's `Format`. 29 | public func highlight(_ code: String) -> Format.Builder.Output { 30 | var builder = format.makeBuilder() 31 | var state: (token: String, tokenType: TokenType?)? 32 | 33 | func handle(_ token: String, ofType type: TokenType?, trailingWhitespace: String?) { 34 | guard let whitespace = trailingWhitespace else { 35 | state = (token, type) 36 | return 37 | } 38 | 39 | builder.addToken(token, ofType: type) 40 | builder.addWhitespace(whitespace) 41 | state = nil 42 | } 43 | 44 | for segment in tokenizer.segmentsByTokenizing(code, using: grammar) { 45 | let token = segment.tokens.current 46 | let whitespace = segment.trailingWhitespace 47 | 48 | guard !token.isEmpty else { 49 | whitespace.map { builder.addWhitespace($0) } 50 | continue 51 | } 52 | 53 | let tokenType = typeOfToken(in: segment) 54 | 55 | guard var currentState = state else { 56 | handle(token, ofType: tokenType, trailingWhitespace: whitespace) 57 | continue 58 | } 59 | 60 | guard currentState.tokenType == tokenType else { 61 | builder.addToken(currentState.token, ofType: currentState.tokenType) 62 | handle(token, ofType: tokenType, trailingWhitespace: whitespace) 63 | continue 64 | } 65 | 66 | currentState.token.append(token) 67 | handle(currentState.token, ofType: tokenType, trailingWhitespace: whitespace) 68 | } 69 | 70 | if let lastState = state { 71 | builder.addToken(lastState.token, ofType: lastState.tokenType) 72 | } 73 | 74 | return builder.build() 75 | } 76 | 77 | private func typeOfToken(in segment: Segment) -> TokenType? { 78 | let rule = grammar.syntaxRules.first { $0.matches(segment) } 79 | return rule?.tokenType 80 | } 81 | } 82 | 83 | private extension OutputBuilder { 84 | mutating func addToken(_ token: String, ofType type: TokenType?) { 85 | if let type = type { 86 | addToken(token, ofType: type) 87 | } else { 88 | addPlainText(token) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Syntax/SyntaxRule.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Protocol used to define syntax rules for a language `Grammar`. 10 | /// Each rule is associated with a certain `TokenType` and, when 11 | /// evaluated, is asked to check whether it matches a given segment 12 | /// of code. If the rule matches then the rule's token type will be 13 | /// associated with the given segment's current token. 14 | public protocol SyntaxRule { 15 | /// The token type that this syntax rule represents 16 | var tokenType: TokenType { get } 17 | 18 | /// Determine if the syntax rule matches a given segment. If it's 19 | /// a match, then the rule's `tokenType` will be associated with 20 | /// the segment's current token. 21 | func matches(_ segment: Segment) -> Bool 22 | } 23 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Theming/Color.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | #if os(iOS) 8 | import UIKit 9 | public typealias Color = UIColor 10 | #elseif os(macOS) 11 | import AppKit 12 | public typealias Color = NSColor 13 | #endif 14 | 15 | #if !os(Linux) 16 | internal extension Color { 17 | convenience init(red: CGFloat, green: CGFloat, blue: CGFloat) { 18 | self.init(red: red, green: green, blue: blue, alpha: 1) 19 | } 20 | } 21 | #endif 22 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Theming/Font.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | #if !os(Linux) 10 | 11 | /// A representation of a font, for use with a `Theme`. 12 | /// Since Splash aims to be cross-platform, it uses this 13 | /// simplified font representation rather than `NSFont` 14 | /// or `UIFont`. 15 | public struct Font { 16 | /// The underlying resource used to load the font 17 | public var resource: Resource 18 | /// The size (in points) of the font 19 | public var size: Double 20 | 21 | public init(name: String, size: Double) { 22 | resource = .postScriptName(name) 23 | self.size = size 24 | } 25 | 26 | /// Initialize an instance with a path to a font file 27 | /// on disk and a size. 28 | public init(path: String, size: Double) { 29 | resource = .path((path as NSString).expandingTildeInPath) 30 | self.size = size 31 | } 32 | 33 | /// Initialize an instance with a size, and use an 34 | /// appropriate system font to render text. 35 | public init(size: Double) { 36 | resource = .system 37 | self.size = size 38 | } 39 | } 40 | 41 | public extension Font { 42 | /// Enum describing how to load the underlying resource for a font 43 | enum Resource { 44 | /// Use an appropriate system font 45 | case system 46 | /// Use a pre-loaded font 47 | case preloaded(Loaded) 48 | /// Load a font file from a given file system path 49 | case path(String) 50 | /// Load a font by its PostScript name 51 | case postScriptName(String) 52 | } 53 | } 54 | 55 | internal extension Font { 56 | func load() -> Loaded { 57 | switch resource { 58 | case .system: 59 | return loadDefaultFont() 60 | case .preloaded(let font): 61 | return font 62 | case .path(let path): 63 | return load(fromPath: path) ?? loadDefaultFont() 64 | case .postScriptName(let name): 65 | return load(withPostScriptName: name) ?? loadDefaultFont() 66 | } 67 | } 68 | 69 | private func loadDefaultFont() -> Loaded { 70 | let font: Loaded? 71 | 72 | #if os(iOS) 73 | font = UIFont(name: "Menlo-Regular", size: CGFloat(size)) 74 | #else 75 | font = load(fromPath: "/Library/Fonts/Courier New.ttf") 76 | #endif 77 | 78 | return font ?? .systemFont(ofSize: CGFloat(size)) 79 | } 80 | 81 | private func load(fromPath path: String) -> Loaded? { 82 | guard 83 | let url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, path as CFString, .cfurlposixPathStyle, false), 84 | let provider = CGDataProvider(url: url), 85 | let font = CGFont(provider) 86 | else { 87 | return nil 88 | } 89 | 90 | return CTFontCreateWithGraphicsFont(font, CGFloat(size), nil, nil) 91 | } 92 | 93 | private func load(withPostScriptName postScriptName: String) -> Loaded? { 94 | guard let font = CGFont((postScriptName as CFString)) else { return nil } 95 | return CTFontCreateWithGraphicsFont(font, CGFloat(size), nil, nil) 96 | } 97 | } 98 | 99 | #endif 100 | 101 | #if os(iOS) 102 | 103 | import UIKit 104 | 105 | public extension Font { 106 | typealias Loaded = UIFont 107 | } 108 | 109 | #elseif os(macOS) 110 | 111 | import AppKit 112 | 113 | public extension Font { 114 | typealias Loaded = NSFont 115 | } 116 | 117 | #endif 118 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Theming/Theme+Defaults.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | #if !os(Linux) 10 | 11 | public extension Theme { 12 | /// Create a theme matching the "Sundell's Colors" Xcode theme 13 | static func sundellsColors(withFont font: Font) -> Theme { 14 | return Theme( 15 | font: font, 16 | plainTextColor: Color( 17 | red: 0.66, 18 | green: 0.74, 19 | blue: 0.74 20 | ), 21 | tokenColors: [ 22 | .keyword: Color(red: 0.91, green: 0.2, blue: 0.54), 23 | .string: Color(red: 0.98, green: 0.39, blue: 0.12), 24 | .type: Color(red: 0.51, green: 0.51, blue: 0.79), 25 | .call: Color(red: 0.2, green: 0.56, blue: 0.9), 26 | .number: Color(red: 0.86, green: 0.44, blue: 0.34), 27 | .comment: Color(red: 0.42, green: 0.54, blue: 0.58), 28 | .property: Color(red: 0.13, green: 0.67, blue: 0.62), 29 | .dotAccess: Color(red: 0.57, green: 0.7, blue: 0), 30 | .preprocessing: Color(red: 0.71, green: 0.54, blue: 0) 31 | ], 32 | backgroundColor: Color( 33 | red: 0.098, 34 | green: 0.098, 35 | blue: 0.098 36 | ) 37 | ) 38 | } 39 | 40 | /// Create a theme matching Xcode's "Midnight" theme 41 | static func midnight(withFont font: Font) -> Theme { 42 | return Theme( 43 | font: font, 44 | plainTextColor: Color( 45 | red: 1, 46 | green: 1, 47 | blue: 1 48 | ), 49 | tokenColors: [ 50 | .keyword: Color(red: 0.828, green: 0.095, blue: 0.583), 51 | .string: Color(red: 1.0, green: 0.171, blue: 0.219), 52 | .type: Color(red: 0.137, green: 1.0, blue: 0.512), 53 | .call: Color(red: 0.137, green: 1.0, blue: 0.512), 54 | .number: Color(red: 0.469, green: 0.426, blue: 1.00), 55 | .comment: Color(red: 0.255, green: 0.801, blue: 0.27), 56 | .property: Color(red: 0.431, green: 0.714, blue: 0.533), 57 | .dotAccess: Color(red: 0.431, green: 0.714, blue: 0.533), 58 | .preprocessing: Color(red: 0.896, green: 0.488, blue: 0.284) 59 | ], 60 | backgroundColor: Color( 61 | red: 0, 62 | green: 0, 63 | blue: 0 64 | ) 65 | ) 66 | } 67 | 68 | /// Creating a theme matching the colors used for the WWDC 2017 sample code 69 | static func wwdc17(withFont font: Font) -> Theme { 70 | return Theme( 71 | font: font, 72 | plainTextColor: Color( 73 | red: 0.84, 74 | green: 0.84, 75 | blue: 0.84 76 | ), 77 | tokenColors: [ 78 | .keyword: Color(red: 0.992, green: 0.791, blue: 0.45), 79 | .string: Color(red: 0.966, green: 0.517, blue: 0.29), 80 | .type: Color(red: 0.431, green: 0.714, blue: 0.533), 81 | .call: Color(red: 0.431, green: 0.714, blue: 0.533), 82 | .number: Color(red: 0.559, green: 0.504, blue: 0.745), 83 | .comment: Color(red: 0.484, green: 0.483, blue: 0.504), 84 | .property: Color(red: 0.431, green: 0.714, blue: 0.533), 85 | .dotAccess: Color(red: 0.431, green: 0.714, blue: 0.533), 86 | .preprocessing: Color(red: 0.992, green: 0.791, blue: 0.45) 87 | ], 88 | backgroundColor: Color( 89 | red: 0.18, 90 | green: 0.19, 91 | blue: 0.2 92 | ) 93 | ) 94 | } 95 | 96 | /// Creating a theme matching the colors used for the WWDC 2018 sample code 97 | static func wwdc18(withFont font: Font) -> Theme { 98 | return Theme( 99 | font: font, 100 | plainTextColor: Color( 101 | red: 1, 102 | green: 1, 103 | blue: 1 104 | ), 105 | tokenColors: [ 106 | .keyword: Color(red: 0.948, green: 0.140, blue: 0.547), 107 | .string: Color(red: 0.988, green: 0.273, blue: 0.317), 108 | .type: Color(red: 0.584, green: 0.898, blue: 0.361), 109 | .call: Color(red: 0.584, green: 0.898, blue: 0.361), 110 | .number: Color(red: 0.587, green: 0.517, blue: 0.974), 111 | .comment: Color(red: 0.424, green: 0.475, blue: 0.529), 112 | .property: Color(red: 0.584, green: 0.898, blue: 0.361), 113 | .dotAccess: Color(red: 0.584, green: 0.898, blue: 0.361), 114 | .preprocessing: Color(red: 0.952, green: 0.526, blue: 0.229) 115 | ], 116 | backgroundColor: Color( 117 | red: 0.163, 118 | green: 0.163, 119 | blue: 0.182 120 | ) 121 | ) 122 | } 123 | 124 | /// Create a theme matching Xcode's "Sunset" theme 125 | static func sunset(withFont font: Font) -> Theme { 126 | return Theme( 127 | font: font, 128 | plainTextColor: Color( 129 | red: 0, 130 | green: 0, 131 | blue: 0 132 | ), 133 | tokenColors: [ 134 | .keyword: Color(red: 0.161, green: 0.259, blue: 0.467), 135 | .string: Color(red: 0.875, green: 0.027, blue: 0.0), 136 | .type: Color(red: 0.706, green: 0.27, blue: 0.0), 137 | .call: Color(red: 0.278, green: 0.415, blue: 0.593), 138 | .number: Color(red: 0.161, green: 0.259, blue: 0.467), 139 | .comment: Color(red: 0.765, green: 0.455, blue: 0.11), 140 | .property: Color(red: 0.278, green: 0.415, blue: 0.593), 141 | .dotAccess: Color(red: 0.278, green: 0.415, blue: 0.593), 142 | .preprocessing: Color(red: 0.392, green: 0.391, blue: 0.52) 143 | ], 144 | backgroundColor: Color( 145 | red: 1, 146 | green: 0.99, 147 | blue: 0.9 148 | ) 149 | ) 150 | } 151 | 152 | /// Create a theme matching Xcode's "Presentation" theme 153 | static func presentation(withFont font: Font) -> Theme { 154 | return Theme( 155 | font: font, 156 | plainTextColor: Color( 157 | red: 0, 158 | green: 0, 159 | blue: 0 160 | ), 161 | tokenColors: [ 162 | .keyword: Color(red: 0.706, green: 0.0, blue: 0.384), 163 | .string: Color(red: 0.729, green: 0.0, blue: 0.067), 164 | .type: Color(red: 0.267, green: 0.537, blue: 0.576), 165 | .call: Color(red: 0.267, green: 0.537, blue: 0.576), 166 | .number: Color(red: 0.0, green: 0.043, blue: 1.0), 167 | .comment: Color(red: 0.336, green: 0.376, blue: 0.42), 168 | .property: Color(red: 0.267, green: 0.537, blue: 0.576), 169 | .dotAccess: Color(red: 0.267, green: 0.537, blue: 0.576), 170 | .preprocessing: Color(red: 0.431, green: 0.125, blue: 0.051) 171 | ], 172 | backgroundColor: Color( 173 | red: 1, 174 | green: 1, 175 | blue: 1 176 | ) 177 | ) 178 | } 179 | } 180 | 181 | #endif 182 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Theming/Theme.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | #if !os(Linux) 10 | 11 | #if os(macOS) 12 | import AppKit 13 | #endif 14 | 15 | /// A theme describes what fonts and colors to use when rendering 16 | /// certain output formats - such as `NSAttributedString`. Several 17 | /// default implementations are provided - see Theme+Defaults.swift. 18 | public struct Theme { 19 | /// What font to use to render the highlighted text 20 | public var font: Font 21 | /// What color to use for plain text (no highlighting) 22 | public var plainTextColor: Color 23 | /// What color to use for the background 24 | public var backgroundColor: Color 25 | /// What color to use for the text's highlighted tokens 26 | public var tokenColors: [TokenType: Color] 27 | 28 | public init(font: Font, 29 | plainTextColor: Color, 30 | tokenColors: [TokenType: Color], 31 | backgroundColor: Color = Color(white: 0.12, alpha: 1)) { 32 | self.font = font 33 | self.plainTextColor = plainTextColor 34 | self.tokenColors = tokenColors 35 | self.backgroundColor = backgroundColor 36 | } 37 | } 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Tokenizing/Segment.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | /// A representation of a segment of code, used to determine the type 10 | /// of a given token when passed to a `SyntaxRule` implementation. 11 | public struct Segment { 12 | /// The code that prefixes this segment, that is all the characters 13 | /// up to where the segment's current token begins. 14 | public var prefix: Substring 15 | /// The collection of tokens that the segment includes 16 | public var tokens: Tokens 17 | /// Any whitespace that immediately follows the segment's current token 18 | public var trailingWhitespace: String? 19 | 20 | internal let currentTokenIsDelimiter: Bool 21 | internal var isLastOnLine: Bool 22 | } 23 | 24 | public extension Segment { 25 | /// A collection of tokens included in a code segment 26 | struct Tokens { 27 | /// All tokens that have been found so far (excluding the current one) 28 | public var all: [String] 29 | /// The number of times a given token has been found up until this point 30 | public var counts: [String: Int] 31 | /// The tokens that were previously found on the same line as the current one 32 | public var onSameLine: [String] 33 | /// The token that was previously found (may be on a different line) 34 | public var previous: String? 35 | /// The current token which is currently being evaluated 36 | public var current: String 37 | /// Any upcoming token that will follow the current one 38 | public var next: String? 39 | } 40 | } 41 | 42 | public extension Segment.Tokens { 43 | /// Return the number of times a given token has been found up until this point. 44 | /// This is a convenience API over the `counts` dictionary. 45 | func count(of token: String) -> Int { 46 | return counts[token] ?? 0 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Tokenizing/TokenType.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | /// Enum defining the possible types of tokens that can be highlighted 10 | public enum TokenType: Hashable { 11 | /// A keyword, such as `if`, `class`, `let` or attributes such as @available 12 | case keyword 13 | /// A token that is part of a string literal 14 | case string 15 | /// A reference to a type 16 | case type 17 | /// A call to a function or method 18 | case call 19 | /// A number, either interger of floating point 20 | case number 21 | /// A comment, either single or multi-line 22 | case comment 23 | /// A property being accessed, such as `object.property` 24 | case property 25 | /// A symbol being accessed through dot notation, such as `.myCase` 26 | case dotAccess 27 | /// A preprocessing symbol, such as `#if` 28 | case preprocessing 29 | /// A custom token type, containing an arbitrary string 30 | case custom(String) 31 | } 32 | 33 | public extension TokenType { 34 | /// Return a string value representing the token type 35 | var string: String { 36 | if case .custom(let type) = self { 37 | return type 38 | } 39 | 40 | return "\(self)" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Splash/Sources/Splash/Tokenizing/Tokenizer.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | 9 | internal struct Tokenizer { 10 | func segmentsByTokenizing(_ code: String, 11 | using grammar: Grammar) -> AnySequence { 12 | return AnySequence { 13 | Buffer(iterator: Iterator(code: code, grammar: grammar)) 14 | } 15 | } 16 | } 17 | 18 | private extension Tokenizer { 19 | struct Buffer: IteratorProtocol { 20 | private var iterator: Iterator 21 | private var nextSegment: Segment? 22 | 23 | init(iterator: Iterator) { 24 | self.iterator = iterator 25 | } 26 | 27 | mutating func next() -> Segment? { 28 | var segment = nextSegment ?? iterator.next() 29 | nextSegment = iterator.next() 30 | segment?.tokens.next = nextSegment?.tokens.current 31 | return segment 32 | } 33 | } 34 | 35 | struct Iterator: IteratorProtocol { 36 | struct Component { 37 | enum Kind { 38 | case token 39 | case delimiter 40 | case whitespace 41 | case newline 42 | } 43 | 44 | let character: Character 45 | let kind: Kind 46 | } 47 | 48 | private let code: String 49 | private let grammar: Grammar 50 | private var index: String.Index? 51 | private var tokenCounts = [String: Int]() 52 | private var allTokens = [String]() 53 | private var lineTokens = [String]() 54 | private var segments: (current: Segment?, previous: Segment?) 55 | 56 | init(code: String, grammar: Grammar) { 57 | self.code = code 58 | self.grammar = grammar 59 | segments = (nil, nil) 60 | } 61 | 62 | mutating func next() -> Segment? { 63 | let nextIndex = makeNextIndex() 64 | 65 | guard nextIndex != code.endIndex else { 66 | let segment = segments.current 67 | segments.current = nil 68 | return segment 69 | } 70 | 71 | index = nextIndex 72 | let component = makeComponent(at: nextIndex) 73 | 74 | switch component.kind { 75 | case .token, .delimiter: 76 | guard var segment = segments.current else { 77 | segments.current = makeSegment(with: component, at: nextIndex) 78 | return next() 79 | } 80 | 81 | guard segment.trailingWhitespace == nil, 82 | component.isDelimiter == segment.currentTokenIsDelimiter else { 83 | return finish(segment, with: component, at: nextIndex) 84 | } 85 | 86 | if component.isDelimiter { 87 | let previousCharacter = segment.tokens.current.last! 88 | let shouldMerge = grammar.isDelimiter(previousCharacter, 89 | mergableWith: component.character) 90 | 91 | guard shouldMerge else { 92 | return finish(segment, with: component, at: nextIndex) 93 | } 94 | } 95 | 96 | segment.tokens.current.append(component.character) 97 | segments.current = segment 98 | return next() 99 | case .whitespace, .newline: 100 | guard var segment = segments.current else { 101 | var segment = makeSegment(with: component, at: nextIndex) 102 | segment.trailingWhitespace = component.token 103 | segment.isLastOnLine = component.isNewline 104 | segments.current = segment 105 | return next() 106 | } 107 | 108 | if var existingWhitespace = segment.trailingWhitespace { 109 | existingWhitespace.append(component.character) 110 | segment.trailingWhitespace = existingWhitespace 111 | } else { 112 | segment.trailingWhitespace = component.token 113 | } 114 | 115 | if component.isNewline { 116 | segment.isLastOnLine = true 117 | } 118 | 119 | segments.current = segment 120 | return next() 121 | } 122 | } 123 | 124 | private func makeNextIndex() -> String.Index { 125 | guard let index = index else { 126 | return code.startIndex 127 | } 128 | 129 | return code.index(after: index) 130 | } 131 | 132 | private func makeComponent(at index: String.Index) -> Component { 133 | func kind(for character: Character) -> Component.Kind { 134 | if character.isWhitespace { 135 | return .whitespace 136 | } 137 | 138 | if character.isNewline { 139 | return .newline 140 | } 141 | 142 | if grammar.delimiters.contains(character) { 143 | return .delimiter 144 | } 145 | 146 | return .token 147 | } 148 | 149 | let character = code[index] 150 | 151 | return Component( 152 | character: character, 153 | kind: kind(for: character) 154 | ) 155 | } 156 | 157 | private func makeSegment(with component: Component, at index: String.Index) -> Segment { 158 | let tokens = Segment.Tokens( 159 | all: allTokens, 160 | counts: tokenCounts, 161 | onSameLine: lineTokens, 162 | previous: segments.current?.tokens.current, 163 | current: component.token, 164 | next: nil 165 | ) 166 | 167 | return Segment( 168 | prefix: code[.. Segment { 179 | var count = tokenCounts[segment.tokens.current] ?? 0 180 | count += 1 181 | tokenCounts[segment.tokens.current] = count 182 | 183 | allTokens.append(segment.tokens.current) 184 | 185 | if segment.isLastOnLine { 186 | lineTokens = [] 187 | } else { 188 | lineTokens.append(segment.tokens.current) 189 | } 190 | 191 | segments.previous = segment 192 | segments.current = makeSegment(with: component, at: index) 193 | 194 | return segment 195 | } 196 | } 197 | } 198 | 199 | extension Tokenizer.Iterator.Component { 200 | var token: String { 201 | return String(character) 202 | } 203 | 204 | var isDelimiter: Bool { 205 | switch kind { 206 | case .token, .whitespace, .newline: 207 | return false 208 | case .delimiter: 209 | return true 210 | } 211 | } 212 | 213 | var isNewline: Bool { 214 | switch kind { 215 | case .token, .whitespace, .delimiter: 216 | return false 217 | case .newline: 218 | return true 219 | } 220 | } 221 | } 222 | 223 | private extension Character { 224 | var isWhitespace: Bool { 225 | return CharacterSet.whitespaces.contains(self) 226 | } 227 | 228 | var isNewline: Bool { 229 | return CharacterSet.newlines.contains(self) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /Splash/Sources/SplashHTMLGen/main.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import Splash 9 | 10 | guard CommandLine.arguments.count > 1 else { 11 | print("⚠️ Please supply the code to generate HTML for as a string argument") 12 | exit(1) 13 | } 14 | 15 | let code = CommandLine.arguments[1] 16 | let highlighter = SyntaxHighlighter(format: HTMLOutputFormat()) 17 | print(highlighter.highlight(code)) 18 | -------------------------------------------------------------------------------- /Splash/Sources/SplashImageGen/Extensions/CGImage+WriteToURL.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | #if os(macOS) 8 | 9 | import Foundation 10 | import ImageIO 11 | 12 | extension CGImage { 13 | func write(to url: URL) { 14 | let destination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypePNG, 1, nil)! 15 | CGImageDestinationAddImage(destination, self, nil) 16 | CGImageDestinationFinalize(destination) 17 | } 18 | } 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /Splash/Sources/SplashImageGen/Extensions/CommandLine+Options.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | #if os(macOS) 8 | 9 | import Foundation 10 | import Splash 11 | 12 | extension CommandLine { 13 | struct Options { 14 | let code: String 15 | let outputURL: URL 16 | let padding: CGFloat 17 | let font: Font 18 | } 19 | 20 | static func makeOptions() -> Options? { 21 | guard arguments.count > 2 else { 22 | return nil 23 | } 24 | 25 | let defaults = UserDefaults.standard 26 | 27 | return Options( 28 | code: arguments[1], 29 | outputURL: resolveOutputURL(), 30 | padding: CGFloat(defaults.int(forKey: "p", default: 20)), 31 | font: resolveFont(from: defaults) 32 | ) 33 | } 34 | 35 | private static func resolveOutputURL() -> URL { 36 | let path = arguments[2] as NSString 37 | return URL(fileURLWithPath: path.expandingTildeInPath) 38 | } 39 | 40 | private static func resolveFont(from defaults: UserDefaults) -> Font { 41 | let size = Double(defaults.int(forKey: "s", default: 20)) 42 | 43 | guard let path = defaults.string(forKey: "f") else { 44 | return Font(size: size) 45 | } 46 | 47 | return Font(path: path, size: size) 48 | } 49 | } 50 | 51 | private extension UserDefaults { 52 | func int(forKey key: String, default: CGFloat) -> CGFloat { 53 | guard value(forKey: key) != nil else { 54 | return `default` 55 | } 56 | 57 | return CGFloat(integer(forKey: key)) 58 | } 59 | } 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /Splash/Sources/SplashImageGen/Extensions/NSGraphicsContext+Fill.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | #if os(macOS) 8 | 9 | import Cocoa 10 | 11 | extension NSGraphicsContext { 12 | func fill(with color: NSColor, in rect: CGRect) { 13 | cgContext.setFillColor(color.cgColor) 14 | cgContext.fill(rect) 15 | } 16 | } 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /Splash/Sources/SplashImageGen/Extensions/NSGraphicsContext+InitWithSize.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | #if os(macOS) 8 | 9 | import Cocoa 10 | 11 | extension NSGraphicsContext { 12 | convenience init(size: CGSize) { 13 | let scale: CGFloat = 2 14 | 15 | let context = CGContext( 16 | data: nil, 17 | width: Int(size.width * scale), 18 | height: Int(size.height * scale), 19 | bitsPerComponent: 8, 20 | bytesPerRow: 0, 21 | space: CGColorSpaceCreateDeviceRGB(), 22 | bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue 23 | )! 24 | 25 | context.scaleBy(x: scale, y: scale) 26 | 27 | self.init(cgContext: context, flipped: false) 28 | } 29 | } 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /Splash/Sources/SplashImageGen/main.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | #if os(macOS) 8 | 9 | import Cocoa 10 | import Splash 11 | 12 | guard let options = CommandLine.makeOptions() else { 13 | print(""" 14 | ⚠️ Two arguments are required: 15 | - The code to generate an image for 16 | - The path to write the generated image to 17 | 18 | Optionally, the following arguments can be passed: 19 | -p The amount of padding (in pixels) to apply around the code 20 | -f A path to a font to use when rendering 21 | -s The size of text to use when rendering 22 | """) 23 | exit(1) 24 | } 25 | 26 | let theme = Theme.sundellsColors(withFont: options.font) 27 | let outputFormat = AttributedStringOutputFormat(theme: theme) 28 | let highlighter = SyntaxHighlighter(format: outputFormat) 29 | let string = highlighter.highlight(options.code) 30 | let stringSize = string.size() 31 | 32 | let contextRect = CGRect( 33 | x: 0, 34 | y: 0, 35 | width: stringSize.width + options.padding * 2, 36 | height: stringSize.height + options.padding * 2 37 | ) 38 | 39 | let context = NSGraphicsContext(size: contextRect.size) 40 | NSGraphicsContext.current = context 41 | 42 | context.fill(with: theme.backgroundColor, in: contextRect) 43 | 44 | string.draw(in: CGRect( 45 | x: options.padding, 46 | y: options.padding, 47 | width: stringSize.width, 48 | height: stringSize.height 49 | )) 50 | 51 | let image = context.cgContext.makeImage()! 52 | image.write(to: options.outputURL) 53 | 54 | #else 55 | print("😞 SplashImageGen currently only supports macOS") 56 | #endif 57 | -------------------------------------------------------------------------------- /Splash/Sources/SplashMarkdown/main.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2019 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import Splash 9 | 10 | guard CommandLine.arguments.count > 1 else { 11 | print("⚠️ Please supply the path to a Markdown file to process as an argument") 12 | exit(1) 13 | } 14 | 15 | let markdown: String = { 16 | let path = CommandLine.arguments[1] 17 | 18 | do { 19 | let path = (path as NSString).expandingTildeInPath 20 | return try String(contentsOfFile: path) 21 | } catch { 22 | print(""" 23 | 🛑 Failed to open Markdown file at '\(path)': 24 | --- 25 | \(error.localizedDescription) 26 | --- 27 | """) 28 | exit(1) 29 | } 30 | }() 31 | 32 | let decorator = MarkdownDecorator() 33 | print(decorator.decorate(markdown)) 34 | -------------------------------------------------------------------------------- /Splash/Sources/SplashTokenizer/TokenizerOutputFormat.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import Splash 9 | 10 | struct TokenizerOutputFormat: OutputFormat { 11 | func makeBuilder() -> Builder { 12 | return Builder() 13 | } 14 | } 15 | 16 | extension TokenizerOutputFormat { 17 | struct Builder: OutputBuilder { 18 | private var components = [String]() 19 | 20 | mutating func addToken(_ token: String, ofType type: TokenType) { 21 | components.append("\(type.string.capitalized) token: \(token)") 22 | } 23 | 24 | mutating func addPlainText(_ text: String) { 25 | components.append("Plain text: \(text)") 26 | } 27 | 28 | mutating func addWhitespace(_ whitespace: String) { 29 | // Ignore whitespace 30 | } 31 | 32 | func build() -> String { 33 | return components.joined(separator: "\n") 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Splash/Sources/SplashTokenizer/main.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import Splash 9 | 10 | guard CommandLine.arguments.count > 1 else { 11 | print("⚠️ Please supply the code to tokenize as a string argument") 12 | exit(1) 13 | } 14 | 15 | let code = CommandLine.arguments[1] 16 | let highlighter = SyntaxHighlighter(format: TokenizerOutputFormat()) 17 | print(highlighter.highlight(code)) 18 | -------------------------------------------------------------------------------- /Splash/Tests/SplashTests/Core/SyntaxHighlighterTestCase.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import XCTest 9 | import Splash 10 | 11 | /// Test case used as an abstract base class for all tests relating to 12 | /// syntax highlighting. For all such tests, the Swift grammar is used. 13 | class SyntaxHighlighterTestCase: XCTestCase { 14 | private(set) var highlighter: SyntaxHighlighter! 15 | private(set) var builder: OutputBuilderMock! 16 | 17 | override func setUp() { 18 | super.setUp() 19 | builder = OutputBuilderMock() 20 | highlighter = SyntaxHighlighter(format: OutputFormatMock(builder: builder)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Splash/Tests/SplashTests/Mocks/OutputBuilderMock.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import Splash 9 | 10 | struct OutputBuilderMock: OutputBuilder { 11 | private var components = [Component]() 12 | 13 | mutating func addToken(_ token: String, ofType type: TokenType) { 14 | components.append(.token(token, type)) 15 | } 16 | 17 | mutating func addPlainText(_ text: String) { 18 | components.append(.plainText(text)) 19 | } 20 | 21 | mutating func addWhitespace(_ whitespace: String) { 22 | components.append(.whitespace(whitespace)) 23 | } 24 | 25 | func build() -> [Component] { 26 | return components 27 | } 28 | } 29 | 30 | extension OutputBuilderMock { 31 | enum Component: Equatable { 32 | case token(String, TokenType) 33 | case plainText(String) 34 | case whitespace(String) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Splash/Tests/SplashTests/Mocks/OutputFormatMock.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import Splash 9 | 10 | struct OutputFormatMock: OutputFormat { 11 | let builder: OutputBuilderMock 12 | 13 | func makeBuilder() -> OutputBuilderMock { 14 | return builder 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Splash/Tests/SplashTests/Tests/ClosureTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import XCTest 9 | import Splash 10 | 11 | final class ClosureTests: SyntaxHighlighterTestCase { 12 | func testTrailingClosureWithArguments() { 13 | let components = highlighter.highlight("call() { arg in }") 14 | 15 | XCTAssertEqual(components, [ 16 | .token("call", .call), 17 | .plainText("()"), 18 | .whitespace(" "), 19 | .plainText("{"), 20 | .whitespace(" "), 21 | .plainText("arg"), 22 | .whitespace(" "), 23 | .token("in", .keyword), 24 | .whitespace(" "), 25 | .plainText("}") 26 | ]) 27 | } 28 | 29 | func testTrailingClosureWithoutParanthesis() { 30 | let components = highlighter.highlight("call { $0 }") 31 | 32 | XCTAssertEqual(components, [ 33 | .token("call", .call), 34 | .whitespace(" "), 35 | .plainText("{"), 36 | .whitespace(" "), 37 | .plainText("$0"), 38 | .whitespace(" "), 39 | .plainText("}") 40 | ]) 41 | } 42 | 43 | func testEmptyTrailingClosure() { 44 | let components = highlighter.highlight("call {}") 45 | 46 | XCTAssertEqual(components, [ 47 | .token("call", .call), 48 | .whitespace(" "), 49 | .plainText("{}") 50 | ]) 51 | } 52 | 53 | func testClosureArgumentWithSingleArgument() { 54 | let components = highlighter.highlight("func add(closure: (String) -> Void)") 55 | 56 | XCTAssertEqual(components, [ 57 | .token("func", .keyword), 58 | .whitespace(" "), 59 | .plainText("add(closure:"), 60 | .whitespace(" "), 61 | .plainText("("), 62 | .token("String", .type), 63 | .plainText(")"), 64 | .whitespace(" "), 65 | .plainText("->"), 66 | .whitespace(" "), 67 | .token("Void", .type), 68 | .plainText(")") 69 | ]) 70 | } 71 | 72 | func testClosureArgumentWithMultipleArguments() { 73 | let components = highlighter.highlight("func add(closure: (String, Int) -> Void)") 74 | 75 | XCTAssertEqual(components, [ 76 | .token("func", .keyword), 77 | .whitespace(" "), 78 | .plainText("add(closure:"), 79 | .whitespace(" "), 80 | .plainText("("), 81 | .token("String", .type), 82 | .plainText(","), 83 | .whitespace(" "), 84 | .token("Int", .type), 85 | .plainText(")"), 86 | .whitespace(" "), 87 | .plainText("->"), 88 | .whitespace(" "), 89 | .token("Void", .type), 90 | .plainText(")") 91 | ]) 92 | } 93 | 94 | func testEscapingClosureArgument() { 95 | let components = highlighter.highlight("func add(closure: @escaping () -> Void)") 96 | 97 | XCTAssertEqual(components, [ 98 | .token("func", .keyword), 99 | .whitespace(" "), 100 | .plainText("add(closure:"), 101 | .whitespace(" "), 102 | .token("@escaping", .keyword), 103 | .whitespace(" "), 104 | .plainText("()"), 105 | .whitespace(" "), 106 | .plainText("->"), 107 | .whitespace(" "), 108 | .token("Void", .type), 109 | .plainText(")") 110 | ]) 111 | } 112 | 113 | func testClosureWithInoutArgument() { 114 | let components = highlighter.highlight("func add(closure: (inout Value) -> Void)") 115 | 116 | XCTAssertEqual(components, [ 117 | .token("func", .keyword), 118 | .whitespace(" "), 119 | .plainText("add(closure:"), 120 | .whitespace(" "), 121 | .plainText("("), 122 | .token("inout", .keyword), 123 | .whitespace(" "), 124 | .token("Value", .type), 125 | .plainText(")"), 126 | .whitespace(" "), 127 | .plainText("->"), 128 | .whitespace(" "), 129 | .token("Void", .type), 130 | .plainText(")") 131 | ]) 132 | } 133 | 134 | func testPassingClosureAsArgument() { 135 | let components = highlighter.highlight("object.call({ $0 })") 136 | 137 | XCTAssertEqual(components, [ 138 | .plainText("object."), 139 | .token("call", .call), 140 | .plainText("({"), 141 | .whitespace(" "), 142 | .plainText("$0"), 143 | .whitespace(" "), 144 | .plainText("})") 145 | ]) 146 | } 147 | 148 | func testNestedEscapingClosure() { 149 | let components = highlighter.highlight("let closures = [(@escaping () -> Void) -> Void]()") 150 | 151 | XCTAssertEqual(components, [ 152 | .token("let", .keyword), 153 | .whitespace(" "), 154 | .plainText("closures"), 155 | .whitespace(" "), 156 | .plainText("="), 157 | .whitespace(" "), 158 | .plainText("[("), 159 | .token("@escaping", .keyword), 160 | .whitespace(" "), 161 | .plainText("()"), 162 | .whitespace(" "), 163 | .plainText("->"), 164 | .whitespace(" "), 165 | .token("Void", .type), 166 | .plainText(")"), 167 | .whitespace(" "), 168 | .plainText("->"), 169 | .whitespace(" "), 170 | .token("Void", .type), 171 | .plainText("]()") 172 | ]) 173 | } 174 | 175 | func testClosureArgumentShorthands() { 176 | let components = highlighter.highlight(""" 177 | call { 178 | print($0) 179 | _ = $1 180 | $2() 181 | } 182 | """) 183 | 184 | XCTAssertEqual(components, [ 185 | .token("call", .call), 186 | .whitespace(" "), 187 | .plainText("{"), 188 | .whitespace("\n "), 189 | .token("print", .call), 190 | .plainText("($0)"), 191 | .whitespace("\n "), 192 | .token("_", .keyword), 193 | .whitespace(" "), 194 | .plainText("="), 195 | .whitespace(" "), 196 | .plainText("$1"), 197 | .whitespace("\n "), 198 | .plainText("$2()"), 199 | .whitespace("\n"), 200 | .plainText("}") 201 | ]) 202 | } 203 | 204 | func testClosureWithWeakSelfCaptureList() { 205 | let components = highlighter.highlight("closure { [weak self] in }") 206 | 207 | XCTAssertEqual(components, [ 208 | .token("closure", .call), 209 | .whitespace(" "), 210 | .plainText("{"), 211 | .whitespace(" "), 212 | .plainText("["), 213 | .token("weak", .keyword), 214 | .whitespace(" "), 215 | .token("self", .keyword), 216 | .plainText("]"), 217 | .whitespace(" "), 218 | .token("in", .keyword), 219 | .whitespace(" "), 220 | .plainText("}") 221 | ]) 222 | } 223 | 224 | func testClosureWithUnownedSelfCaptureList() { 225 | let components = highlighter.highlight("closure { [unowned self] in }") 226 | 227 | XCTAssertEqual(components, [ 228 | .token("closure", .call), 229 | .whitespace(" "), 230 | .plainText("{"), 231 | .whitespace(" "), 232 | .plainText("["), 233 | .token("unowned", .keyword), 234 | .whitespace(" "), 235 | .token("self", .keyword), 236 | .plainText("]"), 237 | .whitespace(" "), 238 | .token("in", .keyword), 239 | .whitespace(" "), 240 | .plainText("}") 241 | ]) 242 | } 243 | 244 | func testClosureWithSingleFunctionCall() { 245 | let components = highlighter.highlight("closure { a in call(a) }") 246 | 247 | XCTAssertEqual(components, [ 248 | .token("closure", .call), 249 | .whitespace(" "), 250 | .plainText("{"), 251 | .whitespace(" "), 252 | .plainText("a"), 253 | .whitespace(" "), 254 | .token("in", .keyword), 255 | .whitespace(" "), 256 | .token("call", .call), 257 | .plainText("(a)"), 258 | .whitespace(" "), 259 | .plainText("}") 260 | ]) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Splash/Tests/SplashTests/Tests/CommentTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import XCTest 9 | import Splash 10 | 11 | final class CommentTests: SyntaxHighlighterTestCase { 12 | func testSingleLineComment() { 13 | let components = highlighter.highlight("call() // Hello call() var \"string\"\ncall()") 14 | 15 | XCTAssertEqual(components, [ 16 | .token("call", .call), 17 | .plainText("()"), 18 | .whitespace(" "), 19 | .token("//", .comment), 20 | .whitespace(" "), 21 | .token("Hello", .comment), 22 | .whitespace(" "), 23 | .token("call()", .comment), 24 | .whitespace(" "), 25 | .token("var", .comment), 26 | .whitespace(" "), 27 | .token("\"string\"", .comment), 28 | .whitespace("\n"), 29 | .token("call", .call), 30 | .plainText("()") 31 | ]) 32 | } 33 | 34 | func testMultiLineComment() { 35 | let components = highlighter.highlight(""" 36 | struct Foo {} 37 | /* Comment 38 | Hello! 39 | */ call() 40 | """) 41 | 42 | XCTAssertEqual(components, [ 43 | .token("struct", .keyword), 44 | .whitespace(" "), 45 | .plainText("Foo"), 46 | .whitespace(" "), 47 | .plainText("{}"), 48 | .whitespace("\n"), 49 | .token("/*", .comment), 50 | .whitespace(" "), 51 | .token("Comment", .comment), 52 | .whitespace("\n "), 53 | .token("Hello!", .comment), 54 | .whitespace("\n"), 55 | .token("*/", .comment), 56 | .whitespace(" "), 57 | .token("call", .call), 58 | .plainText("()") 59 | ]) 60 | } 61 | 62 | func testMultiLineCommentWithDoubleAsterisks() { 63 | let components = highlighter.highlight(""" 64 | struct Foo {} 65 | /** Comment 66 | Hello! 67 | */ call() 68 | """) 69 | 70 | XCTAssertEqual(components, [ 71 | .token("struct", .keyword), 72 | .whitespace(" "), 73 | .plainText("Foo"), 74 | .whitespace(" "), 75 | .plainText("{}"), 76 | .whitespace("\n"), 77 | .token("/**", .comment), 78 | .whitespace(" "), 79 | .token("Comment", .comment), 80 | .whitespace("\n "), 81 | .token("Hello!", .comment), 82 | .whitespace("\n"), 83 | .token("*/", .comment), 84 | .whitespace(" "), 85 | .token("call", .call), 86 | .plainText("()") 87 | ]) 88 | } 89 | 90 | func testMutliLineDocumentationComment() { 91 | let components = highlighter.highlight(""" 92 | /** 93 | * Documentation 94 | */ 95 | class MyClass {} 96 | """) 97 | 98 | XCTAssertEqual(components, [ 99 | .token("/**", .comment), 100 | .whitespace("\n "), 101 | .token("*", .comment), 102 | .whitespace(" "), 103 | .token("Documentation", .comment), 104 | .whitespace("\n "), 105 | .token("*/", .comment), 106 | .whitespace("\n"), 107 | .token("class", .keyword), 108 | .whitespace(" "), 109 | .plainText("MyClass"), 110 | .whitespace(" "), 111 | .plainText("{}") 112 | ]) 113 | } 114 | 115 | func testCommentStartingWithPunctuation() { 116 | let components = highlighter.highlight("//.call()") 117 | XCTAssertEqual(components, [.token("//.call()", .comment)]) 118 | } 119 | 120 | func testCommentEndingWithComma() { 121 | let components = highlighter.highlight(""" 122 | // Hello, 123 | class World {} 124 | """) 125 | 126 | XCTAssertEqual(components, [ 127 | .token("//", .comment), 128 | .whitespace(" "), 129 | .token("Hello,", .comment), 130 | .whitespace("\n"), 131 | .token("class", .keyword), 132 | .whitespace(" "), 133 | .plainText("World"), 134 | .whitespace(" "), 135 | .plainText("{}") 136 | ]) 137 | } 138 | 139 | func testCommentPrecededByComma() { 140 | let components = highlighter.highlight(""" 141 | func find( 142 | string: String,//TODO: Remove 143 | options: Options 144 | ) 145 | """) 146 | 147 | XCTAssertEqual(components, [ 148 | .token("func", .keyword), 149 | .whitespace(" "), 150 | .plainText("find("), 151 | .whitespace("\n "), 152 | .plainText("string:"), 153 | .whitespace(" "), 154 | .token("String", .type), 155 | .plainText(","), 156 | .token("//TODO:", .comment), 157 | .whitespace(" "), 158 | .token("Remove", .comment), 159 | .whitespace("\n "), 160 | .plainText("options:"), 161 | .whitespace(" "), 162 | .token("Options", .type), 163 | .whitespace("\n"), 164 | .plainText(")") 165 | ]) 166 | } 167 | 168 | func testCommentWithNumber() { 169 | let components = highlighter.highlight("// 1") 170 | 171 | XCTAssertEqual(components, [ 172 | .token("//", .comment), 173 | .whitespace(" "), 174 | .token("1", .comment) 175 | ]) 176 | } 177 | 178 | func testCommentWithNoWhiteSpaceToPunctuation() { 179 | let components = highlighter.highlight(""" 180 | (/* Hello */) 181 | .// World 182 | (/**/) 183 | """) 184 | 185 | XCTAssertEqual(components, [ 186 | .plainText("("), 187 | .token("/*", .comment), 188 | .whitespace(" "), 189 | .token("Hello", .comment), 190 | .whitespace(" "), 191 | .token("*/", .comment), 192 | .plainText(")"), 193 | .whitespace("\n"), 194 | .plainText("."), 195 | .token("//", .comment), 196 | .whitespace(" "), 197 | .token("World", .comment), 198 | .whitespace("\n"), 199 | .plainText("("), 200 | .token("/**/", .comment), 201 | .plainText(")"), 202 | ]) 203 | } 204 | 205 | func testCommentsNextToCurlyBrackets() { 206 | let components = highlighter.highlight(""" 207 | call {//commentA 208 | }//commentB 209 | """) 210 | 211 | XCTAssertEqual(components, [ 212 | .token("call", .call), 213 | .whitespace(" "), 214 | .plainText("{"), 215 | .token("//commentA", .comment), 216 | .whitespace("\n"), 217 | .plainText("}"), 218 | .token("//commentB", .comment) 219 | ]) 220 | } 221 | 222 | func testCommentWithinGenericTypeList() { 223 | let components = highlighter.highlight(""" 224 | struct Box {} 225 | """) 226 | 227 | XCTAssertEqual(components, [ 228 | .token("struct", .keyword), 229 | .whitespace(" "), 230 | .plainText("Box"), 239 | .whitespace(" "), 240 | .plainText("{}") 241 | ]) 242 | } 243 | 244 | func testCommentsNextToGenericTypeList() { 245 | let components = highlighter.highlight(""" 246 | struct Box/*Start*//*End*/ {} 247 | """) 248 | 249 | XCTAssertEqual(components, [ 250 | .token("struct", .keyword), 251 | .whitespace(" "), 252 | .plainText("Box"), 253 | .token("/*Start*/", .comment), 254 | .plainText(""), 255 | .token("/*End*/", .comment), 256 | .whitespace(" "), 257 | .plainText("{}") 258 | ]) 259 | } 260 | 261 | func testCommentsNextToInitialization() { 262 | let components = highlighter.highlight("/*Start*/Object()/*End*/") 263 | 264 | XCTAssertEqual(components, [ 265 | .token("/*Start*/", .comment), 266 | .token("Object", .type), 267 | .plainText("()"), 268 | .token("/*End*/", .comment) 269 | ]) 270 | } 271 | 272 | func testCommentsNextToProtocolName() { 273 | let components = highlighter.highlight(""" 274 | struct Model: /*Start*/Equatable/*End*/ {} 275 | """) 276 | 277 | XCTAssertEqual(components, [ 278 | .token("struct", .keyword), 279 | .whitespace(" "), 280 | .plainText("Model:"), 281 | .whitespace(" "), 282 | .token("/*Start*/", .comment), 283 | .token("Equatable", .type), 284 | .token("/*End*/", .comment), 285 | .whitespace(" "), 286 | .plainText("{}") 287 | ]) 288 | } 289 | 290 | func testCommentsAfterOptionalTypes() { 291 | let components = highlighter.highlight(""" 292 | struct Model { 293 | var one: String?//One 294 | var two: String?/*Two*/ 295 | } 296 | """) 297 | 298 | XCTAssertEqual(components, [ 299 | .token("struct", .keyword), 300 | .whitespace(" "), 301 | .plainText("Model"), 302 | .whitespace(" "), 303 | .plainText("{"), 304 | .whitespace("\n "), 305 | .token("var", .keyword), 306 | .whitespace(" "), 307 | .plainText("one:"), 308 | .whitespace(" "), 309 | .token("String", .type), 310 | .plainText("?"), 311 | .token("//One", .comment), 312 | .whitespace("\n "), 313 | .token("var", .keyword), 314 | .whitespace(" "), 315 | .plainText("two:"), 316 | .whitespace(" "), 317 | .token("String", .type), 318 | .plainText("?"), 319 | .token("/*Two*/", .comment), 320 | .whitespace("\n"), 321 | .plainText("}") 322 | ]) 323 | } 324 | 325 | func testCommentsAfterArrayTypes() { 326 | let components = highlighter.highlight(""" 327 | struct Model { 328 | var one: [String]//One 329 | var two: [String]/*Two*/ 330 | } 331 | """) 332 | 333 | XCTAssertEqual(components, [ 334 | .token("struct", .keyword), 335 | .whitespace(" "), 336 | .plainText("Model"), 337 | .whitespace(" "), 338 | .plainText("{"), 339 | .whitespace("\n "), 340 | .token("var", .keyword), 341 | .whitespace(" "), 342 | .plainText("one:"), 343 | .whitespace(" "), 344 | .plainText("["), 345 | .token("String", .type), 346 | .plainText("]"), 347 | .token("//One", .comment), 348 | .whitespace("\n "), 349 | .token("var", .keyword), 350 | .whitespace(" "), 351 | .plainText("two:"), 352 | .whitespace(" "), 353 | .plainText("["), 354 | .token("String", .type), 355 | .plainText("]"), 356 | .token("/*Two*/", .comment), 357 | .whitespace("\n"), 358 | .plainText("}") 359 | ]) 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /Splash/Tests/SplashTests/Tests/EnumTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import XCTest 9 | import Splash 10 | 11 | final class EnumTests: SyntaxHighlighterTestCase { 12 | func testEnumDotSyntaxInAssignment() { 13 | let components = highlighter.highlight("let value: Enum = .aCase") 14 | 15 | XCTAssertEqual(components, [ 16 | .token("let", .keyword), 17 | .whitespace(" "), 18 | .plainText("value:"), 19 | .whitespace(" "), 20 | .token("Enum", .type), 21 | .whitespace(" "), 22 | .plainText("="), 23 | .whitespace(" "), 24 | .plainText("."), 25 | .token("aCase", .dotAccess) 26 | ]) 27 | } 28 | 29 | func testEnumDotSyntaxAsArgument() { 30 | let components = highlighter.highlight("call(.aCase)") 31 | 32 | XCTAssertEqual(components, [ 33 | .token("call", .call), 34 | .plainText("(."), 35 | .token("aCase", .dotAccess), 36 | .plainText(")") 37 | ]) 38 | } 39 | 40 | func testEnumDotSyntaxWithAssociatedValueTreatedAsCall() { 41 | let components = highlighter.highlight("call(.error(error))") 42 | 43 | XCTAssertEqual(components, [ 44 | .token("call", .call), 45 | .plainText("(."), 46 | .token("error", .call), 47 | .plainText("(error))") 48 | ]) 49 | } 50 | 51 | func testUsingEnumInSubscript() { 52 | let components = highlighter.highlight("dictionary[.key]") 53 | 54 | XCTAssertEqual(components, [ 55 | .plainText("dictionary[."), 56 | .token("key", .dotAccess), 57 | .plainText("]") 58 | ]) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Splash/Tests/SplashTests/Tests/FunctionCallTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import XCTest 9 | import Splash 10 | 11 | final class FunctionCallTests: SyntaxHighlighterTestCase { 12 | func testFunctionCallWithIntegers() { 13 | let components = highlighter.highlight("add(1, 2)") 14 | 15 | XCTAssertEqual(components, [ 16 | .token("add", .call), 17 | .plainText("("), 18 | .token("1", .number), 19 | .plainText(","), 20 | .whitespace(" "), 21 | .token("2", .number), 22 | .plainText(")") 23 | ]) 24 | } 25 | 26 | func testFunctionCallWithNil() { 27 | let components = highlighter.highlight("handler(nil)") 28 | 29 | XCTAssertEqual(components, [ 30 | .token("handler", .call), 31 | .plainText("("), 32 | .token("nil", .keyword), 33 | .plainText(")") 34 | ]) 35 | } 36 | 37 | func testImplicitInitializerCall() { 38 | let components = highlighter.highlight("let string = String()") 39 | 40 | XCTAssertEqual(components, [ 41 | .token("let", .keyword), 42 | .whitespace(" "), 43 | .plainText("string"), 44 | .whitespace(" "), 45 | .plainText("="), 46 | .whitespace(" "), 47 | .token("String", .type), 48 | .plainText("()") 49 | ]) 50 | } 51 | 52 | func testExplicitInitializerCall() { 53 | let components = highlighter.highlight("let string = String.init()") 54 | 55 | XCTAssertEqual(components, [ 56 | .token("let", .keyword), 57 | .whitespace(" "), 58 | .plainText("string"), 59 | .whitespace(" "), 60 | .plainText("="), 61 | .whitespace(" "), 62 | .token("String", .type), 63 | .plainText("."), 64 | .token("init", .keyword), 65 | .plainText("()") 66 | ]) 67 | } 68 | 69 | func testExplicitInitializerCallUsingTrailingClosureSyntax() { 70 | let components = highlighter.highlight("let task = Task.init {}") 71 | 72 | XCTAssertEqual(components, [ 73 | .token("let", .keyword), 74 | .whitespace(" "), 75 | .plainText("task"), 76 | .whitespace(" "), 77 | .plainText("="), 78 | .whitespace(" "), 79 | .token("Task", .type), 80 | .plainText("."), 81 | .token("init", .keyword), 82 | .whitespace(" "), 83 | .plainText("{}") 84 | ]) 85 | } 86 | 87 | func testDotSyntaxInitializerCall() { 88 | let components = highlighter.highlight("let string: String = .init()") 89 | 90 | XCTAssertEqual(components, [ 91 | .token("let", .keyword), 92 | .whitespace(" "), 93 | .plainText("string:"), 94 | .whitespace(" "), 95 | .token("String", .type), 96 | .whitespace(" "), 97 | .plainText("="), 98 | .whitespace(" "), 99 | .plainText("."), 100 | .token("init", .keyword), 101 | .plainText("()") 102 | ]) 103 | } 104 | 105 | func testAccessingPropertyAfterFunctionCallWithoutArguments() { 106 | let components = highlighter.highlight("call().property") 107 | 108 | XCTAssertEqual(components, [ 109 | .token("call", .call), 110 | .plainText("()."), 111 | .token("property", .property) 112 | ]) 113 | } 114 | 115 | func testAccessingPropertyAfterFunctionCallWithArguments() { 116 | let components = highlighter.highlight("call(argument).property") 117 | 118 | XCTAssertEqual(components, [ 119 | .token("call", .call), 120 | .plainText("(argument)."), 121 | .token("property", .property) 122 | ]) 123 | } 124 | 125 | func testCallingStaticMethodOnGenericType() { 126 | let components = highlighter.highlight("Array.call()") 127 | 128 | XCTAssertEqual(components, [ 129 | .token("Array", .type), 130 | .plainText("<"), 131 | .token("String", .type), 132 | .plainText(">."), 133 | .token("call", .call), 134 | .plainText("()") 135 | ]) 136 | } 137 | 138 | func testPassingTypeToFunction() { 139 | let components = highlighter.highlight("call(String.self)") 140 | 141 | XCTAssertEqual(components, [ 142 | .token("call", .call), 143 | .plainText("("), 144 | .token("String", .type), 145 | .plainText("."), 146 | .token("self", .keyword), 147 | .plainText(")") 148 | ]) 149 | } 150 | 151 | func testPassingBoolToUnnamedArgument() { 152 | let components = highlighter.highlight("setCachingEnabled(true)") 153 | 154 | XCTAssertEqual(components, [ 155 | .token("setCachingEnabled", .call), 156 | .plainText("("), 157 | .token("true", .keyword), 158 | .plainText(")") 159 | ]) 160 | } 161 | 162 | func testIndentedFunctionCalls() { 163 | let components = highlighter.highlight(""" 164 | variable 165 | .callOne() 166 | .callTwo() 167 | """) 168 | 169 | XCTAssertEqual(components, [ 170 | .plainText("variable"), 171 | .whitespace("\n "), 172 | .plainText("."), 173 | .token("callOne", .call), 174 | .plainText("()"), 175 | .whitespace("\n "), 176 | .plainText("."), 177 | .token("callTwo", .call), 178 | .plainText("()") 179 | ]) 180 | } 181 | 182 | func testXCTAssertCalls() { 183 | let components = highlighter.highlight("XCTAssertTrue(variable)") 184 | 185 | XCTAssertEqual(components, [ 186 | .token("XCTAssertTrue", .call), 187 | .plainText("(variable)") 188 | ]) 189 | } 190 | 191 | func testUsingTryKeywordWithinFunctionCall() { 192 | let components = highlighter.highlight("XCTAssertThrowsError(try function())") 193 | 194 | XCTAssertEqual(components, [ 195 | .token("XCTAssertThrowsError", .call), 196 | .plainText("("), 197 | .token("try", .keyword), 198 | .whitespace(" "), 199 | .token("function", .call), 200 | .plainText("())") 201 | ]) 202 | } 203 | 204 | func testCallingFunctionsWithProjectedPropertyWrapperValues() { 205 | let components = highlighter.highlight(""" 206 | call($value) 207 | call(self.$value) 208 | """) 209 | 210 | XCTAssertEqual(components, [ 211 | .token("call", .call), 212 | .plainText("("), 213 | .token("$value", .property), 214 | .plainText(")"), 215 | .whitespace("\n"), 216 | .token("call", .call), 217 | .plainText("("), 218 | .token("self", .keyword), 219 | .plainText("."), 220 | .token("$value", .property), 221 | .plainText(")") 222 | ]) 223 | } 224 | 225 | func testCallingFunctionWithInoutProjectedPropertyWrapperValue() { 226 | let components = highlighter.highlight("call(&$value)") 227 | 228 | XCTAssertEqual(components, [ 229 | .token("call", .call), 230 | .plainText("(&"), 231 | .token("$value", .property), 232 | .plainText(")") 233 | ]) 234 | } 235 | 236 | func testCallingMethodWithSameNameAsKeywordWithTrailingClosureSyntax() { 237 | let components = highlighter.highlight("publisher.catch { error in }") 238 | 239 | XCTAssertEqual(components, [ 240 | .plainText("publisher."), 241 | .token("catch", .call), 242 | .whitespace(" "), 243 | .plainText("{"), 244 | .whitespace(" "), 245 | .plainText("error"), 246 | .whitespace(" "), 247 | .token("in", .keyword), 248 | .whitespace(" "), 249 | .plainText("}") 250 | ]) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /Splash/Tests/SplashTests/Tests/HTMLOutputFormatTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import Splash 4 | 5 | final class HTMLOutputFormatTests: XCTestCase { 6 | private var highlighter: SyntaxHighlighter! 7 | 8 | override func setUp() { 9 | super.setUp() 10 | highlighter = SyntaxHighlighter(format: HTMLOutputFormat()) 11 | } 12 | 13 | func testBasicGeneration() { 14 | let html = highlighter.highlight(""" 15 | public struct Test: SomeProtocol { 16 | func hello() -> Int { return 7 } 17 | } 18 | """) 19 | 20 | XCTAssertEqual(html, """ 21 | public struct Test: SomeProtocol { 22 | func hello() -> Int { return 7 } 23 | } 24 | """) 25 | } 26 | 27 | func testStrippingGreaterAndLessThanCharactersFromOutput() { 28 | let html = highlighter.highlight("Array") 29 | 30 | XCTAssertEqual(html, """ 31 | Array<String> 32 | """) 33 | } 34 | 35 | func testCommentMerging() { 36 | let html = highlighter.highlight("// Hey I'm a comment!") 37 | 38 | XCTAssertEqual(html, """ 39 | // Hey I'm a comment! 40 | """) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Splash/Tests/SplashTests/Tests/LiteralTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import XCTest 9 | import Splash 10 | 11 | final class LiteralTests: SyntaxHighlighterTestCase { 12 | func testStringLiteral() { 13 | let components = highlighter.highlight("let string = \"Hello, world!\"") 14 | 15 | XCTAssertEqual(components, [ 16 | .token("let", .keyword), 17 | .whitespace(" "), 18 | .plainText("string"), 19 | .whitespace(" "), 20 | .plainText("="), 21 | .whitespace(" "), 22 | .token("\"Hello,", .string), 23 | .whitespace(" "), 24 | .token("world!\"", .string) 25 | ]) 26 | } 27 | 28 | func testStringLiteralPassedToFunction() { 29 | let components = highlighter.highlight("call(\"Hello, world!\")") 30 | 31 | XCTAssertEqual(components, [ 32 | .token("call", .call), 33 | .plainText("("), 34 | .token("\"Hello,", .string), 35 | .whitespace(" "), 36 | .token("world!\"", .string), 37 | .plainText(")") 38 | ]) 39 | } 40 | 41 | func testStringLiteralWithEscapedQuote() { 42 | let components = highlighter.highlight("\"Hello \\\" World\"; call()") 43 | 44 | XCTAssertEqual(components, [ 45 | .token("\"Hello", .string), 46 | .whitespace(" "), 47 | .token("\\\"", .string), 48 | .whitespace(" "), 49 | .token("World\"", .string), 50 | .plainText(";"), 51 | .whitespace(" "), 52 | .token("call", .call), 53 | .plainText("()") 54 | ]) 55 | } 56 | 57 | func testStringLiteralWithAttribute() { 58 | let components = highlighter.highlight("\"@escaping\"") 59 | XCTAssertEqual(components, [.token("\"@escaping\"", .string)]) 60 | } 61 | 62 | func testStringLiteralInterpolation() { 63 | let components = highlighter.highlight("\"Hello \\(variable) world \\(call())\"") 64 | 65 | XCTAssertEqual(components, [ 66 | .token("\"Hello", .string), 67 | .whitespace(" "), 68 | .plainText("\\(variable)"), 69 | .whitespace(" "), 70 | .token("world", .string), 71 | .whitespace(" "), 72 | .plainText("\\("), 73 | .token("call", .call), 74 | .plainText("())"), 75 | .token("\"", .string) 76 | ]) 77 | } 78 | 79 | func testStringLiteralWithInterpolatedClosureArgumentShorthand() { 80 | let components = highlighter.highlight(#""\($0)""#) 81 | 82 | XCTAssertEqual(components, [ 83 | .token("\"", .string), 84 | .plainText(#"\($0)"#), 85 | .token("\"", .string) 86 | ]) 87 | } 88 | 89 | func testStringLiteralWithCustomIterpolation() { 90 | let components = highlighter.highlight(""" 91 | "Hello \\(label: a, b) world \\(label: call())" 92 | """) 93 | 94 | XCTAssertEqual(components, [ 95 | .token("\"Hello", .string), 96 | .whitespace(" "), 97 | .plainText("\\(label:"), 98 | .whitespace(" "), 99 | .plainText("a,"), 100 | .whitespace(" "), 101 | .plainText("b)"), 102 | .whitespace(" "), 103 | .token("world", .string), 104 | .whitespace(" "), 105 | .plainText("\\(label:"), 106 | .whitespace(" "), 107 | .token("call", .call), 108 | .plainText("())"), 109 | .token("\"", .string) 110 | ]) 111 | } 112 | 113 | func testStringLiteralWithInterpolationSurroundedByBrackets() { 114 | let components = highlighter.highlight(#""[\(text)]""#) 115 | 116 | XCTAssertEqual(components, [ 117 | .token(#""["#, .string), 118 | .plainText(#"\(text)"#), 119 | .token(#"]""#, .string) 120 | ]) 121 | } 122 | 123 | func testStringLiteralWithInterpolationPrefixedByPunctuation() { 124 | let components = highlighter.highlight(#"".\(text)""#) 125 | 126 | XCTAssertEqual(components, [ 127 | .token("\".", .string), 128 | .plainText(#"\(text)"#), 129 | .token("\"", .string) 130 | ]) 131 | } 132 | 133 | func testStringLiteralWithInterpolationContainingString() { 134 | let components = highlighter.highlight(#""\(name ?? "name")""#) 135 | 136 | XCTAssertEqual(components, [ 137 | .token("\"", .string), 138 | .plainText("\\(name"), 139 | .whitespace(" "), 140 | .plainText("??"), 141 | .whitespace(" "), 142 | .token("\"name\"", .string), 143 | .plainText(")"), 144 | .token("\"", .string) 145 | ]) 146 | } 147 | 148 | func testMultiLineStringLiteral() { 149 | let components = highlighter.highlight(""" 150 | let string = \"\"\" 151 | Hello \\(variable) 152 | \"\"\" 153 | """) 154 | 155 | XCTAssertEqual(components, [ 156 | .token("let", .keyword), 157 | .whitespace(" "), 158 | .plainText("string"), 159 | .whitespace(" "), 160 | .plainText("="), 161 | .whitespace(" "), 162 | .token("\"\"\"", .string), 163 | .whitespace("\n"), 164 | .token("Hello", .string), 165 | .whitespace(" "), 166 | .plainText("\\(variable)"), 167 | .whitespace("\n"), 168 | .token("\"\"\"", .string) 169 | ]) 170 | } 171 | 172 | func testSingleLineRawStringLiteral() { 173 | let components = highlighter.highlight(""" 174 | #"A raw string \\(withoutInterpolation) yes"# 175 | """) 176 | 177 | XCTAssertEqual(components, [ 178 | .token("#\"A", .string), 179 | .whitespace(" "), 180 | .token("raw", .string), 181 | .whitespace(" "), 182 | .token("string", .string), 183 | .whitespace(" "), 184 | .token("\\(withoutInterpolation)", .string), 185 | .whitespace(" "), 186 | .token("yes\"#", .string) 187 | ]) 188 | } 189 | 190 | func testMultiLineRawStringLiteral() { 191 | let components = highlighter.highlight(""" 192 | #\"\"\" 193 | A raw string \\(withoutInterpolation) 194 | with multiple lines. #" Nested "# 195 | \"\"\"# 196 | """) 197 | 198 | XCTAssertEqual(components, [ 199 | .token("#\"\"\"", .string), 200 | .whitespace("\n"), 201 | .token("A", .string), 202 | .whitespace(" "), 203 | .token("raw", .string), 204 | .whitespace(" "), 205 | .token("string", .string), 206 | .whitespace(" "), 207 | .token("\\(withoutInterpolation)", .string), 208 | .whitespace("\n"), 209 | .token("with", .string), 210 | .whitespace(" "), 211 | .token("multiple", .string), 212 | .whitespace(" "), 213 | .token("lines.", .string), 214 | .whitespace(" "), 215 | .token("#\"", .string), 216 | .whitespace(" "), 217 | .token("Nested", .string), 218 | .whitespace(" "), 219 | .token("\"#", .string), 220 | .whitespace("\n"), 221 | .token("\"\"\"#", .string) 222 | ]) 223 | } 224 | 225 | func testRawStringWithInterpolation() { 226 | let components = highlighter.highlight("#\"Hello \\#(variable) world\"#") 227 | 228 | XCTAssertEqual(components, [ 229 | .token("#\"Hello", .string), 230 | .whitespace(" "), 231 | .plainText("\\#(variable)"), 232 | .whitespace(" "), 233 | .token("world\"#", .string) 234 | ]) 235 | } 236 | 237 | func testStringLiteralContainingOnlyNewLine() { 238 | let components = highlighter.highlight(#"text.split(separator: "\n")"#) 239 | 240 | XCTAssertEqual(components, [ 241 | .plainText("text."), 242 | .token("split", .call), 243 | .plainText("(separator:"), 244 | .whitespace(" "), 245 | .token(#""\n""#, .string), 246 | .plainText(")") 247 | ]) 248 | } 249 | 250 | func testDoubleLiteral() { 251 | let components = highlighter.highlight("let double = 1.13") 252 | 253 | XCTAssertEqual(components, [ 254 | .token("let", .keyword), 255 | .whitespace(" "), 256 | .plainText("double"), 257 | .whitespace(" "), 258 | .plainText("="), 259 | .whitespace(" "), 260 | .token("1.13", .number) 261 | ]) 262 | } 263 | 264 | func testIntegerLiteralWithSeparators() { 265 | let components = highlighter.highlight("let int = 1_000_000") 266 | 267 | XCTAssertEqual(components, [ 268 | .token("let", .keyword), 269 | .whitespace(" "), 270 | .plainText("int"), 271 | .whitespace(" "), 272 | .plainText("="), 273 | .whitespace(" "), 274 | .token("1_000_000", .number) 275 | ]) 276 | } 277 | 278 | func testKeyPathLiteral() { 279 | let components = highlighter.highlight("let value = object[keyPath: \\.property]") 280 | 281 | XCTAssertEqual(components, [ 282 | .token("let", .keyword), 283 | .whitespace(" "), 284 | .plainText("value"), 285 | .whitespace(" "), 286 | .plainText("="), 287 | .whitespace(" "), 288 | .plainText("object[keyPath:"), 289 | .whitespace(" "), 290 | .plainText("\\."), 291 | .token("property", .property), 292 | .plainText("]") 293 | ]) 294 | } 295 | 296 | func testKeyPathLiteralsAsArguments() { 297 | let components = highlighter.highlight(#"user.bind(\.name, to: \.text)"#) 298 | 299 | XCTAssertEqual(components, [ 300 | .plainText("user."), 301 | .token("bind", .call), 302 | .plainText(#"(\."#), 303 | .token("name", .property), 304 | .plainText(","), 305 | .whitespace(" "), 306 | .plainText("to:"), 307 | .whitespace(" "), 308 | .plainText(#"\."#), 309 | .token("text", .property), 310 | .plainText(")") 311 | ]) 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /Splash/Tests/SplashTests/Tests/MarkdownTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2019 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import XCTest 8 | import Splash 9 | 10 | final class MarkdownTests: XCTestCase { 11 | private var decorator: MarkdownDecorator! 12 | 13 | override func setUp() { 14 | super.setUp() 15 | decorator = MarkdownDecorator() 16 | } 17 | 18 | func testConvertingCodeBlock() { 19 | let markdown = """ 20 | # Title 21 | 22 | Text text text `inline.code.shouldNotBeHighlighted()`. 23 | 24 | ``` 25 | struct Hello: Protocol {} 26 | ``` 27 | 28 | Text. 29 | """ 30 | 31 | let expectedResult = """ 32 | # Title 33 | 34 | Text text text `inline.code.shouldNotBeHighlighted()`. 35 | 36 |
struct Hello: Protocol {}
37 | 38 | Text. 39 | """ 40 | 41 | XCTAssertEqual(decorator.decorate(markdown), expectedResult) 42 | } 43 | 44 | func testSkippingHighlightingForCodeBlock() { 45 | let markdown = """ 46 | Text text. 47 | 48 | ```no-highlight 49 | struct Hello: Protocol {} 50 | ``` 51 | 52 | Text. 53 | """ 54 | 55 | let expectedResult = """ 56 | Text text. 57 | 58 |
struct Hello: Protocol {}
59 | 60 | Text. 61 | """ 62 | 63 | XCTAssertEqual(decorator.decorate(markdown), expectedResult) 64 | } 65 | 66 | func testEscapingSpecialCharactersWithinHighlightedCodeBlock() { 67 | let markdown = """ 68 | Text text. 69 | 70 | ``` 71 | let a = "" 72 | ``` 73 | 74 | Text. 75 | """ 76 | 77 | let expectedResult = """ 78 | Text text. 79 | 80 |
let a = "<Hello&World>"
81 | 82 | Text. 83 | """ 84 | 85 | XCTAssertEqual(decorator.decorate(markdown), expectedResult) 86 | } 87 | 88 | func testEscapingSpecialCharactersWithinSkippedCodeBlock() { 89 | let markdown = """ 90 | Text text. 91 | 92 | ```no-highlight 93 | let a = "" 94 | ``` 95 | 96 | Text. 97 | """ 98 | 99 | let expectedResult = """ 100 | Text text. 101 | 102 |
let a = "<Hello&World>"
103 | 104 | Text. 105 | """ 106 | 107 | XCTAssertEqual(decorator.decorate(markdown), expectedResult) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Splash/Tests/SplashTests/Tests/OptionalTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import XCTest 9 | import Splash 10 | 11 | final class OptionalTests: SyntaxHighlighterTestCase { 12 | func testAssigningPropertyWithOptionalChaining() { 13 | let components = highlighter.highlight("object?.property = true") 14 | 15 | XCTAssertEqual(components, [ 16 | .plainText("object?."), 17 | .token("property", .property), 18 | .whitespace(" "), 19 | .plainText("="), 20 | .whitespace(" "), 21 | .token("true", .keyword) 22 | ]) 23 | } 24 | 25 | func testReadingPropertyWithOptionalChaining() { 26 | let components = highlighter.highlight("call(object?.property)") 27 | 28 | XCTAssertEqual(components, [ 29 | .token("call", .call), 30 | .plainText("(object?."), 31 | .token("property", .property), 32 | .plainText(")") 33 | ]) 34 | } 35 | 36 | func testCallingMethodwithOptionalChaining() { 37 | let components = highlighter.highlight("object?.call()") 38 | 39 | XCTAssertEqual(components, [ 40 | .plainText("object?."), 41 | .token("call", .call), 42 | .plainText("()") 43 | ]) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Splash/Tests/SplashTests/Tests/PreprocessorTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import XCTest 9 | import Splash 10 | 11 | final class PreprocessorTests: SyntaxHighlighterTestCase { 12 | func testPreprocessing() { 13 | let components = highlighter.highlight(""" 14 | #if os(iOS) 15 | call() 16 | #endif 17 | """) 18 | 19 | XCTAssertEqual(components, [ 20 | .token("#if", .preprocessing), 21 | .whitespace(" "), 22 | .token("os(iOS)", .preprocessing), 23 | .whitespace("\n"), 24 | .token("call", .call), 25 | .plainText("()"), 26 | .whitespace("\n"), 27 | .token("#endif", .preprocessing) 28 | ]) 29 | } 30 | 31 | func testSelector() { 32 | let components = highlighter.highlight("addObserver(self, selector: #selector(function(_:)))") 33 | 34 | XCTAssertEqual(components, [ 35 | .token("addObserver", .call), 36 | .plainText("("), 37 | .token("self", .keyword), 38 | .plainText(","), 39 | .whitespace(" "), 40 | .plainText("selector:"), 41 | .whitespace(" "), 42 | .token("#selector", .keyword), 43 | .plainText("("), 44 | .token("function", .call), 45 | .plainText("("), 46 | .token("_", .keyword), 47 | .plainText(":)))") 48 | ]) 49 | } 50 | 51 | func testFunctionAttribute() { 52 | let components = highlighter.highlight("@NSApplicationMain class AppDelegate {}") 53 | 54 | XCTAssertEqual(components, [ 55 | .token("@NSApplicationMain", .keyword), 56 | .whitespace(" "), 57 | .token("class", .keyword), 58 | .whitespace(" "), 59 | .plainText("AppDelegate"), 60 | .whitespace(" "), 61 | .plainText("{}") 62 | ]) 63 | } 64 | 65 | func testAvailabilityCheck() { 66 | let components = highlighter.highlight("if #available(iOS 13, *) {}") 67 | 68 | XCTAssertEqual(components, [ 69 | .token("if", .keyword), 70 | .whitespace(" "), 71 | .token("#available", .keyword), 72 | .plainText("(iOS"), 73 | .whitespace(" "), 74 | .token("13", .number), 75 | .plainText(","), 76 | .whitespace(" "), 77 | .plainText("*)"), 78 | .whitespace(" "), 79 | .plainText("{}") 80 | ]) 81 | } 82 | 83 | func testWarningDirective() { 84 | let components = highlighter.highlight(#"#warning("Hey!")"#) 85 | 86 | XCTAssertEqual(components, [ 87 | .token("#warning", .preprocessing), 88 | .plainText("("), 89 | .token(#""Hey!""#, .string), 90 | .plainText(")") 91 | ]) 92 | } 93 | 94 | func testErrorDirective() { 95 | let components = highlighter.highlight(#"#error("No!")"#) 96 | 97 | XCTAssertEqual(components, [ 98 | .token("#error", .preprocessing), 99 | .plainText("("), 100 | .token(#""No!""#, .string), 101 | .plainText(")") 102 | ]) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Splash/Tests/SplashTests/Tests/StatementTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import XCTest 9 | import Splash 10 | 11 | final class StatementTests: SyntaxHighlighterTestCase { 12 | func testImportStatement() { 13 | let components = highlighter.highlight("import UIKit") 14 | 15 | XCTAssertEqual(components, [ 16 | .token("import", .keyword), 17 | .whitespace(" "), 18 | .plainText("UIKit") 19 | ]) 20 | } 21 | 22 | func testImportStatementWithSubmodule() { 23 | let components = highlighter.highlight("import os.log") 24 | 25 | XCTAssertEqual(components, [ 26 | .token("import", .keyword), 27 | .whitespace(" "), 28 | .plainText("os.log") 29 | ]) 30 | } 31 | 32 | func testChainedIfElseStatements() { 33 | let components = highlighter.highlight("if condition { } else if call() { } else { \"string\" }") 34 | 35 | XCTAssertEqual(components, [ 36 | .token("if", .keyword), 37 | .whitespace(" "), 38 | .plainText("condition"), 39 | .whitespace(" "), 40 | .plainText("{"), 41 | .whitespace(" "), 42 | .plainText("}"), 43 | .whitespace(" "), 44 | .token("else", .keyword), 45 | .whitespace(" "), 46 | .token("if", .keyword), 47 | .whitespace(" "), 48 | .token("call", .call), 49 | .plainText("()"), 50 | .whitespace(" "), 51 | .plainText("{"), 52 | .whitespace(" "), 53 | .plainText("}"), 54 | .whitespace(" "), 55 | .token("else", .keyword), 56 | .whitespace(" "), 57 | .plainText("{"), 58 | .whitespace(" "), 59 | .token("\"string\"", .string), 60 | .whitespace(" "), 61 | .plainText("}") 62 | ]) 63 | } 64 | 65 | func testIfLetStatementWithKeywordSymbolName() { 66 | let components = highlighter.highlight("if let override = optional {}") 67 | 68 | XCTAssertEqual(components, [ 69 | .token("if", .keyword), 70 | .whitespace(" "), 71 | .token("let", .keyword), 72 | .whitespace(" "), 73 | .plainText("override"), 74 | .whitespace(" "), 75 | .plainText("="), 76 | .whitespace(" "), 77 | .plainText("optional"), 78 | .whitespace(" "), 79 | .plainText("{}") 80 | ]) 81 | } 82 | 83 | func testGuardStatementUnwrappingWeakSelf() { 84 | let components = highlighter.highlight("guard let self = self else {}") 85 | 86 | XCTAssertEqual(components, [ 87 | .token("guard", .keyword), 88 | .whitespace(" "), 89 | .token("let", .keyword), 90 | .whitespace(" "), 91 | .token("self", .keyword), 92 | .whitespace(" "), 93 | .plainText("="), 94 | .whitespace(" "), 95 | .token("self", .keyword), 96 | .whitespace(" "), 97 | .token("else", .keyword), 98 | .whitespace(" "), 99 | .plainText("{}") 100 | ]) 101 | } 102 | 103 | func testSwitchStatement() { 104 | let components = highlighter.highlight(""" 105 | switch variable { 106 | case .one: break 107 | case .two: callA() 108 | default: 109 | callB() 110 | } 111 | """) 112 | 113 | XCTAssertEqual(components, [ 114 | .token("switch", .keyword), 115 | .whitespace(" "), 116 | .plainText("variable"), 117 | .whitespace(" "), 118 | .plainText("{"), 119 | .whitespace("\n"), 120 | .token("case", .keyword), 121 | .whitespace(" "), 122 | .plainText("."), 123 | .token("one", .dotAccess), 124 | .plainText(":"), 125 | .whitespace(" "), 126 | .token("break", .keyword), 127 | .whitespace("\n"), 128 | .token("case", .keyword), 129 | .whitespace(" "), 130 | .plainText("."), 131 | .token("two", .dotAccess), 132 | .plainText(":"), 133 | .whitespace(" "), 134 | .token("callA", .call), 135 | .plainText("()"), 136 | .whitespace("\n"), 137 | .token("default", .keyword), 138 | .plainText(":"), 139 | .whitespace("\n "), 140 | .token("callB", .call), 141 | .plainText("()"), 142 | .whitespace("\n"), 143 | .plainText("}") 144 | ]) 145 | } 146 | 147 | func testSwitchStatementWithSingleAssociatedValue() { 148 | let components = highlighter.highlight(""" 149 | switch value { 150 | case .one(let a): break 151 | } 152 | """) 153 | 154 | XCTAssertEqual(components, [ 155 | .token("switch", .keyword), 156 | .whitespace(" "), 157 | .plainText("value"), 158 | .whitespace(" "), 159 | .plainText("{"), 160 | .whitespace("\n"), 161 | .token("case", .keyword), 162 | .whitespace(" "), 163 | .plainText("."), 164 | .token("one", .dotAccess), 165 | .plainText("("), 166 | .token("let", .keyword), 167 | .whitespace(" "), 168 | .plainText("a):"), 169 | .whitespace(" "), 170 | .token("break", .keyword), 171 | .whitespace("\n"), 172 | .plainText("}") 173 | ]) 174 | } 175 | 176 | func testSwitchStatementWithMultipleAssociatedValues() { 177 | let components = highlighter.highlight(""" 178 | switch value { 179 | case .one(let a), .two(let b): break 180 | } 181 | """) 182 | 183 | XCTAssertEqual(components, [ 184 | .token("switch", .keyword), 185 | .whitespace(" "), 186 | .plainText("value"), 187 | .whitespace(" "), 188 | .plainText("{"), 189 | .whitespace("\n"), 190 | .token("case", .keyword), 191 | .whitespace(" "), 192 | .plainText("."), 193 | .token("one", .dotAccess), 194 | .plainText("("), 195 | .token("let", .keyword), 196 | .whitespace(" "), 197 | .plainText("a),"), 198 | .whitespace(" "), 199 | .plainText("."), 200 | .token("two", .dotAccess), 201 | .plainText("("), 202 | .token("let", .keyword), 203 | .whitespace(" "), 204 | .plainText("b):"), 205 | .whitespace(" "), 206 | .token("break", .keyword), 207 | .whitespace("\n"), 208 | .plainText("}") 209 | ]) 210 | } 211 | 212 | func testSwitchStatementWithFallthrough() { 213 | let components = highlighter.highlight(""" 214 | switch variable { 215 | case .one: fallthrough 216 | default: 217 | callB() 218 | } 219 | """) 220 | 221 | XCTAssertEqual(components, [ 222 | .token("switch", .keyword), 223 | .whitespace(" "), 224 | .plainText("variable"), 225 | .whitespace(" "), 226 | .plainText("{"), 227 | .whitespace("\n"), 228 | .token("case", .keyword), 229 | .whitespace(" "), 230 | .plainText("."), 231 | .token("one", .dotAccess), 232 | .plainText(":"), 233 | .whitespace(" "), 234 | .token("fallthrough", .keyword), 235 | .whitespace("\n"), 236 | .token("default", .keyword), 237 | .plainText(":"), 238 | .whitespace("\n "), 239 | .token("callB", .call), 240 | .plainText("()"), 241 | .whitespace("\n"), 242 | .plainText("}") 243 | ]) 244 | } 245 | 246 | func testSwitchStatementWithTypePatternMatching() { 247 | let components = highlighter.highlight(""" 248 | switch variable { 249 | case is MyType: break 250 | default: break 251 | } 252 | """) 253 | 254 | XCTAssertEqual(components, [ 255 | .token("switch", .keyword), 256 | .whitespace(" "), 257 | .plainText("variable"), 258 | .whitespace(" "), 259 | .plainText("{"), 260 | .whitespace("\n"), 261 | .token("case", .keyword), 262 | .whitespace(" "), 263 | .token("is", .keyword), 264 | .whitespace(" "), 265 | .token("MyType", .type), 266 | .plainText(":"), 267 | .whitespace(" "), 268 | .token("break", .keyword), 269 | .whitespace("\n"), 270 | .token("default", .keyword), 271 | .plainText(":"), 272 | .whitespace(" "), 273 | .token("break", .keyword), 274 | .whitespace("\n"), 275 | .plainText("}") 276 | ]) 277 | } 278 | 279 | func testSwitchStatementWithOptional() { 280 | let components = highlighter.highlight(""" 281 | switch anOptional { 282 | case nil: break 283 | case "value"?: break 284 | default: break 285 | } 286 | """) 287 | 288 | XCTAssertEqual(components, [ 289 | .token("switch", .keyword), 290 | .whitespace(" "), 291 | .plainText("anOptional"), 292 | .whitespace(" "), 293 | .plainText("{"), 294 | .whitespace("\n"), 295 | .token("case", .keyword), 296 | .whitespace(" "), 297 | .token("nil", .keyword), 298 | .plainText(":"), 299 | .whitespace(" "), 300 | .token("break", .keyword), 301 | .whitespace("\n"), 302 | .token("case", .keyword), 303 | .whitespace(" "), 304 | .token("\"value\"", .string), 305 | .plainText("?:"), 306 | .whitespace(" "), 307 | .token("break", .keyword), 308 | .whitespace("\n"), 309 | .token("default", .keyword), 310 | .plainText(":"), 311 | .whitespace(" "), 312 | .token("break", .keyword), 313 | .whitespace("\n"), 314 | .plainText("}") 315 | ]) 316 | } 317 | 318 | func testSwitchStatementWithProperty() { 319 | let components = highlighter.highlight(""" 320 | switch object.value { default: break } 321 | """) 322 | 323 | XCTAssertEqual(components, [ 324 | .token("switch", .keyword), 325 | .whitespace(" "), 326 | .plainText("object."), 327 | .token("value", .property), 328 | .whitespace(" "), 329 | .plainText("{"), 330 | .whitespace(" "), 331 | .token("default", .keyword), 332 | .plainText(":"), 333 | .whitespace(" "), 334 | .token("break", .keyword), 335 | .whitespace(" "), 336 | .plainText("}") 337 | ]) 338 | } 339 | 340 | func testForStatementWithStaticProperty() { 341 | let components = highlighter.highlight("for value in Enum.allCases { }") 342 | 343 | XCTAssertEqual(components, [ 344 | .token("for", .keyword), 345 | .whitespace(" "), 346 | .plainText("value"), 347 | .whitespace(" "), 348 | .token("in", .keyword), 349 | .whitespace(" "), 350 | .token("Enum", .type), 351 | .plainText("."), 352 | .token("allCases", .property), 353 | .whitespace(" "), 354 | .plainText("{"), 355 | .whitespace(" "), 356 | .plainText("}") 357 | ]) 358 | } 359 | 360 | func testForStatementWithContinue() { 361 | let components = highlighter.highlight("for value in Enum.allCases { continue }") 362 | 363 | XCTAssertEqual(components, [ 364 | .token("for", .keyword), 365 | .whitespace(" "), 366 | .plainText("value"), 367 | .whitespace(" "), 368 | .token("in", .keyword), 369 | .whitespace(" "), 370 | .token("Enum", .type), 371 | .plainText("."), 372 | .token("allCases", .property), 373 | .whitespace(" "), 374 | .plainText("{"), 375 | .whitespace(" "), 376 | .token("continue", .keyword), 377 | .whitespace(" "), 378 | .plainText("}") 379 | ]) 380 | } 381 | 382 | func testRepeatWhileStatement() { 383 | let components = highlighter.highlight(""" 384 | var x = 5 385 | repeat { 386 | print(x) 387 | x = x - 1 388 | } while x > 1 389 | """) 390 | 391 | XCTAssertEqual(components, [ 392 | .token("var", .keyword), 393 | .whitespace(" "), 394 | .plainText("x"), 395 | .whitespace(" "), 396 | .plainText("="), 397 | .whitespace(" "), 398 | .token("5", .number), 399 | .whitespace("\n"), 400 | .token("repeat", .keyword), 401 | .whitespace(" "), 402 | .plainText("{"), 403 | .whitespace("\n "), 404 | .token("print", .call), 405 | .plainText("(x)"), 406 | .whitespace("\n "), 407 | .plainText("x"), 408 | .whitespace(" "), 409 | .plainText("="), 410 | .whitespace(" "), 411 | .plainText("x"), 412 | .whitespace(" "), 413 | .plainText("-"), 414 | .whitespace(" "), 415 | .token("1", .number), 416 | .whitespace("\n"), 417 | .plainText("}"), 418 | .whitespace(" "), 419 | .token("while", .keyword), 420 | .whitespace(" "), 421 | .plainText("x"), 422 | .whitespace(" "), 423 | .plainText(">"), 424 | .whitespace(" "), 425 | .token("1", .number) 426 | ]) 427 | } 428 | 429 | func testInitializingTypeWithLeadingUnderscore() { 430 | let components = highlighter.highlight("_MyType()") 431 | 432 | XCTAssertEqual(components, [ 433 | .token("_MyType", .type), 434 | .plainText("()") 435 | ]) 436 | } 437 | 438 | func testCallingFunctionWithLeadingUnderscore() { 439 | let components = highlighter.highlight("_myFunction()") 440 | 441 | XCTAssertEqual(components, [ 442 | .token("_myFunction", .call), 443 | .plainText("()") 444 | ]) 445 | } 446 | 447 | func testTernaryOperationContainingNil() { 448 | let components = highlighter.highlight(""" 449 | components.queryItems = queryItems.isEmpty ? nil : queryItems 450 | """) 451 | 452 | XCTAssertEqual(components, [ 453 | .plainText("components."), 454 | .token("queryItems", .property), 455 | .whitespace(" "), 456 | .plainText("="), 457 | .whitespace(" "), 458 | .plainText("queryItems."), 459 | .token("isEmpty", .property), 460 | .whitespace(" "), 461 | .plainText("?"), 462 | .whitespace(" "), 463 | .token("nil", .keyword), 464 | .whitespace(" "), 465 | .plainText(":"), 466 | .whitespace(" "), 467 | .plainText("queryItems") 468 | ]) 469 | } 470 | 471 | func testAwaitingFunctionCall() { 472 | let components = highlighter.highlight("let result = await call()") 473 | 474 | XCTAssertEqual(components, [ 475 | .token("let", .keyword), 476 | .whitespace(" "), 477 | .plainText("result"), 478 | .whitespace(" "), 479 | .plainText("="), 480 | .whitespace(" "), 481 | .token("await", .keyword), 482 | .whitespace(" "), 483 | .token("call", .call), 484 | .plainText("()") 485 | ]) 486 | } 487 | 488 | func testAwaitingVariable() { 489 | let components = highlighter.highlight("let result = await value") 490 | 491 | XCTAssertEqual(components, [ 492 | .token("let", .keyword), 493 | .whitespace(" "), 494 | .plainText("result"), 495 | .whitespace(" "), 496 | .plainText("="), 497 | .whitespace(" "), 498 | .token("await", .keyword), 499 | .whitespace(" "), 500 | .plainText("value") 501 | ]) 502 | } 503 | 504 | func testAwaitingAsyncSequenceElement() { 505 | let components = highlighter.highlight("for await value in sequence {}") 506 | 507 | XCTAssertEqual(components, [ 508 | .token("for", .keyword), 509 | .whitespace(" "), 510 | .token("await", .keyword), 511 | .whitespace(" "), 512 | .plainText("value"), 513 | .whitespace(" "), 514 | .token("in", .keyword), 515 | .whitespace(" "), 516 | .plainText("sequence"), 517 | .whitespace(" "), 518 | .plainText("{}") 519 | ]) 520 | } 521 | 522 | func testAwaitingThrowingAsyncSequenceElement() { 523 | let components = highlighter.highlight("for try await value in sequence {}") 524 | 525 | XCTAssertEqual(components, [ 526 | .token("for", .keyword), 527 | .whitespace(" "), 528 | .token("try", .keyword), 529 | .whitespace(" "), 530 | .token("await", .keyword), 531 | .whitespace(" "), 532 | .plainText("value"), 533 | .whitespace(" "), 534 | .token("in", .keyword), 535 | .whitespace(" "), 536 | .plainText("sequence"), 537 | .whitespace(" "), 538 | .plainText("{}") 539 | ]) 540 | } 541 | 542 | func testAsyncLetExpression() { 543 | let components = highlighter.highlight("async let result = call()") 544 | 545 | XCTAssertEqual(components, [ 546 | .token("async", .keyword), 547 | .whitespace(" "), 548 | .token("let", .keyword), 549 | .whitespace(" "), 550 | .plainText("result"), 551 | .whitespace(" "), 552 | .plainText("="), 553 | .whitespace(" "), 554 | .token("call", .call), 555 | .plainText("()") 556 | ]) 557 | } 558 | } 559 | -------------------------------------------------------------------------------- /Splash/Tests/SplashTests/Tests/TokenTypeTests.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Splash 3 | * Copyright (c) John Sundell 2018 4 | * MIT license - see LICENSE.md 5 | */ 6 | 7 | import Foundation 8 | import XCTest 9 | import Splash 10 | 11 | final class TokenTypeTests: XCTestCase { 12 | func testConvertingToString() { 13 | let standardType = TokenType.comment 14 | XCTAssertEqual(standardType.string, "comment") 15 | 16 | let customType = TokenType.custom("MyCustomType") 17 | XCTAssertEqual(customType.string, "MyCustomType") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metal-by-example/book-of-shaders-metal/12bb2366697cba9c5f660d54fead7bdcd73b6b8a/screenshots/1.png --------------------------------------------------------------------------------