-
Notifications
You must be signed in to change notification settings - Fork 1
/
SimpleWeb.swift
495 lines (437 loc) · 15.1 KB
/
SimpleWeb.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
/**
SimpleWeb
---------
This is a module for simple CGI web programming using Swift 3.
v.1.0 - Oct, 2016
© pak.lebah
*/
import Foundation
/* --- INTERNAL --- */
var webInput = ""
var webQuery = [String]()
var inputIndex = 0
var leftWidth = 0
var listOrdered = false
var start: Date? = nil
// various basic print methods
func write (_ x: Any...) { for i in x { print(i, terminator:"") } }
func writeln(_ x: Any...) { for i in x { print(i, terminator:"") }; print("") }
func write <T>(_ x: T) { print(x, terminator:"") }
func writeln<T>(_ x: T) { print(x) }
func writeln() { print("") }
// string extension
extension String {
// decode http text to plain string
func decodeHTTP() -> String {
return self.removingPercentEncoding!.replacingOccurrences(of:"+",with:" ")
}
// encode plain string to html text
func encodeHTML() -> String {
return self.replacingOccurrences(of:"&",with:"&")
.replacingOccurrences(of:"<",with:"<")
.replacingOccurrences(of:">",with:">")
}
}
// remove empty elements from an array of string
func removeEmptyString(from array: inout [String]) {
if array.count > 0 {
for (i, element) in array.enumerated().reversed() {
if element.isEmpty { array.remove(at: i) }
}
}
}
/*** web input/output operation ***/
// read all web request values
func readInput() -> String {
// read from get and post methods
var g, p: String
if let s = String(utf8String: getenv("QUERY_STRING")) { g = s } else { g = "" }
if let s = readLine() { p = s } else { p = "" }
return g == "" ? p : (g + (p == "" ? "" : "&" + p)) // combine all
}
// check for input existence by index
func hasInput(at index: Int) -> Bool {
for item in webQuery {
if item.hasPrefix("input_\(index)=") { return true }
}
return false
}
// read web input value by index
func getInput(at index: Int) -> String {
for item in webQuery {
if item.hasPrefix("input_\(index)=") {
return item.components(separatedBy: "=")[1].decodeHTTP()
}
}
return ""
}
// write hidden var input element
func writeVarInput<T>(name: String, value: T) {
writeln("<input type='hidden' name='\(name)' value='\(value)'/>")
// don't forget to increase input counter each time it writes an input element
inputIndex += 1
}
// write checkbox input element
func writeBoolInput(_ value: Bool, label: String, newLine: Bool) {
write("<label><input type='checkbox' ")
write("id='input_\(inputIndex)' name='input_\(inputIndex)' value='true'")
write(value ? " checked> " : "/> ")
write(label,"</label>")
write(newLine ? "<br>\n" : "\n")
inputIndex += 1
}
// write text input element
func writeTextInput<T>(_ value: T, newLine: Bool, asHolder: Bool) {
write("<input type='text' id='input_\(inputIndex)' name='input_\(inputIndex)' ")
write(asHolder ? "value='' placeholder='\(value)'/>" : "value='\(value)'/>")
write(newLine ? "<br>\n" : "\n")
inputIndex += 1
}
// write text memo input element
func writeMemoInput(_ value: String, newLine: Bool) {
write("<textarea id='input_\(inputIndex)' name='input_\(inputIndex)'>\n")
write(value,"</textarea>\n")
write(newLine ? "<br>\n" : "\n")
inputIndex += 1
}
// write radio button input element
func writeOptionInput(_ value: Int, labels: [String], newLine: Bool) {
for (i, label) in labels.enumerated() {
if newLine && i > 0 {
if leftWidth > 0 {
write("<span class='input' style='width:\(leftWidth)px'> </span>")
} else {
write("<span class='input'> </span>") // left alignment from css
}
}
write("<label><input type='radio' id='input_\(inputIndex)' name='input_\(inputIndex)' ")
write("value='option_\(i)'")
write(i == value ? " checked/> " : "/> ")
write(label,"</label>")
write(newLine ? "<br>\n" : (i < labels.count-1 ? " │ \n" : "\n"))
}
inputIndex += 1
}
// write combo box input element
func writeSelectInput(_ value: Int, items: [String], newLine: Bool) {
write("<select id='input_\(inputIndex)' name='input_\(inputIndex)'>\n")
if value < 0 { write("<option></option>\n") } // enable no selection
for (i, item) in items.enumerated() {
write("<option value='item_\(i)'")
write(i == value ? " selected> " : "> ")
write(item,"</option>\n")
}
write("</select>")
write(newLine ? "<br>\n" : "\n")
inputIndex += 1
}
// write button input element
func writeButtonInput(caption: String, newLine: Bool ) {
write("<button type='submit' id='input_\(inputIndex)' name='input_\(inputIndex)' ")
write("value='clicked'>\(caption)</button>")
write(newLine ? "<br>\n" : "\n")
inputIndex += 1
}
// write default html header
func writeHeader(_ appName: String, cssFile: [String], jsFile: [String]) {
writeln("cache-control: no-cache, no-store, must-revalidate");
writeln("pragma: no-cache");
writeln("vary:*");
writeln("content-type: text/html;")
writeln()
writeln("<!DOCTYPE html>")
writeln("<html lang='en'>")
writeln(" <head>")
writeln(" <meta charset='utf-8'>")
writeln(" <meta name='viewport' content='width=device-width,initial-scale=1'>")
writeln(" <style>span.input { display:inline-block; vertical-align:top; }</style>")
for css in cssFile { writeln(" <link rel='stylesheet' href='\(css)'>")}
for js in jsFile { writeln(" <script type='text/javascript' src='\(js)'></script>")}
writeln(" <title>\(appName) - SimpleWeb</title>")
writeln(" </head>")
writeln(" <body>")
}
// write default html footer
func writeFooter(cssFile: [String], jsFile: [String]) {
for css in cssFile { writeln(" <link rel='stylesheet' href='\(css)'>")}
for js in jsFile { writeln(" <script type='text/javascript' src='\(js)'></script>")}
writeln(" </body>")
write ("</html>")
}
/* --- PUBLIC --- */
public enum HTTPMethod { case GET, HEAD, POST, PUT, DELETE }
public private(set) var webHasInput = false // read only var
/* basic web template */
// open html doc
public func openHTML(title: String = "", method: HTTPMethod = .POST, cssFile: [String] = [], jsFile: [String] = []) {
// start timer
start = Date()
// read local CGI executable URL
let exeURL = URL(fileURLWithPath: CommandLine.arguments[0])
// let exeFull = exeURL.absoluteString
// let exePath = exeURL.deletingLastPathComponent().path
let exeName = exeURL.deletingPathExtension().lastPathComponent
let exeExt = exeURL.pathExtension
// read web input and save them into an array
webInput = readInput()
webHasInput = !webInput.isEmpty
if webHasInput {
// save web input as array
webQuery = webInput.components(separatedBy: "&")
removeEmptyString(from: &webQuery)
}
// check for default css/js file
var cssFiles = cssFile
var jsFiles = jsFile
if FileManager.default.fileExists(atPath: "\(exeName).css") { cssFiles += ["\(exeName).css"] }
if FileManager.default.fileExists(atPath: "\(exeName).js") { jsFiles += ["\(exeName).js"] }
// write web header and create form
writeHeader((title.isEmpty ? exeName : title), cssFile: cssFiles, jsFile: jsFiles)
writeln(" <form method='\(method == .POST ? "post" : "get")' action='\(exeName).\(exeExt)'>")
writeln(" <!-- ***** user code begin here ***** --!>")
}
// close hmtl doc
public func closeHTML(cssFile: [String] = [], jsFile: [String] = []) {
writeln(" <!-- ***** user code end here ***** --!>")
writeln(" <hr>")
// close form and write web footer
if inputIndex > 0 { writeln(" <input type='submit' value=' SUBMIT '/>") }
writeln(" </form>")
// stop timer
let stop = String(format: "%.1f", abs(start!.timeIntervalSinceNow)*100) // in ms
writeln(" <p align='right'><small><i>This page is served in \(stop) ms.</i></small>")
writeFooter(cssFile: cssFile, jsFile: jsFile)
}
/* common value read/write operation */
// check for a web var using its name
public func webHasVar(_ name: String) -> Bool {
for item in webQuery {
if item.hasPrefix("\(name)=") { return true }
}
return false
}
// write value into a web variable
public func writeWebVar<T>(_ name: String, value: T) {
writeVarInput(name: name, value: value)
}
// read value from a web variable
public func readWebVar(_ name: String) -> String {
for item in webQuery {
if item.hasPrefix("\(name)=") {
return item.components(separatedBy: "=")[1].decodeHTTP()
}
}
return ""
}
// write output without new line into html doc
public func webWrite<T>(_ value: T, width: Int = leftWidth) {
if width == 0 { write(value) }
else if width < 0 { write("<span class='input'>\(value)</span>"); }
else { write("<span class='input' style='width:\(width)px'>\(value)</span>"); }
leftWidth = width
}
// write output with new line into html doc and reset left alignment
public func webWriteln<T>(_ value: T) { writeln(value,"<br>"); leftWidth = 0 }
public func webWriteln() { writeln("<br>"); leftWidth = 0 }
// read boolean value from web input
public func webRead(_ value: Bool = false, label: String, newLine: Bool = false) -> Bool {
let webValue = webHasInput ? (getInput(at: inputIndex) == "true") : value
if webHasInput {
writeBoolInput(webValue, label: label, newLine: newLine)
return webValue
} else {
writeBoolInput(value, label: label, newLine: newLine)
return value
}
}
public func webReadln(_ value: Bool = false, label: String) -> Bool {
return webRead(value, label: label, newLine: true)
}
// read string value from web input
public func webRead(_ value: String = "", newLine: Bool = false) -> String {
let webValue = webHasInput ? getInput(at: inputIndex) : value
if webHasInput {
writeTextInput(webValue, newLine: newLine, asHolder: false)
return webValue
} else {
writeTextInput(value, newLine: newLine, asHolder: false)
return value
}
}
public func webReadln(_ value: String = "") -> String {
return webRead(value, newLine: true)
}
// read integer value from web input
public func webRead(_ value: Int = 0, newLine: Bool = false) -> Int {
let webValue = getInput(at: inputIndex)
if webHasInput {
if webValue.isEmpty {
writeTextInput(value, newLine: newLine, asHolder: true)
} else {
if let int = Int(webValue) {
writeTextInput(webValue, newLine: newLine, asHolder: false)
return int
} else {
writeTextInput("<invalid input>", newLine: newLine, asHolder: true)
}
}
} else {
writeTextInput(value == 0 ? "" : String(value), newLine: newLine, asHolder: false)
}
return value
}
public func webReadln(_ value: Int = 0) -> Int {
return webRead(value, newLine: true)
}
// read floating point value from web input
public func webRead(_ value: Double = 0.0, newLine: Bool = false) -> Double {
let webValue = getInput(at: inputIndex)
if webHasInput {
if webValue.isEmpty {
writeTextInput(value, newLine: newLine, asHolder: true)
} else {
if let float = Double(webValue) {
writeTextInput(webValue, newLine: newLine, asHolder: false)
return float
} else {
writeTextInput("<invalid input>", newLine: newLine, asHolder: true)
}
}
} else {
writeTextInput(value == 0.0 ? "" : String(value), newLine: newLine, asHolder: false)
}
return value
}
public func webReadln(_ value: Double = 0.0) -> Double {
return webRead(value, newLine: true)
}
// read long text from web input
public func webReadMemo(_ value: String = "", newLine: Bool = true) -> String {
let webValue = getInput(at: inputIndex)
if webHasInput {
writeMemoInput(webValue, newLine: newLine)
return webValue
} else {
writeMemoInput(value, newLine: newLine)
return value
}
}
// read options from web input
public func webReadOption(_ value: Int = -1, labels: [String], newLine: Bool = true) -> Int {
let webValue = getInput(at: inputIndex)
if webHasInput {
var option = -1 // non chosen
if !webValue.isEmpty {
// read picked option
if let r = webValue.range(of: "_") {
let s = webValue[webValue.index(r.lowerBound, offsetBy:1) ..< webValue.endIndex]
if let n = Int(s) { option = n }
}
}
writeOptionInput(option, labels: labels, newLine: newLine)
return option
} else {
writeOptionInput(value, labels: labels, newLine: newLine)
return value
}
}
// read selection from web input
public func webReadSelect(_ value: Int = -1, items: [String], newLine: Bool = true) -> Int {
let webValue = getInput(at: inputIndex)
if webHasInput {
var selected = -1 // non selected
if !webValue.isEmpty {
// read selected item
if let r = webValue.range(of: "_") {
let s = webValue[webValue.index(r.lowerBound, offsetBy:1) ..< webValue.endIndex]
if let n = Int(s) { selected = n }
}
}
writeSelectInput(selected, items: items, newLine: newLine)
return selected
} else {
writeSelectInput(value, items: items, newLine: newLine)
return value
}
}
// read button clicked state
public func webReadButton(_ caption: String, newLine: Bool = true) -> Bool {
let clicked = (getInput(at: inputIndex) == "clicked")
writeButtonInput(caption: caption, newLine: newLine)
return clicked
}
/* common html output tags */
// write web page header
public func webPageHeader(_ text: String, level: Int = 3) {
writeln("<h\(level)>\(text)</h\(level)>")
}
// write blockquoted text
public func webWriteBlock(_ text: String) {
writeln("<blockquote>\(text)</blockquote>")
}
// get link text
public func webGetLink(_ url: String, caption: String = "", newTab: Bool = false) -> String {
var result = "<a href='\(url)'"
if newTab { result += " target=_blank>"} else { result += ">"}
if !caption.isEmpty { result += caption } else { result += url }
result += "</a>"
return result
}
/* html table operation */
// open a html table tag with header
public func webOpenTable(_ headers: [String], tClass: String = "", tID: String = "") {
write("<table")
if !tClass.isEmpty { write(" class='\(tClass)'") }
if !tID.isEmpty { write(" id='\(tID)'") }
writeln(">")
if headers.count > 0 {
write("<tr>")
for header in headers { write("<th>\(header)</th>") }
writeln("</tr>")
}
}
// write a table row
public func webTableRow(_ cells: [String]) {
write("<tr>")
if cells.count > 0 {
for cell in cells { write("<td>\(cell)</td>") }
}
writeln("</tr>")
}
// close html table tag
public func webCloseTable() {
writeln("</table>")
}
/* html list operation */
// open html list tag
public func webOpenList(ordered: Bool = true, lClass: String = "", lID: String = "" ) {
listOrdered = ordered
if listOrdered { write("<ol") } else { write("<ul") }
if !lClass.isEmpty { write(" class='\(lClass)'") }
if !lID.isEmpty { write(" id='\(lID)'")}
writeln(">")
}
// add list item
public func webListItem(_ item: String) {
writeln("<li>\(item)</li>")
}
// close html list tag
public func webCloseList() {
if listOrdered { writeln("</ol>") } else { writeln("</ul>") }
}
/* other html operation */
// insert stylesheet into html
public func webWriteCSS(_ css: String) {
writeln("\n<style>\(css)</style>")
}
// insert javascript into html
public func webWriteJS(_ js: String) {
writeln("\n<script>\(js)</script>")
}
// check user agent to check whether user device type is mobile
public func isMobile() -> Bool {
if let s = String(utf8String: getenv("HTTP_USER_AGENT")) {
return s.lowercased().range(of: "mobile") != nil ? true : false
}
return false
}