├── .dockerignore
├── .github
├── FUNDING.yml
├── renovate.json
└── workflows
│ ├── codeql-analysis.yml
│ ├── spm.yml
│ └── test.yml
├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── .vscode
├── launch.json
└── settings.json
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── LICENSE
├── Package.resolved
├── Package.swift
├── Public
├── css
│ ├── common.css
│ ├── embedded.css
│ ├── share_sheet.css
│ └── version_picker.css
├── embedded.html
├── embedded.js
├── error.html
├── favicons
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── browserconfig.xml
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── mstile-150x150.png
│ ├── safari-pinned-tab.svg
│ └── site.webmanifest
├── images
│ ├── C.png
│ ├── C@2x.png
│ ├── C@3x.png
│ ├── E.png
│ ├── E@2x.png
│ ├── E@3x.png
│ ├── F.png
│ ├── F@2x.png
│ ├── F@3x.png
│ ├── I.png
│ ├── I@2x.png
│ ├── I@3x.png
│ ├── M.png
│ ├── M@2x.png
│ ├── M@3x.png
│ ├── P.png
│ ├── P@2x.png
│ ├── P@3x.png
│ ├── S.png
│ ├── S@2x.png
│ ├── S@3x.png
│ ├── icon_swift-ast-explorer.com.svg
│ ├── icon_swift-format.com.svg
│ ├── icon_swiftfiddle.com.svg
│ ├── icon_swiftregex.com.svg
│ ├── lsp.svg
│ ├── lsp_fill.svg
│ ├── ogp_large.jpeg
│ └── ogp_small.png
├── index.html
├── index.js
├── js
│ ├── app.js
│ ├── console.js
│ ├── decoder.js
│ ├── editor.js
│ ├── embed_view.js
│ ├── encoder.js
│ ├── icon.js
│ ├── icon_embed.js
│ ├── language_server.js
│ ├── main_view.js
│ ├── runner.js
│ ├── share_sheet.js
│ ├── snackbar.js
│ ├── textlinesteam.js
│ ├── ui_control.js
│ ├── ui_control_embed.js
│ ├── unescape.js
│ ├── uuid.js
│ ├── version_picker.js
│ └── worker.js
├── robots.txt
└── scss
│ └── default.scss
├── README.md
├── Resources
└── Package.swift.json
├── SECURITY.md
├── SourceHanCodeJP-Regular.otf
├── Sources
└── App
│ ├── Controllers
│ ├── Base32.swift
│ ├── Firestore.swift
│ ├── Gist.swift
│ ├── ShareImage.swift
│ ├── SharedLink.swift
│ └── SwiftPackage.swift
│ ├── Middlewares
│ ├── CommonErrorMiddleware.swift
│ └── CustomHeaderMiddleware.swift
│ ├── Models
│ ├── EmbeddedPageResponse.swift
│ ├── ExecutionRequestParameter.swift
│ ├── ExecutionResponse.swift
│ ├── FoldRange.swift
│ ├── InitialPageResponse.swift
│ ├── PackageInfo.swift
│ ├── SharedLinkRequestParameter.swift
│ └── VersionGroup.swift
│ ├── configure.swift
│ ├── entrypoint.swift
│ ├── routes.swift
│ └── versions.swift
├── Tests
└── AppTests
│ └── AppTests.swift
├── package-lock.json
├── package.json
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | .build/
2 | .swiftpm/
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: kishikawakatsumi
2 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "html": {
4 | "fileMatch": ["\\.html?$", "\\.leaf?$"]
5 | },
6 | "packageRules": [
7 | {
8 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"],
9 | "automerge": true
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [master]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [master]
20 | schedule:
21 | - cron: "44 8 * * 0"
22 |
23 | env:
24 | CODEQL_ENABLE_EXPERIMENTAL_FEATURES_SWIFT: true
25 |
26 | jobs:
27 | analyze:
28 | name: Analyze
29 | runs-on: ubuntu-latest
30 | permissions:
31 | actions: read
32 | contents: read
33 | security-events: write
34 |
35 | strategy:
36 | fail-fast: false
37 | matrix:
38 | language: ["javascript"]
39 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
40 | # Learn more:
41 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
42 |
43 | steps:
44 | - name: Checkout repository
45 | uses: actions/checkout@v4
46 |
47 | # Initializes the CodeQL tools for scanning.
48 | - name: Initialize CodeQL
49 | uses: github/codeql-action/init@v3
50 | with:
51 | languages: ${{ matrix.language }}
52 | # If you wish to specify custom queries, you can do so here or in a config file.
53 | # By default, queries listed here will override any specified in a config file.
54 | # Prefix the list here with "+" to use these queries and those in the config file.
55 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
56 |
57 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
58 | # If this step fails, then you should remove it and run the build manually (see below)
59 | - name: Autobuild
60 | uses: github/codeql-action/autobuild@v3
61 | if: matrix.language == 'javascript'
62 |
63 | # ℹ️ Command-line programs to run using the OS shell.
64 | # 📚 https://git.io/JvXDl
65 |
66 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
67 | # and modify them (or add more) to build your code if your project
68 | # uses a compiled language
69 |
70 | - run: swift build
71 | if: matrix.language == 'swift'
72 |
73 | - name: Perform CodeQL Analysis
74 | uses: github/codeql-action/analyze@v3
75 | with:
76 | category: "/language:${{matrix.language}}"
77 |
--------------------------------------------------------------------------------
/.github/workflows/spm.yml:
--------------------------------------------------------------------------------
1 | name: Update Package.resolved
2 | on:
3 | schedule:
4 | - cron: "0 0 * * 1"
5 | workflow_dispatch:
6 |
7 | jobs:
8 | run:
9 | runs-on: macos-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - uses: SwiftyLab/setup-swift@latest
13 | - name: Update Package.swift.json
14 | run: |
15 | set -ex
16 |
17 | tempdir=$(mktemp -d)
18 | filename="Package.swift"
19 | curl -sfL -o "$tempdir/$filename" https://raw.github.com/swiftfiddle/swiftfiddle-lsp/main/Resources/ProjectTemplate/$filename
20 | swift package --package-path "$tempdir" dump-package > Resources/$filename.json
21 | - name: Build
22 | run: |
23 | set -ex
24 |
25 | swift package update
26 | swift build
27 | - name: Create Pull Request
28 | id: cpr
29 | uses: peter-evans/create-pull-request@v7
30 | with:
31 | token: ${{ secrets.GH_PAT }}
32 | base: "master"
33 | commit-message: "Update Swift Packages"
34 | title: "Update Swift Packages"
35 | add-paths: |
36 | Resources/Package.swift.json
37 | Package.resolved
38 | - name: Enable Pull Request Automerge
39 | if: ${{ steps.cpr.outputs.pull-request-url }}
40 | run: gh pr merge --merge --auto ${{ steps.cpr.outputs.pull-request-url }}
41 | env:
42 | GH_TOKEN: ${{ secrets.GH_PAT }}
43 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | pull_request:
4 | branches: [master]
5 | workflow_dispatch:
6 |
7 | env:
8 | FONTAWESOME_TOKEN: ${{ secrets.FONTAWESOME_TOKEN }}
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Build
16 | run: |
17 | set -ex
18 | docker build --rm --no-cache --build-arg FONTAWESOME_TOKEN=${{ env.FONTAWESOME_TOKEN }} .
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### https://raw.github.com/github/gitignore/991e760c1c6d50fdda246e0178b9c58b06770b90/Global/macOS.gitignore
2 |
3 | # General
4 | .DS_Store
5 | .AppleDouble
6 | .LSOverride
7 |
8 | # Icon must end with two \r
9 | Icon
10 |
11 | # Thumbnails
12 | ._*
13 |
14 | # Files that might appear in the root of a volume
15 | .DocumentRevisions-V100
16 | .fseventsd
17 | .Spotlight-V100
18 | .TemporaryItems
19 | .Trashes
20 | .VolumeIcon.icns
21 | .com.apple.timemachine.donotpresent
22 |
23 | # Directories potentially created on remote AFP share
24 | .AppleDB
25 | .AppleDesktop
26 | Network Trash Folder
27 | Temporary Items
28 | .apdisk
29 |
30 |
31 | ### https://raw.github.com/github/gitignore/991e760c1c6d50fdda246e0178b9c58b06770b90/Swift.gitignore
32 |
33 | # Xcode
34 | #
35 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
36 |
37 | ## User settings
38 | xcuserdata/
39 |
40 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
41 | *.xcscmblueprint
42 | *.xccheckout
43 |
44 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
45 | build/
46 | DerivedData/
47 | *.moved-aside
48 | *.pbxuser
49 | !default.pbxuser
50 | *.mode1v3
51 | !default.mode1v3
52 | *.mode2v3
53 | !default.mode2v3
54 | *.perspectivev3
55 | !default.perspectivev3
56 |
57 | ## Obj-C/Swift specific
58 | *.hmap
59 |
60 | ## App packaging
61 | *.ipa
62 | *.dSYM.zip
63 | *.dSYM
64 |
65 | ## Playgrounds
66 | timeline.xctimeline
67 | playground.xcworkspace
68 |
69 | # Swift Package Manager
70 | #
71 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
72 | # Packages/
73 | # Package.pins
74 | # Package.resolved
75 | # *.xcodeproj
76 | #
77 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
78 | # hence it is not needed unless you have added a package configuration file to your project
79 | # .swiftpm
80 |
81 | .build/
82 |
83 | # CocoaPods
84 | #
85 | # We recommend against adding the Pods directory to your .gitignore. However
86 | # you should judge for yourself, the pros and cons are mentioned at:
87 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
88 | #
89 | # Pods/
90 | #
91 | # Add this line if you want to avoid checking in source code from the Xcode workspace
92 | # *.xcworkspace
93 |
94 | # Carthage
95 | #
96 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
97 | # Carthage/Checkouts
98 |
99 | Carthage/Build/
100 |
101 | # Accio dependency management
102 | Dependencies/
103 | .accio/
104 |
105 | # fastlane
106 | #
107 | # It is recommended to not store the screenshots in the git repo.
108 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
109 | # For more information about the recommended setup visit:
110 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
111 |
112 | fastlane/report.xml
113 | fastlane/Preview.html
114 | fastlane/screenshots/**/*.png
115 | fastlane/test_output
116 |
117 | # Code Injection
118 | #
119 | # After new code Injection tools there's a generated folder /iOSInjectionProject
120 | # https://github.com/johnno1962/injectionforxcode
121 |
122 | iOSInjectionProject/
123 |
124 |
125 | ### https://raw.github.com/github/gitignore/991e760c1c6d50fdda246e0178b9c58b06770b90/Node.gitignore
126 |
127 | # Logs
128 | logs
129 | *.log
130 | npm-debug.log*
131 | yarn-debug.log*
132 | yarn-error.log*
133 | lerna-debug.log*
134 | .pnpm-debug.log*
135 |
136 | # Diagnostic reports (https://nodejs.org/api/report.html)
137 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
138 |
139 | # Runtime data
140 | pids
141 | *.pid
142 | *.seed
143 | *.pid.lock
144 |
145 | # Directory for instrumented libs generated by jscoverage/JSCover
146 | lib-cov
147 |
148 | # Coverage directory used by tools like istanbul
149 | coverage
150 | *.lcov
151 |
152 | # nyc test coverage
153 | .nyc_output
154 |
155 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
156 | .grunt
157 |
158 | # Bower dependency directory (https://bower.io/)
159 | bower_components
160 |
161 | # node-waf configuration
162 | .lock-wscript
163 |
164 | # Compiled binary addons (https://nodejs.org/api/addons.html)
165 | build/Release
166 |
167 | # Dependency directories
168 | node_modules/
169 | jspm_packages/
170 |
171 | # Snowpack dependency directory (https://snowpack.dev/)
172 | web_modules/
173 |
174 | # TypeScript cache
175 | *.tsbuildinfo
176 |
177 | # Optional npm cache directory
178 | .npm
179 |
180 | # Optional eslint cache
181 | .eslintcache
182 |
183 | # Microbundle cache
184 | .rpt2_cache/
185 | .rts2_cache_cjs/
186 | .rts2_cache_es/
187 | .rts2_cache_umd/
188 |
189 | # Optional REPL history
190 | .node_repl_history
191 |
192 | # Output of 'npm pack'
193 | *.tgz
194 |
195 | # Yarn Integrity file
196 | .yarn-integrity
197 |
198 | # dotenv environment variables file
199 | .env
200 | .env.test
201 | .env.production
202 |
203 | # parcel-bundler cache (https://parceljs.org/)
204 | .cache
205 | .parcel-cache
206 |
207 | # Next.js build output
208 | .next
209 | out
210 |
211 | # Nuxt.js build / generate output
212 | .nuxt
213 | dist
214 |
215 | # Gatsby files
216 | .cache/
217 | # Comment in the public line in if your project uses Gatsby and not Next.js
218 | # https://nextjs.org/blog/next-9-1#public-directory-support
219 | # public
220 |
221 | # vuepress build output
222 | .vuepress/dist
223 |
224 | # Serverless directories
225 | .serverless/
226 |
227 | # FuseBox cache
228 | .fusebox/
229 |
230 | # DynamoDB Local files
231 | .dynamodb/
232 |
233 | # TernJS port file
234 | .tern-port
235 |
236 | # Stores VSCode versions used for testing VSCode extensions
237 | .vscode-test
238 |
239 | # yarn v2
240 | .yarn/cache
241 | .yarn/unplugged
242 | .yarn/build-state.yml
243 | .yarn/install-state.gz
244 | .pnp.*
245 |
246 |
247 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "configurations": [
3 | {
4 | "type": "swift",
5 | "request": "launch",
6 | "sourceLanguages": ["swift"],
7 | "name": "Debug App",
8 | "program": "${workspaceFolder:swiftfiddle-web}/.build/debug/App",
9 | "args": [],
10 | "cwd": "${workspaceFolder:swiftfiddle-web}",
11 | "preLaunchTask": "swift: Build Debug App"
12 | },
13 | {
14 | "type": "swift",
15 | "request": "launch",
16 | "sourceLanguages": ["swift"],
17 | "name": "Release App",
18 | "program": "${workspaceFolder:swiftfiddle-web}/.build/release/App",
19 | "args": [],
20 | "cwd": "${workspaceFolder:swiftfiddle-web}",
21 | "preLaunchTask": "swift: Build Release App"
22 | },
23 | {
24 | "type": "lldb",
25 | "request": "launch",
26 | "name": "Test swift-playground",
27 | "program": "/Applications/Xcode.app/Contents/Developer/usr/bin/xctest",
28 | "args": [".build/debug/swift-playgroundPackageTests.xctest"],
29 | "cwd": "${workspaceFolder:swiftfiddle-web}",
30 | "preLaunchTask": "swift: Build All"
31 | }
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "lldb.library": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB",
3 | "lldb.launch.expressions": "native"
4 | }
5 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | [@kishikawakatsumi](https://github.com/kishikawakatsumi).
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts-slim as node
2 |
3 | WORKDIR /build
4 |
5 | ARG FONTAWESOME_TOKEN
6 | COPY package*.json ./
7 | RUN echo "@fortawesome:registry=https://npm.fontawesome.com/\n//npm.fontawesome.com/:_authToken=${FONTAWESOME_TOKEN}" > ./.npmrc \
8 | && npm ci \
9 | && rm -f ./.npmrc
10 |
11 | COPY webpack.*.js ./
12 | COPY Public ./Public/
13 | RUN npx webpack --config webpack.prod.js
14 |
15 |
16 | FROM swift:6.1-jammy as swift
17 | RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
18 | && apt-get -q update \
19 | && apt-get -q dist-upgrade -y \
20 | && apt-get install -y --no-install-recommends \
21 | curl \
22 | expat \
23 | libxml2-dev \
24 | pkg-config \
25 | libasound2-dev \
26 | libssl-dev \
27 | cmake \
28 | libfreetype6-dev \
29 | libexpat1-dev \
30 | libxcb-composite0-dev \
31 | libharfbuzz-dev \
32 | libfontconfig1-dev \
33 | g++ \
34 | && rm -rf /var/lib/apt/lists/*
35 |
36 | WORKDIR /build
37 | COPY --from=node /build /build
38 | COPY ./Package.* ./
39 | RUN swift package resolve
40 |
41 | COPY . .
42 | RUN swift build -c release --static-swift-stdlib
43 |
44 | WORKDIR /staging
45 | RUN cp "$(swift build --package-path /build -c release --show-bin-path)/App" ./
46 |
47 | RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \;
48 |
49 | RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true
50 | RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true
51 |
52 | RUN useradd -m -s /bin/bash linuxbrew && \
53 | echo 'linuxbrew ALL=(ALL) NOPASSWD:ALL' >>/etc/sudoers
54 | USER linuxbrew
55 | RUN /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
56 | RUN /home/linuxbrew/.linuxbrew/bin/brew install silicon
57 |
58 | FROM swift:6.1-jammy-slim
59 | RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
60 | && apt-get -q update \
61 | && apt-get -q dist-upgrade -y \
62 | && apt-get -q install -y \
63 | ca-certificates \
64 | tzdata \
65 | && rm -r /var/lib/apt/lists/*
66 | RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor
67 |
68 | WORKDIR /app
69 | COPY --from=swift --chown=vapor:vapor /staging /app
70 | COPY --from=swift /home/linuxbrew/.linuxbrew/ /home/linuxbrew/.linuxbrew/
71 | COPY ./SourceHanCodeJP-Regular.otf /usr/share/fonts/truetype/SourceHanCodeJP-Regular.otf
72 |
73 | USER vapor:vapor
74 | EXPOSE 8080
75 |
76 | ENTRYPOINT ["./App"]
77 | CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
78 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Kishikawa Katsumi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "async-http-client",
6 | "repositoryURL": "https://github.com/swift-server/async-http-client.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "60235983163d040f343a489f7e2e77c1918a8bd9",
10 | "version": "1.26.1"
11 | }
12 | },
13 | {
14 | "package": "async-kit",
15 | "repositoryURL": "https://github.com/vapor/async-kit.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "e048c8ee94967e8d8a1c2ec0e1156d6f7fa34d31",
19 | "version": "1.20.0"
20 | }
21 | },
22 | {
23 | "package": "console-kit",
24 | "repositoryURL": "https://github.com/vapor/console-kit.git",
25 | "state": {
26 | "branch": null,
27 | "revision": "742f624a998cba2a9e653d9b1e91ad3f3a5dff6b",
28 | "version": "4.15.2"
29 | }
30 | },
31 | {
32 | "package": "leaf",
33 | "repositoryURL": "https://github.com/vapor/leaf.git",
34 | "state": {
35 | "branch": null,
36 | "revision": "d469584b9186851c5a4012d11325fb9db3207ebb",
37 | "version": "4.5.0"
38 | }
39 | },
40 | {
41 | "package": "leaf-kit",
42 | "repositoryURL": "https://github.com/vapor/leaf-kit.git",
43 | "state": {
44 | "branch": null,
45 | "revision": "cf186d8f2ef33e16fd1dd78df36466c22c2e632f",
46 | "version": "1.13.1"
47 | }
48 | },
49 | {
50 | "package": "multipart-kit",
51 | "repositoryURL": "https://github.com/vapor/multipart-kit.git",
52 | "state": {
53 | "branch": null,
54 | "revision": "3498e60218e6003894ff95192d756e238c01f44e",
55 | "version": "4.7.1"
56 | }
57 | },
58 | {
59 | "package": "routing-kit",
60 | "repositoryURL": "https://github.com/vapor/routing-kit.git",
61 | "state": {
62 | "branch": null,
63 | "revision": "93f7222c8e195cbad39fafb5a0e4cc85a8def7ea",
64 | "version": "4.9.2"
65 | }
66 | },
67 | {
68 | "package": "swift-algorithms",
69 | "repositoryURL": "https://github.com/apple/swift-algorithms.git",
70 | "state": {
71 | "branch": null,
72 | "revision": "87e50f483c54e6efd60e885f7f5aa946cee68023",
73 | "version": "1.2.1"
74 | }
75 | },
76 | {
77 | "package": "swift-asn1",
78 | "repositoryURL": "https://github.com/apple/swift-asn1.git",
79 | "state": {
80 | "branch": null,
81 | "revision": "a54383ada6cecde007d374f58f864e29370ba5c3",
82 | "version": "1.3.2"
83 | }
84 | },
85 | {
86 | "package": "swift-async-algorithms",
87 | "repositoryURL": "https://github.com/apple/swift-async-algorithms.git",
88 | "state": {
89 | "branch": null,
90 | "revision": "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b",
91 | "version": "1.0.4"
92 | }
93 | },
94 | {
95 | "package": "swift-atomics",
96 | "repositoryURL": "https://github.com/apple/swift-atomics.git",
97 | "state": {
98 | "branch": null,
99 | "revision": "b601256eab081c0f92f059e12818ac1d4f178ff7",
100 | "version": "1.3.0"
101 | }
102 | },
103 | {
104 | "package": "swift-certificates",
105 | "repositoryURL": "https://github.com/apple/swift-certificates.git",
106 | "state": {
107 | "branch": null,
108 | "revision": "999fd70c7803da89f3904d635a6815a2a7cd7585",
109 | "version": "1.10.0"
110 | }
111 | },
112 | {
113 | "package": "swift-collections",
114 | "repositoryURL": "https://github.com/apple/swift-collections.git",
115 | "state": {
116 | "branch": null,
117 | "revision": "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
118 | "version": "1.2.0"
119 | }
120 | },
121 | {
122 | "package": "swift-crypto",
123 | "repositoryURL": "https://github.com/apple/swift-crypto.git",
124 | "state": {
125 | "branch": null,
126 | "revision": "e8d6eba1fef23ae5b359c46b03f7d94be2f41fed",
127 | "version": "3.12.3"
128 | }
129 | },
130 | {
131 | "package": "swift-distributed-tracing",
132 | "repositoryURL": "https://github.com/apple/swift-distributed-tracing.git",
133 | "state": {
134 | "branch": null,
135 | "revision": "a64a0abc2530f767af15dd88dda7f64d5f1ff9de",
136 | "version": "1.2.0"
137 | }
138 | },
139 | {
140 | "package": "swift-http-structured-headers",
141 | "repositoryURL": "https://github.com/apple/swift-http-structured-headers.git",
142 | "state": {
143 | "branch": null,
144 | "revision": "db6eea3692638a65e2124990155cd220c2915903",
145 | "version": "1.3.0"
146 | }
147 | },
148 | {
149 | "package": "swift-http-types",
150 | "repositoryURL": "https://github.com/apple/swift-http-types.git",
151 | "state": {
152 | "branch": null,
153 | "revision": "a0a57e949a8903563aba4615869310c0ebf14c03",
154 | "version": "1.4.0"
155 | }
156 | },
157 | {
158 | "package": "swift-log",
159 | "repositoryURL": "https://github.com/apple/swift-log.git",
160 | "state": {
161 | "branch": null,
162 | "revision": "3d8596ed08bd13520157f0355e35caed215ffbfa",
163 | "version": "1.6.3"
164 | }
165 | },
166 | {
167 | "package": "swift-metrics",
168 | "repositoryURL": "https://github.com/apple/swift-metrics.git",
169 | "state": {
170 | "branch": null,
171 | "revision": "4c83e1cdf4ba538ef6e43a9bbd0bcc33a0ca46e3",
172 | "version": "2.7.0"
173 | }
174 | },
175 | {
176 | "package": "swift-nio",
177 | "repositoryURL": "https://github.com/apple/swift-nio.git",
178 | "state": {
179 | "branch": null,
180 | "revision": "34d486b01cd891297ac615e40d5999536a1e138d",
181 | "version": "2.83.0"
182 | }
183 | },
184 | {
185 | "package": "swift-nio-extras",
186 | "repositoryURL": "https://github.com/apple/swift-nio-extras.git",
187 | "state": {
188 | "branch": null,
189 | "revision": "145db1962f4f33a4ea07a32e751d5217602eea29",
190 | "version": "1.28.0"
191 | }
192 | },
193 | {
194 | "package": "swift-nio-http2",
195 | "repositoryURL": "https://github.com/apple/swift-nio-http2.git",
196 | "state": {
197 | "branch": null,
198 | "revision": "4281466512f63d1bd530e33f4aa6993ee7864be0",
199 | "version": "1.36.0"
200 | }
201 | },
202 | {
203 | "package": "swift-nio-ssl",
204 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
205 | "state": {
206 | "branch": null,
207 | "revision": "4b38f35946d00d8f6176fe58f96d83aba64b36c7",
208 | "version": "2.31.0"
209 | }
210 | },
211 | {
212 | "package": "swift-nio-transport-services",
213 | "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git",
214 | "state": {
215 | "branch": null,
216 | "revision": "cd1e89816d345d2523b11c55654570acd5cd4c56",
217 | "version": "1.24.0"
218 | }
219 | },
220 | {
221 | "package": "swift-numerics",
222 | "repositoryURL": "https://github.com/apple/swift-numerics.git",
223 | "state": {
224 | "branch": null,
225 | "revision": "e0ec0f5f3af6f3e4d5e7a19d2af26b481acb6ba8",
226 | "version": "1.0.3"
227 | }
228 | },
229 | {
230 | "package": "swift-service-context",
231 | "repositoryURL": "https://github.com/apple/swift-service-context.git",
232 | "state": {
233 | "branch": null,
234 | "revision": "1983448fefc717a2bc2ebde5490fe99873c5b8a6",
235 | "version": "1.2.1"
236 | }
237 | },
238 | {
239 | "package": "swift-service-lifecycle",
240 | "repositoryURL": "https://github.com/swift-server/swift-service-lifecycle.git",
241 | "state": {
242 | "branch": null,
243 | "revision": "e7187309187695115033536e8fc9b2eb87fd956d",
244 | "version": "2.8.0"
245 | }
246 | },
247 | {
248 | "package": "swift-system",
249 | "repositoryURL": "https://github.com/apple/swift-system.git",
250 | "state": {
251 | "branch": null,
252 | "revision": "61e4ca4b81b9e09e2ec863b00c340eb13497dac6",
253 | "version": "1.5.0"
254 | }
255 | },
256 | {
257 | "package": "vapor",
258 | "repositoryURL": "https://github.com/vapor/vapor.git",
259 | "state": {
260 | "branch": null,
261 | "revision": "4014016aad591a120f244f9b9e8a57252b7e62b4",
262 | "version": "4.115.0"
263 | }
264 | },
265 | {
266 | "package": "websocket-kit",
267 | "repositoryURL": "https://github.com/vapor/websocket-kit.git",
268 | "state": {
269 | "branch": null,
270 | "revision": "8666c92dbbb3c8eefc8008c9c8dcf50bfd302167",
271 | "version": "2.16.1"
272 | }
273 | }
274 | ]
275 | },
276 | "version": 1
277 | }
278 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "swift-playground",
6 | platforms: [
7 | .macOS(.v12)
8 | ],
9 | dependencies: [
10 | .package(url: "https://github.com/vapor/vapor.git", from: "4.115.0"),
11 | .package(url: "https://github.com/vapor/leaf.git", from: "4.5.0"),
12 | ],
13 | targets: [
14 | .executableTarget(
15 | name: "App",
16 | dependencies: [
17 | .product(name: "Vapor", package: "vapor"),
18 | .product(name: "Leaf", package: "leaf"),
19 | ],
20 | swiftSettings: [
21 | .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)),
22 | ]
23 | ),
24 | .testTarget(
25 | name: "AppTests", dependencies: [
26 | .target(name: "App"),
27 | .product(name: "XCTVapor", package: "vapor"),
28 | ]
29 | )
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/Public/css/common.css:
--------------------------------------------------------------------------------
1 | .btn.rounded-circle {
2 | width: 2rem;
3 | height: 2rem;
4 | }
5 |
6 | .svg-inline--fa.fa-fw {
7 | width: 1em;
8 | }
9 |
10 | .terminal.xterm {
11 | padding: calc(1rem * 0.25);
12 | height: 100%;
13 | }
14 |
15 | a .card-body {
16 | -o-transition: 0.5s;
17 | -ms-transition: 0.5s;
18 | -moz-transition: 0.5s;
19 | -webkit-transition: 0.5s;
20 | transition: 0.5s;
21 | }
22 |
23 | a .card-body:hover {
24 | background-color: #f8f9fa;
25 | }
26 |
--------------------------------------------------------------------------------
/Public/css/embedded.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: transparent;
3 | }
4 |
5 | .terminal {
6 | height: 100%;
7 | }
8 |
9 | div.run-button-background {
10 | position: absolute;
11 | top: 0;
12 | right: calc(2.4em + 2px + 2px);
13 | width: 2.4em;
14 | height: 2.4em;
15 | color: white;
16 | z-index: 1081;
17 | background-color: rgba(100, 100, 100, 0.6);
18 | }
19 |
20 | #run-button {
21 | color: white;
22 | }
23 |
24 | div.open-button-background {
25 | position: absolute;
26 | top: 0;
27 | right: 2px;
28 | width: 2.4em;
29 | height: 2.4em;
30 | color: white;
31 | z-index: 1081;
32 | background-color: rgba(100, 100, 100, 0.6);
33 | }
34 |
35 | #open-button {
36 | color: white;
37 | }
38 |
--------------------------------------------------------------------------------
/Public/css/share_sheet.css:
--------------------------------------------------------------------------------
1 | .popover {
2 | max-width: 100vw;
3 | }
4 |
5 | .share-sheet-textarea {
6 | resize: none;
7 | }
8 |
9 | .grow-textfield {
10 | vertical-align: top;
11 | align-items: center;
12 | position: relative;
13 | }
14 |
15 | .grow-textfield::after {
16 | content: attr(data-value) " ";
17 | visibility: hidden;
18 | white-space: pre-wrap;
19 | }
20 |
21 | .grow-textfield.stacked {
22 | align-items: stretch;
23 | }
24 |
25 | .grow-textfield.stacked::after,
26 | .grow-textfield.stacked input,
27 | .grow-textfield.stacked textarea {
28 | grid-area: 2/1;
29 | }
30 |
31 | .grow-textfield::after,
32 | .grow-textfield input,
33 | .grow-textfield textarea {
34 | grid-area: 1/2;
35 | }
36 |
--------------------------------------------------------------------------------
/Public/css/version_picker.css:
--------------------------------------------------------------------------------
1 | #version-picker {
2 | max-height: 80vh;
3 | overflow-y: auto;
4 | line-height: 1.2;
5 | }
6 |
7 | .version-picker-item.active-tick a::after {
8 | font-family: "Font Awesome 6 Pro";
9 | content: "\f00c";
10 | color: #0d6efd;
11 | float: right;
12 | display: none;
13 | }
14 |
15 | .version-picker-item.active-tick svg {
16 | color: #0d6efd;
17 | float: right;
18 | }
19 |
--------------------------------------------------------------------------------
/Public/embedded.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SwiftFiddle - Swift Online Playground
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
23 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
41 |
42 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/Public/embedded.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import "./scss/default.scss";
4 | import "./css/embedded.css";
5 |
6 | import "./js/icon_embed.js";
7 |
8 | import { EmbedView } from "./js/embed_view.js";
9 | new EmbedView(window.appConfig);
10 |
--------------------------------------------------------------------------------
/Public/error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
15 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | #(title) | #(status) - #(error)
30 |
333 |
334 |
335 |
336 |
337 |
#(error) Error #(status)
338 |
#(reason)
339 |
340 |
341 |
342 |
--------------------------------------------------------------------------------
/Public/favicons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/favicons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/Public/favicons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/favicons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/Public/favicons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/favicons/apple-touch-icon.png
--------------------------------------------------------------------------------
/Public/favicons/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/Public/favicons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/favicons/favicon-16x16.png
--------------------------------------------------------------------------------
/Public/favicons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/favicons/favicon-32x32.png
--------------------------------------------------------------------------------
/Public/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/favicons/favicon.ico
--------------------------------------------------------------------------------
/Public/favicons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/favicons/mstile-150x150.png
--------------------------------------------------------------------------------
/Public/favicons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
39 |
--------------------------------------------------------------------------------
/Public/favicons/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/favicons/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/favicons/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/Public/images/C.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/C.png
--------------------------------------------------------------------------------
/Public/images/C@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/C@2x.png
--------------------------------------------------------------------------------
/Public/images/C@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/C@3x.png
--------------------------------------------------------------------------------
/Public/images/E.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/E.png
--------------------------------------------------------------------------------
/Public/images/E@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/E@2x.png
--------------------------------------------------------------------------------
/Public/images/E@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/E@3x.png
--------------------------------------------------------------------------------
/Public/images/F.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/F.png
--------------------------------------------------------------------------------
/Public/images/F@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/F@2x.png
--------------------------------------------------------------------------------
/Public/images/F@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/F@3x.png
--------------------------------------------------------------------------------
/Public/images/I.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/I.png
--------------------------------------------------------------------------------
/Public/images/I@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/I@2x.png
--------------------------------------------------------------------------------
/Public/images/I@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/I@3x.png
--------------------------------------------------------------------------------
/Public/images/M.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/M.png
--------------------------------------------------------------------------------
/Public/images/M@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/M@2x.png
--------------------------------------------------------------------------------
/Public/images/M@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/M@3x.png
--------------------------------------------------------------------------------
/Public/images/P.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/P.png
--------------------------------------------------------------------------------
/Public/images/P@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/P@2x.png
--------------------------------------------------------------------------------
/Public/images/P@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/P@3x.png
--------------------------------------------------------------------------------
/Public/images/S.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/S.png
--------------------------------------------------------------------------------
/Public/images/S@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/S@2x.png
--------------------------------------------------------------------------------
/Public/images/S@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/S@3x.png
--------------------------------------------------------------------------------
/Public/images/icon_swift-ast-explorer.com.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Public/images/icon_swift-format.com.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Public/images/icon_swiftfiddle.com.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Public/images/icon_swiftregex.com.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Public/images/lsp.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Public/images/lsp_fill.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Public/images/ogp_large.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/ogp_large.jpeg
--------------------------------------------------------------------------------
/Public/images/ogp_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/images/ogp_small.png
--------------------------------------------------------------------------------
/Public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
14 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
42 |
43 | SwiftFiddle - Swift Online Playground
44 |
45 |
46 |
47 |
48 |
49 |
50 |
56 |
57 |
58 |
64 |
65 |
77 |
78 |
79 |
85 |
89 |
95 |
100 |
105 |
107 |
108 |
112 |
113 |
118 |
119 |
120 |
125 |
126 |
127 |
128 |
129 |
130 |
133 |
136 |
137 |
138 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
179 |
180 |
182 |
192 |
193 |
194 |
195 |
196 |
197 |
201 |
202 |
206 |
216 |
217 |
218 |
219 |
233 |
234 |
235 |
236 |
267 |
268 |
269 |
363 |
364 |
365 |
367 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
380 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
401 |
402 |
403 |
404 |
--------------------------------------------------------------------------------
/Public/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import "./scss/default.scss";
4 | import "./css/common.css";
5 |
6 | import "./js/icon.js";
7 |
8 | import { MainView } from "./js/main_view.js";
9 | new MainView(window.appConfig);
10 |
--------------------------------------------------------------------------------
/Public/js/app.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import Worker from "worker-loader!./worker.js";
4 |
5 | import { Tooltip } from "bootstrap";
6 | import { LanguageServer } from "./language_server.js";
7 | import { Runner } from "./runner.js";
8 | import { uuidv4 } from "./uuid.js";
9 | import {
10 | runButton,
11 | stopButton,
12 | clearConsoleButton,
13 | formatButton,
14 | shareButton,
15 | } from "./ui_control.js";
16 |
17 | export class App {
18 | constructor(editor, terminal, versionPicker) {
19 | this.editor = editor;
20 | this.terminal = terminal;
21 | this.versionPicker = versionPicker;
22 |
23 | if (window.Worker) {
24 | const debounce = (() => {
25 | const timers = {};
26 | return function (callback, delay, id) {
27 | delay = delay || 400;
28 | id = id || "duplicated event";
29 | if (timers[id]) {
30 | clearTimeout(timers[id]);
31 | }
32 | timers[id] = setTimeout(callback, delay);
33 | };
34 | })();
35 | this.worker = new Worker();
36 | this.worker.onmessage = (e) => {
37 | if (e.data && e.data.type === "encode") {
38 | debounce(
39 | () => {
40 | history.replaceState(null, "", e.data.value);
41 | },
42 | 400,
43 | "update_location"
44 | );
45 | }
46 | };
47 | }
48 |
49 | this.history = [];
50 |
51 | const promises = [];
52 | let sequence = 0;
53 |
54 | const languageServer = new LanguageServer(
55 | "wss://lsp.swift-playground.com/lang-server/api"
56 | );
57 |
58 | languageServer.onconnect = () => {
59 | languageServer.openDocument(this.editor.getValue());
60 | };
61 | languageServer.onclose = () => {
62 | this.updateLanguageServerStatus(false);
63 | };
64 |
65 | languageServer.onresponse = (response) => {
66 | const promise = promises[response.id];
67 | switch (response.method) {
68 | case "hover":
69 | if (!promise) {
70 | return;
71 | }
72 | if (response.value) {
73 | const range = {
74 | startLineNumber: response.position.line,
75 | startColumn: response.position.utf16index,
76 | endLineNumber: response.position.line,
77 | endColumn: response.position.utf16index,
78 | };
79 | promise.fulfill({
80 | range: range,
81 | contents: [{ value: response.value.contents.value }],
82 | });
83 | } else {
84 | promise.fulfill();
85 | }
86 | break;
87 | case "completion":
88 | if (!promise) {
89 | return;
90 | }
91 | if (response.value) {
92 | const completions = {
93 | suggestions: response.value.items.map((item) => {
94 | const textEdit = item.textEdit;
95 | const start = textEdit.range.start;
96 | const end = textEdit.range.end;
97 | const kind = languageServer.convertCompletionItemKind(
98 | item.kind
99 | );
100 | const range = {
101 | startLineNumber: start.line + 1,
102 | startColumn: start.character + 1,
103 | endLineNumber: end.line + 1,
104 | endColumn: end.character + 1,
105 | };
106 | return {
107 | label: item.label,
108 | kind: kind,
109 | detail: item.detail,
110 | filterText: item.filterText,
111 | insertText: textEdit.newText,
112 | insertTextRules: languageServer.insertTextRule(),
113 | range: range,
114 | };
115 | }),
116 | };
117 | promise.fulfill(completions);
118 | } else {
119 | promise.fulfill();
120 | }
121 | case "diagnostics":
122 | this.updateLanguageServerStatus(true);
123 | this.editor.clearMarkers();
124 |
125 | if (!response.value) {
126 | return;
127 | }
128 | const diagnostics = response.value.diagnostics;
129 | if (!diagnostics || !diagnostics.length) {
130 | return;
131 | }
132 |
133 | const markers = diagnostics.map((diagnostic) => {
134 | const start = diagnostic.range.start;
135 | const end = diagnostic.range.end;
136 | const startLineNumber = start.line + 1;
137 | const startColumn = start.character + 1;
138 | const endLineNumber = end.line + 1;
139 | const endColumn = start.character + 1;
140 |
141 | let severity = languageServer.convertDiagnosticSeverity(
142 | diagnostic.severity
143 | );
144 |
145 | return {
146 | startLineNumber: startLineNumber,
147 | startColumn: startColumn,
148 | endLineNumber: endLineNumber,
149 | endColumn: endColumn,
150 | message: diagnostic.message,
151 | severity: severity,
152 | source: diagnostic.source,
153 | };
154 | });
155 |
156 | this.editor.updateMarkers(markers);
157 | break;
158 | case "format":
159 | if (response.value) {
160 | this.editor.setValue(response.value);
161 | }
162 | default:
163 | break;
164 | }
165 | };
166 |
167 | this.editor.onaction = (action) => {
168 | switch (action) {
169 | case "run":
170 | this.run();
171 | break;
172 | case "share":
173 | const shareButton = document.getElementById("share-button");
174 | if (shareButton.classList.contains("disabled")) {
175 | return;
176 | }
177 | shareButton.click();
178 | break;
179 | default:
180 | break;
181 | }
182 | };
183 |
184 | window.addEventListener("unload", () => {
185 | languageServer.close();
186 | });
187 |
188 | this.editor.onchange = () => {
189 | if (!languageServer.isReady) {
190 | return;
191 | }
192 |
193 | const value = this.editor.getValue();
194 | languageServer.syncDocument(value);
195 |
196 | this.updateButtonState();
197 | this.saveEditState();
198 | };
199 | this.updateButtonState();
200 |
201 | this.editor.onhover = (position) => {
202 | if (!languageServer.isReady) {
203 | return;
204 | }
205 |
206 | sequence++;
207 | const row = position.lineNumber - 1;
208 | const column = position.column - 1;
209 | languageServer.requestHover(sequence, row, column);
210 |
211 | return new Promise((fulfill, reject) => {
212 | promises[sequence] = { fulfill: fulfill, reject: reject };
213 | });
214 | };
215 |
216 | this.editor.oncompletion = (position) => {
217 | if (!languageServer.isReady) {
218 | return;
219 | }
220 |
221 | sequence++;
222 | const row = position.lineNumber - 1;
223 | const column = position.column - 1;
224 | languageServer.requestCompletion(sequence, row, column);
225 |
226 | const promise = new Promise((fulfill, reject) => {
227 | promises[sequence] = { fulfill: fulfill, reject: reject };
228 | });
229 | return promise;
230 | };
231 |
232 | this.editor.focus();
233 | this.editor.scrollToBottm();
234 |
235 | if (formatButton) {
236 | formatButton.addEventListener("click", (event) => {
237 | event.preventDefault();
238 |
239 | if (!languageServer.isReady) {
240 | return;
241 | }
242 | languageServer.requestFormat(this.editor.getValue());
243 | });
244 | }
245 |
246 | runButton.addEventListener("click", (event) => {
247 | event.preventDefault();
248 | this.run();
249 | });
250 | if (stopButton) {
251 | stopButton.addEventListener("click", (event) => {
252 | event.preventDefault();
253 | this.run();
254 | });
255 | }
256 |
257 | if (clearConsoleButton) {
258 | clearConsoleButton.addEventListener("click", (event) => {
259 | event.preventDefault();
260 | this.terminal.clear();
261 | this.history.length = 0;
262 | });
263 | }
264 |
265 | const editorContainer = document.getElementById("editor-container");
266 | editorContainer.addEventListener(
267 | "dragover",
268 | this.handleDragOver.bind(this),
269 | false
270 | );
271 | editorContainer.addEventListener(
272 | "drop",
273 | this.handleFileSelect.bind(this),
274 | false
275 | );
276 |
277 | this.versionPicker.onchange = () => {
278 | this.saveEditState();
279 | };
280 | }
281 |
282 | handleDragOver(event) {
283 | event.stopPropagation();
284 | event.preventDefault();
285 | event.dataTransfer.dropEffect = "copy";
286 | }
287 |
288 | handleFileSelect(event) {
289 | event.stopPropagation();
290 | event.preventDefault();
291 |
292 | const files = event.dataTransfer.files;
293 | const reader = new FileReader();
294 | reader.onload = (event) => {
295 | this.editor.setValue(event.target.result);
296 | this.editor.setSelection(0, 0, 0, 0);
297 | };
298 | reader.readAsText(files[0], "UTF-8");
299 | }
300 |
301 | async run() {
302 | if (runButton.classList.contains("disabled")) {
303 | return;
304 | }
305 |
306 | runButton.classList.add("disabled");
307 | if (stopButton) {
308 | stopButton.classList.remove("disabled");
309 | }
310 |
311 | document.getElementById("run-button-icon").classList.add("d-none");
312 | document.getElementById("run-button-spinner").classList.remove("d-none");
313 |
314 | this.editor.clearMarkers();
315 |
316 | const params = {
317 | toolchain_version: this.versionPicker.selected,
318 | code: this.editor.getValue(),
319 | _color: true,
320 | _nonce: uuidv4(),
321 | };
322 | if (window.appConfig.timeout) {
323 | const integer = Number.parseInt(window.appConfig.timeout, 10);
324 | if (integer && integer >= 30 && integer <= 600) {
325 | params.timeout = Math.max(30, Math.min(600, integer));
326 | }
327 | }
328 | if (window.appConfig.compilerOptions) {
329 | params.options = window.appConfig.compilerOptions;
330 | }
331 |
332 | const runner = new Runner(this.terminal);
333 |
334 | let stopRunner;
335 | if (stopButton) {
336 | stopRunner = () => {
337 | runner.stop();
338 | stopButton.removeEventListener("click", stopRunner);
339 | };
340 | stopButton.addEventListener("click", stopRunner);
341 | }
342 |
343 | const markers = await runner.run(params);
344 |
345 | runButton.classList.remove("disabled");
346 | if (stopButton) {
347 | stopButton.classList.add("disabled");
348 | }
349 |
350 | document.getElementById("run-button-icon").classList.remove("d-none");
351 | document.getElementById("run-button-spinner").classList.add("d-none");
352 |
353 | this.editor.updateMarkers(markers);
354 | this.editor.focus();
355 |
356 | if (stopButton) {
357 | stopButton.removeEventListener("click", stopRunner);
358 | }
359 | }
360 |
361 | saveEditState() {
362 | if (!this.worker) {
363 | return;
364 | }
365 | const code = this.editor.getValue();
366 | const version = this.versionPicker.selected;
367 | if (!code || !version) {
368 | return;
369 | }
370 | this.worker.postMessage({
371 | type: "encode",
372 | value: {
373 | code: code,
374 | version: version,
375 | },
376 | });
377 | }
378 |
379 | updateButtonState() {
380 | const value = this.editor.getValue();
381 | if (!value || !value.trim()) {
382 | runButton.classList.add("disabled");
383 | if (shareButton) {
384 | shareButton.classList.add("disabled");
385 | }
386 | } else {
387 | runButton.classList.remove("disabled");
388 | if (shareButton) {
389 | shareButton.classList.remove("disabled");
390 | }
391 | }
392 | }
393 |
394 | updateLanguageServerStatus(enabled) {
395 | const statusIcon = document.getElementById("lang-server-status-icon");
396 | const statusContainer = document.getElementById("lang-server-status");
397 | if (statusIcon) {
398 | if (enabled) {
399 | statusIcon.src = "/images/lsp_fill.svg";
400 | if (statusContainer) {
401 | statusContainer.setAttribute(
402 | "data-bs-original-title",
403 | "Language Server Status:
Ready
"
404 | );
405 | const tooltip = Tooltip.getInstance(statusContainer);
406 | tooltip.hide();
407 | }
408 | } else {
409 | statusIcon.src = "/images/lsp.svg";
410 | if (statusContainer) {
411 | statusContainer.setAttribute(
412 | "data-bs-original-title",
413 | "Language Server Status:
Initializing...
"
414 | );
415 | const tooltip = Tooltip.getInstance(statusContainer);
416 | tooltip.hide();
417 | }
418 | }
419 | }
420 | }
421 | }
422 |
--------------------------------------------------------------------------------
/Public/js/console.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import "xterm/css/xterm.css";
4 | import { Terminal } from "xterm";
5 | import { FitAddon } from "xterm-addon-fit";
6 | import { WebLinksAddon } from "xterm-addon-web-links";
7 |
8 | const ESC = "\u001B[";
9 |
10 | export class Console {
11 | constructor(container) {
12 | this.terminal = new Terminal({
13 | theme: {
14 | // https://github.com/sonph/onehalf
15 | background: "#282C34",
16 | black: "#282C34",
17 | blue: "#61AFEF",
18 | brightBlack: "#282C34",
19 | brightBlue: "#61AFEF",
20 | brightCyan: "#56B6C2",
21 | brightGreen: "#98C379",
22 | brightPurple: "#C678DD",
23 | brightRed: "#E06C75",
24 | brightWhite: "#DCDFE4",
25 | brightYellow: "#E5C07B",
26 | cyan: "#56B6C2",
27 | foreground: "#969AA0", // Modified
28 | green: "#98C379",
29 | name: "One Half Dark",
30 | purple: "#C678DD",
31 | red: "#E06C75",
32 | white: "#DCDFE4",
33 | yellow: "#E5C07B",
34 | cursorAccent: "#282C34",
35 | },
36 | fontFamily:
37 | "Menlo, Consolas, 'DejaVu Sans Mono', 'Ubuntu Mono', monospace",
38 | fontSize: 15,
39 | lineHeight: 1.1,
40 | convertEol: true,
41 | cursorStyle: "block",
42 | cursorBlink: true,
43 | scrollback: 100000,
44 | });
45 | this.terminal.open(container);
46 |
47 | this.terminal.loadAddon(new WebLinksAddon());
48 |
49 | const fitAddon = new FitAddon();
50 | this.terminal.loadAddon(fitAddon);
51 | fitAddon.fit();
52 |
53 | this.terminal.writeln(`${ESC}37mWelcome to SwiftFiddle.${ESC}0m`);
54 | this.terminal.writeln(
55 | `${ESC}32mEmpower our project through your generous support on GitHub Sponsors! 💖${ESC}0m`
56 | );
57 | this.terminal.writeln(
58 | `${ESC}32mhttps://github.com/sponsors/kishikawakatsumi/${ESC}0m`
59 | );
60 | }
61 |
62 | get rows() {
63 | return this.terminal.rows;
64 | }
65 |
66 | get cols() {
67 | return this.terminal.cols;
68 | }
69 |
70 | moveCursorTo(x, y) {
71 | if (typeof x !== "number") {
72 | throw new TypeError("The `x` argument is required");
73 | }
74 | if (typeof y !== "number") {
75 | this.terminal.write(`${ESC}${x + 1}G`);
76 | }
77 | this.terminal.write(`${ESC}${y + 1};${x + 1}H`);
78 | }
79 |
80 | cursorUp(count = 1) {
81 | this.terminal.write(`${ESC}${count}A`);
82 | }
83 |
84 | cursorDown(count = 1) {
85 | this.terminal.write(`${ESC}${count}B`);
86 | }
87 |
88 | cursorForward(count = 1) {
89 | this.terminal.write(`${ESC}${count}C`);
90 | }
91 |
92 | cursorBackward(count = 1) {
93 | this.terminal.write(`${ESC}${count}D`);
94 | }
95 |
96 | saveCursorPosition() {
97 | this.terminal.write(`${ESC}s`);
98 | }
99 |
100 | restoreCursorPosition() {
101 | this.terminal.write(`${ESC}u`);
102 | }
103 |
104 | hideCursor() {
105 | this.terminal.write(`${ESC}?25l`);
106 | }
107 |
108 | showCursor() {
109 | this.terminal.write(`${ESC}?25h`);
110 | }
111 |
112 | eraseLine() {
113 | this.terminal.write(`${ESC}2K\r`);
114 | }
115 |
116 | eraseLines(count) {
117 | for (let i = 0; i < count; i++) {
118 | this.terminal.write(`${ESC}1F`);
119 | this.terminal.write(`${ESC}2K\r`);
120 | }
121 | }
122 |
123 | switchNormalBuffer() {
124 | this.terminal.write("\x9B?47l");
125 | }
126 |
127 | switchAlternateBuffer() {
128 | this.terminal.write("\x9B?47h");
129 | }
130 |
131 | showSpinner(message) {
132 | const self = this;
133 | const startTime = performance.now();
134 | const interval = 200;
135 | const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
136 | let spins = 0;
137 | function updateSpinner(message) {
138 | const progressText = `${SPINNER[spins % SPINNER.length]} ${message}`;
139 | const dotCount = Math.floor((spins * 2) / 4) % 4;
140 | const animationText = `${progressText} ${".".repeat(dotCount)}`;
141 | const endTime = performance.now();
142 | const seconds = `${((endTime - startTime) / 1000).toFixed(0)}s`;
143 | const speces = " ".repeat(
144 | self.terminal.cols - animationText.length - seconds.length
145 | );
146 | self.terminal.write(
147 | `${ESC}1m${ESC}34m${animationText}${ESC}0m${speces}${seconds}`
148 | );
149 | spins++;
150 | }
151 |
152 | updateSpinner(message);
153 | return setInterval(() => {
154 | this.eraseLine();
155 | updateSpinner(message);
156 | }, interval);
157 | }
158 |
159 | hideSpinner(cancelToken) {
160 | clearInterval(cancelToken);
161 | this.eraseLine();
162 | }
163 |
164 | write(text) {
165 | this.terminal.write(text);
166 | }
167 |
168 | writeln(text) {
169 | this.terminal.writeln(text);
170 | }
171 |
172 | clear() {
173 | this.terminal.clear();
174 | }
175 |
176 | reset() {
177 | this.terminal.reset();
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/Public/js/decoder.js:
--------------------------------------------------------------------------------
1 | import { ungzip } from "pako";
2 |
3 | export class Decoder {
4 | static decode(string) {
5 | const base64 = decodeURIComponent(string);
6 | const gziped = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
7 | const json = ungzip(gziped, { to: "string" });
8 | return JSON.parse(json);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Public/js/editor.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
4 |
5 | export class Editor {
6 | constructor(container, options) {
7 | this.editor = monaco.editor.create(container, options);
8 |
9 | this.editor.onDidChangeModelContent(() => {
10 | this.onchange();
11 | });
12 |
13 | monaco.languages.registerHoverProvider("swift", {
14 | provideHover: (model, position) => {
15 | return this.onhover(position);
16 | },
17 | });
18 |
19 | this.editor.addAction({
20 | id: "run",
21 | label: "Run",
22 | keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
23 | run: () => {
24 | this.onaction("run");
25 | },
26 | });
27 | this.editor.addAction({
28 | id: "share",
29 | label: "Share",
30 | keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
31 | run: () => {
32 | this.onaction("share");
33 | },
34 | });
35 |
36 | monaco.languages.registerCompletionItemProvider("swift", {
37 | triggerCharacters: ["."],
38 | provideCompletionItems: (model, position) => {
39 | return this.oncompletion(position);
40 | },
41 | });
42 |
43 | this.onchange = () => {};
44 | this.onhover = () => {};
45 | this.oncompletion = () => {};
46 | this.onaction = () => {};
47 | }
48 |
49 | getValue() {
50 | return this.editor.getValue();
51 | }
52 |
53 | setValue(value) {
54 | this.editor.setValue(value);
55 | }
56 |
57 | setSelection(startLineNumber, startColumn, endLineNumber, endColumn) {
58 | this.editor.setSelection(
59 | new monaco.Selection(
60 | startLineNumber,
61 | startColumn,
62 | endLineNumber,
63 | endColumn
64 | )
65 | );
66 | }
67 |
68 | focus() {
69 | this.editor.focus();
70 | }
71 |
72 | scrollToBottm() {
73 | const model = this.editor.getModel();
74 | const lineCount = model.getLineCount();
75 | this.editor.setPosition({
76 | column: model.getLineLength(lineCount) + 1,
77 | lineNumber: lineCount,
78 | });
79 |
80 | this.editor.revealLine(lineCount);
81 | }
82 |
83 | updateMarkers(markers) {
84 | this.clearMarkers();
85 | monaco.editor.setModelMarkers(this.editor.getModel(), "swift", markers);
86 | }
87 |
88 | clearMarkers() {
89 | monaco.editor.setModelMarkers(this.editor.getModel(), "swift", []);
90 | }
91 |
92 | fold(foldingRanges) {
93 | monaco.languages.registerFoldingRangeProvider("swift", {
94 | provideFoldingRanges: function (model, context, token) {
95 | return foldingRanges;
96 | },
97 | });
98 | this.editor.trigger("fold", "editor.foldAll");
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Public/js/embed_view.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import { Editor } from "./editor.js";
4 | import { Console } from "./console.js";
5 | import { VersionPicker } from "./version_picker.js";
6 | import { App } from "./app.js";
7 | import { runButton } from "./ui_control.js";
8 | import { unescapeHTML } from "./unescape.js";
9 |
10 | export class EmbedView {
11 | constructor(config) {
12 | this.editor = new Editor(document.getElementById("editor-container"), {
13 | value: unescapeHTML(config.initialText),
14 | fontSize: "14pt",
15 | lineHeight: 21,
16 | language: "swift",
17 | wordWrap: "on",
18 | wrappingIndent: "indent",
19 | tabSize: 2,
20 | lightbulb: {
21 | enabled: false,
22 | },
23 | minimap: {
24 | enabled: false,
25 | },
26 | theme: "vs-light",
27 | showFoldingControls: "mouseover",
28 |
29 | readOnly: true,
30 | renderIndentGuides: false,
31 | glyphMargin: false,
32 | lineNumbersMinChars: 4,
33 | lineDecorationsWidth: 6,
34 | });
35 | if (config.foldingRanges && config.foldingRanges.length) {
36 | this.editor.fold(config.foldingRanges);
37 | }
38 | this.console = new Console(document.getElementById("terminal-container"));
39 | this.versionPicker = new VersionPicker();
40 | this.app = new App(this.editor, this.console, this.versionPicker);
41 |
42 | this.init();
43 | }
44 |
45 | init() {
46 | runButton.classList.remove("disabled");
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Public/js/encoder.js:
--------------------------------------------------------------------------------
1 | import { gzip } from "pako";
2 |
3 | export class Encoder {
4 | static encode(data) {
5 | const json = JSON.stringify(data);
6 | const gziped = gzip(json);
7 | const base64 = btoa(String.fromCharCode(...gziped));
8 | return encodeURIComponent(base64);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Public/js/icon.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import { config, library, dom } from "@fortawesome/fontawesome-svg-core";
4 | import {
5 | faCodeBranch,
6 | faPlay,
7 | faStop,
8 | faEraser,
9 | faAlignLeft,
10 | faShareNodes,
11 | faCog,
12 | faQuestion,
13 | faCheckCircle as faCheckCircleSolid,
14 | faExclamationTriangle,
15 | faHeart,
16 | } from "@fortawesome/pro-solid-svg-icons";
17 | import {
18 | faCheck,
19 | faClipboard,
20 | faFileImport,
21 | faKeyboard,
22 | faToolbox,
23 | faMessageSmile,
24 | faCheckCircle,
25 | faAt,
26 | faDonate,
27 | } from "@fortawesome/pro-regular-svg-icons";
28 | import { faMonitorHeartRate } from "@fortawesome/pro-light-svg-icons";
29 | import { faSpinnerThird, faRobot } from "@fortawesome/pro-duotone-svg-icons";
30 | import {
31 | faSwift,
32 | faGithub,
33 | faTwitter,
34 | faFacebookSquare,
35 | } from "@fortawesome/free-brands-svg-icons";
36 |
37 | config.searchPseudoElements = true;
38 | library.add(
39 | faCodeBranch,
40 | faPlay,
41 | faStop,
42 | faEraser,
43 | faAlignLeft,
44 | faShareNodes,
45 | faCog,
46 | faQuestion,
47 | faCheckCircleSolid,
48 | faExclamationTriangle,
49 | faHeart,
50 |
51 | faCheck,
52 | faClipboard,
53 | faFileImport,
54 | faKeyboard,
55 | faToolbox,
56 | faMessageSmile,
57 | faCheckCircle,
58 | faAt,
59 | faDonate,
60 |
61 | faMonitorHeartRate,
62 |
63 | faSpinnerThird,
64 | faRobot,
65 |
66 | faSwift,
67 | faGithub,
68 | faTwitter,
69 | faFacebookSquare
70 | );
71 | dom.watch();
72 |
--------------------------------------------------------------------------------
/Public/js/icon_embed.js:
--------------------------------------------------------------------------------
1 | import { library, dom } from "@fortawesome/fontawesome-svg-core";
2 | import {
3 | faCodeBranch,
4 | faPlay,
5 | faExternalLinkAlt,
6 | } from "@fortawesome/pro-solid-svg-icons";
7 | import { faSpinnerThird } from "@fortawesome/pro-duotone-svg-icons";
8 |
9 | library.add(faCodeBranch, faPlay, faExternalLinkAlt, faSpinnerThird);
10 | dom.watch();
11 |
--------------------------------------------------------------------------------
/Public/js/language_server.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
4 | import ReconnectingWebSocket from "reconnecting-websocket";
5 | import { uuidv4 } from "./uuid.js";
6 |
7 | export class LanguageServer {
8 | constructor(endpoint) {
9 | this.connection = this.createConnection(endpoint);
10 |
11 | this.onconnect = () => {};
12 | this.onresponse = () => {};
13 | this.onerror = () => {};
14 | this.onclose = () => {};
15 | }
16 |
17 | get isReady() {
18 | return this.connection.readyState === 1;
19 | }
20 |
21 | openDocument(code) {
22 | const params = {
23 | method: "didOpen",
24 | code: code || "",
25 | sessionId: this.sessionId,
26 | };
27 | this.connection.send(JSON.stringify(params));
28 | }
29 |
30 | syncDocument(code) {
31 | const params = {
32 | method: "didChange",
33 | code: code,
34 | sessionId: this.sessionId,
35 | };
36 | this.connection.send(JSON.stringify(params));
37 | }
38 |
39 | requestHover(sequence, row, column) {
40 | const params = {
41 | method: "hover",
42 | id: sequence,
43 | row: row,
44 | column: column,
45 | sessionId: this.sessionId,
46 | };
47 | this.connection.send(JSON.stringify(params));
48 | }
49 |
50 | requestCompletion(sequence, row, column) {
51 | const params = {
52 | method: "completion",
53 | id: sequence,
54 | row: row,
55 | column: column,
56 | sessionId: this.sessionId,
57 | };
58 | this.connection.send(JSON.stringify(params));
59 | }
60 |
61 | requestFormat(code) {
62 | const params = {
63 | method: "format",
64 | code: code,
65 | };
66 | this.connection.send(JSON.stringify(params));
67 | }
68 |
69 | convertCompletionItemKind(kind) {
70 | switch (kind) {
71 | case 1:
72 | return monaco.languages.CompletionItemKind.Text;
73 | case 2:
74 | return monaco.languages.CompletionItemKind.Method;
75 | case 3:
76 | return monaco.languages.CompletionItemKind.Function;
77 | case 4:
78 | return monaco.languages.CompletionItemKind.Constructor;
79 | case 5:
80 | return monaco.languages.CompletionItemKind.Field;
81 | case 6:
82 | return monaco.languages.CompletionItemKind.Variable;
83 | case 7:
84 | return monaco.languages.CompletionItemKind.Class;
85 | case 8:
86 | return monaco.languages.CompletionItemKind.Interface;
87 | case 9:
88 | return monaco.languages.CompletionItemKind.Module;
89 | case 10:
90 | return monaco.languages.CompletionItemKind.Property;
91 | case 11:
92 | return monaco.languages.CompletionItemKind.Unit;
93 | case 12:
94 | return monaco.languages.CompletionItemKind.Value;
95 | case 13:
96 | return monaco.languages.CompletionItemKind.Enum;
97 | case 14:
98 | return monaco.languages.CompletionItemKind.Keyword;
99 | case 15:
100 | return monaco.languages.CompletionItemKind.Snippet;
101 | case 16:
102 | return monaco.languages.CompletionItemKind.Color;
103 | case 17:
104 | return monaco.languages.CompletionItemKind.File;
105 | case 18:
106 | return monaco.languages.CompletionItemKind.Reference;
107 | case 19:
108 | return monaco.languages.CompletionItemKind.Folder;
109 | case 20:
110 | return monaco.languages.CompletionItemKind.EnumMember;
111 | case 21:
112 | return monaco.languages.CompletionItemKind.Constant;
113 | case 22:
114 | return monaco.languages.CompletionItemKind.Struct;
115 | case 23:
116 | return monaco.languages.CompletionItemKind.Event;
117 | case 24:
118 | return monaco.languages.CompletionItemKind.Operator;
119 | case 25:
120 | return monaco.languages.CompletionItemKind.TypeParameter;
121 | default:
122 | return kind;
123 | }
124 | }
125 |
126 | insertTextRule() {
127 | return monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet;
128 | }
129 |
130 | convertDiagnosticSeverity(severity) {
131 | switch (severity) {
132 | case 1:
133 | return monaco.MarkerSeverity.Error;
134 | case 2:
135 | return monaco.MarkerSeverity.Warning;
136 | case 3:
137 | return monaco.MarkerSeverity.Info;
138 | case 4:
139 | return monaco.MarkerSeverity.Hint;
140 | default:
141 | return severity;
142 | }
143 | }
144 |
145 | createConnection(endpoint) {
146 | if (
147 | this.connection &&
148 | (this.connection.readyState === 0 || this.connection.readyState === 1)
149 | ) {
150 | return this.connection;
151 | }
152 |
153 | this.sessionId = uuidv4();
154 | const connection = new ReconnectingWebSocket(endpoint, [], {
155 | maxReconnectionDelay: 10000,
156 | minReconnectionDelay: 1000,
157 | reconnectionDelayGrowFactor: 1.3,
158 | connectionTimeout: 10000,
159 | maxRetries: Infinity,
160 | debug: false,
161 | });
162 |
163 | connection.onopen = () => {
164 | this.onconnect();
165 | };
166 |
167 | connection.onerror = (event) => {
168 | this.onerror(event);
169 | connection.close();
170 | };
171 |
172 | connection.onclose = (event) => {
173 | this.onclose(event);
174 | };
175 |
176 | connection.onmessage = (event) => {
177 | const response = JSON.parse(event.data);
178 | this.onresponse(response);
179 | };
180 | return connection;
181 | }
182 |
183 | close() {
184 | const params = {
185 | method: "didClose",
186 | sessionId: this.sessionId,
187 | };
188 | this.connection.send(JSON.stringify(params));
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/Public/js/main_view.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import "../css/version_picker.css";
4 |
5 | import Worker from "worker-loader!./worker.js";
6 |
7 | import { Tooltip } from "bootstrap";
8 | import { Editor } from "./editor.js";
9 | import { Console } from "./console.js";
10 | import { VersionPicker } from "./version_picker.js";
11 | import { ShareSheet } from "./share_sheet.js";
12 | import { App } from "./app.js";
13 | import {
14 | clearConsoleButton,
15 | formatButton,
16 | runButton,
17 | shareButton,
18 | } from "./ui_control.js";
19 | import { unescapeHTML } from "./unescape.js";
20 |
21 | export class MainView {
22 | constructor(config) {
23 | this.editor = new Editor(document.getElementById("editor-container"), {
24 | value: unescapeHTML(config.initialText),
25 | fontSize: "14pt",
26 | lineHeight: 21,
27 | language: "swift",
28 | wordWrap: "on",
29 | wrappingIndent: "indent",
30 | tabSize: 2,
31 | lightbulb: {
32 | enabled: true,
33 | },
34 | minimap: {
35 | enabled: false,
36 | },
37 | theme: "vs-light",
38 | showFoldingControls: "mouseover",
39 | });
40 | this.console = new Console(document.getElementById("terminal-container"));
41 | this.versionPicker = new VersionPicker();
42 | this.shareSheet = new ShareSheet(this.editor, this.versionPicker);
43 | this.app = new App(this.editor, this.console, this.versionPicker);
44 |
45 | if (window.location.search && window.Worker) {
46 | this.worker = new Worker();
47 | this.worker.onmessage = (e) => {
48 | if (e.data && e.data.type === "decode") {
49 | this.editor.setValue(e.data.value.code);
50 | this.versionPicker.selected = e.data.value.version;
51 | }
52 | };
53 | this.worker.postMessage({
54 | type: "decode",
55 | value: window.location.search,
56 | });
57 | }
58 |
59 | this.init();
60 | }
61 |
62 | init() {
63 | [].slice
64 | .call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
65 | .map((trigger) => {
66 | return new Tooltip(trigger);
67 | });
68 |
69 | runButton.classList.remove("disabled");
70 | clearConsoleButton.classList.remove("disabled");
71 | formatButton.classList.remove("disabled");
72 | shareButton.classList.remove("disabled");
73 |
74 | const settingsModal = document.getElementById("settings-modal");
75 | settingsModal.addEventListener("show.bs.modal", function (event) {
76 | const input = document.getElementById("settings-timeout");
77 | const timeout = window.appConfig.timeout;
78 | if (!timeout) {
79 | input.value = "";
80 | } else if (timeout && Number.isInteger(timeout)) {
81 | input.value = timeout;
82 | } else {
83 | input.value = "60";
84 | }
85 |
86 | const compilerOptions = window.appConfig.compilerOptions;
87 | document.getElementById("settings-compiler-options").value =
88 | compilerOptions;
89 | });
90 |
91 | const settingsSaveButton = document.getElementById("settings-save-button");
92 | settingsSaveButton.addEventListener("click", function (event) {
93 | const timeout = document.getElementById("settings-timeout").value;
94 | if (timeout && /^\+?(0|[1-9]\d*)$/.test(timeout)) {
95 | const integer = Number.parseInt(timeout, 10);
96 | if (integer && integer >= 30 && integer <= 600) {
97 | window.appConfig.timeout = integer;
98 | }
99 | }
100 | const compilerOptions = document.getElementById(
101 | "settings-compiler-options"
102 | ).value;
103 | window.appConfig.compilerOptions = compilerOptions;
104 | });
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/Public/js/runner.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import { TextLineStream } from "./textlinesteam.js";
4 |
5 | export class Runner {
6 | constructor(terminal) {
7 | this.abortController = new AbortController();
8 | this.terminal = terminal;
9 | this.onmessage = () => {};
10 | }
11 |
12 | async run(params) {
13 | const cancelToken = this.terminal.showSpinner("Running");
14 |
15 | this.terminal.hideCursor();
16 |
17 | try {
18 | const version = params.toolchain_version;
19 | const path = (() => {
20 | switch (version) {
21 | case "nightly-5.3":
22 | case "nightly-5.4":
23 | case "nightly-5.5":
24 | case "nightly-5.6":
25 | case "nightly-5.7":
26 | case "nightly-5.8":
27 | case "nightly-5.9":
28 | case "nightly-5.10":
29 | case "nightly-6.0":
30 | case "nightly-6.1":
31 | case "nightly-6.2":
32 | case "nightly-main": {
33 | const suffix = version.split(".").join("").split("-").join("");
34 | return `https://runner-functions-${suffix}.blackwater-cac8eec1.westus2.azurecontainerapps.io/runner/${version}/run`;
35 | }
36 | case "2.2":
37 | case "2.2.1":
38 | case "3.0":
39 | case "3.0.1":
40 | case "3.0.2":
41 | case "3.1":
42 | case "3.1.1":
43 | case "4.0":
44 | case "4.0.2":
45 | case "4.0.3":
46 | case "4.1":
47 | case "4.1.1":
48 | case "4.1.2":
49 | case "4.1.3":
50 | case "4.2":
51 | case "4.2.1":
52 | case "4.2.2":
53 | case "4.2.3":
54 | case "4.2.4":
55 | case "5.0":
56 | case "5.0.1":
57 | case "5.0.2":
58 | case "5.0.3":
59 | case "5.1":
60 | case "5.1.1":
61 | case "5.1.2":
62 | case "5.1.3":
63 | case "5.1.4":
64 | case "5.1.5":
65 | case "5.2":
66 | case "5.2.1":
67 | case "5.2.2":
68 | case "5.2.3":
69 | case "5.2.4":
70 | case "5.2.5":
71 | case "5.3":
72 | case "5.3.1":
73 | case "5.3.2":
74 | case "5.3.3":
75 | case "5.4":
76 | case "5.4.1":
77 | case "5.4.2":
78 | case "5.4.3":
79 | case "5.5":
80 | case "5.5.1":
81 | case "5.5.2":
82 | case "5.5.3":
83 | case "5.6":
84 | case "5.6.1":
85 | case "5.6.2":
86 | case "5.6.3":
87 | case "5.7":
88 | case "5.7.1":
89 | case "5.7.2":
90 | case "5.7.3":
91 | case "5.8":
92 | case "5.8.1":
93 | case "5.9":
94 | case "5.9.1":
95 | case "5.9.2":
96 | case "5.10":
97 | case "6.0":
98 | case "6.0.1":
99 | case "6.0.2":
100 | case "6.0.3":
101 | case "6.1":
102 | case "6.1.1": {
103 | const suffix = version.split(".").join("");
104 | return `https://swiftfiddle-runner-functions-${suffix}.blackwater-cac8eec1.westus2.azurecontainerapps.io/runner/${version}/run`;
105 | }
106 | case "5.10.1": {
107 | const suffix = version.split(".").join("");
108 | return `https://swiftfiddle-runner-functions${suffix}.blackwater-cac8eec1.westus2.azurecontainerapps.io/runner/${version}/run`;
109 | }
110 | case "6.1.2": {
111 | return `https://runner.swift-playground.com/runner/${version}/run`;
112 | }
113 |
114 | default:
115 | return `https://runner.swift-playground.com/runner/${version}/run`;
116 | }
117 | })();
118 |
119 | params._streaming = true;
120 | const response = await fetch(path, {
121 | method: "POST",
122 | headers: {
123 | Accept: "application/json",
124 | "Content-Type": "application/json",
125 | },
126 | body: JSON.stringify(params),
127 | signal: this.abortController.signal,
128 | });
129 |
130 | const reader = response.body
131 | .pipeThrough(new TextDecoderStream())
132 | .pipeThrough(new TextLineStream())
133 | .getReader();
134 | let result = await reader.read();
135 |
136 | this.terminal.hideSpinner(cancelToken);
137 | this.printTimestamp();
138 |
139 | if (!response.ok) {
140 | this.terminal.writeln(
141 | `\x1b[37m❌ ${response.status} ${response.statusText}\x1b[0m`
142 | );
143 | this.terminal.hideSpinner(cancelToken);
144 | }
145 |
146 | const markers = [];
147 | while (!result.done) {
148 | const value = result.value;
149 | if (value) {
150 | const response = JSON.parse(value);
151 | switch (response.kind) {
152 | case "stdout":
153 | response.text
154 | .split("\n")
155 | .filter(Boolean)
156 | .forEach((line) => {
157 | this.terminal.writeln(`\x1b[37m${line}\x1b[0m`);
158 | });
159 | break;
160 | case "stderr":
161 | response.text
162 | .split("\n")
163 | .filter(Boolean)
164 | .forEach((line) => {
165 | this.terminal.writeln(`\x1b[2m\x1b[37m${line}\x1b[0m`);
166 | });
167 | markers.push(...parseErrorMessage(response.text));
168 | break;
169 | case "version":
170 | response.text
171 | .split("\n")
172 | .filter(Boolean)
173 | .forEach((line) => {
174 | this.terminal.writeln(`\x1b[38;2;127;168;183m${line}\x1b[0m`); // #7FA8B7
175 | });
176 | break;
177 | default:
178 | break;
179 | }
180 | }
181 | result = await reader.read();
182 | }
183 |
184 | return markers;
185 | } catch (error) {
186 | this.terminal.hideSpinner(cancelToken);
187 | this.terminal.writeln(`\x1b[37m❌ ${error}\x1b[0m`);
188 | } finally {
189 | this.terminal.showCursor();
190 | }
191 | }
192 |
193 | stop() {
194 | this.abortController.abort();
195 | }
196 |
197 | printTimestamp() {
198 | const now = new Date();
199 | const timestamp = now.toLocaleString("en-US", {
200 | hour: "numeric",
201 | minute: "2-digit",
202 | second: "2-digit",
203 | hour12: false,
204 | });
205 | const padding = this.terminal.cols - timestamp.length;
206 | this.terminal.writeln(
207 | `\x1b[2m\x1b[38;5;15;48;5;238m${" ".repeat(padding)}${timestamp}\x1b[0m`
208 | );
209 | }
210 | }
211 |
212 | function parseErrorMessage(message) {
213 | const matches = message
214 | .replace(
215 | // Remove all ANSI colors/styles from strings
216 | // https://stackoverflow.com/a/29497680/1733883
217 | // https://github.com/chalk/ansi-regex/blob/main/index.js#L3
218 | /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
219 | ""
220 | )
221 | .matchAll(
222 | /:(\d+): (error|warning|note): ([\s\S]*?)\n*(?=(?:\/|$))/gi
223 | );
224 | return [...matches].map((match) => {
225 | const row = +match[1];
226 | let column = +match[2];
227 | const text = match[4];
228 | const type = match[3];
229 | let severity;
230 | switch (type) {
231 | case "warning":
232 | severity = 4; // monaco.MarkerSeverity.Warning;
233 | break;
234 | case "error":
235 | severity = 8; // monaco.MarkerSeverity.Error;
236 | break;
237 | default: // monaco.MarkerSeverity.Info;
238 | severity = 2;
239 | break;
240 | }
241 |
242 | let length;
243 | if (text.match(/~+\^~+/)) {
244 | // ~~~^~~~
245 | length = text.match(/~+\^~+/)[0].length;
246 | column -= text.match(/~+\^/)[0].length - 1;
247 | } else if (text.match(/\^~+/)) {
248 | // ^~~~
249 | length = text.match(/\^~+/)[0].length;
250 | } else if (text.match(/~+\^/)) {
251 | // ~~~^
252 | length = text.match(/~+\^/)[0].length;
253 | column -= length - 1;
254 | } else if (text.match(/\^/)) {
255 | // ^
256 | length = 1;
257 | }
258 |
259 | return {
260 | startLineNumber: row,
261 | startColumn: column,
262 | endLineNumber: row,
263 | endColumn: column + length,
264 | message: text,
265 | severity: severity,
266 | };
267 | });
268 | }
269 |
--------------------------------------------------------------------------------
/Public/js/share_sheet.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import "../css/share_sheet.css";
4 |
5 | import { Popover } from "bootstrap";
6 | import { Snackbar } from "./snackbar.js";
7 |
8 | import { shareButton } from "./ui_control.js";
9 |
10 | export class ShareSheet {
11 | constructor(editor, versionPicker) {
12 | this.editor = editor;
13 | this.versionPicker = versionPicker;
14 | this.init();
15 | }
16 |
17 | init() {
18 | const placeholderData =
19 | "0000000000000000000000000000000000000000000000000000000000000000000000000000";
20 |
21 | const popoverContent = document.getElementById("share-sheet");
22 | const popover = new Popover(shareButton, {
23 | title: "",
24 | trigger: "click",
25 | html: true,
26 | content: popoverContent,
27 | container: "body",
28 | });
29 |
30 | const link = document.getElementById("share-sheet-link-label");
31 | const linkField = document.getElementById("share-sheet-link-field");
32 | const linkCopyButton = document.getElementById(
33 | "share-sheet-link-copy-button"
34 | );
35 | const linkCopyButtonIcon = document.getElementById(
36 | "share-sheet-link-copy-button-icon"
37 | );
38 | const linkCopyButtonSpinner = document.getElementById(
39 | "share-sheet-link-copy-button-spinner"
40 | );
41 | const embed = document.getElementById("share-sheet-embed-label");
42 | const embedField = document.getElementById("share-sheet-embed-field");
43 | const embedCopyButton = document.getElementById(
44 | "share-sheet-embed-copy-button"
45 | );
46 | const embedCopyButtonIcon = document.getElementById(
47 | "share-sheet-embed-copy-button-icon"
48 | );
49 | const embedCopyButtonSpinner = document.getElementById(
50 | "share-sheet-embed-copy-button-spinner"
51 | );
52 |
53 | shareButton.addEventListener("show.bs.popover", () => {
54 | linkField.value = "";
55 | embedField.value = "";
56 |
57 | popoverContent.classList.remove("d-none");
58 | embed.dataset.value = placeholderData;
59 |
60 | linkCopyButton.classList.add("disabled");
61 | linkCopyButtonIcon.classList.add("d-none");
62 | linkCopyButtonSpinner.classList.remove("d-none");
63 |
64 | embedCopyButton.classList.add("disabled");
65 | embedCopyButtonIcon.classList.add("d-none");
66 | embedCopyButtonSpinner.classList.remove("d-none");
67 |
68 | const params = {
69 | toolchain_version: this.versionPicker.selected,
70 | code: this.editor.getValue(),
71 | };
72 | const method = "POST";
73 | const body = JSON.stringify(params);
74 | const headers = {
75 | Accept: "application/json",
76 | "Content-Type": "application/json",
77 | };
78 | fetch("/shared_link", { method, headers, body })
79 | .then((response) => {
80 | return response.json();
81 | })
82 | .then((response) => {
83 | const url = response.url;
84 |
85 | link.dataset.value = url;
86 | linkField.value = url;
87 | linkCopyButton.classList.remove("disabled");
88 |
89 | embed.dataset.value = placeholderData;
90 | embedField.value = ``;
93 | embedCopyButton.classList.remove("disabled");
94 |
95 | const shareTwitterButton = document.getElementById(
96 | "share-twitter-button"
97 | );
98 | shareTwitterButton.href = `https://twitter.com/intent/tweet?text=&url=${url}`;
99 |
100 | const shareFacebookButton = document.getElementById(
101 | "share-facebook-button"
102 | );
103 | shareFacebookButton.href = `https://www.facebook.com/sharer/sharer.php?u=${url}`;
104 | })
105 | .catch((error) => {
106 | if (error.response) {
107 | Snackbar.alert(error.response.statusText);
108 | } else {
109 | Snackbar.alert(error);
110 | }
111 | })
112 | .finally(() => {
113 | linkCopyButtonIcon.classList.remove("d-none");
114 | linkCopyButtonSpinner.classList.add("d-none");
115 |
116 | embedCopyButtonIcon.classList.remove("d-none");
117 | embedCopyButtonSpinner.classList.add("d-none");
118 | });
119 | });
120 |
121 | document.body.addEventListener("click", (event) => {
122 | if (event.target !== shareButton && !event.target.closest(".popover")) {
123 | popover.hide();
124 | }
125 | });
126 |
127 | linkCopyButton.addEventListener("click", (event) => {
128 | if (navigator.clipboard) {
129 | navigator.clipboard.writeText(linkField.value);
130 | Snackbar.info("Copied!");
131 | }
132 | });
133 |
134 | embedCopyButton.addEventListener("click", (event) => {
135 | if (navigator.clipboard) {
136 | navigator.clipboard.writeText(embedField.value);
137 | Snackbar.info("Copied!");
138 | }
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/Public/js/snackbar.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import { Toast } from "bootstrap";
4 |
5 | const infoContainerBlock = document.getElementById("snackbar-info-container");
6 | const infoBlock = document.getElementById("snackbar-info");
7 | const alertContainerBlock = document.getElementById("snackbar-alert-container");
8 | const alertBlock = document.getElementById("snackbar-alert");
9 |
10 | export class Snackbar {
11 | static info(message) {
12 | const messageContainer = document.getElementById("snackbar-info-message");
13 | messageContainer.textContent = message;
14 | infoContainerBlock.classList.remove("d-none");
15 | infoBlock.classList.remove("d-none");
16 | new Toast(infoBlock).show();
17 | }
18 |
19 | static alert(message) {
20 | const messageContainer = document.getElementById("snackbar-alert-message");
21 | messageContainer.textContent = message;
22 | alertContainerBlock.classList.remove("d-none");
23 | alertBlock.classList.remove("d-none");
24 | new Toast(alertBlock).show();
25 | }
26 | }
27 |
28 | if (infoBlock) {
29 | infoBlock.addEventListener("hidden.bs.toast", () => {
30 | infoContainerBlock.classList.add("d-none");
31 | infoBlock.classList.add("d-none");
32 | });
33 | infoBlock.addEventListener("hidden.bs.toast", () => {
34 | infoContainerBlock.classList.add("d-none");
35 | infoBlock.classList.add("d-none");
36 | });
37 | }
38 | if (alertBlock) {
39 | alertBlock.addEventListener("hidden.bs.toast", () => {
40 | alertContainerBlock.classList.add("d-none");
41 | alertBlock.classList.add("d-none");
42 | });
43 | alertBlock.addEventListener("hidden.bs.toast", () => {
44 | alertContainerBlock.classList.add("d-none");
45 | alertBlock.classList.add("d-none");
46 | });
47 | }
48 |
--------------------------------------------------------------------------------
/Public/js/textlinesteam.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // Vendored from Deno -
4 | // https://github.com/denoland/deno_std/blob/main/streams/delimiter.ts
5 | // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
6 |
7 | /** Transform a stream into a stream where each chunk is divided by a newline,
8 | * be it `\n` or `\r\n`. `\r` can be enabled via the `allowCR` option.
9 | *
10 | * ```ts
11 | * import { TextLineStream } from "./delimiter.ts";
12 | * const res = await fetch("https://example.com");
13 | * const lines = res.body!
14 | * .pipeThrough(new TextDecoderStream())
15 | * .pipeThrough(new TextLineStream());
16 | * ```
17 | */
18 | export class TextLineStream extends TransformStream {
19 | #buf = "";
20 | #allowCR = false;
21 | #returnEmptyLines = false;
22 | #mapperFun = (line) => line;
23 |
24 | constructor(options) {
25 | super({
26 | transform: (chunk, controller) => this.#handle(chunk, controller),
27 | flush: (controller) => this.#handle("\r\n", controller),
28 | });
29 |
30 | this.#allowCR = options?.allowCR ?? false;
31 | this.#returnEmptyLines = options?.returnEmptyLines ?? false;
32 | this.#mapperFun = options?.mapperFun ?? this.#mapperFun;
33 | }
34 |
35 | #handle(chunk, controller) {
36 | chunk = this.#buf + chunk;
37 |
38 | for (;;) {
39 | const lfIndex = chunk.indexOf("\n");
40 |
41 | if (this.#allowCR) {
42 | const crIndex = chunk.indexOf("\r");
43 |
44 | if (
45 | crIndex !== -1 &&
46 | crIndex !== chunk.length - 1 &&
47 | (lfIndex === -1 || lfIndex - 1 > crIndex)
48 | ) {
49 | const curChunk = this.#mapperFun(chunk.slice(0, crIndex));
50 | if (this.#returnEmptyLines || curChunk) {
51 | controller.enqueue(curChunk);
52 | }
53 | chunk = chunk.slice(crIndex + 1);
54 | continue;
55 | }
56 | }
57 |
58 | if (lfIndex !== -1) {
59 | let crOrLfIndex = lfIndex;
60 | if (chunk[lfIndex - 1] === "\r") {
61 | crOrLfIndex--;
62 | }
63 | const curChunk = this.#mapperFun(chunk.slice(0, crOrLfIndex));
64 | if (this.#returnEmptyLines || curChunk) {
65 | controller.enqueue(curChunk);
66 | }
67 | chunk = chunk.slice(lfIndex + 1);
68 | continue;
69 | }
70 |
71 | break;
72 | }
73 |
74 | this.#buf = chunk;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Public/js/ui_control.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const runButton = document.getElementById("run-button");
4 | const stopButton = document.getElementById("stop-button");
5 | const clearConsoleButton = document.getElementById("clear-console-button");
6 | const formatButton = document.getElementById("format-button");
7 | const shareButton = document.getElementById("share-button");
8 |
9 | module.exports = {
10 | runButton,
11 | stopButton,
12 | clearConsoleButton,
13 | formatButton,
14 | shareButton,
15 | };
16 |
--------------------------------------------------------------------------------
/Public/js/ui_control_embed.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const runButton = document.getElementById("run-button");
4 |
5 | module.exports = {
6 | runButton,
7 | };
8 |
--------------------------------------------------------------------------------
/Public/js/unescape.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | export function unescapeHTML(text) {
4 | const parser = new DOMParser().parseFromString(text, "text/html");
5 | return parser.documentElement.textContent;
6 | }
7 |
--------------------------------------------------------------------------------
/Public/js/uuid.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | export function uuidv4() {
4 | return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
5 | (
6 | c ^
7 | (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
8 | ).toString(16)
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/Public/js/version_picker.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | let instance = null;
4 |
5 | export class VersionPicker {
6 | constructor() {
7 | if (instance) {
8 | return instance;
9 | }
10 | instance = this;
11 |
12 | this.onchange = () => {};
13 |
14 | document.querySelectorAll(".version-picker-item").forEach((listItem) => {
15 | listItem.addEventListener("click", (event) => {
16 | for (let sibling of listItem.parentNode.children) {
17 | sibling.classList.remove("active-tick");
18 | }
19 | listItem.classList.add("active-tick");
20 |
21 | const version = listItem.querySelector(".dropdown-item").textContent;
22 | document.getElementById("version-value").textContent = version;
23 |
24 | this.onchange(version);
25 | });
26 | });
27 | }
28 |
29 | get selected() {
30 | return document.getElementById("version-value").textContent;
31 | }
32 |
33 | set selected(version) {
34 | document.getElementById("version-value").textContent = version;
35 | for (let listItem of document.querySelectorAll(".version-picker-item")) {
36 | if (listItem.querySelector(".dropdown-item").textContent === version) {
37 | listItem.click();
38 | return;
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Public/js/worker.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import { Decoder } from "./decoder.js";
4 | import { Encoder } from "./encoder.js";
5 |
6 | onmessage = (e) => {
7 | if (!e.data || !e.data.type || !e.data.value) {
8 | return;
9 | }
10 | switch (e.data.type) {
11 | case "decode": {
12 | const searchParams = new URLSearchParams(e.data.value);
13 | const query = Object.fromEntries(searchParams.entries());
14 | if (!query.c) {
15 | return;
16 | }
17 | try {
18 | const data = Decoder.decode(query.c);
19 | if (!data) {
20 | return;
21 | }
22 | const code = data.c || data.code;
23 | const version = data.v || data.version;
24 | if (!code || !version) {
25 | return;
26 | }
27 | postMessage({
28 | type: e.data.type,
29 | value: {
30 | code: code,
31 | version: version,
32 | },
33 | });
34 | } catch (error) {}
35 | break;
36 | }
37 | case "encode": {
38 | if (!e.data.value.code || !e.data.value.version) {
39 | return;
40 | }
41 | postMessage({
42 | type: e.data.type,
43 | value: `?c=${Encoder.encode({
44 | c: e.data.value.code,
45 | v: e.data.value.version,
46 | })}`,
47 | });
48 | break;
49 | }
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/Public/robots.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/Public/robots.txt
--------------------------------------------------------------------------------
/Public/scss/default.scss:
--------------------------------------------------------------------------------
1 | $input-border-color: #6c757d;
2 | $input-placeholder-color: #adb5bd;
3 |
4 | @import "bootstrap/scss/functions";
5 | @import "bootstrap/scss/variables";
6 | @import "bootstrap/scss/variables-dark";
7 | @import "bootstrap/scss/mixins";
8 | @import "bootstrap/scss/maps";
9 | @import "bootstrap/scss/utilities";
10 |
11 | @import "bootstrap/scss/root";
12 | @import "bootstrap/scss/reboot";
13 | @import "bootstrap/scss/type";
14 | @import "bootstrap/scss/containers";
15 | @import "bootstrap/scss/grid";
16 | @import "bootstrap/scss/tables";
17 | @import "bootstrap/scss/forms";
18 | @import "bootstrap/scss/buttons";
19 | @import "bootstrap/scss/transitions";
20 | @import "bootstrap/scss/dropdown";
21 | @import "bootstrap/scss/card";
22 | @import "bootstrap/scss/close";
23 | @import "bootstrap/scss/toasts";
24 | @import "bootstrap/scss/modal";
25 | @import "bootstrap/scss/tooltip";
26 | @import "bootstrap/scss/popover";
27 | @import "bootstrap/scss/spinners";
28 |
29 | @import "bootstrap/scss/utilities/api";
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | # SwiftFiddle (Swift Online Playground)
12 |
13 | SwiftFiddle is an online playground for creating, sharing and embedding Swift fiddles (little Swift programs that run directly in your browser).
14 |
15 |
16 |
17 | https://swiftfiddle.com
18 |
19 | ## Supporters & Sponsors
20 |
21 | Open source projects thrive on the generosity and support of people like you. If you find this project valuable, please consider extending your support. Contributing to the project not only sustains its growth, but also helps drive innovation and improve its features.
22 |
23 | To support this project, you can become a sponsor through [GitHub Sponsors](https://github.com/sponsors/kishikawakatsumi). Your contribution will be greatly appreciated and will help keep the project alive and thriving. Thanks for your consideration! :heart:
24 |
25 | ### Related Projects
26 |
27 | - [SwiftFiddle LSP](https://github.com/swiftfiddle/swiftfiddle-lsp) (Provide Code Completion powered by [SourceKit-LSP](https://github.com/apple/sourcekit-lsp))
28 |
--------------------------------------------------------------------------------
/Resources/Package.swift.json:
--------------------------------------------------------------------------------
1 | {
2 | "cLanguageStandard" : null,
3 | "cxxLanguageStandard" : null,
4 | "dependencies" : [
5 | {
6 | "sourceControl" : [
7 | {
8 | "identity" : "swift-algorithms",
9 | "location" : {
10 | "remote" : [
11 | {
12 | "urlString" : "https://github.com/apple/swift-algorithms"
13 | }
14 | ]
15 | },
16 | "productFilter" : null,
17 | "requirement" : {
18 | "range" : [
19 | {
20 | "lowerBound" : "1.2.1",
21 | "upperBound" : "2.0.0"
22 | }
23 | ]
24 | },
25 | "traits" : [
26 | {
27 | "name" : "default"
28 | }
29 | ]
30 | }
31 | ]
32 | },
33 | {
34 | "sourceControl" : [
35 | {
36 | "identity" : "swift-atomics",
37 | "location" : {
38 | "remote" : [
39 | {
40 | "urlString" : "https://github.com/apple/swift-atomics"
41 | }
42 | ]
43 | },
44 | "productFilter" : null,
45 | "requirement" : {
46 | "range" : [
47 | {
48 | "lowerBound" : "1.3.0",
49 | "upperBound" : "2.0.0"
50 | }
51 | ]
52 | },
53 | "traits" : [
54 | {
55 | "name" : "default"
56 | }
57 | ]
58 | }
59 | ]
60 | },
61 | {
62 | "sourceControl" : [
63 | {
64 | "identity" : "swift-collections",
65 | "location" : {
66 | "remote" : [
67 | {
68 | "urlString" : "https://github.com/apple/swift-collections"
69 | }
70 | ]
71 | },
72 | "productFilter" : null,
73 | "requirement" : {
74 | "range" : [
75 | {
76 | "lowerBound" : "1.2.0",
77 | "upperBound" : "2.0.0"
78 | }
79 | ]
80 | },
81 | "traits" : [
82 | {
83 | "name" : "default"
84 | }
85 | ]
86 | }
87 | ]
88 | },
89 | {
90 | "sourceControl" : [
91 | {
92 | "identity" : "swift-crypto",
93 | "location" : {
94 | "remote" : [
95 | {
96 | "urlString" : "https://github.com/apple/swift-crypto"
97 | }
98 | ]
99 | },
100 | "productFilter" : null,
101 | "requirement" : {
102 | "range" : [
103 | {
104 | "lowerBound" : "3.12.3",
105 | "upperBound" : "4.0.0"
106 | }
107 | ]
108 | },
109 | "traits" : [
110 | {
111 | "name" : "default"
112 | }
113 | ]
114 | }
115 | ]
116 | },
117 | {
118 | "sourceControl" : [
119 | {
120 | "identity" : "swift-numerics",
121 | "location" : {
122 | "remote" : [
123 | {
124 | "urlString" : "https://github.com/apple/swift-numerics"
125 | }
126 | ]
127 | },
128 | "productFilter" : null,
129 | "requirement" : {
130 | "range" : [
131 | {
132 | "lowerBound" : "1.0.3",
133 | "upperBound" : "2.0.0"
134 | }
135 | ]
136 | },
137 | "traits" : [
138 | {
139 | "name" : "default"
140 | }
141 | ]
142 | }
143 | ]
144 | },
145 | {
146 | "sourceControl" : [
147 | {
148 | "identity" : "swift-system",
149 | "location" : {
150 | "remote" : [
151 | {
152 | "urlString" : "https://github.com/apple/swift-system"
153 | }
154 | ]
155 | },
156 | "productFilter" : null,
157 | "requirement" : {
158 | "range" : [
159 | {
160 | "lowerBound" : "1.5.0",
161 | "upperBound" : "2.0.0"
162 | }
163 | ]
164 | },
165 | "traits" : [
166 | {
167 | "name" : "default"
168 | }
169 | ]
170 | }
171 | ]
172 | }
173 | ],
174 | "name" : "App",
175 | "packageKind" : {
176 | "root" : [
177 | "/private/var/folders/y6/nj790rtn62lfktb1sh__79hc0000gn/T/tmp.tqyHkUPMHg"
178 | ]
179 | },
180 | "pkgConfig" : null,
181 | "platforms" : [
182 | {
183 | "options" : [
184 |
185 | ],
186 | "platformName" : "macos",
187 | "version" : "10.15"
188 | }
189 | ],
190 | "products" : [
191 |
192 | ],
193 | "providers" : null,
194 | "swiftLanguageVersions" : null,
195 | "targets" : [
196 | {
197 | "dependencies" : [
198 | {
199 | "product" : [
200 | "Algorithms",
201 | "swift-algorithms",
202 | null,
203 | null
204 | ]
205 | },
206 | {
207 | "product" : [
208 | "Atomics",
209 | "swift-atomics",
210 | null,
211 | null
212 | ]
213 | },
214 | {
215 | "product" : [
216 | "Collections",
217 | "swift-collections",
218 | null,
219 | null
220 | ]
221 | },
222 | {
223 | "product" : [
224 | "Crypto",
225 | "swift-crypto",
226 | null,
227 | null
228 | ]
229 | },
230 | {
231 | "product" : [
232 | "Numerics",
233 | "swift-numerics",
234 | null,
235 | null
236 | ]
237 | },
238 | {
239 | "product" : [
240 | "SystemPackage",
241 | "swift-system",
242 | null,
243 | null
244 | ]
245 | }
246 | ],
247 | "exclude" : [
248 |
249 | ],
250 | "name" : "App",
251 | "packageAccess" : false,
252 | "resources" : [
253 |
254 | ],
255 | "settings" : [
256 |
257 | ],
258 | "type" : "executable"
259 | }
260 | ],
261 | "toolsVersion" : {
262 | "_version" : "5.5.0"
263 | },
264 | "traits" : [
265 |
266 | ]
267 | }
268 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | For security related problems, please don't use the public issue tracker, but email [@kishikawakatsumi](https://github.com/kishikawakatsumi).
4 |
--------------------------------------------------------------------------------
/SourceHanCodeJP-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SwiftFiddle/swiftfiddle-web/6ea476ccffd0d82c9a044cd82fd7bf4320aff029/SourceHanCodeJP-Regular.otf
--------------------------------------------------------------------------------
/Sources/App/Controllers/Base32.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | private let chars: [UInt8] = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".utf8)
4 |
5 | struct Base32 {
6 | static func encoode(bytes: [UInt8]) -> String? {
7 | let length = bytes.count
8 | var result = [UInt8](repeating: 0, count: 26);
9 |
10 | var count = 0
11 | var buffer = Int(bytes[0])
12 | var next = 1
13 | var bitsLeft: Int8 = 8
14 | while bitsLeft > 0 || next < length {
15 | if bitsLeft < 5 {
16 | if next < length {
17 | buffer <<= 8;
18 | buffer |= Int(bytes[next] & 0xFF);
19 | next += 1
20 | bitsLeft += 8;
21 | } else {
22 | let pad = Int8(5 - bitsLeft);
23 | buffer <<= pad;
24 | bitsLeft += pad;
25 | }
26 | }
27 | let index = Int(0x1F & (buffer >> Int8(bitsLeft - 5)))
28 | bitsLeft -= 5;
29 | result[count] = chars[index]
30 | count += 1
31 | }
32 |
33 | return String(bytes: result, encoding: .ascii)
34 | }
35 | }
36 |
37 | func convertHexToBytes(_ str: String) -> [UInt8] {
38 | let values = str.compactMap { $0.hexDigitValue } // map char to value of 0-15 or nil
39 | var bytes = [UInt8]()
40 | for x in stride(from: 0, to: values.count, by: 2) {
41 | let byte = (values[x] << 4) + values[x+1] // concat high and low bits
42 | bytes.append(UInt8(byte))
43 | }
44 | return bytes
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/App/Controllers/Firestore.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | private let projectId = Environment.get("GCP_PROJECT")!
4 | private let apiKey = Environment.get("FIREBASE_API_KEY")!
5 | private let refreshToken = Environment.get("FIREBASE_REFRESH_TOKEN")!
6 |
7 | struct Firestore {
8 | static func documentsGet(client: Client, id: String) async throws -> Document {
9 | let token = try await refreshAccessToken(client: client)
10 | let request = ClientRequest(
11 | method: .GET,
12 | url: "https://firestore.googleapis.com/v1/projects/\(projectId)/databases/(default)/documents/code_snippets/\(id)?key=\(apiKey)",
13 | headers: [
14 | "Authorization": "Bearer \(token.access_token)",
15 | "Accept": "application/json",
16 | ]
17 | )
18 | let response = try await client.send(request)
19 | guard let body = response.body else { throw Abort(.notFound) }
20 |
21 | let decoder = JSONDecoder()
22 | decoder.dateDecodingStrategy = .formatted(Document.dateFormatter)
23 |
24 | let document = try decoder.decode(Document.self, from: body)
25 | return document
26 | }
27 |
28 | static func createDocument(client: Client, id: String, type: String = "plain_text", code: String, swiftVersion: String) async throws -> Document {
29 | let token = try await refreshAccessToken(client: client)
30 | let url = URI(
31 | string: "https://firestore.googleapis.com/v1/projects/\(projectId)/databases/(default)/documents/code_snippets/?documentId=\(id)&key=\(apiKey)"
32 | )
33 | var request = ClientRequest(
34 | method: .POST,
35 | url: url,
36 | headers: [
37 | "Authorization": "Bearer \(token.access_token)",
38 | "Accept": "application/json",
39 | "Content-Type": "application/json",
40 | ]
41 | )
42 | let fields = DocumentFields(
43 | type: DocumentField(stringValue: type),
44 | id: DocumentField(stringValue: id),
45 | shared_link: DocumentSharedLink(
46 | mapValue: DocumentSharedLinkMapValue(
47 | fields: DocumentSharedLinkMapValueFields(
48 | content: DocumentField(stringValue: code),
49 | swift_version: DocumentField(stringValue: swiftVersion),
50 | url: DocumentField(stringValue: "https://swiftfiddle.com/\(id)")
51 | )
52 | )
53 | )
54 | )
55 | try request.content.encode(
56 | ["fields": fields],
57 | as: .json
58 | )
59 | let response = try await client.send(request)
60 | guard let body = response.body else { throw Abort(.notFound) }
61 |
62 | let decoder = JSONDecoder()
63 | decoder.dateDecodingStrategy = .formatted(Document.dateFormatter)
64 |
65 | let document = try decoder.decode(Document.self, from: body)
66 | return document
67 | }
68 |
69 | private static func refreshAccessToken(client: Client) async throws -> Token {
70 | var request = ClientRequest(
71 | method: .POST,
72 | url: "https://securetoken.googleapis.com/v1/token?key=\(apiKey)",
73 | headers: ["Content-Type": "application/x-www-form-urlencoded"]
74 | )
75 | try request.content.encode(
76 | ["grant_type": "refresh_token", "refresh_token": refreshToken],
77 | as: .urlEncodedForm
78 | )
79 | let response = try await client.send(request)
80 | guard let body = response.body else { throw Abort(.internalServerError) }
81 |
82 | let token = try JSONDecoder().decode(Token.self, from: body)
83 | return token
84 | }
85 | }
86 |
87 | struct Document: Codable {
88 | let name: String
89 | let fields: DocumentFields
90 | let createTime: Date
91 | let updateTime: Date
92 |
93 | static var dateFormatter: DateFormatter = {
94 | let dateFormatter = DateFormatter()
95 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
96 | return dateFormatter
97 | }()
98 | }
99 |
100 | struct DocumentFields: Codable {
101 | let type: DocumentField
102 | let id: DocumentField
103 | let shared_link: DocumentSharedLink
104 | }
105 |
106 | struct DocumentSharedLink: Codable {
107 | let mapValue: DocumentSharedLinkMapValue
108 | }
109 |
110 | struct DocumentSharedLinkMapValue: Codable {
111 | let fields: DocumentSharedLinkMapValueFields
112 | }
113 |
114 | struct DocumentSharedLinkMapValueFields: Codable {
115 | let content: DocumentField
116 | let swift_version: DocumentField
117 | let url: DocumentField
118 | }
119 |
120 | struct DocumentField: Codable {
121 | let stringValue: String
122 | }
123 |
124 | private struct Token: Codable {
125 | let access_token: String
126 | let expires_in: String
127 | let token_type: String
128 | let refresh_token: String
129 | let id_token: String
130 | let user_id: String
131 | let project_id: String
132 | }
133 |
--------------------------------------------------------------------------------
/Sources/App/Controllers/Gist.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | struct Gist: Codable {
4 | let url: String
5 | let id: String
6 | let files: [String: File]
7 |
8 | struct File: Codable {
9 | let filename: String
10 | let type: String
11 | let language: String?
12 | let raw_url: String
13 | let size: Int
14 | let truncated: Bool
15 | let content: String
16 | }
17 |
18 | static func id(from path: String) throws -> String? {
19 | guard let pattern = try? NSRegularExpression(pattern: #"^([a-f0-9]{32}(|.png))$"#, options: [.caseInsensitive]) else {
20 | throw Abort(.internalServerError)
21 | }
22 |
23 | let matches = pattern.matches(in: path, options: [], range: NSRange(location: 0, length: path.utf16.count))
24 | guard matches.count == 1 && matches[0].numberOfRanges == 3 else {
25 | return nil
26 | }
27 |
28 | return path
29 | }
30 |
31 | static func content(client: Client, id: String) async throws -> Gist {
32 | let response = try await client.send(
33 | ClientRequest(
34 | method: .GET,
35 | url: "https://api.github.com/gists/\(id)",
36 | headers: ["User-Agent": "SwiftFiddle"]
37 | )
38 | )
39 | guard let body = response.body else { throw Abort(.notFound) }
40 |
41 | let content = try JSONDecoder().decode(Gist.self, from: body)
42 | return content
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/App/Controllers/ShareImage.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | struct ShareImage {
4 | @available(macOS 12.0.0, *)
5 | static func generate(code: String) async throws -> Data? {
6 | let process = Process()
7 | #if os(macOS)
8 | process.executableURL = URL(fileURLWithPath: "/opt/homebrew/bin/silicon")
9 | #else
10 | process.executableURL = URL(fileURLWithPath: "/home/linuxbrew/.linuxbrew/bin/silicon")
11 | #endif
12 | let output = "\(UUID().uuidString).png"
13 | process.arguments = [
14 | "--language", "swift",
15 | "--pad-horiz", "0",
16 | "--pad-vert", "0",
17 | "--font", "Source Han Code JP",
18 | "--no-window-controls",
19 | "--theme", "GitHub",
20 | "--output", output,
21 | ]
22 |
23 | let standardInput = Pipe()
24 | process.standardInput = standardInput
25 | process.standardOutput = Pipe()
26 | process.standardError = Pipe()
27 |
28 | try standardInput.fileHandleForWriting.write(contentsOf: Data(code.utf8))
29 | try standardInput.fileHandleForWriting.close()
30 |
31 | _ = try await withCheckedThrowingContinuation { (continuation) in
32 | process.terminationHandler = { (process) in
33 | continuation.resume(returning: process.terminationStatus)
34 | }
35 | do {
36 | try process.run()
37 | } catch {
38 | continuation.resume(throwing: error)
39 | }
40 | }
41 |
42 | return try Data(contentsOf: URL(fileURLWithPath: output))
43 | }
44 | }
45 |
46 |
47 |
--------------------------------------------------------------------------------
/Sources/App/Controllers/SharedLink.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | struct SharedLink {
4 | static func id(from path: String) throws -> String? {
5 | guard let pattern = try? NSRegularExpression(pattern: #"^([A-Z2-7]{26}(|.png))$"#, options: [.caseInsensitive]) else {
6 | throw Abort(.internalServerError)
7 | }
8 |
9 | let matches = pattern.matches(in: path, options: [], range: NSRange(location: 0, length: path.utf16.count))
10 | guard matches.count == 1 && matches[0].numberOfRanges == 3 else {
11 | return nil
12 | }
13 |
14 | return path
15 | }
16 |
17 | static func content(client: Client, id: String) async throws -> Document {
18 | return try await Firestore.documentsGet(client: client, id: id)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/App/Controllers/SwiftPackage.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct Package: Codable {
4 | let name: String
5 | let dependencies: [Dependency]
6 | let targets: [Target]
7 | }
8 |
9 | struct Dependency: Codable {
10 | let sourceControl: [SourceControl]
11 | }
12 |
13 | struct SourceControl: Codable {
14 | let identity: String
15 | let location: Location
16 | let requirement: Requirement
17 | }
18 |
19 | struct Location: Codable {
20 | let remote: [Remote]
21 | }
22 |
23 | struct Remote: Codable {
24 | let urlString: String
25 | }
26 |
27 | struct Requirement: Codable {
28 | let range: [Range]
29 | }
30 |
31 | struct Range: Codable {
32 | let lowerBound: String
33 | let upperBound: String
34 | }
35 |
36 | struct Target: Codable {
37 | let dependencies: [TargetDependency]
38 | let name: String
39 | let type: String
40 | }
41 |
42 | struct TargetDependency: Codable {
43 | let product: [String?]
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/App/Middlewares/CommonErrorMiddleware.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | final class CommonErrorMiddleware: Middleware {
4 | func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture {
5 | return next.respond(to: request).flatMapError { (error) in
6 | let headers: HTTPHeaders
7 | let status: HTTPResponseStatus
8 | switch error {
9 | case let abort as AbortError:
10 | headers = abort.headers
11 | status = abort.status
12 | default:
13 | headers = [:]
14 | status = .internalServerError
15 | }
16 |
17 | let errotTitles: [UInt: String] = [
18 | 400: "Bad Request",
19 | 401: "Unauthorized",
20 | 403: "Access Denied",
21 | 404: "Resource not found",
22 | 500: "Webservice currently unavailable",
23 | 503: "Webservice currently unavailable",
24 | ]
25 |
26 | let errotReasons: [UInt: String] = [
27 | 400: "The server cannot process the request due to something that is perceived to be a client error.",
28 | 401: "The requested resource requires an authentication.",
29 | 403: "The requested resource requires an authentication.",
30 | 404: "The requested resource could not be found but may be available again in the future.",
31 | 500: "An unexpected condition was encountered. Our service team has been dispatched to bring it back online.",
32 | 503: "We've got some trouble with our backend upstream cluster. Our service team has been dispatched to bring it back online.",
33 | ]
34 |
35 | if request.headers[.accept].map({ $0.lowercased() }).contains("application/json") {
36 | return request.eventLoop.makeSucceededFuture(["error": status.code])
37 | .encodeResponse(status: status, headers: headers, for: request)
38 | } else {
39 | return request.view.render("error", [
40 | "title": "We've got some trouble",
41 | "error": errotTitles[status.code],
42 | "reason": errotReasons[status.code],
43 | "status": "\(status.code)",
44 | ])
45 | .encodeResponse(status: status, headers: headers, for: request)
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/App/Middlewares/CustomHeaderMiddleware.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | final class CustomHeaderMiddleware: Middleware {
4 | func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture {
5 | return next.respond(to: request).map { (response) in
6 | if !request.url.path.hasSuffix("embedded") && !request.url.path.hasSuffix("embedded/") {
7 | response.headers.add(name: "X-Frame-Options", value: "DENY")
8 | }
9 | response.headers.add(name: "Permissions-Policy", value: "interest-cohort=()")
10 | return response
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/App/Models/EmbeddedPageResponse.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct EmbeddedPageResponse: Encodable {
4 | let title: String
5 | let versions: [VersionGroup]
6 | let stableVersion: String
7 | let latestVersion: String
8 | let codeSnippet: String
9 | let url: String
10 | let foldRanges: [FoldRange]
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/App/Models/ExecutionRequestParameter.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct ExecutionRequestParameter: Decodable {
4 | let toolchain_version: String?
5 | let command: String?
6 | let options: String?
7 | let code: String?
8 | let timeout: Int?
9 | let _color: Bool?
10 | let _nonce: String?
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/App/Models/ExecutionResponse.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Vapor
3 |
4 | struct ExecutionResponse: Content {
5 | let output: String
6 | let errors: String
7 | let version: String
8 | }
9 |
--------------------------------------------------------------------------------
/Sources/App/Models/FoldRange.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct FoldRange: Codable {
4 | let start: Int
5 | let end: Int
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/App/Models/InitialPageResponse.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct InitialPageResponse: Encodable {
4 | let title: String
5 | let versions: [VersionGroup]
6 | let stableVersion: String
7 | let latestVersion: String
8 | let codeSnippet: String
9 | let ogpImageUrl: String
10 | let packageInfo: [PackageInfo]
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/App/Models/PackageInfo.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct PackageInfo: Encodable {
4 | let url: String
5 | let name: String
6 | let productName: String
7 | let version: String
8 | }
9 |
--------------------------------------------------------------------------------
/Sources/App/Models/SharedLinkRequestParameter.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct SharedLinkRequestParameter: Decodable {
4 | let toolchain_version: String
5 | let code: String
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/App/Models/VersionGroup.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | final class VersionGroup: Encodable {
4 | let majorVersion: String
5 | var versions: [String]
6 |
7 | init(majorVersion: String, versions: [String]) {
8 | self.majorVersion = majorVersion
9 | self.versions = versions
10 | }
11 |
12 | static func grouped(versions: [String]) -> [VersionGroup] {
13 | versions.reduce(into: [VersionGroup]()) { (versionGroup, version) in
14 | let nightlyVersion = version.split(separator: "-")
15 | if nightlyVersion.count == 2 {
16 | let majorVersion = String(nightlyVersion[0])
17 | if majorVersion != versionGroup.last?.majorVersion {
18 | versionGroup.append(VersionGroup(majorVersion: majorVersion, versions: [version]))
19 | } else {
20 | versionGroup.last?.versions.append(version)
21 | }
22 | } else if nightlyVersion.count == 4 {
23 | let majorVersion = "snapshot"
24 | if majorVersion != versionGroup.last?.majorVersion {
25 | versionGroup.append(VersionGroup(majorVersion: majorVersion, versions: [nightlyVersion.joined(separator: "-")]))
26 | } else {
27 | versionGroup.last?.versions.append(nightlyVersion.joined(separator: "-"))
28 | }
29 | } else {
30 | let majorVersion = "Swift \(version.split(separator: ".")[0])"
31 | if majorVersion != versionGroup.last?.majorVersion {
32 | versionGroup.append(VersionGroup(majorVersion: majorVersion, versions: [version]))
33 | } else {
34 | versionGroup.last?.versions.append(version)
35 | }
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/App/configure.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 | import Leaf
3 |
4 | public func configure(_ app: Application) throws {
5 | app.middleware = Middlewares()
6 | app.middleware.use(CommonErrorMiddleware())
7 | app.middleware.use(CustomHeaderMiddleware())
8 |
9 | let publicDirectory = "\(app.directory.publicDirectory)/dist"
10 | app.middleware.use(FileMiddleware(publicDirectory: publicDirectory))
11 |
12 | app.http.server.configuration.port = Environment.process.PORT.flatMap { Int($0) } ?? 8080
13 | app.http.server.configuration.requestDecompression = .enabled
14 | app.http.server.configuration.responseCompression = .enabled
15 | app.http.server.configuration.supportPipelining = true
16 |
17 | app.caches.use(.memory)
18 |
19 | app.views.use(.leaf)
20 | app.leaf.configuration.rootDirectory = publicDirectory
21 | app.leaf.cache.isEnabled = app.environment.isRelease
22 |
23 | try routes(app)
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/App/entrypoint.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 | import Dispatch
3 | import Logging
4 |
5 | /// This extension is temporary and can be removed once Vapor gets this support.
6 | private extension Vapor.Application {
7 | static let baseExecutionQueue = DispatchQueue(label: "vapor.codes.entrypoint")
8 |
9 | func runFromAsyncMainEntrypoint() async throws {
10 | try await withCheckedThrowingContinuation { continuation in
11 | Vapor.Application.baseExecutionQueue.async { [self] in
12 | do {
13 | try self.run()
14 | continuation.resume()
15 | } catch {
16 | continuation.resume(throwing: error)
17 | }
18 | }
19 | }
20 | }
21 | }
22 |
23 | @main
24 | enum Entrypoint {
25 | static func main() async throws {
26 | var env = try Environment.detect()
27 | try LoggingSystem.bootstrap(from: &env)
28 |
29 | let app = try await Application.make(env)
30 | defer { app.shutdown() }
31 |
32 | try configure(app)
33 | try await app.runFromAsyncMainEntrypoint()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/App/routes.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | func routes(_ app: Application) throws {
4 | app.get("health") { _ in ["status": "pass"] }
5 |
6 | app.get { (req) in try await index(req) }
7 | app.get("index.html") { (req) in try await index(req) }
8 | func index(_ req: Request) async throws -> View {
9 | try await req.view.render(
10 | "index",
11 | InitialPageResponse(
12 | title: "Swift Playground",
13 | versions: try VersionGroup.grouped(versions: availableVersions()),
14 | stableVersion: stableVersion(),
15 | latestVersion: try latestVersion(),
16 | codeSnippet: escape(defaultCodeSnippet),
17 | ogpImageUrl: "https://swiftfiddle.com/images/ogp_small.png",
18 | packageInfo: swiftPackageInfo(app)
19 | )
20 | )
21 | }
22 |
23 | app.get(":id") { req -> Response in
24 | if let path = req.parameters.get("id"), let id = try SharedLink.id(from: path) {
25 | let content = try await SharedLink.content(
26 | client: req.client,
27 | id: id.replacingOccurrences(of: ".png", with: "")
28 | )
29 |
30 | let code = content.fields.shared_link.mapValue.fields.content.stringValue
31 | let swiftVersion = content.fields.shared_link.mapValue.fields.swift_version.stringValue
32 | return try await makeImportResponse(req, id, code, swiftVersion)
33 | } else if let path = req.parameters.get("id"), let id = try Gist.id(from: path) {
34 | let content = try await Gist.content(
35 | client: req.client,
36 | id: id.replacingOccurrences(of: ".png", with: "")
37 | )
38 |
39 | let code = Array(content.files.values)[0].content
40 | return try await makeImportResponse(req, id, code, nil)
41 | } else {
42 | throw Abort(.notFound)
43 | }
44 | }
45 |
46 | app.get(":id", "embedded") { req -> Response in
47 | let foldRanges: [FoldRange] = req.query[[String].self, at: "fold"]?.compactMap {
48 | let lines = $0.split(separator: "-")
49 | guard lines.count == 2 else { return nil }
50 | guard let start = Int(lines[0]), let end = Int(lines[1]) else { return nil }
51 | guard start <= end else { return nil }
52 | return FoldRange(start: start, end: end)
53 | } ?? []
54 |
55 | if let path = req.parameters.get("id"), let id = try SharedLink.id(from: path) {
56 | let document = try await SharedLink.content(client: req.client, id: id)
57 |
58 | let code = document.fields.shared_link.mapValue.fields.content.stringValue
59 | let swiftVersion = document.fields.shared_link.mapValue.fields.swift_version.stringValue
60 |
61 | return try await makeEmbeddedResponse(req, id, code, swiftVersion, foldRanges)
62 | } else if let path = req.parameters.get("id"), let id = try Gist.id(from: path) {
63 | let content = try await Gist.content(client: req.client, id: id)
64 | let code = Array(content.files.values)[0].content
65 |
66 | return try await makeEmbeddedResponse(req, id, code, nil, foldRanges)
67 | } else {
68 | throw Abort(.notFound)
69 | }
70 | }
71 |
72 | app.on(.POST, "shared_link", body: .collect(maxSize: "10mb")) { (req) -> [String: String] in
73 | let parameter = try req.content.decode(SharedLinkRequestParameter.self)
74 | let code = parameter.code
75 | let swiftVersion = parameter.toolchain_version
76 |
77 | guard let id = Base32.encoode(bytes: convertHexToBytes(UUID().uuidString.replacingOccurrences(of: "-", with: "")))?.lowercased() else { throw Abort(.internalServerError) }
78 |
79 | _ = try await Firestore.createDocument(
80 | client: req.client,
81 | id: id,
82 | code: parameter.code,
83 | swiftVersion: swiftVersion
84 | )
85 | if let data = try? await ShareImage.generate(code: code) {
86 | try? await req.cache.set("/\(id).png", to: data, expiresIn: .days(14))
87 | }
88 | return [
89 | "swift_version": swiftVersion,
90 | "content": code,
91 | "url": "https://swiftfiddle.com/\(id)",
92 | ]
93 | }
94 |
95 | app.get("versions") { (req) in try availableVersions() }
96 |
97 | app.on(.POST, "run", body: .collect(maxSize: "10mb")) { (req) -> ClientResponse in
98 | guard let data = req.body.data else { throw Abort(.badRequest) }
99 | guard let parameter = try? req.content.decode(ExecutionRequestParameter.self) else {
100 | throw Abort(.badRequest)
101 | }
102 | let version = parameter.toolchain_version ?? stableVersion()
103 |
104 | let url = URI(scheme: .https, host: "swiftfiddle.com", path: "/runner/\(version)/run")
105 | let clientRequest = ClientRequest(
106 | method: .POST,
107 | url: url,
108 | headers: HTTPHeaders([("Content-type", "application/json")]),
109 | body: data
110 | )
111 |
112 | return try await req.client.send(clientRequest)
113 | }
114 |
115 | app.on(.POST, "runner", "stable", "run", body: .collect(maxSize: "10mb")) { (req) -> ClientResponse in
116 | guard let data = req.body.data else { throw Abort(.badRequest) }
117 |
118 | let path = "/runner/\(stableVersion())/run"
119 | let clientRequest = ClientRequest(
120 | method: .POST,
121 | url: URI(scheme: .https, host: "swiftfiddle.com", path: path),
122 | headers: HTTPHeaders([("Content-type", "application/json")]),
123 | body: data
124 | )
125 | return try await req.client.send(clientRequest)
126 | }
127 |
128 | app.on(.POST, "runner", "latest", "run", body: .collect(maxSize: "10mb")) { (req) -> ClientResponse in
129 | guard let data = req.body.data else { throw Abort(.badRequest) }
130 | let latestVersion = (try? latestVersion()) ?? stableVersion()
131 |
132 | let path = "/runner/\(latestVersion)/run"
133 | let clientRequest = ClientRequest(
134 | method: .POST,
135 | url: URI(scheme: .https, host: "swiftfiddle.com", path: path),
136 | headers: HTTPHeaders([("Content-type", "application/json")]),
137 | body: data
138 | )
139 | return try await req.client.send(clientRequest)
140 | }
141 | }
142 |
143 | private func makeImportResponse(_ req: Request, _ id: String, _ code: String, _ swiftVersion: String?) async throws -> Response {
144 | let path = req.url.path
145 | if path.hasSuffix(".png") {
146 | if let data = try await req.cache.get(path, as: Data.self) {
147 | return Response(
148 | status: .ok,
149 | headers: ["Content-Type": "image/png"],
150 | body: Response.Body(buffer: ByteBuffer(data: data))
151 | )
152 | } else {
153 | guard let data = try await ShareImage.generate(code: code) else { throw Abort(.notFound) }
154 | try? await req.cache.set(path, to: data, expiresIn: .days(14))
155 | return Response(
156 | status: .ok,
157 | headers: ["Content-Type": "image/png"],
158 | body: Response.Body(buffer: ByteBuffer(data: data))
159 | )
160 | }
161 | } else {
162 | let version: String
163 | if let swiftVersion = swiftVersion {
164 | if swiftVersion == "nightly-master" {
165 | version = "nightly-main"
166 | } else {
167 | version = swiftVersion
168 | }
169 | } else {
170 | version = stableVersion()
171 | }
172 | return try await req.view.render(
173 | "index", InitialPageResponse(
174 | title: "Swift Playground",
175 | versions: try VersionGroup.grouped(versions: availableVersions()),
176 | stableVersion: version,
177 | latestVersion: try latestVersion(),
178 | codeSnippet: escape(code),
179 | ogpImageUrl: "https://swiftfiddle.com/\(id).png",
180 | packageInfo: swiftPackageInfo(req.application)
181 | )
182 | )
183 | .encodeResponse(for: req)
184 | }
185 | }
186 |
187 | private func makeEmbeddedResponse(_ req: Request, _ id: String, _ code: String, _ swiftVersion: String?, _ foldRanges: [FoldRange]) async throws -> Response {
188 | return try await req.view.render(
189 | "embedded", EmbeddedPageResponse(
190 | title: "Swift Playground",
191 | versions: try VersionGroup.grouped(versions: availableVersions()),
192 | stableVersion: swiftVersion ?? stableVersion(),
193 | latestVersion: try latestVersion(),
194 | codeSnippet: escape(code),
195 | url: "https://swiftfiddle.com/\(id)",
196 | foldRanges: foldRanges
197 | )
198 | )
199 | .encodeResponse(for: req)
200 | }
201 |
202 | private func swiftPackageInfo(_ app: Application) -> [PackageInfo] {
203 | let packagePath = URL(fileURLWithPath: app.directory.resourcesDirectory).appendingPathComponent("Package.swift.json")
204 | let decoder = JSONDecoder()
205 | do {
206 | let package = try decoder.decode(Package.self, from: Data(contentsOf: packagePath))
207 | guard let target = package.targets.first else { return [] }
208 | return zip(package.dependencies, target.dependencies).compactMap { (dependency, target) -> PackageInfo? in
209 | guard let product = target.product.first, let productName = product else { return nil }
210 | guard let sourceControl = dependency.sourceControl.first else { return nil }
211 | guard let range = sourceControl.requirement.range.first else { return nil }
212 | return PackageInfo(
213 | url: sourceControl.location.remote.first?.urlString ?? "",
214 | name: sourceControl.identity,
215 | productName: productName,
216 | version: range.lowerBound
217 | )
218 | }
219 | } catch {
220 | return []
221 | }
222 | }
223 |
224 | private func escape(_ s: String) -> String {
225 | s
226 | .replacingOccurrences(of: #"\"#, with: #"\\"#)
227 | .replacingOccurrences(of: #"`"#, with: #"\`"#)
228 | }
229 |
230 | private let defaultCodeSnippet = #"""
231 | import Foundation
232 |
233 | let hello = "Hello, world!"
234 | let multilineString = """
235 | @@@
236 | @@ @@@@
237 | @@ @@@ @@@@@
238 | @@@@@@@@@ @@@@@
239 | @@@@@@@@@@ @@@@@@
240 | @@@@@@@@@@ @@@@@@
241 | @@@@@@@@@@@@@@@@@
242 | @ @@@@@@@@@@@@@@@
243 | @@@@@@ @@@@@@@@@@@@@
244 | @@@@@@@@@@@@@@@@@@@@@@@@@@
245 | @@@@@@@@@@@@@@@@@@@@@@@@
246 | @@@@@@@@@@@@@ @
247 | \(hello)
248 | """
249 |
250 | print(multilineString)
251 |
252 | """#
253 |
--------------------------------------------------------------------------------
/Sources/App/versions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | func latestVersion() throws -> String { try availableVersions()[0] }
4 | func stableVersion() -> String { "6.1.2" }
5 |
6 | func availableVersions() throws -> [String] {
7 | [
8 | "nightly-main",
9 | "nightly-6.2",
10 | "nightly-6.1",
11 | "nightly-6.0",
12 | "nightly-5.10",
13 | "nightly-5.9",
14 | "nightly-5.8",
15 | "nightly-5.7",
16 | "nightly-5.6",
17 | "nightly-5.5",
18 | "nightly-5.4",
19 | "nightly-5.3",
20 | "6.1.2",
21 | "6.1.1",
22 | "6.1",
23 | "6.0.3",
24 | "6.0.2",
25 | "6.0.1",
26 | "6.0",
27 | "5.10.1",
28 | "5.10",
29 | "5.9.2",
30 | "5.9.1",
31 | "5.9",
32 | "5.8.1",
33 | "5.8",
34 | "5.7.3",
35 | "5.7.2",
36 | "5.7.1",
37 | "5.7",
38 | "5.6.3",
39 | "5.6.2",
40 | "5.6.1",
41 | "5.6",
42 | "5.5.3",
43 | "5.5.2",
44 | "5.5.1",
45 | "5.5",
46 | "5.4.3",
47 | "5.4.2",
48 | "5.4.1",
49 | "5.4",
50 | "5.3.3",
51 | "5.3.2",
52 | "5.3.1",
53 | "5.3",
54 | "5.2.5",
55 | "5.2.4",
56 | "5.2.3",
57 | "5.2.2",
58 | "5.2.1",
59 | "5.2",
60 | "5.1.5",
61 | "5.1.4",
62 | "5.1.3",
63 | "5.1.2",
64 | "5.1.1",
65 | "5.1",
66 | "5.0.3",
67 | "5.0.2",
68 | "5.0.1",
69 | "5.0",
70 | "4.2.4",
71 | "4.2.3",
72 | "4.2.2",
73 | "4.2.1",
74 | "4.2",
75 | "4.1.3",
76 | "4.1.2",
77 | "4.1.1",
78 | "4.1",
79 | "4.0.3",
80 | "4.0.2",
81 | "4.0",
82 | "3.1.1",
83 | "3.1",
84 | "3.0.2",
85 | "3.0.1",
86 | "3.0",
87 | "2.2.1",
88 | "2.2",
89 | ]
90 | }
91 |
--------------------------------------------------------------------------------
/Tests/AppTests/AppTests.swift:
--------------------------------------------------------------------------------
1 | @testable import App
2 | import XCTVapor
3 |
4 | final class AppTests: XCTestCase {
5 | func testRootPath() throws {
6 | let app = Application(.testing)
7 | defer { app.shutdown() }
8 | try configure(app)
9 |
10 | try app.test(.GET, "/", afterResponse: { res in
11 | XCTAssertEqual(res.status, .ok)
12 | })
13 | }
14 |
15 | func testGistPath() throws {
16 | let path = "/b4f866efb1c1dc63b0a9cce000cf5688"
17 |
18 | let pattern = try! NSRegularExpression(pattern: #"^\/([a-f0-9]{32})$"#, options: [.caseInsensitive])
19 | let matches = pattern.matches(in: path, options: [], range: NSRange(location: 0, length: NSString(string: path).length))
20 | guard matches.count == 1 && matches[0].numberOfRanges == 2 else {
21 | XCTFail()
22 | return
23 | }
24 | let gistId = NSString(string: path).substring(with: matches[0].range(at: 1))
25 |
26 | let ex = expectation(description: "")
27 |
28 | let session = URLSession(configuration: .default)
29 | let request = URLRequest(url: URL(string: "https://api.github.com/gists/\(gistId)")!)
30 | session.dataTask(with: request) { (data, response, error) in
31 | guard let data = data else {
32 | XCTFail()
33 | return
34 | }
35 | if let contents = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
36 | let files = contents["files"] as? [String: Any],
37 | let filename = files.keys.first, let file = files[filename] as? [String: Any],
38 | let content = file["content"] as? String {
39 | XCTAssertEqual(
40 | content,
41 | """
42 | struct Player {
43 | var name: String
44 | var highScore: Int = 0
45 | var history: [Int] = []
46 |
47 | init(_ name: String) {
48 | self.name = name
49 | }
50 | }
51 |
52 | var player = Player("Tomas")
53 |
54 | """
55 | )
56 | } else {
57 | XCTFail()
58 | }
59 |
60 | ex.fulfill()
61 | }
62 | .resume()
63 |
64 | waitForExpectations(timeout: 5)
65 | }
66 |
67 | func testShareImage() async throws {
68 | let app = Application(.testing)
69 | defer { app.shutdown() }
70 | try configure(app)
71 |
72 | let data = try await ShareImage.generate(code: "import Foundation")
73 | _ = try XCTUnwrap(data)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "prod": "webpack --progress --config webpack.prod.js",
4 | "dev": "webpack --progress --config webpack.dev.js"
5 | },
6 | "dependencies": {
7 | "@fortawesome/fontawesome-svg-core": "6.7.2",
8 | "@fortawesome/free-brands-svg-icons": "6.7.2",
9 | "@fortawesome/pro-duotone-svg-icons": "6.4.0",
10 | "@fortawesome/pro-light-svg-icons": "6.4.0",
11 | "@fortawesome/pro-regular-svg-icons": "6.4.0",
12 | "@fortawesome/pro-solid-svg-icons": "6.4.0",
13 | "@popperjs/core": "2.11.8",
14 | "bootstrap": "5.3.6",
15 | "monaco-editor": "0.52.2",
16 | "pako": "2.1.0",
17 | "reconnecting-websocket": "4.4.0",
18 | "xterm": "5.3.0",
19 | "xterm-addon-fit": "0.8.0",
20 | "xterm-addon-web-links": "0.9.0"
21 | },
22 | "devDependencies": {
23 | "autoprefixer": "10.4.21",
24 | "copy-webpack-plugin": "13.0.0",
25 | "css-loader": "7.1.2",
26 | "html-webpack-plugin": "5.6.3",
27 | "mini-css-extract-plugin": "2.9.2",
28 | "monaco-editor-webpack-plugin": "7.1.0",
29 | "postcss": "8.5.4",
30 | "postcss-loader": "8.1.1",
31 | "sass": "1.89.1",
32 | "sass-loader": "16.0.5",
33 | "style-loader": "4.0.0",
34 | "webpack": "5.99.9",
35 | "webpack-bundle-analyzer": "4.10.2",
36 | "webpack-cli": "6.0.1",
37 | "webpack-merge": "6.0.1",
38 | "worker-loader": "3.0.8"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
3 | const HtmlWebpackPlugin = require("html-webpack-plugin");
4 | const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
5 | const CopyWebbackPlugin = require("copy-webpack-plugin");
6 |
7 | module.exports = {
8 | entry: {
9 | index: "./Public/index.js",
10 | embedded: "./Public/embedded.js",
11 | "editor.worker": "monaco-editor/esm/vs/editor/editor.worker.js",
12 | },
13 | output: {
14 | globalObject: "self",
15 | filename: "[name].[contenthash].js",
16 | path: path.resolve(__dirname, "Public/dist"),
17 | publicPath: "/",
18 | clean: true,
19 | },
20 | module: {
21 | rules: [
22 | {
23 | test: /\.scss$/,
24 | use: [
25 | {
26 | loader: MiniCssExtractPlugin.loader,
27 | },
28 | {
29 | loader: "css-loader",
30 | options: {
31 | url: false,
32 | sourceMap: true,
33 | importLoaders: 2,
34 | },
35 | },
36 | {
37 | loader: "postcss-loader",
38 | options: {
39 | sourceMap: true,
40 | postcssOptions: {
41 | plugins: ["autoprefixer"],
42 | },
43 | },
44 | },
45 | {
46 | loader: "sass-loader",
47 | options: {
48 | sourceMap: true,
49 | },
50 | },
51 | ],
52 | },
53 | {
54 | test: /\.css$/,
55 | use: ["style-loader", "css-loader"],
56 | },
57 | {
58 | test: /\.(woff|woff2|eot|ttf|otf)$/i,
59 | type: "asset/resource",
60 | },
61 | {
62 | test: "/.worker.js$/",
63 | loader: "worker-loader",
64 | options: {
65 | filename: "[name].[contenthash].worker.js",
66 | },
67 | },
68 | ],
69 | },
70 | plugins: [
71 | new MiniCssExtractPlugin({
72 | filename: "[name].[contenthash].css",
73 | }),
74 | new HtmlWebpackPlugin({
75 | chunks: ["index"],
76 | filename: "index.leaf",
77 | template: "./Public/index.html",
78 | }),
79 | new HtmlWebpackPlugin({
80 | chunks: ["embedded"],
81 | filename: "embedded.leaf",
82 | template: "./Public/embedded.html",
83 | }),
84 | new MonacoWebpackPlugin({
85 | filename: "[name].[contenthash].worker.js",
86 | languages: ["swift"],
87 | }),
88 | new CopyWebbackPlugin({
89 | patterns: [
90 | { from: "./Public/images/*.*", to: "images/[name][ext]" },
91 | { from: "./Public/favicons/*.*", to: "[name][ext]" },
92 | { from: "./Public/error.html", to: "error.leaf" },
93 | { from: "./Public/robots.txt", to: "robots.txt" },
94 | ],
95 | }),
96 | ],
97 | optimization: {
98 | splitChunks: {
99 | cacheGroups: {
100 | "monaco-editor": {
101 | test: /[\\/]monaco-editor[\\/]/,
102 | chunks: "initial",
103 | name: "monaco-editor",
104 | },
105 | xterm: {
106 | test: /[\\/]xterm[\\/]/,
107 | chunks: "initial",
108 | name: "xterm",
109 | },
110 | },
111 | },
112 | },
113 | };
114 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const { merge } = require("webpack-merge");
2 | const common = require("./webpack.common.js");
3 |
4 | module.exports = merge(common, {
5 | mode: "development",
6 | devtool: "inline-source-map",
7 | });
8 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const { merge } = require("webpack-merge");
2 | const common = require("./webpack.common.js");
3 | const BundleAnalyzerPlugin =
4 | require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
5 |
6 | module.exports = merge(common, {
7 | mode: "production",
8 | devtool: "hidden-source-map",
9 | plugins: [
10 | new BundleAnalyzerPlugin({ analyzerMode: "static", openAnalyzer: false }),
11 | ],
12 | });
13 |
--------------------------------------------------------------------------------