Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the Game Gig event page #113

Merged
merged 60 commits into from
Nov 29, 2017
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
72320db
Add a development route for the Game Gig page
moosichu Nov 5, 2017
7a65e86
Add Game Gig event content
moosichu Nov 5, 2017
b7e742c
Move reusable components for hackathons to place that make sense
moosichu Nov 5, 2017
293b6bc
Make the clock actually functional!
moosichu Nov 5, 2017
d2b7d1e
Make CurrentTime work on an id basis
moosichu Nov 6, 2017
c7b6695
Add custom CSS mechanism
moosichu Nov 6, 2017
dca8442
Merge branch 'master' of ssh://github.com/hackersatcambridge/hac-webs…
moosichu Nov 6, 2017
74cd5ee
Replace dictionaries with lists of tuples
moosichu Nov 6, 2017
a82c407
Add a functional countdown timer
moosichu Nov 6, 2017
c63a609
Merge branch 'master' of ssh://github.com/hackersatcambridge/hac-webs…
moosichu Nov 6, 2017
4774a7a
Make PostCard Nodeable
moosichu Nov 6, 2017
020ca52
Update code to remove explicit .node cases
moosichu Nov 6, 2017
d675e1f
Implement JavaScript templating in Script.swift
moosichu Nov 6, 2017
f47dec3
Refactor the twitter feed
moosichu Nov 6, 2017
6f72c79
Make CurrentTime.swift use CurrentTime.js
moosichu Nov 6, 2017
45d5ce5
Aim to ensure Date behaviour is correct
moosichu Nov 6, 2017
502bfc6
Clearly label JavaScript string escape location
moosichu Nov 6, 2017
28c410b
Remove hardcoded date objects in CountDownTimer.swift
moosichu Nov 6, 2017
9a621c9
Make CountDownTimer messages configurable
moosichu Nov 6, 2017
129a222
Make the event scheduler use Swift Date objects
moosichu Nov 6, 2017
7aa6208
Cleanup Hackathon protocol
moosichu Nov 7, 2017
41b5125
Move Script.swift to a more central folder
moosichu Nov 7, 2017
2b1e486
Replace usage of 'map' with more sensible for loop
moosichu Nov 7, 2017
5b809a2
Merge branch 'master' of https://github.com/hackersatcambridge/hac-we…
moosichu Nov 7, 2017
452d54c
Merge branch 'master' of github.com:hackersatcambridge/hac-website in…
moosichu Nov 11, 2017
2561af5
Merge branch 'master' of https://github.com/hackersatcambridge/hac-we…
moosichu Nov 16, 2017
d27d9a6
Update Gamegig to use new markdown format
moosichu Nov 16, 2017
0c1f6c0
Make section on rules use multi-line strings and Markdown
moosichu Nov 16, 2017
323c7d1
Make minor GameGig corrections
moosichu Nov 17, 2017
4776fcc
Perform initial styling of the Game Gig 3000
moosichu Nov 17, 2017
4c6d4fd
Highlight important GameGig rules
moosichu Nov 17, 2017
45cdae2
Add cheesy link-hover effect
moosichu Nov 17, 2017
435e0d6
Make Gobo and ES logos links, update Rules
moosichu Nov 17, 2017
616c7d9
Fix typo
moosichu Nov 17, 2017
6e2b1c5
Make corrections based on feedback from Bogdan
moosichu Nov 17, 2017
77631b6
Replace BACKEND_JS_DIR from environment vars with const
moosichu Nov 17, 2017
b05fe5f
Improve styling and remove twitter feed
moosichu Nov 19, 2017
d24ec90
Improve styling of bottom bar
moosichu Nov 19, 2017
42bd916
Merge branch 'master' into feature/game-gig
moosichu Nov 19, 2017
e248f42
Merge branch 'master' into feature/game-gig
moosichu Nov 21, 2017
6bedd71
Add todo for custom .styl files
moosichu Nov 21, 2017
2eee8f9
Update styles
Pinpickle Nov 28, 2017
30c1ff3
Update GameGig event feature and make it point to live event URL
moosichu Nov 28, 2017
bc9c90c
Merge branch 'master' into feature/game-gig
moosichu Nov 28, 2017
9ad9790
Make semicolons in `CountDownTimer.js` consistent
moosichu Nov 29, 2017
403798b
Make semicolons in `CurrentTime.js` consistent
moosichu Nov 29, 2017
a2bd02d
Remove old images
moosichu Nov 29, 2017
aca7ecb
Merge branch 'master' into feature/game-gig
moosichu Nov 29, 2017
a7931aa
Comment on CountDownTimer struct
moosichu Nov 29, 2017
e4e3bfd
Lowercase variable names
moosichu Nov 29, 2017
d4d75a4
Remove traces of twitter feed from game gig
moosichu Nov 29, 2017
3daa214
Link to our custom code of conduct
moosichu Nov 29, 2017
8eb2d00
Improve function for getting gameGigDates
moosichu Nov 29, 2017
68bd026
Add poster link back to the event
moosichu Nov 29, 2017
757c35c
Replace CSS vars with stylus ones
moosichu Nov 29, 2017
100bc45
Rename escapes to definitions
moosichu Nov 29, 2017
8e41e46
Merge branch 'master' into feature/game-gig
moosichu Nov 29, 2017
c86d65e
Add comment to JavaScript protocol
moosichu Nov 29, 2017
5ec4388
Fix error in comments
moosichu Nov 29, 2017
d31a02e
Make links target blank
moosichu Nov 29, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/HaCWebsiteLib/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ public struct Config {
}

public static func checkEnvVars() {
checkEnvVarsExist(for: "DATA_DIR")
checkEnvVarsExist(for: "DATA_DIR", "BACKEND_JS_DIR")
}
}
17 changes: 17 additions & 0 deletions Sources/HaCWebsiteLib/Controllers/HackathonController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation
import Kitura
import HaCTML
import LoggerAPI
import HeliumLogger
import DotEnv
import SwiftyJSON

struct HackathonController {
static func handler(hackathon: Hackathon) -> RouterHandler {
return { request, response, next in
try response.send(
hackathon.node.render()
).end()
}
}
}
38 changes: 38 additions & 0 deletions Sources/HaCWebsiteLib/ViewModels/Hackathons/CountDownTimer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
function updateCountDownTimer() {
const startDate = {{startDate}};
const endDate = {{endDate}};
const CountDownTimerId = {{id}};
const CountDownTimerPreId = {{preId}};
const beforeEventMessage = {{beforeEventMessage}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be consistent with semicolons

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth noting that we're going semicolonless in our gulpfile, following this standard

(up for discussion on this, can probably wait until later)

const duringEventMessage = {{duringEventMessage}}
const afterEventMessage = {{afterEventMessage}}

const current = new Date();

let timeLeft = 0;
if(current < startDate) {
document.getElementById(CountDownTimerPreId).innerHTML = beforeEventMessage;
timeLeft = startDate.getTime() - current.getTime();
} else {
document.getElementById(CountDownTimerPreId).innerHTML = duringEventMessage;
timeLeft = endDate.getTime() - current.getTime();
}

// Abort if time is up
if(timeLeft < 0)
{
document.getElementById(CountDownTimerId).innerHTML = afterEventMessage;
return;
}

let hours = Math.floor(timeLeft / (1000*60*60));
let mins = Math.floor(timeLeft/(1000 * 60) - hours * 60);
let secs = Math.floor(timeLeft/1000 - mins * 60 - hours * 60 * 60);

document.getElementById(CountDownTimerId).innerHTML =
hours + ":" + (mins<10?"0":"") + mins + "<span id=\"seconds\">:" + (secs<10?"0":"") + secs +"</span>";
}

updateCountDownTimer();

setInterval(updateCountDownTimer,1000);
36 changes: 36 additions & 0 deletions Sources/HaCWebsiteLib/ViewModels/Hackathons/CountDownTimer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import HaCTML
import Foundation

struct CountDownTimer : Nodeable {
let startDate : Date
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me this reads like the date that the timer starts counting down. If this is specific to events, maybe we could use some /// doc comments on this struct

let endDate : Date
let id = "CountDownTimer\(UUID().description)"

let preId = "CountDownTimerPre\(UUID().description)" // the id of the countdown message
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't love that we are changing the ID of an element. I expect we can get away without this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have created issue #170 for this, I think fixing this is low priority.

let beforeEventMessage = "Time left to start"
let duringEventMessage = "Time remaining"
let afterEventMessage = "Time's up!"

var node: Node {
return Fragment(
El.Div[
Attr.className => "CountDownTimer__pre",
Attr.id => preId
].containing(""),
El.Div[
Attr.className => "CountDownTimer",
Attr.id => id
].containing("YOU SHOULD SEE THE TIME REMAINING HERE"),
Script(
file: "Hackathons/CountDownTimer.js",
escapes: [
"startDate": startDate, "endDate": endDate,
"id": id, "preId": preId,
"beforeEventMessage": beforeEventMessage,
"duringEventMessage": duringEventMessage,
"afterEventMessage" : afterEventMessage
]
)
)
}
}
9 changes: 9 additions & 0 deletions Sources/HaCWebsiteLib/ViewModels/Hackathons/CurrentTime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function updateClock() {
const id = {{id}}
const current = new Date();
document.getElementById(id).innerHTML = current.getHours()+":"+(current.getMinutes()<10?"0":"") + current.getMinutes();
}

updateClock();

setInterval(updateClock,1000);
16 changes: 16 additions & 0 deletions Sources/HaCWebsiteLib/ViewModels/Hackathons/CurrentTime.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import HaCTML
import Foundation

struct CurrentTime : Nodeable {
let id = "CurrentTime\(UUID().description)"

var node: Node {
return Fragment(
El.Span[Attr.id => id, Attr.className => "CurrentTime"].containing("Current Time"),
Script(
file: "Hackathons/CurrentTime.js",
escapes: ["id": id]
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import HaCTML
import Foundation

// swiftlint:disable line_length


extension String {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! We could move this out to somewhere else. Files for extensions conventionally take the form: e.g. String+idMangle.swift

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created issue #171 for this.

func idMangle() -> String {
return self.replacingOccurrences(of: " ", with: "-").lowercased()
}
}

// TODO: investigate if the information here can be extracted from the landing feature
struct GameGig2017: Hackathon {
let gameEngines = [
("Unreal Engine", "https://www.unrealengine.com"),
("Unity", "https://unity3d.com/"),
("LÖVE", "https://love2d.org/"),
("GameMaker", "lhttps://www.yoyogames.com/gamemaker"),
("raylib", "http://www.raylib.com/")
]

let socialMediaLinks = [
("Facebook Page", "https://www.facebook.com/hackersatcambridge"),
("Facebook Event", "https://www.facebook.com/events/124219834921040/"),
("Twitter", "https://twitter.com/hackersatcam")
]

let tutorials = [
("Web Dev with Mozilla", "https://globalgamejam.org/news/dev-web-mozilla")
]

/**
* Creates a GameGigCard element, the title becomes its id (spaces are replaced with hyphons)
*/
func GameGigCard(title: String, content: Nodeable) -> Node {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear from reading this what a GameGigCard is used for. For the whole game gig page? For sections?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Cards? Like, literally cards. I used the same naming conventions as for the home page here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realised that reads more aggresively than I intended. What I'm saying is, how could I word it better? Because they best way to describe these is that they literally look like floating cards hovering over the background.

return El.Div[Attr.className => "GameGigCard"].containing(
El.Span[
Attr.className => "GameGigCard__title",
Attr.id => title.idMangle()
].containing(title),
content
)
}

func ListOfLinks(dict: [(String, String)]) -> Nodeable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use Link from #163 when that ships

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yip

return El.Ul.containing(
dict.map { (key, value) in
El.Li.containing(
El.A[Attr.href => value].containing(key)
)
}
)
}

func GameGigTwitterFeed() -> Nodeable {
return El.Div[Attr.className => "GameGigTwitterFeed"].containing(
TextNode(
"<a class=\"twitter-timeline\" data-dnt=\"true\" href=\"https://twitter.com/hashtag/hacgamegig\" data-widget-id=\"927201930149093377\">#hacgamegig Tweets</a><script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+\"://platform.twitter.com/widgets.js\";fjs.parentNode.insertBefore(js,fjs);}}(document,\"script\",\"twitter-wjs\");</script>",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have used multi-line strings to avoid the bunch of escapes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was added before then, and tbh the twitter feed isn't used anymore, so might as well delete this.

escapeLevel: .unsafeRaw
)
)
}

// TODO: grab the rulse from the constitution Repo
func Rules() -> Nodeable {
return Fragment(
El.P.containing("The aim of the Game Gig is to create a fun, exciting, original game from scratch in less than 12 hours. We've created a few simple rules to help the process go smoothly for everyone."),
El.Ul.containing(
El.Li.containing("Please respect and look after the Computer Lab. Note that no food or drinks are permitted in the Intel Laboratory, and we will be checking"),
El.Li.containing("You can work on your game in a team of up to four people."),
El.Li.containing("Game-making commences at 10:30 and finishes at 20:00."),
El.Li.containing("You are free to do whatever you like with your game after the Game Gig. You own the copyright to all the material you create during the Game Gig."),
El.Li.containing("You are free to use any tools or libraries available to you to create your game. You can start with any pre-existing code or content that you like and you are free to use third-party assets, as long as you let the judges know what you created yourself and what you didn't. Failure to do so could risk disqualification."),
El.Li.containing("It's your responsibility to make sure that you have the right to use third-party assets (for example, that they are public domain or available under an appropriate license)."),
El.Li.containing(TextNode(
Text(markdown: "Be kind and considerate to your fellow hackers and our volunteers. We're all here to have fun! By participating in the hackathon, you agree to abide by this [Code of Conduct](https://hackcodeofconduct.org/).").html,
escapeLevel: .unsafeRaw
))
)
)
}

func GameGigCardsContainer(content: Nodeable) -> Node {
return El.Div[Attr.className => "GameGigCardsContainer"].containing(
content
)
}

func GameGigTopBanner() -> Node {
return El.Div[Attr.className => "GameGigTopBanner"].containing(
El.Div[Attr.className => "GameGigTopBanner__right"].containing(CurrentTime()),
El.Div[Attr.className => "GameGigTopBanner__left"].containing("Hackers at Cambridge Game Gig 80's")
)
}

func NavBar(elements: [(String, Nodeable)]) -> Node {
return El.Div[Attr.className => "GameGigNavBar"].containing(
El.Span[Attr.className => "GameGigNavBar__logo"].containing("Powered by Studio Gobo and Electric Square"),
Fragment(elements.map{ title, content in
El.Span[Attr.className => "GameGigNavBar__item"].containing(
El.A[
Attr.href => "#\(title.idMangle())"
].containing(
title
)
)
})
)
}

// Convert a given time in String format, (eg. "12:00"), to the corresponding
// Swift Date object representing that time on the day of the event itself
let eventDate = Date.from(year: 2017, month: 12, day: 1, hour: 0, minute: 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably clearer to move this into the function itself. It's a little confusing as it's named eventDate but is not the startDate of the game gig (since game gig doesn't start at midnight) so don't want it used elsewhere accidentally

func gameGigDate(_ time: String) -> Date {
// To the dirty work by converting the events date to a string, appending the time, and convertin to a date again
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙈

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could have been nicer just to do:

func gameGigDate(hour: Int, minute: Int) -> Date {
  return Date.from(year: 2017, month: 12, day: 1, hour: hour, minute: minute)
}

;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's why we do these code reviews!

I spent way too long thinking about this :/

let outFormatter = DateFormatter()
outFormatter.dateFormat = "yyyy-MM-dd"
outFormatter.timeZone = TimeZone(identifier: "Europe/London")
outFormatter.locale = Locale(identifier: "en_GB")

let dateTimeString = outFormatter.string(from: eventDate) + " " + time

let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
formatter.timeZone = TimeZone(identifier: "Europe/London")
formatter.locale = Locale(identifier: "en_GB")
let dateTime = formatter.date(from: dateTimeString)
return dateTime ?? Date()
}

var node: Node {
// Define all time related info
let gigStartDate = gameGigDate("10:30")
let gigEndDate = gameGigDate("20:30")


let schedule = [
("Arrival", gameGigDate("10:00")),
("Start Jamming!", gigStartDate),
("Lunch", gameGigDate("12:00")),
("Dinner", gameGigDate("18:00")),
("Stop Jamming!", gigEndDate),
("LT1 Prizes and wrap-up", gameGigDate("21:00"))
]

// Define the list of game gig "cards" that are displayed as content
let gameGigCards = [
("Schedule", Schedule(schedule: schedule)),
("Feed", GameGigTwitterFeed()),
("Get Involved", ListOfLinks(dict: socialMediaLinks)),
("Game Engines", ListOfLinks(dict: gameEngines)),
("Tutorials", ListOfLinks(dict: tutorials)),
("Rules", Rules())
]

return UI.Pages.base(
title: "Hackers at Cambridge Game Gig 80's",
customStylesheets: ["gamegig2017"],
content: Fragment(
GameGigTopBanner(),
NavBar(elements: gameGigCards),
CountDownTimer(startDate: gigStartDate, endDate: gigEndDate),
GameGigCardsContainer(content: Fragment(
gameGigCards.map{ title, content in
GameGigCard(title: title, content: content)
}
))
)
)
}
}
5 changes: 5 additions & 0 deletions Sources/HaCWebsiteLib/ViewModels/Hackathons/Hackathon.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import HaCTML

public protocol Hackathon: Nodeable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because in the future we will have multiple hackathons. TBH I created this when I wasn't sure if there was going to be a set format or not. Shuold I remove?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would maybe be useful if there was any specific data that we needed Hackathons to have or thought that we would require Hackathons to do something or expose some data in the future. If you can see a use case then leave it in but otherwise let's ditch

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think this will become useful once we have properly defined event streams, so I will leave it in.


}
24 changes: 24 additions & 0 deletions Sources/HaCWebsiteLib/ViewModels/Hackathons/Schedule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import HaCTML
import Foundation

// TODO: add the ability to highlight the item in the schedule best on the time!
struct Schedule : Nodeable {
let schedule: [(String, Date)]

var node: Node {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm"
formatter.timeZone = TimeZone(identifier: "Europe/London")
formatter.locale = Locale(identifier: "en_GB")
return El.Ul.containing(
schedule.map {event, date in
let timeString = formatter.string(from: date)
return El.Li.containing(
timeString,
" ",
event
)
}
)
}
}
62 changes: 62 additions & 0 deletions Sources/HaCWebsiteLib/ViewModels/Hackathons/Script.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import HaCTML
import DotEnv
import Foundation

protocol JavaScriptable {
var javaScript: String {get}
}

extension String: JavaScriptable {
var javaScript : String {
// TODO: PROPERLY ESCAPE THIS! (eg. conver newlines to backslash)
// NOTE: A JSON LIBRARY COULD BE USED FOR THIS!
return "\"\(self)\""
}
}

extension Date: JavaScriptable {
var javaScript : String {
let calendar = Calendar.current
let year = calendar.component(.year, from: self)
// We have to subtract 1 from the month as JavaScript months count from 0
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
let month = calendar.component(.month, from: self) - 1
let day = calendar.component(.day, from: self)
let hour = calendar.component(.hour, from: self)
let minute = calendar.component(.minute, from: self)
let second = calendar.component(.second, from: self)
let milliseconds = 0
return "new Date(Date.UTC(\(year),\(month),\(day),\(hour),\(minute),\(second),\(milliseconds)))"
}
}

struct UnsafeRawJavaScript {
let rawScript : String
var javaScript : String {
return rawScript
}
}

/**
* This class is used in order load front-end scripts from a file relative to the current path for browsing pleasure.
*
* It depends on the build system loading files into the 'Data' folder before running.
*/
struct Script : Nodeable {
let file : String
let escapes : [String: JavaScriptable]
let directory : String = DotEnv.get("BACKEND_JS_DIR")!
var node: Node {
let pathToFile = directory + "/" + file
do {
var script = try String(contentsOfFile: pathToFile, encoding: .utf8)
escapes.map({ key, value in
// TODO: find out if there is a way of doing these escapes in a Type-Safe manner!
script = script.replacingOccurrences(of: "{{\(key)}}", with: "\(value.javaScript)")
})
return El.Script.containing(TextNode(script, escapeLevel: .unsafeRaw))
} catch {
return El.Script.containing(TextNode("console.log(\"failed to load \(pathToFile)\");", escapeLevel: .unsafeRaw))
}
}
}
Loading