├── .circleci └── config.yml ├── .codebeatignore ├── .codecov.yml ├── .gitignore ├── .swiftlint.yml ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Bootstrap │ ├── Extensions │ └── LeafTagConfig.swift │ ├── Models │ └── ColorKeys.swift │ └── Tags │ ├── Alert.swift │ ├── Badge.swift │ ├── Breadcrumb.swift │ ├── Button.swift │ ├── ButtonGroup.swift │ ├── ButtonToolbar.swift │ ├── Card.swift │ ├── FormCheckbox.swift │ ├── FormFile.swift │ ├── FormRadio.swift │ ├── Input.swift │ └── TextArea.swift └── Tests ├── BootstrapTests ├── BootstrapTests.swift └── XCTestManifests.swift └── LinuxMain.swift /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | MacOS: 4 | macos: 5 | xcode: "9.3.0" 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | keys: 10 | - v1-spm-deps-{{ checksum "Package.swift" }} 11 | - run: 12 | name: Install CMySQL and CTLS 13 | command: | 14 | export HOMEBREW_NO_AUTO_UPDATE=1 15 | brew tap vapor/homebrew-tap 16 | brew install cmysql 17 | brew install ctls 18 | brew install libressl 19 | - run: 20 | name: Build and Run Tests 21 | no_output_timeout: 1800 22 | command: | 23 | swift package generate-xcodeproj --enable-code-coverage 24 | xcodebuild -scheme Bootstrap-Package -enableCodeCoverage YES test | xcpretty 25 | - run: 26 | name: Report coverage to Codecov 27 | command: | 28 | bash <(curl -s https://codecov.io/bash) 29 | - save_cache: 30 | key: v1-spm-deps-{{ checksum "Package.swift" }} 31 | paths: 32 | - .build 33 | Linux: 34 | docker: 35 | - image: nodesvapor/vapor-ci:swift-4.1 36 | steps: 37 | - checkout 38 | - restore_cache: 39 | keys: 40 | - v2-spm-deps-{{ checksum "Package.swift" }} 41 | - run: 42 | name: Copy Package file 43 | command: cp Package.swift res 44 | - run: 45 | name: Build and Run Tests 46 | no_output_timeout: 1800 47 | command: | 48 | swift test -Xswiftc -DNOJSON 49 | - run: 50 | name: Restoring Package file 51 | command: mv res Package.swift 52 | - save_cache: 53 | key: v2-spm-deps-{{ checksum "Package.swift" }} 54 | paths: 55 | - .build 56 | workflows: 57 | version: 2 58 | build-and-test: 59 | jobs: 60 | - MacOS 61 | - Linux 62 | experimental: 63 | notify: 64 | branches: 65 | only: 66 | - master 67 | - develop 68 | -------------------------------------------------------------------------------- /.codebeatignore: -------------------------------------------------------------------------------- 1 | Public/** 2 | Resources/Assets/** -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "0...100" 3 | ignore: 4 | - "Tests" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Packages 2 | .build 3 | .idea 4 | .DS_Store 5 | *.xcodeproj 6 | DerivedData/ 7 | Package.resolved 8 | .swiftpm 9 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | function_body_length: 4 | warning: 60 5 | variable_name: 6 | min_length: 7 | warning: 2 8 | line_length: 80 9 | disabled_rules: 10 | - opening_brace 11 | colon: 12 | flexible_right_spacing: true 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Nodes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.1 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Bootstrap", 6 | products: [ 7 | .library(name: "Bootstrap", targets: ["Bootstrap"]) 8 | ], 9 | dependencies: [ 10 | .package(url: "https://github.com/nodes-vapor/sugar.git", from: "4.0.0"), 11 | .package(url: "https://github.com/vapor/leaf.git", from: "3.0.0"), 12 | .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), 13 | ], 14 | targets: [ 15 | .target(name: "Bootstrap", dependencies: ["Leaf", "Vapor", "Sugar"]), 16 | .testTarget(name: "BootstrapTests", dependencies: ["Bootstrap"]) 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bootstrap 🍃 2 | 3 | [![Swift Version](https://img.shields.io/badge/Swift-4.1-brightgreen.svg)](http://swift.org) 4 | [![Vapor Version](https://img.shields.io/badge/Vapor-3-30B6FC.svg)](http://vapor.codes) 5 | [![Circle CI](https://circleci.com/gh/nodes-vapor/bootstrap/tree/master.svg?style=shield)](https://circleci.com/gh/nodes-vapor/bootstrap) 6 | [![codebeat badge](https://codebeat.co/badges/40b8811e-2949-427a-a2a7-437209475f7d)](https://codebeat.co/projects/github-com-nodes-vapor-bootstrap-master) 7 | [![codecov](https://codecov.io/gh/nodes-vapor/bootstrap/branch/master/graph/badge.svg)](https://codecov.io/gh/nodes-vapor/bootstrap) 8 | [![Readme Score](http://readme-score-api.herokuapp.com/score.svg?url=https://github.com/nodes-vapor/bootstrap)](http://clayallsopp.github.io/readme-score?url=https://github.com/nodes-vapor/bootstrap) 9 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/nodes-vapor/bootstrap/master/LICENSE) 10 | 11 | This package wraps Bootstrap elements into convenient Leaf-Tags. 12 | 13 | 14 | # Installation 15 | 16 | Add `Bootstrap` to the package dependencies (in your `Package.swift` file): 17 | 18 | ```swift 19 | dependencies: [ 20 | ..., 21 | .package(url: "https://github.com/nodes-vapor/bootstrap.git", from: "4.0.0") 22 | ] 23 | ``` 24 | 25 | as well as to your target (e.g. "App"): 26 | 27 | ```swift 28 | targets: [ 29 | ... 30 | .target( 31 | name: "App", 32 | dependencies: [... "Bootstrap" ...] 33 | ), 34 | ... 35 | ] 36 | ``` 37 | 38 | ## Getting started 🚀 39 | 40 | First import Bootstrap and Leaf inside your `configure.swift` 41 | 42 | ```swift 43 | import Bootstrap 44 | import Leaf 45 | ``` 46 | 47 | ### Adding the Leaf tags 48 | 49 | In order to render the Bootstrap elements, you will need to add the Bootstrap Leaf tags: 50 | 51 | ```swift 52 | public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { 53 | services.register { _ -> LeafTagConfig in 54 | var tags = LeafTagConfig.default() 55 | tags.useBootstrapLeafTags() 56 | return tags 57 | } 58 | } 59 | ``` 60 | 61 | ## Supported tags 62 | 63 | - [Alert](#alert) 64 | - [Badge](#badge) 65 | - [Button](#button) 66 | - [Button Group](#button-group) 67 | - [Button Toolbar](#button-toolbar) 68 | - [Input](#input) 69 | - [Breadcrumb](#breadcrumb) 70 | - [Textarea](#textarea) 71 | 72 | ### Alert 73 | 74 | ``` 75 | #bs:alert() { alert text } 76 | ``` 77 | 78 | ### Badge 79 | 80 | ``` 81 | #bs:badge(type?, classExtras?, attributes?) { badge text } 82 | ``` 83 | 84 | ### Button 85 | 86 | ``` 87 | #bs:button(type?, classExtras?, attributes?) { btn text } 88 | ``` 89 | 90 | ### Button Group 91 | 92 | ``` 93 | #bs:buttonGroup(isVertical, classExtras?, Aria?) { } 94 | ``` 95 | 96 | ``` 97 | #bs:buttonGroup(false, "btn-group-sm") { 98 | #bs:button() { First Option } 99 | #bs:button("danger") { Second Option} 100 | #bs:button() { Third Option} 101 | } 102 | 103 | ``` 104 | 105 | ### Button Toolbar 106 | 107 | ``` 108 | #bs:buttonToolbar(classExtras?, Aria?) { } 109 | ``` 110 | 111 | ``` 112 | #bs:buttonToolbar() { 113 | #bs:button() { First Option } 114 | #bs:button("danger") { Second Option} 115 | #bs:button() { Third Option} 116 | } 117 | ``` 118 | 119 | ### Input 120 | 121 | ``` 122 | #bs:input(type?, classExtras?, attributes?) 123 | ``` 124 | 125 | ### Breadcrumb 126 | 127 | ``` 128 | #bs:breadcrumb(classExtras?, attributes?) { 129 | #bs:breadcrumbItem(classExtras?, attributes?) { Home } 130 | #bs:breadcrumbItem(classExtras?, attributes?) { Profile } 131 | } 132 | ``` 133 | 134 | ### Textarea 135 | 136 | ``` 137 | #bs:textArea(classExtras?, attributes?, value?) 138 | ``` 139 | 140 | ### Card 141 | 142 | ``` 143 | #bs:card(title?, classExtras?, attributes?) { } 144 | ``` 145 | 146 | or 147 | 148 | ``` 149 | #bs:card:outer(title?, classExtras?, attributes?) { 150 | #bs:card:header(classExtras?, attributes?) { } 151 | #bs:card:body(classExtras?, attributes?) { } 152 | #bs:card:footer(classExtras?, attributes?) { } 153 | } 154 | ``` 155 | 156 | ## 🏆 Credits 157 | 158 | This package is developed and maintained by the Vapor team at [Nodes](https://www.nodesagency.com). 159 | 160 | ## 📄 License 161 | 162 | This package is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT) 163 | -------------------------------------------------------------------------------- /Sources/Bootstrap/Extensions/LeafTagConfig.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | 3 | extension LeafTagConfig { 4 | public mutating func useBootstrapLeafTags() { 5 | use([ 6 | "bs:alert": AlertTag(), 7 | "bs:badge": BadgeTag(), 8 | "bs:breadCrumb": BreadCrumbTag(), 9 | "bs:breadCrumbItem": BreadCrumbItemTag(), 10 | "bs:button": ButtonTag(), 11 | "bs:buttonGroup": ButtonGroupTag(), 12 | "bs:buttonToolbar": ButtonToolbarTag(), 13 | "bs:card": CardTag(), 14 | "bs:card:body": CardBodyTag(), 15 | "bs:card:footer": CardFooterTag(), 16 | "bs:card:header": CardHeaderTag(), 17 | "bs:card:outer": CardOuterTag(), 18 | "bs:formCheckbox": FormCheckbox(), 19 | "bs:formFile": FormFile(), 20 | "bs:formRadio": FormRadio(), 21 | "bs:input": InputTag(), 22 | "bs:textArea": TextAreaTag() 23 | ]) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Bootstrap/Models/ColorKeys.swift: -------------------------------------------------------------------------------- 1 | /// Bootstrap Color Definitions 2 | enum ColorKeys: String { 3 | /// Bootstrap Primary Color 4 | case primary 5 | /// Bootstrap Secondary Color 6 | case secondary 7 | /// Bootstrap Success Color 8 | case success 9 | /// Bootstrap Danger Color 10 | case danger 11 | /// Bootstrap Warning Color 12 | case warning 13 | /// Bootstrap Info Color 14 | case info 15 | /// Bootstrap Light Color 16 | case light 17 | /// Bootstrap Dark Color 18 | case dark 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Bootstrap/Tags/Alert.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | import TemplateKit 3 | 4 | /// Bootstrap Alert Tag 5 | public final class AlertTag: TagRenderer { 6 | public func render(tag: TagContext) throws -> Future { 7 | var style = ColorKeys.primary.rawValue 8 | var classes: String? 9 | var attributes: String? 10 | 11 | if tag.parameters.count > 0 { 12 | guard let param = tag.parameters[0].string else { 13 | throw tag.error( 14 | reason: "Wrong type given (expected a string): \(type(of: tag.parameters[0]))" 15 | ) 16 | } 17 | 18 | if param.count > 0 { 19 | style = param 20 | } 21 | } 22 | 23 | if tag.parameters.count > 1 { 24 | guard let param = tag.parameters[1].string else { 25 | throw tag.error( 26 | reason: "Wrong type given (expected a string): \(type(of: tag.parameters[1]))" 27 | ) 28 | } 29 | 30 | if param.count > 0 { 31 | classes = param 32 | } 33 | } 34 | 35 | if tag.parameters.count > 2 { 36 | guard let param = tag.parameters[2].string else { 37 | throw tag.error( 38 | reason: "Wrong type given (expected a string): \(type(of: tag.parameters[2]))" 39 | ) 40 | } 41 | 42 | if param.count > 0 { 43 | attributes = param 44 | } 45 | } 46 | 47 | guard let parsedStyle = ColorKeys(rawValue: style) else { 48 | throw tag.error(reason: "Wrong argument given: \(style)") 49 | } 50 | 51 | guard let body = tag.body else { 52 | throw tag.error(reason: "Wrong body given: \(String(describing: tag.body))") 53 | } 54 | 55 | return tag.serializer.serialize(ast: body).map(to: TemplateData.self) { er in 56 | let body = String(data: er.data, encoding: .utf8) ?? "" 57 | 58 | var alert = "" 64 | 65 | return .string(alert) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/Bootstrap/Tags/Badge.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | import Sugar 3 | import TemplateKit 4 | 5 | public final class BadgeTag: TagRenderer { 6 | public func render(tag: TagContext) throws -> Future { 7 | let body = try tag.requireBody() 8 | 9 | var style = ColorKeys.primary.rawValue 10 | var classes = "" 11 | var attributes = "" 12 | 13 | for index in 0...2 { 14 | if 15 | let param = tag.parameters[safe: index]?.string, 16 | !param.isEmpty 17 | { 18 | switch index { 19 | case 0: style = param 20 | case 1: classes = param 21 | case 2: attributes = param 22 | default: break 23 | } 24 | } 25 | } 26 | 27 | guard let parsedStyle = ColorKeys(rawValue: style) else { 28 | throw tag.error(reason: "Wrong argument given: \(style)") 29 | } 30 | 31 | return tag.serializer.serialize(ast: body).map(to: TemplateData.self) { body in 32 | let c = "badge badge-\(parsedStyle) \(classes)" 33 | let b = String(data: body.data, encoding: .utf8) ?? "" 34 | 35 | let badge = "\(b)" 36 | return .string(badge) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Bootstrap/Tags/Breadcrumb.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | import Sugar 3 | import TemplateKit 4 | 5 | public final class BreadCrumbTag: TagRenderer { 6 | public func render(tag: TagContext) throws -> Future { 7 | let body = try tag.requireBody() 8 | var classes = "" 9 | var attributes = "" 10 | 11 | for index in 0...1 { 12 | if 13 | let param = tag.parameters[safe: index]?.string, 14 | !param.isEmpty 15 | { 16 | switch index { 17 | case 0: classes = param 18 | case 1: attributes = param 19 | default: break 20 | } 21 | } 22 | } 23 | 24 | return tag.serializer.serialize(ast: body).map { body in 25 | let b = String(data: body.data, encoding: .utf8) ?? "" 26 | 27 | let breadcrumb = """ 28 |
29 | 32 |
33 | """ 34 | return .string(breadcrumb) 35 | } 36 | } 37 | } 38 | 39 | public final class BreadCrumbItemTag: TagRenderer { 40 | public func render(tag: TagContext) throws -> Future { 41 | let body = try tag.requireBody() 42 | var classes = "" 43 | var attributes = "" 44 | 45 | for index in 0...2 { 46 | if 47 | let param = tag.parameters[safe: index]?.string, 48 | !param.isEmpty 49 | { 50 | switch index { 51 | case 0: classes = param 52 | case 1: attributes = param 53 | default: break 54 | } 55 | } 56 | } 57 | 58 | return tag.serializer.serialize(ast: body).map { body in 59 | let c = "breadcrumb-item \(classes)" 60 | let b = String(data: body.data, encoding: .utf8) ?? "" 61 | let breadcrumbItem = "
  • \(b)
  • " 62 | return .string(breadcrumbItem) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/Bootstrap/Tags/Button.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | import Sugar 3 | import TemplateKit 4 | 5 | public final class ButtonTag: TagRenderer { 6 | /// A button style can be any of `ColorKeys` or "link". 7 | public enum Keys: String { 8 | case link 9 | } 10 | 11 | public func render(tag: TagContext) throws -> Future { 12 | let body = try tag.requireBody() 13 | 14 | var style = ColorKeys.primary.rawValue 15 | var classes = "" 16 | var attributes = "" 17 | 18 | for index in 0...2 { 19 | if 20 | let param = tag.parameters[safe: index]?.string, 21 | !param.isEmpty 22 | { 23 | switch index { 24 | case 0: style = param 25 | case 1: classes = param 26 | case 2: attributes = param 27 | default: break 28 | } 29 | } 30 | } 31 | 32 | guard ColorKeys(rawValue: style) != nil || Keys(rawValue: style) != nil else { 33 | throw tag.error(reason: "Wrong argument given: \(style)") 34 | } 35 | 36 | return tag.serializer.serialize(ast: body).map(to: TemplateData.self) { body in 37 | let c = "btn btn-\(style) \(classes)" 38 | let b = String(data: body.data, encoding: .utf8) ?? "" 39 | 40 | let button = "" 41 | return .string(button) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Bootstrap/Tags/ButtonGroup.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | import Sugar 3 | import TemplateKit 4 | 5 | /// Bootstrap Button Group Tag 6 | public final class ButtonGroupTag: TagRenderer { 7 | enum GroupKeys: String { 8 | case standard = "btn-group" 9 | case vertical = "btn-group-vertical" 10 | } 11 | 12 | private static let paramCount: Int = 3 13 | 14 | public func render(tag: TagContext) throws -> Future { 15 | let body = try tag.requireBody() 16 | 17 | try tag.requireParameterCount(upTo: ButtonGroupTag.paramCount) 18 | 19 | var group = GroupKeys.standard 20 | var classes: String? 21 | var aria = UUID().uuidString 22 | 23 | if tag.parameters.count > 0 { 24 | guard let param = tag.parameters[0].bool else { 25 | throw tag.error( 26 | reason: "Wrong type given (expected a bool): \(type(of: tag.parameters[0]))" 27 | ) 28 | } 29 | 30 | if param { 31 | group = .vertical 32 | } 33 | } 34 | 35 | if tag.parameters.count > 1 { 36 | guard let param = tag.parameters[1].string else { 37 | throw tag.error( 38 | reason: "Wrong type given (expected a string): \(type(of: tag.parameters[1]))" 39 | ) 40 | } 41 | 42 | if param.count > 0 { 43 | classes = param 44 | } 45 | } 46 | 47 | if tag.parameters.count > 2 { 48 | guard let param = tag.parameters[2].string else { 49 | throw tag.error( 50 | reason: "Wrong type given (expected a string): \(type(of: tag.parameters[2]))" 51 | ) 52 | } 53 | 54 | if param.count > 0 { 55 | aria = param 56 | } 57 | } 58 | 59 | return tag.serializer.serialize(ast: body).map(to: TemplateData.self) { er in 60 | let body = String(data: er.data, encoding: .utf8) ?? "" 61 | 62 | /// Button Group isn't useful without a body 63 | guard body.count > 0 else { 64 | throw tag.error(reason: "Body Data Expected") 65 | } 66 | 67 | var group = "
    \(body)
    " 72 | 73 | return .string(group) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Bootstrap/Tags/ButtonToolbar.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | import Sugar 3 | import TemplateKit 4 | 5 | /// Button Toolbar 6 | public final class ButtonToolbarTag: TagRenderer { 7 | private static let paramCount: Int = 2 8 | 9 | public func render(tag: TagContext) throws -> Future { 10 | let body = try tag.requireBody() 11 | 12 | try tag.requireParameterCount(upTo: ButtonToolbarTag.paramCount) 13 | 14 | var classes = "" 15 | var aria = UUID().uuidString 16 | 17 | for index in 0...1 { 18 | if let param = tag.parameters[safe: index]?.string, 19 | param.isEmpty == false { 20 | 21 | switch index { 22 | case 0: classes = param 23 | case 1: aria = param 24 | default: break 25 | } 26 | } 27 | } 28 | 29 | return tag.serializer.serialize(ast: body).map(to: TemplateData.self) { er in 30 | let body = String(data: er.data, encoding: .utf8) ?? "" 31 | 32 | guard body.count > 0 else { 33 | throw tag.error(reason: "Body Data Expected") 34 | } 35 | 36 | var group = "" 41 | 42 | return .string(group) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Bootstrap/Tags/Card.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | import TemplateKit 3 | 4 | private func makeCardHeader(_ parameters: [TemplateData]) -> String { 5 | var title: String? 6 | var classes = "" 7 | var attributes = "" 8 | 9 | for index in 0...2 { 10 | if let param = parameters[safe: index]?.string, 11 | !param.isEmpty { 12 | switch index { 13 | case 0: title = param 14 | case 1: classes = param 15 | case 2: attributes = param 16 | default: break 17 | } 18 | } 19 | } 20 | 21 | var titlePart = "" 22 | if let title = title { 23 | titlePart = "
    \(title)
    \n" 24 | } 25 | 26 | return """ 27 |
    28 | \(titlePart) 29 | """ 30 | } 31 | 32 | public final class CardTag: TagRenderer { 33 | public func render(tag: TagContext) throws -> Future { 34 | let body = try tag.requireBody() 35 | 36 | return tag.serializer.serialize(ast: body).map(to: TemplateData.self) { body in 37 | let b = String(data: body.data, encoding: .utf8) ?? "" 38 | 39 | return .string(""" 40 | \(makeCardHeader(tag.parameters)) 41 |
    42 | \(b) 43 |
    44 |
    45 | """) 46 | } 47 | } 48 | } 49 | 50 | public final class CardOuterTag: TagRenderer { 51 | public func render(tag: TagContext) throws -> Future { 52 | let body = try tag.requireBody() 53 | 54 | return tag.serializer.serialize(ast: body).map(to: TemplateData.self) { body in 55 | let b = String(data: body.data, encoding: .utf8) ?? "" 56 | 57 | return .string(""" 58 | \(makeCardHeader(tag.parameters)) 59 | \(b) 60 | 61 | """) 62 | } 63 | } 64 | } 65 | 66 | fileprivate func renderCardPart(_ tag: TagContext, _ tagClass: String) throws -> Future { 67 | let body = try tag.requireBody() 68 | 69 | var classes = "" 70 | var attributes = "" 71 | 72 | for index in 0...2 { 73 | if let param = tag.parameters[safe: index]?.string, 74 | !param.isEmpty { 75 | switch index { 76 | case 0: classes = param 77 | case 1: attributes = param 78 | default: break 79 | } 80 | } 81 | } 82 | 83 | return tag.serializer.serialize(ast: body).map(to: TemplateData.self) { body in 84 | let b = String(data: body.data, encoding: .utf8) ?? "" 85 | 86 | return .string(""" 87 |
    88 | \(b) 89 |
    90 | """) 91 | } 92 | } 93 | 94 | public final class CardHeaderTag: TagRenderer { 95 | public func render(tag: TagContext) throws -> Future { 96 | return try renderCardPart(tag, "card-header") 97 | } 98 | } 99 | 100 | public final class CardBodyTag: TagRenderer { 101 | public func render(tag: TagContext) throws -> Future { 102 | return try renderCardPart(tag, "card-body") 103 | } 104 | } 105 | 106 | public final class CardFooterTag: TagRenderer { 107 | public func render(tag: TagContext) throws -> Future { 108 | return try renderCardPart(tag, "card-footer") 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/Bootstrap/Tags/FormCheckbox.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | import Sugar 3 | import TemplateKit 4 | 5 | public final class FormCheckbox: TagRenderer { 6 | 7 | public func render(tag: TagContext) throws -> Future { 8 | var classes = "" 9 | var attributes = "" 10 | 11 | try tag.requireNoBody() 12 | 13 | for index in 0...1 { 14 | if 15 | let param = tag.parameters[safe: index]?.string, 16 | !param.isEmpty 17 | { 18 | switch index { 19 | case 0: classes = param 20 | case 1: attributes = param 21 | default: () 22 | } 23 | } 24 | } 25 | 26 | let c = "form-control \(classes)" 27 | let button = "" 28 | return Future.map(on: tag) { return .string(button) } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Bootstrap/Tags/FormFile.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | import Sugar 3 | import TemplateKit 4 | 5 | public final class FormFile: TagRenderer { 6 | 7 | public func render(tag: TagContext) throws -> Future { 8 | var classes = "" 9 | var attributes = "" 10 | 11 | try tag.requireNoBody() 12 | 13 | for index in 0...1 { 14 | if 15 | let param = tag.parameters[safe: index]?.string, 16 | !param.isEmpty 17 | { 18 | switch index { 19 | case 0: classes = param 20 | case 1: attributes = param 21 | default: () 22 | } 23 | } 24 | } 25 | 26 | let c = "form-control \(classes)" 27 | let button = "" 28 | return Future.map(on: tag) { return .string(button) } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Bootstrap/Tags/FormRadio.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | import TemplateKit 3 | 4 | public final class FormRadio: TagRenderer { 5 | 6 | public func render(tag: TagContext) throws -> Future { 7 | var classes = "" 8 | var attributes = "" 9 | 10 | try tag.requireNoBody() 11 | 12 | for index in 0...1 { 13 | if 14 | let param = tag.parameters[safe: index]?.string, 15 | !param.isEmpty 16 | { 17 | switch index { 18 | case 0: classes = param 19 | case 1: attributes = param 20 | default: () 21 | } 22 | } 23 | } 24 | 25 | let c = "form-control \(classes)" 26 | let button = "" 27 | return Future.map(on: tag) { return .string(button) } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Bootstrap/Tags/Input.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | import Sugar 3 | import TemplateKit 4 | 5 | public final class InputTag: TagRenderer { 6 | enum Keys: String { 7 | case text 8 | case email 9 | case password 10 | case hidden 11 | } 12 | 13 | public func render(tag: TagContext) throws -> Future { 14 | var inputType = Keys.text.rawValue 15 | var classes = "" 16 | var attributes = "" 17 | 18 | try tag.requireNoBody() 19 | 20 | for index in 0...2 { 21 | if 22 | let param = tag.parameters[safe: index]?.string, 23 | !param.isEmpty 24 | { 25 | switch index { 26 | case 0: inputType = param 27 | case 1: classes = param 28 | case 2: attributes = param 29 | default: break 30 | } 31 | } 32 | } 33 | 34 | guard let parsedType = Keys(rawValue: inputType) else { 35 | throw tag.error(reason: "Wrong argument given: \(inputType)") 36 | } 37 | 38 | let c = "form-control \(classes)" 39 | let button = "" 40 | return tag.future(.string(button)) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Bootstrap/Tags/TextArea.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | import Sugar 3 | import TemplateKit 4 | 5 | public final class TextAreaTag: TagRenderer { 6 | private static let paramCount: Int = 3 7 | 8 | public func render(tag: TagContext) throws -> Future { 9 | try tag.requireNoBody() 10 | try tag.requireParameterCount(upTo: TextAreaTag.paramCount) 11 | 12 | var classes = "form-control" 13 | var attributes = "rows='3'" 14 | var value = "" 15 | 16 | for index in 0...2 { 17 | if 18 | let param = tag.parameters[safe: index]?.string, 19 | !param.isEmpty 20 | { 21 | switch index { 22 | case 0: classes = param 23 | case 1: attributes = param 24 | case 2: value = param 25 | default: break 26 | } 27 | } 28 | } 29 | 30 | let html = "" 31 | return tag.future(.string(html)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/BootstrapTests/BootstrapTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Bootstrap 3 | 4 | final class BootstrapTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(true, true) 10 | } 11 | 12 | 13 | static var allTests = [ 14 | ("testExample", testExample), 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /Tests/BootstrapTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !os(macOS) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(BootstrapTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import BootstrapTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += BootstrapTests.allTests() 7 | XCTMain(tests) 8 | --------------------------------------------------------------------------------