Initial commit
This commit is contained in:
commit
8645db8de0
|
@ -0,0 +1,5 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
Package.resolved
|
|
@ -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/
|
|
@ -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 😀
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 214 KiB |
|
@ -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.
|
|
@ -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
|
|
@ -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"]
|
||||
)
|
||||
]
|
||||
)
|
|
@ -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) 😊
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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: "")
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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 }
|
||||
}
|
|
@ -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(" .")
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
|
@ -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)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue