Initial commit

This commit is contained in:
John Sundell 2018-08-24 18:42:07 +02:00
commit 8645db8de0
57 changed files with 3393 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
Package.resolved

77
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,77 @@
# Splash Code of Conduct
Below is the Code of Conduct that all contributors and participants in the Splash community are expected to adhere to.
It's adopted from the [Contributor Covenant Code of Conduct][homepage].
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project leader at john@sundell.co. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

94
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,94 @@
# Splash Contribution Guide
Welcome to the *Splash Contribution Guide* - a document that aims to give you all the information you need to contribute to the Splash project.
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).
## Bugs, feature requests and support
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.
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 😊.
*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* 😉
**🐞 I found a bug, how do I report it?**
If you find a bug, for example a piece of code that doesn't highlight correctly, here's the recommended workflow:
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).
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.
3. Either fix the bug yourself, or simply submit your failing test as a Pull Request, and we can work on a solution together.
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.
**💡 I have an idea for a feature request!**
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:
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.
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.
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.
**🤔 I have a question that the documentation doesn't yet answer**
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.
Here's the recommended workflow for getting your question answered:
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.
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.
3. Submit your new documentation or your comment as a Pull Request, and we'll work on improving the documentation together.
## Design and technical decisions
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:
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.
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.
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.
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.
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.
## Architectural overview
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.
**SyntaxHighlighter**
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`.
So as an API user, all you have to know in order to use Splash is this:
```swift
let highlighter = SyntaxHighlighter(format: HTMLOutputFormat())
let code = highlighter.highlight("func hello() -> Int")
```
**Tokenizer**
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.
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.
**Grammar**
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).
The decision to not simply hardcode `SwitftGrammar` 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? 😉).
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.
Have a look at `SwiftGrammar` to see all of its `SyntaxRule` implementations and how they decide how to classify each token using code segments.
**OutputFormat**
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.
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.
**Conclusion**
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 😀

BIN
Images/Code.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
Images/Logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 John Sundell
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.

6
Makefile Normal file
View File

@ -0,0 +1,6 @@
install:
swift package update
swift build -c release -Xswiftc -static-stdlib
install .build/Release/SplashHTMLGen /usr/local/bin/SplashHTMLGen
install .build/Release/SplashImageGen /usr/local/bin/SplashImageGen
install .build/Release/SplashTokenizer /usr/local/bin/SplashTokenizer

35
Package.swift Normal file
View File

@ -0,0 +1,35 @@
// swift-tools-version:4.1
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import PackageDescription
let package = Package(
name: "Splash",
products: [
.library(name: "Splash", targets: ["Splash"])
],
targets: [
.target(name: "Splash"),
.target(
name: "SplashHTMLGen",
dependencies: ["Splash"]
),
.target(
name: "SplashImageGen",
dependencies: ["Splash"]
),
.target(
name: "SplashTokenizer",
dependencies: ["Splash"]
),
.testTarget(
name: "SplashTests",
dependencies: ["Splash"]
)
]
)

166
README.md Normal file
View File

@ -0,0 +1,166 @@
<p align="center">
<img src="Images/Logo.png" width="528" max-width="90%" alt="Splash" />
</p>
<p align="center">
<img src="https://img.shields.io/badge/Swift-4.1-orange.svg" />
<a href="https://swift.org/package-manager">
<img src="https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=flat" alt="Swift Package Manager" />
</a>
<img src="https://img.shields.io/badge/platforms-mac+linux-brightgreen.svg?style=flat" alt="Mac + Linux" />
<a href="https://twitter.com/johnsundell">
<img src="https://img.shields.io/badge/twitter-@johnsundell-blue.svg?style=flat" alt="Twitter: @johnsundell" />
</a>
</p>
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.
## Usage
Splash can be used either as a library in your own Swift Package Manager-powered tool or script, or by using one of the three built-in command line tools that act as frontends for the Splash library.
### 🌍 On the web
Thanks to my good friend [Gui Rambo](https://twitter.com/_inside), you can [try out Splash directly in your browser](https://splash.rambo.codes)! His web tool lets you use Splash to generate either HTML or an image, by simply pasting Swift code into a text box.
👉 [splash.rambo.codes](https://splash.rambo.codes)
### 🖥 On the command line
The easiest way to get started building things with Splash is to use one of the three built-in command line tools that each enable you to use Splash in different ways.
#### SplashHTMLGen
`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.
For example, if you call it like this:
```
$ SplashHTMLGen "func hello(world: String) -> Int"
```
You'll get the following output back:
```html
<span class="keyword">func</span> hello(world: <span class="type">String</span>) -> <span class="type">Int</span>
```
For more information about HTML generation with Splash and how to customize it, see `HTMLOutputFormat`.
#### SplashImageGen
`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.
For example, if you call it like this:
```
$ SplashImageGen "func hello(world: String) -> Int" "MyImage.png"
```
The following image will be generated (and written to disk as `MyImage.png`):
<img src="Images/Code.png" max-width="90%" alt="Code sample" />
*`SplashImageGen` is currently only available on macOS.*
#### SplashTokenizer
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).
So if you call it like this:
```
$ SplashTokenizer "func hello(world: String) -> Int"
```
You'll get the following standard output back:
```
Keyword token: func
Plain text: hello(world:
Type token: String
Plain text: )
Plain text: ->
Type token: Int
```
### 📦 As a package
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:
```swift
import Splash
let highlighter = SyntaxHighlighter(format: HTMLOutputFormat())
let html = highlighter.highlight("func hello() -> String")
```
Splash ships with two built-in output formats - HTML and `NSAttributedString`, but you can also easily add your own by implementing the `OutputFormat` protocol.
## Installation
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.
Splash supports both macOS and Linux.
*Before you begin, make sure that you have a Swift 4.1-compatible toolchain installed (for example Xcode 9.4 if you're on a Mac).*
### 📦 As a package
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).
### 🏃‍♂️ As a Marathon dependency
If you want to use Splash in a [Marathon](https://github.com/johnsundell/marathon) script, either add it to your `Marathonfile` (see the Marathon repo for instructions on how to do that), or point Marathon to Splash using the inline dependency syntax:
```swift
import Splash // https://github.com/johnsundell/splash.git
```
### 🛠 Command line tools
If you want to use Splash through one of its built-in command line tools, start by cloning the repo to your local machine:
```
$ git clone https://github.com/johnsundell/splash.git
$ cd Splash
```
To run a tool without installing it, you can use the Swift Package Manager's `run` command, like this:
```
$ swift run SplashHTMLGen "func hello(world: String) -> Int"
```
To install all three command line tools globally on your system, use Make:
```
$ make install
```
That will install the following three tools in your `/usr/local/bin` folder:
```
SplashHTMLGen
SplashImageGen
SplashTokenizer
```
If you only wish to install one of these, compile it and then move it to `/usr/local/bin`, like this:
```
$ swift build -c release -Xswiftc -static-stdlib
$ install .build/Release/SplashHTMLGen /usr/local/bin/SplashHTMLGen
```
## Contributions and support
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.
You might notice that this project does not have GitHub issues enabled. That's because with Splash, I'm trying out a new 100% PR-based open source workflow. This is a bit of an experiment, so let's see how it works out.
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).
## Hope you enjoy using Splash!
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 on [Twitter](https://twitter.com/johnsundell), [Mastodon](https://mastodon.social/@johnsundell) or [Micro.blog](https://micro.blog/johnsundell) 😊

View File

@ -0,0 +1,17 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
internal extension CharacterSet {
func contains(_ character: Character) -> Bool {
guard let scalar = character.unicodeScalars.first else {
return false
}
return contains(scalar)
}
}

View File

@ -0,0 +1,17 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
extension Equatable {
func isAny(of candidates: Self...) -> Bool {
return candidates.contains(self)
}
func isAny(of candidates: [Self]) -> Bool {
return candidates.contains(self)
}
}

View File

@ -0,0 +1,13 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
internal extension Int {
var isEven: Bool {
return self % 2 == 0
}
}

View File

@ -0,0 +1,23 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
internal extension Sequence where Element: Equatable {
func contains(anyOf candidates: Element...) -> Bool {
return contains(anyOf: candidates)
}
func contains(anyOf candidates: [Element]) -> Bool {
for candidate in candidates {
if contains(candidate) {
return true
}
}
return false
}
}

View File

@ -0,0 +1,15 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
internal extension Sequence where Element: Equatable {
func numberOfOccurrences(of target: Element) -> Int {
return reduce(0) { count, element in
return element == target ? count + 1 : count
}
}
}

View File

@ -0,0 +1,13 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
extension String {
var isNumber: Bool {
return Int(self) != nil
}
}

View File

@ -0,0 +1,25 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
internal extension String {
var isCapitalized: Bool {
guard let firstCharacter = first.map(String.init) else {
return false
}
return firstCharacter != firstCharacter.lowercased()
}
var startsWithLetter: Bool {
guard let firstCharacter = first else {
return false
}
return CharacterSet.letters.contains(firstCharacter)
}
}

View File

@ -0,0 +1,13 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
internal extension String {
func removing(_ substring: String) -> String {
return replacingOccurrences(of: substring, with: "")
}
}

View File

@ -0,0 +1,16 @@
import Foundation
#if os(Linux)
internal extension Substring {
func hasSuffix(_ suffix: String) -> Bool {
guard count >= suffix.count else {
return false
}
let startIndex = index(endIndex, offsetBy: -suffix.count)
return self[startIndex...] == suffix
}
}
#endif

View File

@ -0,0 +1,21 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
/// Protocol used to define the grammar of a language to use for
/// syntax highlighting. See `SwiftGrammar` for a default implementation
/// of the Swift language grammar.
public protocol Grammar {
/// The set of characters that make up the delimiters that separates
/// tokens within the language, such as punctuation characters.
var delimiters: CharacterSet { get }
/// The rules that define the syntax of the language. When tokenizing,
/// the rules will be iterated over in sequence, and the first rule
/// that matches a given code segment will be used to determine that
/// segment's token type.
var syntaxRules: [SyntaxRule] { get }
}

View File

@ -0,0 +1,364 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
/// Grammar for the Swift language. Use this implementation when
/// highlighting Swift code. This is the default grammar.
public struct SwiftGrammar: Grammar {
public let delimiters: CharacterSet
public let syntaxRules: [SyntaxRule]
public init() {
var delimiters = CharacterSet.alphanumerics.inverted
delimiters.remove("_")
delimiters.remove("\"")
delimiters.remove("#")
self.delimiters = delimiters
syntaxRules = [
PreprocessingRule(),
CommentRule(),
MultiLineStringRule(),
SingleLineStringRule(),
AttributeRule(),
NumberRule(),
TypeRule(),
CallRule(),
PropertyRule(),
DotAccessRule(),
KeywordRule()
]
}
}
private extension SwiftGrammar {
static let keywords: Set<String> = [
"final", "class", "struct", "enum", "protocol",
"extension", "let", "var", "func", "typealias",
"init", "guard", "if", "else", "return", "get",
"throw", "throws", "for", "in", "open", "weak",
"public", "internal", "private", "fileprivate",
"import", "mutating", "associatedtype", "case",
"switch", "static", "do", "try", "catch", "as",
"super", "self", "set", "true", "false", "nil",
"override", "where", "_", "default", "break",
"#selector", "required", "willSet", "didSet",
"lazy"
]
struct PreprocessingRule: SyntaxRule {
var tokenType: TokenType { return .preprocessing }
private let tokens = ["#if", "#endif", "#elseif", "#else"]
func matches(_ segment: Segment) -> Bool {
if segment.tokens.current.isAny(of: tokens) {
return true
}
return segment.tokens.onSameLine.contains(anyOf: tokens)
}
}
struct CommentRule: SyntaxRule {
var tokenType: TokenType { return .comment }
func matches(_ segment: Segment) -> Bool {
if segment.tokens.current.hasPrefix("//") {
return true
}
if segment.tokens.onSameLine.contains(anyOf: "//", "///") {
return true
}
if segment.tokens.current.isAny(of: "/*", "*/") {
return true
}
return !segment.tokens.containsBalancedOccurrences(of: "/*", and: "*/")
}
}
struct AttributeRule: SyntaxRule {
var tokenType: TokenType { return .keyword }
func matches(_ segment: Segment) -> Bool {
return segment.tokens.current == "@" || segment.tokens.previous == "@"
}
}
struct MultiLineStringRule: SyntaxRule {
var tokenType: TokenType { return .string }
func matches(_ segment: Segment) -> Bool {
guard !segment.tokens.count(of: "\"\"\"").isEven else {
return false
}
return !segment.isWithinStringInterpolation
}
}
struct SingleLineStringRule: SyntaxRule {
var tokenType: TokenType { return .string }
func matches(_ segment: Segment) -> Bool {
guard segment.isWithinStringLiteral else {
return false
}
return !segment.isWithinStringInterpolation
}
}
struct NumberRule: SyntaxRule {
var tokenType: TokenType { return .number }
func matches(_ segment: Segment) -> Bool {
// Don't match against index-based closure arguments
guard segment.tokens.previous != "$" else {
return false
}
// Integers can be separated using "_", so handle that
if segment.tokens.current.removing("_").isNumber {
return true
}
// Double and floating point values that contain a "."
guard segment.tokens.current == "." else {
return false
}
guard let previous = segment.tokens.previous,
let next = segment.tokens.next else {
return false
}
return previous.isNumber && next.isNumber
}
}
struct CallRule: SyntaxRule {
var tokenType: TokenType { return .call }
private let keywordsToAvoid: Set<String>
private let controlFlowTokens = ["if", "&&", "||", "for"]
init() {
var keywordsToAvoid = keywords
keywordsToAvoid.remove("return")
keywordsToAvoid.remove("try")
keywordsToAvoid.remove("throw")
keywordsToAvoid.remove("if")
self.keywordsToAvoid = keywordsToAvoid
}
func matches(_ segment: Segment) -> Bool {
guard segment.tokens.current.startsWithLetter else {
return false
}
if let previousToken = segment.tokens.previous {
guard !keywordsToAvoid.contains(previousToken) else {
return false
}
// Don't treat enums with associated values as function calls
guard !segment.prefixedByDotAccess else {
return false
}
}
// Handle trailing closure syntax
guard segment.trailingWhitespace == nil else {
guard segment.tokens.next.isAny(of: "{", "{}") else {
return false
}
guard !keywords.contains(segment.tokens.current) else {
return false
}
return !segment.tokens.onSameLine.contains(anyOf: controlFlowTokens)
}
// Check so that this is an initializer call, not the declaration
if segment.tokens.current == "init" {
guard segment.tokens.previous == "." else {
return false
}
}
return segment.tokens.next.isAny(of: "(", "()", "())", "(.", "({")
}
}
struct KeywordRule: SyntaxRule {
var tokenType: TokenType { return .keyword }
func matches(_ segment: Segment) -> Bool {
if segment.tokens.next == ":" {
guard segment.tokens.current == "default" else {
return false
}
}
return keywords.contains(segment.tokens.current)
}
}
struct TypeRule: SyntaxRule {
var tokenType: TokenType { return .type }
func matches(_ segment: Segment) -> Bool {
// Types should not be highlighted when declared
if let previousToken = segment.tokens.previous {
let declarationKeywords = ["class", "struct", "enum",
"protocol", "typealias", "import"]
guard !previousToken.isAny(of: declarationKeywords) else {
return false
}
}
guard segment.tokens.current.isCapitalized else {
return false
}
guard !segment.prefixedByDotAccess else {
return false
}
// In a generic declaration, only highlight constraints
if !segment.tokens.onSameLine.contains(anyOf: "var", "let") {
if !segment.tokens.containsBalancedOccurrences(of: "<", and: ">") {
return !segment.tokens.previous.isAny(of: "<", ",")
}
}
return true
}
}
struct DotAccessRule: SyntaxRule {
var tokenType: TokenType { return .dotAccess }
func matches(_ segment: Segment) -> Bool {
guard segment.tokens.previous.isAny(of: ".", "(.") else {
return false
}
guard !segment.tokens.onSameLine.isEmpty else {
return false
}
return segment.tokens.onSameLine.first != "import"
}
}
struct PropertyRule: SyntaxRule {
var tokenType: TokenType { return .property }
func matches(_ segment: Segment) -> Bool {
guard !segment.tokens.onSameLine.isEmpty else {
return false
}
guard segment.tokens.previous.isAny(of: ".", "?.") else {
return false
}
guard !segment.prefixedByDotAccess else {
return false
}
if let previousToken = segment.tokens.previous {
guard !keywords.contains(previousToken) else {
return false
}
}
return segment.tokens.onSameLine.first != "import"
}
}
}
private extension Segment {
var isWithinStringLiteral: Bool {
let delimiter = "\""
if tokens.current.hasPrefix(delimiter) {
return true
}
if tokens.current.hasSuffix(delimiter) {
return true
}
var markerCounts = (start: 0, end: 0)
var previousToken: String?
for token in tokens.onSameLine {
guard previousToken != "\\" else {
previousToken = token
continue
}
if token == delimiter {
if markerCounts.start == markerCounts.end {
markerCounts.start += 1
} else {
markerCounts.end += 1
}
} else {
if token.hasPrefix(delimiter) {
markerCounts.start += 1
}
if token.hasSuffix(delimiter) {
markerCounts.end += 1
}
}
previousToken = token
}
return markerCounts.start != markerCounts.end
}
var isWithinStringInterpolation: Bool {
let delimiter = "\\("
if tokens.current == delimiter || tokens.previous == delimiter {
return true
}
let components = tokens.onSameLine.split(separator: delimiter)
guard components.count > 1 else {
return false
}
let suffix = components.last!
var paranthesisCount = 1
for component in suffix {
paranthesisCount += component.numberOfOccurrences(of: "(")
paranthesisCount -= component.numberOfOccurrences(of: ")")
guard paranthesisCount > 0 else {
return false
}
}
return true
}
var prefixedByDotAccess: Bool {
return tokens.previous == "(." || prefix.hasSuffix(" .")
}
}

View File

@ -0,0 +1,114 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
#if os(macOS)
import Cocoa
/// Output format to use to generate an NSAttributedString from the
/// highlighted code. A `Theme` is used to determine what fonts and
/// colors to use for the various tokens.
public struct AttributedStringOutputFormat: OutputFormat {
public var theme: Theme
public init(theme: Theme) {
self.theme = theme
}
public func makeBuilder() -> Builder {
return Builder(theme: theme)
}
}
public extension AttributedStringOutputFormat {
struct Builder: OutputBuilder {
private let theme: Theme
private lazy var font = loadFont()
private var string = NSMutableAttributedString()
fileprivate init(theme: Theme) {
self.theme = theme
}
public mutating func addToken(_ token: String, ofType type: TokenType) {
let color = theme.tokenColors[type] ?? Color(red: 1, green: 1, blue: 1)
string.append(token, font: font, color: color)
}
public mutating func addPlainText(_ text: String) {
string.append(text, font: font, color: theme.plainTextColor)
}
public mutating func addWhitespace(_ whitespace: String) {
let color = Color(red: 1, green: 1, blue: 1)
string.append(whitespace, font: font, color: color)
}
public func build() -> NSAttributedString {
return NSAttributedString(attributedString: string)
}
private mutating func loadFont() -> NSFont {
let size = CGFloat(theme.font.size)
switch theme.font.resource {
case .system:
return .defaultFont(ofSize: size)
case .path(let path):
guard let font = NSFont.loaded(from: path, size: size) else {
return .defaultFont(ofSize: size)
}
return font
}
}
}
}
private extension NSMutableAttributedString {
func append(_ string: String, font: NSFont, color: Color) {
let color = NSColor(
red: CGFloat(color.red),
green: CGFloat(color.green),
blue: CGFloat(color.blue),
alpha: CGFloat(color.alpha)
)
let attributedString = NSAttributedString(string: string, attributes: [
.foregroundColor: color,
.font: font
])
append(attributedString)
}
}
private extension NSFont {
static func loaded(from path: String, size: CGFloat) -> NSFont? {
let url = CFURLCreateWithFileSystemPath(
kCFAllocatorDefault,
path as CFString,
.cfurlposixPathStyle,
false
)
guard let font = url.flatMap(CGDataProvider.init).flatMap(CGFont.init) else {
return nil
}
return CTFontCreateWithGraphicsFont(font, size, nil, nil)
}
static func defaultFont(ofSize size: CGFloat) -> NSFont {
guard let courier = loaded(from: "/Library/Fonts/Courier New.ttf", size: size) else {
return .systemFont(ofSize: size)
}
return courier
}
}
#endif

View File

@ -0,0 +1,51 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
/// Output format to use to generate an HTML string with a semantic
/// representation of the highlighted code. Each token will be wrapped
/// in a `span` element with a CSS class matching the token's type.
/// Optionally, a `classPrefix` can be set to prefix each CSS class with
/// a given string.
public struct HTMLOutputFormat: OutputFormat {
public var classPrefix: String
public init(classPrefix: String = "") {
self.classPrefix = classPrefix
}
public func makeBuilder() -> Builder {
return Builder(classPrefix: classPrefix)
}
}
public extension HTMLOutputFormat {
struct Builder: OutputBuilder {
private let classPrefix: String
private var html = ""
fileprivate init(classPrefix: String) {
self.classPrefix = classPrefix
}
public mutating func addToken(_ token: String, ofType type: TokenType) {
html.append("<span class=\"\(classPrefix)\(type.rawValue)\">\(token)</span>")
}
public mutating func addPlainText(_ text: String) {
html.append(text)
}
public mutating func addWhitespace(_ whitespace: String) {
html.append(whitespace)
}
public func build() -> String {
return html
}
}
}

View File

@ -0,0 +1,25 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
/// Protocol used to define a builder for a highlighted string that's
/// returned as output from `SyntaxHighlighter`. Each builder defines
/// its own output type through the `Output` associated type, and can
/// add the various tokens and other text found in the highlighted code
/// in whichever fashion it wants.
public protocol OutputBuilder {
/// The type of output that this builder produces
associatedtype Output
/// Add a token with a given type to the builder
mutating func addToken(_ token: String, ofType type: TokenType)
/// Add some plain text, without any formatting, to the builder
mutating func addPlainText(_ text: String)
/// Add some whitespace to the builder
mutating func addWhitespace(_ whitespace: String)
/// Build the final output based on the builder's current state
func build() -> Output
}

View File

@ -0,0 +1,23 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
/// Protocol used to define an output format for a `SyntaxHighlighter`.
/// Default implementations of this protocol are provided for HTML and
/// NSAttributedString outputs, and custom ones can be defined by
/// conforming to this protocol and passing the implementation to a
/// syntax highlighter when it's created.
public protocol OutputFormat {
/// The type of builder that this output format uses. The builder's
/// `Output` type determines the output type of the format.
associatedtype Builder: OutputBuilder
/// Make a new instance of the output format's builder. This will be
/// called once per syntax highlighting session. The builder is expected
/// to be a newly created, blank instance.
func makeBuilder() -> Builder
}

View File

@ -0,0 +1,91 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
/// This class acts as the main API entry point for Splash. Using it,
/// any code string can be highlighted to any desired format, using
/// any language grammar. Per default, Swift's langauge grammar is used.
/// To initialize this class, pass the desired output format, such as
/// `AttributedStringOutputFormat` or `HTMLOutputFormat`, or a custom
/// implementation. One syntax highlighter may be reused multiple times.
public struct SyntaxHighlighter<Format: OutputFormat> {
private let format: Format
private let grammar: Grammar
private let tokenizer = Tokenizer()
/// Initialize an instance with the desired output format.
/// If no grammar is passed, then Swift's grammar is used.
public init(format: Format, grammar: Grammar = SwiftGrammar()) {
self.format = format
self.grammar = grammar
}
/// Highlight the given code, returning output as specified by the
/// syntax highlighter's `Format`.
public func highlight(_ code: String) -> Format.Builder.Output {
var builder = format.makeBuilder()
var state: (token: String, tokenType: TokenType?)?
func handle(_ token: String, ofType type: TokenType?, trailingWhitespace: String?) {
guard let whitespace = trailingWhitespace else {
state = (token, type)
return
}
builder.addToken(token, ofType: type)
builder.addWhitespace(whitespace)
state = nil
}
for segment in tokenizer.segmentsByTokenizing(code, delimiters: grammar.delimiters) {
let token = segment.tokens.current
let whitespace = segment.trailingWhitespace
guard !token.isEmpty else {
whitespace.map { builder.addWhitespace($0) }
continue
}
let tokenType = typeOfToken(in: segment)
guard var currentState = state else {
handle(token, ofType: tokenType, trailingWhitespace: whitespace)
continue
}
guard currentState.tokenType == tokenType else {
builder.addToken(currentState.token, ofType: currentState.tokenType)
handle(token, ofType: tokenType, trailingWhitespace: whitespace)
continue
}
currentState.token.append(token)
handle(currentState.token, ofType: tokenType, trailingWhitespace: whitespace)
}
if let lastState = state {
builder.addToken(lastState.token, ofType: lastState.tokenType)
}
return builder.build()
}
private func typeOfToken(in segment: Segment) -> TokenType? {
let rule = grammar.syntaxRules.first { $0.matches(segment) }
return rule?.tokenType
}
}
private extension OutputBuilder {
mutating func addToken(_ token: String, ofType type: TokenType?) {
if let type = type {
addToken(token, ofType: type)
} else {
addPlainText(token)
}
}
}

View File

@ -0,0 +1,22 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
/// Protocol used to define syntax rules for a language `Grammar`.
/// Each rule is associated with a certain `TokenType` and, when
/// evaluated, is asked to check whether it matches a given segment
/// of code. If the rule matches then the rule's token type will be
/// associated with the given segment's current token.
public protocol SyntaxRule {
/// The token type that this syntax rule represents
var tokenType: TokenType { get }
/// Determine if the syntax rule matches a given segment. If it's
/// a match, then the rule's `tokenType` will be associated with
/// the segment's current token.
func matches(_ segment: Segment) -> Bool
}

View File

@ -0,0 +1,25 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
/// A representation of a color, for use with a `Theme`.
/// Since Splash aims to be cross-platform, it uses this
/// simplified color representation rather than `NSColor`
/// or `UIColor`.
public struct Color {
public var red: Double
public var green: Double
public var blue: Double
public var alpha: Double
public init(red: Double, green: Double, blue: Double, alpha: Double = 1) {
self.red = red
self.green = green
self.blue = blue
self.alpha = alpha
}
}

View File

@ -0,0 +1,47 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
/// A representation of a font, for use with a `Theme`.
/// Since Splash aims to be cross-platform, it uses this
/// simplified color representation rather than `NSFont`
/// or `UIFont`.
public struct Font {
/// The underlying resource used to load the font
public var resource: Resource
/// The size (in points) of the font
public var size: Double
/// Initialize an instance with a path to a font file
/// on disk and a size.
public init(path: String, size: Double) {
#if os(macOS)
resource = .path((path as NSString).expandingTildeInPath)
#else
resource = .path(path)
#endif
self.size = size
}
/// Initialize an instance with a size, and use an
/// appropriate system font to render text.
public init(size: Double) {
resource = .system
self.size = size
}
}
public extension Font {
/// Enum describing how to load the underlying resource for a font
enum Resource {
/// Use an appropriate system font
case system
/// Load a font file from a given file system path
case path(String)
}
}

View File

@ -0,0 +1,51 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
/// A theme describes what fonts and colors to use when rendering
/// certain output formats - such as `NSAttributedString`. A default
/// implementation is provided that matches the "Sundell's Colors"
/// Xcode theme, by using the `sundellsColors(withFont:)` method.
public struct Theme {
/// What font to use to render the highlighted text
public var font: Font
/// What color to use for plain text (no highlighting)
public var plainTextColor: Color
/// What color to use for the text's highlighted tokens
public var tokenColors: [TokenType : Color]
public init(font: Font, plainTextColor: Color, tokenColors: [TokenType : Color]) {
self.font = font
self.plainTextColor = plainTextColor
self.tokenColors = tokenColors
}
}
public extension Theme {
/// Create a theme matching the "Sundell's Colors" Xcode theme
static func sundellsColors(withFont font: Font) -> Theme {
return Theme(
font: font,
plainTextColor: Color(
red: 0.66,
green: 0.74,
blue: 0.74
),
tokenColors: [
.keyword : Color(red: 0.91, green: 0.2, blue: 0.54),
.string : Color(red: 0.98, green: 0.39, blue: 0.12),
.type : Color(red: 0.51, green: 0.51, blue: 0.79),
.call : Color(red: 0.2, green: 0.56, blue: 0.9),
.number : Color(red: 0.86, green: 0.44, blue: 0.34),
.comment : Color(red: 0.42, green: 0.54, blue: 0.58),
.property : Color(red: 0.13, green: 0.67, blue: 0.62),
.dotAccess : Color(red: 0.57, green: 0.7, blue: 0),
.preprocessing : Color(red: 0.71, green: 0.54, blue: 0)
]
)
}
}

View File

@ -0,0 +1,52 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
/// A representation of a segment of code, used to determine the type
/// of a given token when passed to a `SyntaxRule` implementation.
public struct Segment {
/// The code that prefixes this segment, that is all the characters
/// up to where the segment's current token begins.
public var prefix: Substring
/// The collection of tokens that the segment includes
public var tokens: Tokens
/// Any whitespace that immediately follows the segment's current token
public var trailingWhitespace: String?
internal let currentTokenIsDelimiter: Bool
internal var isLastOnLine: Bool
}
public extension Segment {
/// A collection of tokens included in a code segment
struct Tokens {
/// The number of times a given token has been found up until this point
var counts: [String : Int]
/// The tokens that were previously found on the same line as the current one
var onSameLine: [String]
/// The token that was previously found (may be on a different line)
var previous: String?
/// The current token which is currently being evaluated
var current: String
/// Any upcoming token that will follow the current one
var next: String?
}
}
public extension Segment.Tokens {
/// Return the number of times a given token has been found up until this point.
/// This is a convenience API over the `counts` dictionary.
func count(of token: String) -> Int {
return counts[token] ?? 0
}
/// Return whether an equal number of occurrences have been found of two tokens.
/// For example, this can be used to check if a token is encapsulated by parenthesis.
func containsBalancedOccurrences(of tokenA: String, and tokenB: String) -> Bool {
return count(of: tokenA) == count(of: tokenB)
}
}

View File

@ -0,0 +1,29 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
/// Enum defining the possible types of tokens that can be highlighted
public enum TokenType: String, Equatable {
/// A keyword, such as `if`, `class` or `let`
case keyword
/// A token that is part of a string literal
case string
/// A reference to a type
case type
/// A call to a function or method
case call
/// A number, either interger of floating point
case number
/// A comment, either single or multi-line
case comment
/// A property being accessed, such as `object.property`
case property
/// A symbol being accessed through dot notation, such as `.myCase`
case dotAccess
/// A preprocessing symbol, such as `#if` or `@available`
case preprocessing
}

View File

@ -0,0 +1,210 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
internal struct Tokenizer {
func segmentsByTokenizing(_ code: String, delimiters: CharacterSet) -> AnySequence<Segment> {
return AnySequence<Segment> {
return Buffer(iterator: Iterator(code: code, delimiters: delimiters))
}
}
}
private extension Tokenizer {
struct Buffer: IteratorProtocol {
private var iterator: Iterator
private var nextSegment: Segment?
init(iterator: Iterator) {
self.iterator = iterator
}
mutating func next() -> Segment? {
var segment = nextSegment ?? iterator.next()
nextSegment = iterator.next()
segment?.tokens.next = nextSegment?.tokens.current
return segment
}
}
struct Iterator: IteratorProtocol {
enum Component {
case token(String)
case delimiter(String)
case whitespace(String)
case newline(String)
}
private let code: String
private let delimiters: CharacterSet
private var index: String.Index?
private var tokenCounts = [String : Int]()
private var lineTokens = [String]()
private var segments: (current: Segment?, previous: Segment?)
init(code: String, delimiters: CharacterSet) {
self.code = code
self.delimiters = delimiters
segments = (nil, nil)
}
mutating func next() -> Segment? {
let nextIndex = makeNextIndex()
guard nextIndex != code.endIndex else {
let segment = segments.current
segments.current = nil
return segment
}
index = nextIndex
let component = makeComponent(at: nextIndex)
switch component {
case .token(let token), .delimiter(let token):
guard var segment = segments.current else {
segments.current = makeSegment(with: component, at: nextIndex)
return next()
}
guard segment.trailingWhitespace == nil,
component.isDelimiter == segment.currentTokenIsDelimiter else {
return finish(segment, with: component, at: nextIndex)
}
segment.tokens.current.append(token)
segments.current = segment
return next()
case .whitespace(let whitespace), .newline(let whitespace):
guard var segment = segments.current else {
var segment = makeSegment(with: component, at: nextIndex)
segment.trailingWhitespace = whitespace
segment.isLastOnLine = component.isNewline
segments.current = segment
return next()
}
if let existingWhitespace = segment.trailingWhitespace {
segment.trailingWhitespace = existingWhitespace.appending(whitespace)
} else {
segment.trailingWhitespace = whitespace
}
if component.isNewline {
segment.isLastOnLine = true
}
segments.current = segment
return next()
}
}
private func makeNextIndex() -> String.Index {
guard let index = index else {
return code.startIndex
}
return code.index(after: index)
}
private func makeComponent(at index: String.Index) -> Component {
let character = code[index]
let substring = String(character)
if character.isWhitespace {
return .whitespace(substring)
}
if character.isNewline {
return .newline(substring)
}
if delimiters.contains(character) {
return .delimiter(substring)
}
return .token(substring)
}
private func makeSegment(with component: Component, at index: String.Index) -> Segment {
let tokens = Segment.Tokens(
counts: tokenCounts,
onSameLine: lineTokens,
previous: segments.current?.tokens.current,
current: component.token,
next: nil
)
return Segment(
prefix: code[..<index],
tokens: tokens,
trailingWhitespace: nil,
currentTokenIsDelimiter: component.isDelimiter,
isLastOnLine: false
)
}
private mutating func finish(_ segment: Segment,
with component: Component,
at index: String.Index) -> Segment {
var count = tokenCounts[segment.tokens.current] ?? 0
count += 1
tokenCounts[segment.tokens.current] = count
if segment.isLastOnLine {
lineTokens = []
} else {
lineTokens.append(segment.tokens.current)
}
segments.previous = segment
segments.current = makeSegment(with: component, at: index)
return segment
}
}
}
extension Tokenizer.Iterator.Component {
var token: String {
switch self {
case .token(let token),
.delimiter(let token):
return token
case .whitespace, .newline:
return ""
}
}
var isDelimiter: Bool {
switch self {
case .token, .whitespace, .newline:
return false
case .delimiter:
return true
}
}
var isNewline: Bool {
switch self {
case .token, .whitespace, .delimiter:
return false
case .newline:
return true
}
}
}
private extension Character {
var isWhitespace: Bool {
return CharacterSet.whitespaces.contains(self)
}
var isNewline: Bool {
return CharacterSet.newlines.contains(self)
}
}

View File

@ -0,0 +1,17 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import Splash
guard CommandLine.arguments.count > 1 else {
print("⚠️ Please supply the code to generate HTML for as a string argument")
exit(1)
}
let code = CommandLine.arguments[1]
let highlighter = SyntaxHighlighter(format: HTMLOutputFormat())
print(highlighter.highlight(code))

View File

@ -0,0 +1,19 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
#if os(macOS)
import Foundation
extension CGImage {
func write(to url: URL) {
let destination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypePNG, 1, nil)!
CGImageDestinationAddImage(destination, self, nil)
CGImageDestinationFinalize(destination)
}
}
#endif

View File

@ -0,0 +1,61 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
#if os(macOS)
import Foundation
import Splash
extension CommandLine {
struct Options {
let code: String
let outputURL: URL
let padding: CGFloat
let font: Font
}
static func makeOptions() -> Options? {
guard arguments.count > 2 else {
return nil
}
let defaults = UserDefaults.standard
return Options(
code: arguments[1],
outputURL: resolveOutputURL(),
padding: CGFloat(defaults.int(forKey: "p", default: 20)),
font: resolveFont(from: defaults)
)
}
private static func resolveOutputURL() -> URL {
let path = arguments[2] as NSString
return URL(fileURLWithPath: path.expandingTildeInPath)
}
private static func resolveFont(from defaults: UserDefaults) -> Font {
let size = Double(defaults.int(forKey: "s", default: 20))
guard let path = defaults.string(forKey: "f") else {
return Font(size: size)
}
return Font(path: path, size: size)
}
}
private extension UserDefaults {
func int(forKey key: String, default: CGFloat) -> CGFloat {
guard value(forKey: key) != nil else {
return `default`
}
return CGFloat(integer(forKey: key))
}
}
#endif

View File

@ -0,0 +1,18 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
#if os(macOS)
import Cocoa
extension NSGraphicsContext {
func fill(with color: NSColor, in rect: CGRect) {
cgContext.setFillColor(color.cgColor)
cgContext.fill(rect)
}
}
#endif

View File

@ -0,0 +1,31 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
#if os(macOS)
import Cocoa
extension NSGraphicsContext {
convenience init(size: CGSize) {
let scale: CGFloat = 2
let context = CGContext(
data: nil,
width: Int(size.width * scale),
height: Int(size.height * scale),
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue
)!
context.scaleBy(x: scale, y: scale)
self.init(cgContext: context, flipped: false)
}
}
#endif

View File

@ -0,0 +1,57 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
#if os(macOS)
import Cocoa
import Splash
guard let options = CommandLine.makeOptions() else {
print("""
Two arguments are required:
- The code to generate an image for
- The path to write the generated image to
Optionally, the following arguments can be passed:
-p The amount of padding (in pixels) to apply around the code
-f A path to a font to use when rendering
-s The size of text to use when rendering
""")
exit(1)
}
let theme = Theme.sundellsColors(withFont: options.font)
let outputFormat = AttributedStringOutputFormat(theme: theme)
let highlighter = SyntaxHighlighter(format: outputFormat)
let string = highlighter.highlight(options.code)
let stringSize = string.size()
let contextRect = CGRect(
x: 0,
y: 0,
width: stringSize.width + options.padding * 2,
height: stringSize.height + options.padding * 2
)
let context = NSGraphicsContext(size: contextRect.size)
NSGraphicsContext.current = context
let backgroundColor = NSColor(white: 0.12, alpha: 1)
context.fill(with: backgroundColor, in: contextRect)
string.draw(in: CGRect(
x: options.padding,
y: options.padding,
width: stringSize.width,
height: stringSize.height
))
let image = context.cgContext.makeImage()!
image.write(to: options.outputURL)
#else
print("😞 SplashImageGen currently only supports macOS")
#endif

View File

@ -0,0 +1,36 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import Splash
struct TokenizerOutputFormat: OutputFormat {
func makeBuilder() -> Builder {
return Builder()
}
}
extension TokenizerOutputFormat {
struct Builder: OutputBuilder {
private var components = [String]()
mutating func addToken(_ token: String, ofType type: TokenType) {
components.append("\(type.rawValue.capitalized) token: \(token)")
}
mutating func addPlainText(_ text: String) {
components.append("Plain text: \(text)")
}
mutating func addWhitespace(_ whitespace: String) {
// Ignore whitespace
}
func build() -> String {
return components.joined(separator: "\n")
}
}
}

View File

@ -0,0 +1,17 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import Splash
guard CommandLine.arguments.count > 1 else {
print("⚠️ Please supply the code to tokenize as a string argument")
exit(1)
}
let code = CommandLine.arguments[1]
let highlighter = SyntaxHighlighter(format: TokenizerOutputFormat())
print(highlighter.highlight(code))

12
Tests/LinuxMain.swift Normal file
View File

@ -0,0 +1,12 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import XCTest
import SplashTests
var tests = [XCTestCaseEntry]()
tests += SplashTests.makeLinuxTests()
XCTMain(tests)

View File

@ -0,0 +1,35 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import XCTest
import Splash
/// Test case used as an abstract base class for all tests relating to
/// syntax highlighting. For all such tests, the Swift grammar is used.
class SyntaxHighlighterTestCase: XCTestCase {
private(set) var highlighter: SyntaxHighlighter<OutputFormatMock>!
private(set) var builder: OutputBuilderMock!
override func setUp() {
super.setUp()
builder = OutputBuilderMock()
highlighter = SyntaxHighlighter(format: OutputFormatMock(builder: builder))
}
#if os(macOS)
func testHasLinuxVerificationTest() {
let concreteType = type(of: self)
guard concreteType != SyntaxHighlighterTestCase.self else {
return
}
XCTAssertTrue(concreteType.testNames.contains("testAllTestsRunOnLinux"),
"All test cases should have a test that verify that their tests run on Linux")
}
#endif
}

View File

@ -0,0 +1,33 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import XCTest
struct TestCaseVerifier<Case: XCTestCase> {
static func verifyLinuxTests(_ tests: [(String, TestClosure<Case>)]) -> Bool {
#if os(macOS)
let testNames = Set(tests.map { $0.0 })
for name in Case.testNames {
guard name != "testAllTestsRunOnLinux" else {
continue
}
guard name != "testHasLinuxVerificationTest" else {
continue
}
guard testNames.contains(name) else {
XCTFail("Test case \(Case.self) does not include test \(name) on Linux")
return false
}
}
#endif
return true
}
}

View File

@ -0,0 +1,10 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import XCTest
typealias TestClosure<T: XCTestCase> = (T) -> () throws -> Void

View File

@ -0,0 +1,21 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import XCTest
#if os(macOS)
extension XCTestCase {
static var testNames: [String] {
return defaultTestSuite.tests.map { test in
let components = test.name.components(separatedBy: .whitespaces)
return components[1].replacingOccurrences(of: "]", with: "")
}
}
}
#endif

View File

@ -0,0 +1,25 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import XCTest
#if !os(macOS)
public func makeLinuxTests() -> [XCTestCaseEntry] {
return [
testCase(ClosureTests.allTests),
testCase(CommentTests.allTests),
testCase(DeclarationTests.allTests),
testCase(EnumTests.allTests),
testCase(FunctionCallTests.allTests),
testCase(LiteralTests.allTests),
testCase(OptionalTests.allTests),
testCase(PreprocessorTests.allTests),
testCase(StatementTests.allTests)
]
}
#endif

View File

@ -0,0 +1,36 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import Splash
struct OutputBuilderMock: OutputBuilder {
private var components = [Component]()
mutating func addToken(_ token: String, ofType type: TokenType) {
components.append(.token(token, type))
}
mutating func addPlainText(_ text: String) {
components.append(.plainText(text))
}
mutating func addWhitespace(_ whitespace: String) {
components.append(.whitespace(whitespace))
}
func build() -> [Component] {
return components
}
}
extension OutputBuilderMock {
enum Component: Equatable {
case token(String, TokenType)
case plainText(String)
case whitespace(String)
}
}

View File

@ -0,0 +1,16 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import Splash
struct OutputFormatMock: OutputFormat {
let builder: OutputBuilderMock
func makeBuilder() -> OutputBuilderMock {
return builder
}
}

View File

@ -0,0 +1,144 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import XCTest
import Splash
final class ClosureTests: SyntaxHighlighterTestCase {
func testTrailingClosureWithArguments() {
let components = highlighter.highlight("call() { arg in }")
XCTAssertEqual(components, [
.token("call", .call),
.plainText("()"),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.plainText("arg"),
.whitespace(" "),
.token("in", .keyword),
.whitespace(" "),
.plainText("}")
])
}
func testTrailingClosureWithoutParanthesis() {
let components = highlighter.highlight("call { $0 }")
XCTAssertEqual(components, [
.token("call", .call),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.plainText("$0"),
.whitespace(" "),
.plainText("}")
])
}
func testEmptyTrailingClosure() {
let components = highlighter.highlight("call {}")
XCTAssertEqual(components, [
.token("call", .call),
.whitespace(" "),
.plainText("{}")
])
}
func testClosureArgumentWithSingleArgument() {
let components = highlighter.highlight("func add(closure: (String) -> Void)")
XCTAssertEqual(components, [
.token("func", .keyword),
.whitespace(" "),
.plainText("add(closure:"),
.whitespace(" "),
.plainText("("),
.token("String", .type),
.plainText(")"),
.whitespace(" "),
.plainText("->"),
.whitespace(" "),
.token("Void", .type),
.plainText(")")
])
}
func testClosureArgumentWithMultipleArguments() {
let components = highlighter.highlight("func add(closure: (String, Int) -> Void)")
XCTAssertEqual(components, [
.token("func", .keyword),
.whitespace(" "),
.plainText("add(closure:"),
.whitespace(" "),
.plainText("("),
.token("String", .type),
.plainText(","),
.whitespace(" "),
.token("Int", .type),
.plainText(")"),
.whitespace(" "),
.plainText("->"),
.whitespace(" "),
.token("Void", .type),
.plainText(")")
])
}
func testEscapingClosureArgument() {
let components = highlighter.highlight("func add(closure: @escaping () -> Void)")
XCTAssertEqual(components, [
.token("func", .keyword),
.whitespace(" "),
.plainText("add(closure:"),
.whitespace(" "),
.token("@escaping", .keyword),
.whitespace(" "),
.plainText("()"),
.whitespace(" "),
.plainText("->"),
.whitespace(" "),
.token("Void", .type),
.plainText(")")
])
}
func testPassingClosureAsArgument() {
let components = highlighter.highlight("object.call({ $0 })")
XCTAssertEqual(components, [
.plainText("object."),
.token("call", .call),
.plainText("({"),
.whitespace(" "),
.plainText("$0"),
.whitespace(" "),
.plainText("})")
])
}
func testAllTestsRunOnLinux() {
XCTAssertTrue(TestCaseVerifier.verifyLinuxTests((type(of: self)).allTests))
}
}
extension ClosureTests {
static var allTests: [(String, TestClosure<ClosureTests>)] {
return [
("testTrailingClosureWithArguments", testTrailingClosureWithArguments),
("testTrailingClosureWithoutParanthesis", testTrailingClosureWithoutParanthesis),
("testEmptyTrailingClosure", testEmptyTrailingClosure),
("testClosureArgumentWithSingleArgument", testClosureArgumentWithSingleArgument),
("testClosureArgumentWithMultipleArguments", testClosureArgumentWithMultipleArguments),
("testEscapingClosureArgument", testEscapingClosureArgument),
("testPassingClosureAsArgument", testPassingClosureAsArgument)
]
}
}

View File

@ -0,0 +1,74 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import XCTest
import Splash
final class CommentTests: SyntaxHighlighterTestCase {
func testSingleLineComment() {
let components = highlighter.highlight("call() // Hello call() var \"string\"\ncall()")
XCTAssertEqual(components, [
.token("call", .call),
.plainText("()"),
.whitespace(" "),
.token("//", .comment),
.whitespace(" "),
.token("Hello", .comment),
.whitespace(" "),
.token("call()", .comment),
.whitespace(" "),
.token("var", .comment),
.whitespace(" "),
.token("\"string\"", .comment),
.whitespace("\n"),
.token("call", .call),
.plainText("()")
])
}
func testMultiLineComment() {
let components = highlighter.highlight("""
struct Foo {}
/* Comment
Hello!
*/ call()
""")
XCTAssertEqual(components, [
.token("struct", .keyword),
.whitespace(" "),
.plainText("Foo"),
.whitespace(" "),
.plainText("{}"),
.whitespace("\n"),
.token("/*", .comment),
.whitespace(" "),
.token("Comment", .comment),
.whitespace("\n "),
.token("Hello!", .comment),
.whitespace("\n"),
.token("*/", .comment),
.whitespace(" "),
.token("call", .call),
.plainText("()")
])
}
func testAllTestsRunOnLinux() {
XCTAssertTrue(TestCaseVerifier.verifyLinuxTests((type(of: self)).allTests))
}
}
extension CommentTests {
static var allTests: [(String, TestClosure<CommentTests>)] {
return [
("testSingleLineComment", testSingleLineComment),
("testMultiLineComment", testMultiLineComment)
]
}
}

View File

@ -0,0 +1,455 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import XCTest
import Splash
final class DeclarationTests: SyntaxHighlighterTestCase {
func testFunctionDeclaration() {
let components = highlighter.highlight("func hello(world: String) -> Int")
XCTAssertEqual(components, [
.token("func", .keyword),
.whitespace(" "),
.plainText("hello(world:"),
.whitespace(" "),
.token("String", .type),
.plainText(")"),
.whitespace(" "),
.plainText("->"),
.whitespace(" "),
.token("Int", .type)
])
}
func testRequiredFunctionDeclaration() {
let components = highlighter.highlight("required func hello()")
XCTAssertEqual(components, [
.token("required", .keyword),
.whitespace(" "),
.token("func", .keyword),
.whitespace(" "),
.plainText("hello()")
])
}
func testPublicFunctionDeclarationWithDocumentationEndingWithDot() {
let components = highlighter.highlight("""
/// Documentation.
public func hello()
""")
XCTAssertEqual(components, [
.token("///", .comment),
.whitespace(" "),
.token("Documentation.", .comment),
.whitespace("\n"),
.token("public", .keyword),
.whitespace(" "),
.token("func", .keyword),
.whitespace(" "),
.plainText("hello()")
])
}
func testFunctionDeclarationWithEmptyExternalLabel() {
let components = highlighter.highlight("func a(_ b: B)")
XCTAssertEqual(components, [
.token("func", .keyword),
.whitespace(" "),
.plainText("a("),
.token("_", .keyword),
.whitespace(" "),
.plainText("b:"),
.whitespace(" "),
.token("B", .type),
.plainText(")")
])
}
func testGenericFunctionDeclarationWithoutConstraints() {
let components = highlighter.highlight("func hello<A, B>(a: A, b: B)")
XCTAssertEqual(components, [
.token("func", .keyword),
.whitespace(" "),
.plainText("hello<A,"),
.whitespace(" "),
.plainText("B>(a:"),
.whitespace(" "),
.token("A", .type),
.plainText(","),
.whitespace(" "),
.plainText("b:"),
.whitespace(" "),
.token("B", .type),
.plainText(")")
])
}
func testGenericFunctionDeclarationWithSingleConstraint() {
let components = highlighter.highlight("func hello<T: AnyObject>(t: T)")
XCTAssertEqual(components, [
.token("func", .keyword),
.whitespace(" "),
.plainText("hello<T:"),
.whitespace(" "),
.token("AnyObject", .type),
.plainText(">(t:"),
.whitespace(" "),
.token("T", .type),
.plainText(")")
])
}
func testGenericFunctionDeclarationWithMultipleConstraints() {
let components = highlighter.highlight("func hello<A: AnyObject, B: Sequence>(a: A, b: B)")
XCTAssertEqual(components, [
.token("func", .keyword),
.whitespace(" "),
.plainText("hello<A:"),
.whitespace(" "),
.token("AnyObject", .type),
.plainText(","),
.whitespace(" "),
.plainText("B:"),
.whitespace(" "),
.token("Sequence", .type),
.plainText(">(a:"),
.whitespace(" "),
.token("A", .type),
.plainText(","),
.whitespace(" "),
.plainText("b:"),
.whitespace(" "),
.token("B", .type),
.plainText(")")
])
}
func testGenericStructDeclaration() {
let components = highlighter.highlight("struct MyStruct<A: Hello, B> {}")
XCTAssertEqual(components, [
.token("struct", .keyword),
.whitespace(" "),
.plainText("MyStruct<A:"),
.whitespace(" "),
.token("Hello", .type),
.plainText(","),
.whitespace(" "),
.plainText("B>"),
.whitespace(" "),
.plainText("{}")
])
}
func testClassDeclaration() {
let components = highlighter.highlight("""
class Hello {
var required: String
var optional: Int?
}
""")
XCTAssertEqual(components, [
.token("class", .keyword),
.whitespace(" "),
.plainText("Hello"),
.whitespace(" "),
.plainText("{"),
.whitespace("\n "),
.token("var", .keyword),
.whitespace(" "),
.plainText("required:"),
.whitespace(" "),
.token("String", .type),
.whitespace("\n "),
.token("var", .keyword),
.whitespace(" "),
.plainText("optional:"),
.whitespace(" "),
.token("Int", .type),
.plainText("?"),
.whitespace("\n"),
.plainText("}")
])
}
func testCompactClassDeclarationWithInitializer() {
let components = highlighter.highlight("class Foo { init(hello: Int) {} }")
XCTAssertEqual(components, [
.token("class", .keyword),
.whitespace(" "),
.plainText("Foo"),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.token("init", .keyword),
.plainText("(hello:"),
.whitespace(" "),
.token("Int", .type),
.plainText(")"),
.whitespace(" "),
.plainText("{}"),
.whitespace(" "),
.plainText("}")
])
}
func testSubclassDeclaration() {
let components = highlighter.highlight("class ViewController: UIViewController { }")
XCTAssertEqual(components, [
.token("class", .keyword),
.whitespace(" "),
.plainText("ViewController:"),
.whitespace(" "),
.token("UIViewController", .type),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.plainText("}")
])
}
func testProtocolDeclaration() {
let components = highlighter.highlight("""
protocol Hello {
var property: String { get set }
func method()
}
""")
XCTAssertEqual(components, [
.token("protocol", .keyword),
.whitespace(" "),
.plainText("Hello"),
.whitespace(" "),
.plainText("{"),
.whitespace("\n "),
.token("var", .keyword),
.whitespace(" "),
.plainText("property:"),
.whitespace(" "),
.token("String", .type),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.token("get", .keyword),
.whitespace(" "),
.token("set", .keyword),
.whitespace(" "),
.plainText("}"),
.whitespace("\n "),
.token("func", .keyword),
.whitespace(" "),
.plainText("method()"),
.whitespace("\n"),
.plainText("}")
])
}
func testExtensionDeclaration() {
let components = highlighter.highlight("extension UIViewController { }")
XCTAssertEqual(components, [
.token("extension", .keyword),
.whitespace(" "),
.token("UIViewController", .type),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.plainText("}")
])
}
func testExtensionDeclarationWithConstraint() {
let components = highlighter.highlight("extension Hello where Foo == String, Bar: Numeric { }")
XCTAssertEqual(components, [
.token("extension", .keyword),
.whitespace(" "),
.token("Hello", .type),
.whitespace(" "),
.token("where", .keyword),
.whitespace(" "),
.token("Foo", .type),
.whitespace(" "),
.plainText("=="),
.whitespace(" "),
.token("String", .type),
.plainText(","),
.whitespace(" "),
.token("Bar", .type),
.plainText(":"),
.whitespace(" "),
.token("Numeric", .type),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.plainText("}")
])
}
func testLazyPropertyDeclaration() {
let components = highlighter.highlight("""
struct Hello {
lazy var property = 0
}
""")
XCTAssertEqual(components, [
.token("struct", .keyword),
.whitespace(" "),
.plainText("Hello"),
.whitespace(" "),
.plainText("{"),
.whitespace("\n "),
.token("lazy", .keyword),
.whitespace(" "),
.token("var", .keyword),
.whitespace(" "),
.plainText("property"),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.token("0", .number),
.whitespace("\n"),
.plainText("}")
])
}
func testGenericPropertyDeclaration() {
let components = highlighter.highlight("class Hello { var array: Array<String> = [] }")
XCTAssertEqual(components, [
.token("class", .keyword),
.whitespace(" "),
.plainText("Hello"),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.token("var", .keyword),
.whitespace(" "),
.plainText("array:"),
.whitespace(" "),
.token("Array", .type),
.plainText("<"),
.token("String", .type),
.plainText(">"),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.plainText("[]"),
.whitespace(" "),
.plainText("}")
])
}
func testPropertyDeclarationWithWillSet() {
let components = highlighter.highlight("""
struct Hello {
var property: Int { willSet { } }
}
""")
XCTAssertEqual(components, [
.token("struct", .keyword),
.whitespace(" "),
.plainText("Hello"),
.whitespace(" "),
.plainText("{"),
.whitespace("\n "),
.token("var", .keyword),
.whitespace(" "),
.plainText("property:"),
.whitespace(" "),
.token("Int", .type),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.token("willSet", .keyword),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.plainText("}"),
.whitespace(" "),
.plainText("}"),
.whitespace("\n"),
.plainText("}")
])
}
func testPropertyDeclarationWithDidSet() {
let components = highlighter.highlight("""
struct Hello {
var property: Int { didSet { } }
}
""")
XCTAssertEqual(components, [
.token("struct", .keyword),
.whitespace(" "),
.plainText("Hello"),
.whitespace(" "),
.plainText("{"),
.whitespace("\n "),
.token("var", .keyword),
.whitespace(" "),
.plainText("property:"),
.whitespace(" "),
.token("Int", .type),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.token("didSet", .keyword),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.plainText("}"),
.whitespace(" "),
.plainText("}"),
.whitespace("\n"),
.plainText("}")
])
}
func testAllTestsRunOnLinux() {
XCTAssertTrue(TestCaseVerifier.verifyLinuxTests((type(of: self)).allTests))
}
}
extension DeclarationTests {
static var allTests: [(String, TestClosure<DeclarationTests>)] {
return [
("testFunctionDeclaration", testFunctionDeclaration),
("testRequiredFunctionDeclaration", testRequiredFunctionDeclaration),
("testPublicFunctionDeclarationWithDocumentationEndingWithDot", testPublicFunctionDeclarationWithDocumentationEndingWithDot),
("testFunctionDeclarationWithEmptyExternalLabel", testFunctionDeclarationWithEmptyExternalLabel),
("testGenericFunctionDeclarationWithoutConstraints", testGenericFunctionDeclarationWithoutConstraints),
("testGenericFunctionDeclarationWithSingleConstraint", testGenericFunctionDeclarationWithSingleConstraint),
("testGenericFunctionDeclarationWithMultipleConstraints", testGenericFunctionDeclarationWithMultipleConstraints),
("testGenericStructDeclaration", testGenericStructDeclaration),
("testClassDeclaration", testClassDeclaration),
("testCompactClassDeclarationWithInitializer", testCompactClassDeclarationWithInitializer),
("testSubclassDeclaration", testSubclassDeclaration),
("testProtocolDeclaration", testProtocolDeclaration),
("testExtensionDeclaration", testExtensionDeclaration),
("testExtensionDeclarationWithConstraint", testExtensionDeclarationWithConstraint),
("testLazyPropertyDeclaration", testLazyPropertyDeclaration),
("testGenericPropertyDeclaration", testGenericPropertyDeclaration),
("testPropertyDeclarationWithWillSet", testPropertyDeclarationWithWillSet),
("testPropertyDeclarationWithDidSet", testPropertyDeclarationWithDidSet)
]
}
}

View File

@ -0,0 +1,64 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import XCTest
import Splash
final class EnumTests: SyntaxHighlighterTestCase {
func testEnumDotSyntaxInAssignment() {
let components = highlighter.highlight("let value: Enum = .aCase")
XCTAssertEqual(components, [
.token("let", .keyword),
.whitespace(" "),
.plainText("value:"),
.whitespace(" "),
.token("Enum", .type),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.plainText("."),
.token("aCase", .dotAccess)
])
}
func testEnumDotSyntaxAsArgument() {
let components = highlighter.highlight("call(.aCase)")
XCTAssertEqual(components, [
.token("call", .call),
.plainText("(."),
.token("aCase", .dotAccess),
.plainText(")")
])
}
func testEnumDotSyntaxWithAssociatedValue() {
let components = highlighter.highlight("call(.error(error))")
XCTAssertEqual(components, [
.token("call", .call),
.plainText("(."),
.token("error", .dotAccess),
.plainText("(error))")
])
}
func testAllTestsRunOnLinux() {
XCTAssertTrue(TestCaseVerifier.verifyLinuxTests((type(of: self)).allTests))
}
}
extension EnumTests {
static var allTests: [(String, TestClosure<EnumTests>)] {
return [
("testEnumDotSyntaxInAssignment", testEnumDotSyntaxInAssignment),
("testEnumDotSyntaxAsArgument", testEnumDotSyntaxAsArgument),
("testEnumDotSyntaxWithAssociatedValue", testEnumDotSyntaxWithAssociatedValue)
]
}
}

View File

@ -0,0 +1,71 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import XCTest
import Splash
final class FunctionCallTests: SyntaxHighlighterTestCase {
func testFunctionCallWithIntegers() {
let components = highlighter.highlight("add(1, 2)")
XCTAssertEqual(components, [
.token("add", .call),
.plainText("("),
.token("1", .number),
.plainText(","),
.whitespace(" "),
.token("2", .number),
.plainText(")")
])
}
func testImplicitInitializerCall() {
let components = highlighter.highlight("let string = String()")
XCTAssertEqual(components, [
.token("let", .keyword),
.whitespace(" "),
.plainText("string"),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.token("String", .type),
.plainText("()")
])
}
func testExplicitInitializerCall() {
let components = highlighter.highlight("let string = String.init()")
XCTAssertEqual(components, [
.token("let", .keyword),
.whitespace(" "),
.plainText("string"),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.token("String", .type),
.plainText("."),
.token("init", .call),
.plainText("()")
])
}
func testAllTestsRunOnLinux() {
XCTAssertTrue(TestCaseVerifier.verifyLinuxTests((type(of: self)).allTests))
}
}
extension FunctionCallTests {
static var allTests: [(String, TestClosure<FunctionCallTests>)] {
return [
("testFunctionCallWithIntegers", testFunctionCallWithIntegers),
("testImplicitInitializerCall", testImplicitInitializerCall),
("testExplicitInitializerCall", testExplicitInitializerCall)
]
}
}

View File

@ -0,0 +1,149 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import XCTest
import Splash
final class LiteralTests: SyntaxHighlighterTestCase {
func testStringLiteral() {
let components = highlighter.highlight("let string = \"Hello, world!\"")
XCTAssertEqual(components, [
.token("let", .keyword),
.whitespace(" "),
.plainText("string"),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.token("\"Hello,", .string),
.whitespace(" "),
.token("world!\"", .string)
])
}
func testStringLiteralPassedToFunction() {
let components = highlighter.highlight("call(\"Hello, world!\")")
XCTAssertEqual(components, [
.token("call", .call),
.plainText("("),
.token("\"Hello,", .string),
.whitespace(" "),
.token("world!\"", .string),
.plainText(")")
])
}
func testStringLiteralWithEscapedQuote() {
let components = highlighter.highlight("\"Hello \\\" World\"; call()")
XCTAssertEqual(components, [
.token("\"Hello", .string),
.whitespace(" "),
.token("\\\"", .string),
.whitespace(" "),
.token("World\"", .string),
.plainText(";"),
.whitespace(" "),
.token("call", .call),
.plainText("()")
])
}
func testStringLiteralWithAttribute() {
let components = highlighter.highlight("\"@escaping\"")
XCTAssertEqual(components, [.token("\"@escaping\"", .string)])
}
func testStringLiteralInterpolation() {
let components = highlighter.highlight("\"Hello \\(variable) world \\(call())\"")
XCTAssertEqual(components, [
.token("\"Hello", .string),
.whitespace(" "),
.plainText("\\(variable)"),
.whitespace(" "),
.token("world", .string),
.whitespace(" "),
.plainText("\\("),
.token("call", .call),
.plainText("())"),
.token("\"", .string)
])
}
func testMultiLineStringLiteral() {
let components = highlighter.highlight("""
let string = \"\"\"
Hello \\(variable)
\"\"\"
""")
XCTAssertEqual(components, [
.token("let", .keyword),
.whitespace(" "),
.plainText("string"),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.token("\"\"\"", .string),
.whitespace("\n"),
.token("Hello", .string),
.whitespace(" "),
.plainText("\\(variable)"),
.whitespace("\n"),
.token("\"\"\"", .string)
])
}
func testDoubleLiteral() {
let components = highlighter.highlight("let double = 1.13")
XCTAssertEqual(components, [
.token("let", .keyword),
.whitespace(" "),
.plainText("double"),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.token("1.13", .number)
])
}
func testIntegerLiteralWithSeparators() {
let components = highlighter.highlight("let int = 1_000_000")
XCTAssertEqual(components, [
.token("let", .keyword),
.whitespace(" "),
.plainText("int"),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.token("1_000_000", .number)
])
}
func testAllTestsRunOnLinux() {
XCTAssertTrue(TestCaseVerifier.verifyLinuxTests((type(of: self)).allTests))
}
}
extension LiteralTests {
static var allTests: [(String, TestClosure<LiteralTests>)] {
return [
("testStringLiteral", testStringLiteral),
("testStringLiteralPassedToFunction", testStringLiteralPassedToFunction),
("testStringLiteralWithEscapedQuote", testStringLiteralWithEscapedQuote),
("testStringLiteralWithAttribute", testStringLiteralWithAttribute),
("testStringLiteralInterpolation", testStringLiteralInterpolation),
("testMultiLineStringLiteral", testMultiLineStringLiteral),
("testDoubleLiteral", testDoubleLiteral),
("testIntegerLiteralWithSeparators", testIntegerLiteralWithSeparators)
]
}
}

View File

@ -0,0 +1,59 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import XCTest
import Splash
final class OptionalTests: SyntaxHighlighterTestCase {
func testAssigningPropertyWithOptionalChaining() {
let components = highlighter.highlight("object?.property = true")
XCTAssertEqual(components, [
.plainText("object?."),
.token("property", .property),
.whitespace(" "),
.plainText("="),
.whitespace(" "),
.token("true", .keyword)
])
}
func testReadingPropertyWithOptionalChaining() {
let components = highlighter.highlight("call(object?.property)")
XCTAssertEqual(components, [
.token("call", .call),
.plainText("(object?."),
.token("property", .property),
.plainText(")")
])
}
func testCallingMethodwithOptionalChaining() {
let components = highlighter.highlight("object?.call()")
XCTAssertEqual(components, [
.plainText("object?."),
.token("call", .call),
.plainText("()")
])
}
func testAllTestsRunOnLinux() {
XCTAssertTrue(TestCaseVerifier.verifyLinuxTests((type(of: self)).allTests))
}
}
extension OptionalTests {
static var allTests: [(String, TestClosure<OptionalTests>)] {
return [
("testAssigningPropertyWithOptionalChaining", testAssigningPropertyWithOptionalChaining),
("testReadingPropertyWithOptionalChaining", testReadingPropertyWithOptionalChaining),
("testCallingMethodwithOptionalChaining", testCallingMethodwithOptionalChaining)
]
}
}

View File

@ -0,0 +1,78 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import XCTest
import Splash
final class PreprocessorTests: SyntaxHighlighterTestCase {
func testPreprocessing() {
let components = highlighter.highlight("""
#if os(iOS)
call()
#endif
""")
XCTAssertEqual(components, [
.token("#if", .preprocessing),
.whitespace(" "),
.token("os(iOS)", .preprocessing),
.whitespace("\n"),
.token("call", .call),
.plainText("()"),
.whitespace("\n"),
.token("#endif", .preprocessing)
])
}
func testSelector() {
let components = highlighter.highlight("addObserver(self, selector: #selector(function(_:)))")
XCTAssertEqual(components, [
.token("addObserver", .call),
.plainText("("),
.token("self", .keyword),
.plainText(","),
.whitespace(" "),
.plainText("selector:"),
.whitespace(" "),
.token("#selector", .keyword),
.plainText("("),
.token("function", .call),
.plainText("("),
.token("_", .keyword),
.plainText(":)))")
])
}
func testFunctionAttribute() {
let components = highlighter.highlight("@NSApplicationMain class AppDelegate {}")
XCTAssertEqual(components, [
.token("@NSApplicationMain", .keyword),
.whitespace(" "),
.token("class", .keyword),
.whitespace(" "),
.plainText("AppDelegate"),
.whitespace(" "),
.plainText("{}")
])
}
func testAllTestsRunOnLinux() {
XCTAssertTrue(TestCaseVerifier.verifyLinuxTests((type(of: self)).allTests))
}
}
extension PreprocessorTests {
static var allTests: [(String, TestClosure<PreprocessorTests>)] {
return [
("testPreprocessing", testPreprocessing),
("testSelector", testSelector),
("testFunctionAttribute", testFunctionAttribute)
]
}
}

View File

@ -0,0 +1,174 @@
/**
* Splash
* Copyright (c) John Sundell 2018
* MIT license - see LICENSE.md
*/
import Foundation
import XCTest
import Splash
final class StatementTests: SyntaxHighlighterTestCase {
func testImportStatement() {
let components = highlighter.highlight("import UIKit")
XCTAssertEqual(components, [
.token("import", .keyword),
.whitespace(" "),
.plainText("UIKit")
])
}
func testImportStatementWithSubmodule() {
let components = highlighter.highlight("import os.log")
XCTAssertEqual(components, [
.token("import", .keyword),
.whitespace(" "),
.plainText("os.log")
])
}
func testChainedIfElseStatements() {
let components = highlighter.highlight("if condition { } else if call() { } else { \"string\" }")
XCTAssertEqual(components, [
.token("if", .keyword),
.whitespace(" "),
.plainText("condition"),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.plainText("}"),
.whitespace(" "),
.token("else", .keyword),
.whitespace(" "),
.token("if", .keyword),
.whitespace(" "),
.token("call", .call),
.plainText("()"),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.plainText("}"),
.whitespace(" "),
.token("else", .keyword),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.token("\"string\"", .string),
.whitespace(" "),
.plainText("}")
])
}
func testSwitchStatement() {
let components = highlighter.highlight("""
switch variable {
case .one: break
case .two: callA()
default:
callB()
}
""")
XCTAssertEqual(components, [
.token("switch", .keyword),
.whitespace(" "),
.plainText("variable"),
.whitespace(" "),
.plainText("{"),
.whitespace("\n"),
.token("case", .keyword),
.whitespace(" "),
.plainText("."),
.token("one", .dotAccess),
.plainText(":"),
.whitespace(" "),
.token("break", .keyword),
.whitespace("\n"),
.token("case", .keyword),
.whitespace(" "),
.plainText("."),
.token("two", .dotAccess),
.plainText(":"),
.whitespace(" "),
.token("callA", .call),
.plainText("()"),
.whitespace("\n"),
.token("default", .keyword),
.plainText(":"),
.whitespace("\n "),
.token("callB", .call),
.plainText("()"),
.whitespace("\n"),
.plainText("}")
])
}
func testSwitchStatementWithAssociatedValues() {
let components = highlighter.highlight("""
switch value {
case .one(let a): break
}
""")
XCTAssertEqual(components, [
.token("switch", .keyword),
.whitespace(" "),
.plainText("value"),
.whitespace(" "),
.plainText("{"),
.whitespace("\n"),
.token("case", .keyword),
.whitespace(" "),
.plainText("."),
.token("one", .dotAccess),
.plainText("("),
.token("let", .keyword),
.whitespace(" "),
.plainText("a):"),
.whitespace(" "),
.token("break", .keyword),
.whitespace("\n"),
.plainText("}")
])
}
func testForStatementWithStaticProperty() {
let components = highlighter.highlight("for value in Enum.allCases { }")
XCTAssertEqual(components, [
.token("for", .keyword),
.whitespace(" "),
.plainText("value"),
.whitespace(" "),
.token("in", .keyword),
.whitespace(" "),
.token("Enum", .type),
.plainText("."),
.token("allCases", .property),
.whitespace(" "),
.plainText("{"),
.whitespace(" "),
.plainText("}")
])
}
func testAllTestsRunOnLinux() {
XCTAssertTrue(TestCaseVerifier.verifyLinuxTests((type(of: self)).allTests))
}
}
extension StatementTests {
static var allTests: [(String, TestClosure<StatementTests>)] {
return [
("testImportStatement", testImportStatement),
("testImportStatementWithSubmodule", testImportStatementWithSubmodule),
("testChainedIfElseStatements", testChainedIfElseStatements),
("testSwitchStatement", testSwitchStatement),
("testSwitchStatementWithAssociatedValues", testSwitchStatementWithAssociatedValues),
("testForStatementWithStaticProperty", testForStatementWithStaticProperty)
]
}
}