├── images
├── code.png
├── logo.png
├── dropdown.png
└── accordions.png
├── Sources
├── Ignite
│ ├── Resources
│ │ ├── base.txt
│ │ ├── fonts
│ │ │ ├── bootstrap-icons.woff
│ │ │ └── bootstrap-icons.woff2
│ │ ├── js
│ │ │ ├── prism-git.js
│ │ │ ├── prism-objectivec.js
│ │ │ ├── prism-clike.js
│ │ │ ├── prism-go.js
│ │ │ ├── prism-markuptemplating.js
│ │ │ ├── prism-wasm.js
│ │ │ ├── prism-css.js
│ │ │ └── prism-typescript.js
│ │ └── css
│ │ │ ├── prism-tomorrow.css
│ │ │ └── prism-okaidia.min.css
│ ├── Publishing
│ │ ├── Location.swift
│ │ ├── CSS
│ │ │ ├── ImportRule.swift
│ │ │ └── FontFaceRule.swift
│ │ ├── SiteMapGenerator.swift
│ │ ├── RobotsGenerator.swift
│ │ └── BootstrapOptions.swift
│ ├── Rendering
│ │ ├── Category
│ │ │ ├── EmptyCategory.swift
│ │ │ ├── TagCategory.swift
│ │ │ ├── AllTagsCategory.swift
│ │ │ └── Category.swift
│ │ ├── ResponseErrors
│ │ │ ├── EmptyHTTPError.swift
│ │ │ ├── HTTPError.swift
│ │ │ └── PageNotFoundError.swift
│ │ ├── Markdown
│ │ │ ├── Markup-IsInsideList.swift
│ │ │ └── ArticleRenderer.swift
│ │ ├── PageMetadata.swift
│ │ ├── SiteMetadata.swift
│ │ ├── ResultBuilders
│ │ │ └── DocumentElementBuilder.swift
│ │ └── TabFocus.swift
│ ├── Framework
│ │ ├── ElementTypes
│ │ │ ├── HeadElement.swift
│ │ │ ├── ListableElement.swift
│ │ │ ├── Document.swift
│ │ │ ├── PassthroughElement.swift
│ │ │ └── BodyElement.swift
│ │ ├── EmptyLayout.swift
│ │ ├── Robots
│ │ │ ├── RobotsConfiguration.swift
│ │ │ ├── DefaultRobotsConfiguration.swift
│ │ │ ├── DisallowRule.swift
│ │ │ └── KnownRobot.swift
│ │ ├── EmptyTagPage.swift
│ │ ├── Animations
│ │ │ ├── KeyframeProxy.swift
│ │ │ ├── Animatable.swift
│ │ │ ├── ColorArea.swift
│ │ │ ├── AnimationDirection.swift
│ │ │ ├── AnimationTrigger.swift
│ │ │ └── KeyframeBuilder.swift
│ │ ├── EmptyErrorPage.swift
│ │ ├── NavigationBarVisibility.swift
│ │ ├── DefaultLayout.swift
│ │ ├── ControlLabelStyle.swift
│ │ ├── ArticleLoader.swift
│ │ ├── Queries
│ │ │ ├── OrientationQuery.swift
│ │ │ ├── ColorSchemeQuery.swift
│ │ │ ├── MotionQuery.swift
│ │ │ ├── TransparencyQuery.swift
│ │ │ ├── ContrastQuery.swift
│ │ │ ├── ThemeQuery.swift
│ │ │ └── DisplayModeQuery.swift
│ │ ├── Analytics
│ │ │ ├── AnalyticsPlausibleMeasurement.swift
│ │ │ └── AnalyticsService.swift
│ │ ├── BorderStyle.swift
│ │ ├── EmptyInlineElement.swift
│ │ ├── LayoutContent.swift
│ │ ├── ArticlePage.swift
│ │ ├── ControlSize.swift
│ │ ├── TagPage.swift
│ │ ├── Layout.swift
│ │ ├── NavigationItemConfigurable.swift
│ │ ├── Attribute.swift
│ │ ├── Event.swift
│ │ ├── Environment
│ │ │ └── Environment.swift
│ │ ├── StaticPage.swift
│ │ ├── InlineStyle.swift
│ │ └── LinkTarget.swift
│ ├── Extensions
│ │ ├── URL-DecodedPath.swift
│ │ ├── String-StrippingTags.swift
│ │ ├── Date-ISO8601.swift
│ │ ├── Array-ContainsLocation.swift
│ │ ├── URL-RemovingWWW.swift
│ │ ├── String-TitleCase.swift
│ │ ├── String-EscapedForJavaScript.swift
│ │ ├── Array-LocalizedContains.swift
│ │ ├── String-SplitAndTrim.swift
│ │ ├── URL-Relative.swift
│ │ ├── URL-Unwrapped.swift
│ │ ├── String-KebabCased.swift
│ │ ├── Date-RFC822.swift
│ │ ├── String-CSStoJS.swift
│ │ ├── String-AbsoluteLinks.swift
│ │ └── String-TruncatedHash.swift
│ ├── Actions
│ │ ├── Action.swift
│ │ ├── ShowAlert.swift
│ │ ├── ToggleElementVisibility.swift
│ │ ├── CustomAction.swift
│ │ ├── HideElement.swift
│ │ ├── ShowElement.swift
│ │ ├── SwitchTheme.swift
│ │ ├── Event Modifiers
│ │ │ ├── ClickModifier.swift
│ │ │ └── DoubleClickModifier.swift
│ │ └── DismissModal.swift
│ ├── Ignite.swift
│ ├── Types
│ │ ├── ColorScheme.swift
│ │ ├── SpacingAmount.swift
│ │ ├── FillMode.swift
│ │ ├── SpacingType.swift
│ │ ├── Rotation.swift
│ │ ├── UnorderedListMarkerStyle.swift
│ │ ├── Axis.swift
│ │ ├── Angle.swift
│ │ ├── ImageFit.swift
│ │ ├── ColumnWidth.swift
│ │ ├── VerticalAlignment.swift
│ │ └── Edge.swift
│ ├── Modifiers
│ │ ├── Clipped.swift
│ │ ├── AccessibilityLabel.swift
│ │ ├── FixedSize.swift
│ │ ├── LazyLoadable.swift
│ │ ├── TransitionModifier.swift
│ │ ├── SmallCaps.swift
│ │ ├── ImageFitModifier.swift
│ │ ├── TextSelection.swift
│ │ ├── IgnorePageGutters.swift
│ │ ├── PrivacySensitive.swift
│ │ ├── FontWeightModifier.swift
│ │ ├── FontStyleModifier.swift
│ │ ├── Position.swift
│ │ ├── StyleModifier.swift
│ │ ├── Cursor.swift
│ │ └── Attribute Modifiers
│ │ │ └── IDModifier.swift
│ ├── Themes
│ │ ├── AutoTheme.swift
│ │ ├── BootstrapConstants.swift
│ │ └── Fonts
│ │ │ └── FontWeight.swift
│ ├── Elements
│ │ ├── Input.swift
│ │ ├── String.swift
│ │ ├── EmptyHTML.swift
│ │ ├── Divider.swift
│ │ ├── UnderlineProminence.swift
│ │ ├── ControlLabel.swift
│ │ ├── Title.swift
│ │ └── Row.swift
│ ├── Components
│ │ ├── IgniteFooter.swift
│ │ └── FeedLink.swift
│ └── Styles
│ │ └── StyledHTML.swift
└── IgniteCLI
│ ├── QR Generation
│ ├── QRCodeError.swift
│ └── ErrorCorrection.swift
│ └── IgniteCLI.swift
├── Tests
└── IgniteTesting
│ ├── TestWebsitePackage
│ ├── Content
│ │ └── .gitignore
│ ├── Package.swift
│ └── Sources
│ │ ├── TestStory.swift
│ │ ├── TestPage.swift
│ │ ├── TestSitePublisher.swift
│ │ ├── TestSiteWithErrorPage.swift
│ │ ├── TestSite.swift
│ │ └── TestErrorPage.swift
│ ├── Modifiers
│ ├── Frame.swift
│ ├── Padding.swift
│ ├── Position.swift
│ ├── HoverEffect.swift
│ ├── LazyLoadable.swift
│ ├── LetterSpacing.swift
│ ├── IgnorePageGutters.swift
│ ├── AccessibilityLabel.swift
│ ├── ContainerRelativeFrame.swift
│ ├── FixedSize.swift
│ ├── SmallCaps.swift
│ ├── Shadow.swift
│ ├── FontWeightModifier.swift
│ ├── TextDecoration.swift
│ ├── FontStyleModifier.swift
│ ├── PrivacySensitive.swift
│ ├── Clipped.swift
│ ├── LineSpacing.swift
│ ├── Cursor.swift
│ ├── TextSelection.swift
│ ├── AspectRatio.swift
│ ├── Border.swift
│ ├── BadgeModifier.swift
│ └── Hidden.swift
│ ├── Types
│ ├── Content.swift
│ ├── ContentLoader.swift
│ ├── Angle.swift
│ └── AnchorPoint.swift
│ ├── Elements
│ ├── Carousel.swift
│ ├── Strikethrough.swift
│ ├── ButtonGroup.swift
│ ├── ArticlePreview.swift
│ ├── Table.swift
│ ├── Divider.swift
│ ├── Include.swift
│ ├── Spacer.swift
│ ├── Code.swift
│ ├── String.swift
│ ├── ListItem.swift
│ ├── FormFieldLabel.swift
│ ├── Emphasis.swift
│ ├── Label.swift
│ ├── IgniteFooter.swift
│ ├── List.swift
│ ├── Span.swift
│ ├── Body.swift
│ ├── Embed.swift
│ ├── Title.swift
│ ├── CodeBlock.swift
│ ├── Strong.swift
│ ├── Underline.swift
│ ├── Alert.swift
│ ├── Section.swift
│ ├── ZStack.swift
│ ├── Audio.swift
│ ├── Video.swift
│ ├── Quote.swift
│ ├── Row.swift
│ └── VStack.swift
│ ├── Actions
│ ├── ShowAlert.swift
│ ├── ShowModal.swift
│ ├── HideElement.swift
│ ├── ShowElement.swift
│ ├── SwitchTheme.swift
│ ├── DismissModal.swift
│ ├── ClickModifier.swift
│ ├── HoverModifier.swift
│ └── DoubleClickModifier.swift
│ ├── Framework
│ ├── AnimatableData.swift
│ ├── HighlighterLanguage.swift
│ └── Robots
│ │ ├── RobotsConfiguration.swift
│ │ ├── KnownRobot.swift
│ │ └── DefaultRobotsConfiguration.swift
│ ├── TestElement.swift
│ ├── TestSubsite.swift
│ ├── IgniteTestSuite.swift
│ └── Subsite
│ └── IgniteSubsiteTestSuite.swift
├── .editorconfig
├── .gitignore
├── .swiftlint.yml
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDETemplateMacros.plist
├── .github
├── workflows
│ └── swift.yml
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── Makefile
└── LICENSE
/images/code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twostraws/Ignite/HEAD/images/code.png
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twostraws/Ignite/HEAD/images/logo.png
--------------------------------------------------------------------------------
/Sources/Ignite/Resources/base.txt:
--------------------------------------------------------------------------------
1 | This is the base directory for Ignite's CSS and JavaScript.
--------------------------------------------------------------------------------
/Tests/IgniteTesting/TestWebsitePackage/Content/.gitignore:
--------------------------------------------------------------------------------
1 | # Leave this directory alone.
2 |
--------------------------------------------------------------------------------
/images/dropdown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twostraws/Ignite/HEAD/images/dropdown.png
--------------------------------------------------------------------------------
/images/accordions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twostraws/Ignite/HEAD/images/accordions.png
--------------------------------------------------------------------------------
/Sources/Ignite/Resources/fonts/bootstrap-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twostraws/Ignite/HEAD/Sources/Ignite/Resources/fonts/bootstrap-icons.woff
--------------------------------------------------------------------------------
/Sources/Ignite/Resources/fonts/bootstrap-icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twostraws/Ignite/HEAD/Sources/Ignite/Resources/fonts/bootstrap-icons.woff2
--------------------------------------------------------------------------------
/Tests/IgniteTesting/TestWebsitePackage/Package.swift:
--------------------------------------------------------------------------------
1 | // Empty Package.swift file to stub source build directory in tests
2 | // See Publishing/Site.swift
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*.swift]
7 | indent_style = space
8 | indent_size = 4
9 |
--------------------------------------------------------------------------------
/Sources/Ignite/Resources/js/prism-git.js:
--------------------------------------------------------------------------------
1 | Prism.languages.git={comment:/^#.*/m,deleted:/^[-–].*/m,inserted:/^\+.*/m,string:/("|')(?:\\.|(?!\1)[^\\\r\n])*\1/,command:{pattern:/^.*\$ git .*$/m,inside:{parameter:/\s--?\w+/}},coord:/^@@.*@@$/m,"commit-sha1":/^commit \w{40}$/m};
2 |
--------------------------------------------------------------------------------
/Sources/Ignite/Publishing/Location.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Location.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A location that can be written into our sitemap.
9 | struct Location {
10 | var path: String
11 | var priority: Double
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | Package.resolved
3 | /.build
4 | /Packages
5 | Tests/IgniteTesting/TestWebsitePackage/Build
6 | Tests/IgniteTesting/TestWebsitePackage/Content/*.md
7 | xcuserdata/
8 | DerivedData/
9 | .swiftpm/configuration/registries.json
10 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
11 | .netrc
12 |
--------------------------------------------------------------------------------
/Sources/Ignite/Rendering/Category/EmptyCategory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyCategory.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Represents the absense of a category.
9 | struct EmptyCategory: Category {
10 | var name: String { "" }
11 | var articles: [Article] { [] }
12 | }
13 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/TestWebsitePackage/Sources/TestStory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestStory.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Ignite
10 |
11 | struct TestStory: ArticlePage {
12 | var body: some HTML {
13 | EmptyHTML()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/ElementTypes/HeadElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeadElement.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A metadata element that can exist in the `Head` struct.
9 | /// - Warning: Do not conform to this type directly.
10 | public protocol HeadElement: MarkupElement {}
11 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/EmptyLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyLayout.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A layout that applies almost no styling.
9 | public struct EmptyLayout: Layout {
10 | public var body: some Document {
11 | Body()
12 | }
13 |
14 | public init() {}
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/URL-DecodedPath.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL-DecodedPath.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | extension URL {
11 | /// The URL path without percent encoding.
12 | var decodedPath: String {
13 | self.path(percentEncoded: false)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Robots/RobotsConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RobotsConfiguration.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A simple protocol that lets users create custom robot configurations easily.
9 | public protocol RobotsConfiguration {
10 | var disallowRules: [DisallowRule] { get }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/EmptyTagPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyTagPage.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A default tag page that does nothing; used to disable tag pages entirely.
9 | public struct EmptyTagPage: TagPage {
10 | public var body: some BodyElement {
11 | EmptyHTML()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/Ignite/Actions/Action.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Action.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// One action that can be triggered on the page. Actions compile
9 | /// to JavaScript.
10 | public protocol Action: Sendable {
11 | /// Convert this action into the equivalent JavaScript code.
12 | func compile() -> String
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/Ignite/Ignite.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Ignite.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 | import Foundation
8 |
9 | /// The location the the Ignite bundle. Used to access resources.
10 | public var bundle: Bundle { Bundle.module }
11 |
12 | /// The current version. Used to write generator information.
13 | public let version = "Ignite v0.6.0"
14 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Animations/KeyframeProxy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Keyframe.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A proxy type that enables a function-like syntax for creating keyframes.
9 | public struct KeyframeProxy {
10 | public func callAsFunction(_ position: Percentage) -> Keyframe {
11 | Keyframe(position)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - optional_data_string_conversion
3 |
4 | identifier_name:
5 | excluded:
6 | - x
7 | - y
8 | - x1
9 | - x2
10 | - y1
11 | - y2
12 | - i
13 | - j
14 | - id
15 | - to
16 | - up
17 |
18 | # Case-sensitive paths to include during linting. Directory paths supplied on the
19 | # command line will be ignored.
20 | included:
21 | - Sources
22 | - Tests
23 |
24 |
--------------------------------------------------------------------------------
/Sources/Ignite/Rendering/ResponseErrors/EmptyHTTPError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyHTTPError.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Represents the absence of an HTTP error.
11 | struct EmptyHTTPError: HTTPError {
12 | var statusCode: Int { 0 }
13 | var title: String { "" }
14 | var description: String { "" }
15 | }
16 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/TestWebsitePackage/Sources/TestPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestPage.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Ignite
10 |
11 | /// An example page used in tests.
12 | struct TestPage: StaticPage {
13 | var title = "Home"
14 |
15 | var body: some HTML {
16 | Text("Hello, World!")
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Robots/DefaultRobotsConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultRobotsConfiguration.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A simple default robots configuration that disallows nothing.
9 | public struct DefaultRobotsConfiguration: RobotsConfiguration {
10 | public init() { }
11 | public var disallowRules = [DisallowRule]()
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/Ignite/Rendering/Category/TagCategory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TagCategory.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | // A single tag applied to an article.
9 | public struct TagCategory: Category {
10 | /// The name of the tag.
11 | public var name: String
12 | /// An array of content that has this tag.
13 | public var articles: [Article]
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/String-StrippingTags.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String-StrippingTags.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String {
11 | /// Removes all HTML tags from a string, so it's safe to use as plain-text.
12 | func strippingTags() -> String {
13 | self.replacing(#/<.*?>/#, with: "")
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/Ignite/Types/ColorScheme.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorScheme.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// The color scheme appearance for UI elements.
9 | @frozen public enum ColorScheme: String, Sendable, Hashable, Equatable, CaseIterable {
10 | /// The light color scheme.
11 | case light
12 |
13 | /// The dark color scheme.
14 | case dark
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/Date-ISO8601.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Date-ISO8601.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Date {
11 | /// Converts `Date` objects to ISO 8601 format.
12 | public var asISO8601: String {
13 | let formatter = ISO8601DateFormatter()
14 | return formatter.string(from: self)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/ElementTypes/ListableElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InlineElement.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// An element that handles list rendering by placing its
tag manually.
9 | @MainActor
10 | protocol ListableElement: MarkupElement {
11 | /// Render this when we know for sure we're part of a `List`.
12 | func listMarkup() -> Markup
13 | }
14 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FILEHEADER
6 |
7 | // ___FILENAME___
8 | // Ignite
9 | // https://www.github.com/twostraws/Ignite
10 | // See LICENSE for license information.
11 | //
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/Frame.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Frame.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Frame` modifier.
14 | @Suite("Frame Tests")
15 | @MainActor
16 | struct FrameTests {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/Clipped.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Clipped.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | public extension HTML {
9 | /// Applies CSS overflow:hidden to clip the element's content to its bounds.
10 | /// - Returns: A modified copy of the element with clipping applied
11 | func clipped() -> some HTML {
12 | self.style(.overflow, "hidden")
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/Ignite/Rendering/Markdown/Markup-IsInsideList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Padding.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Markdown
9 |
10 | extension Markdown.Markup {
11 | /// A small helper that determines whether this markup or any parent is a list.
12 | var isInsideList: Bool {
13 | self is ListItemContainer || parent?.isInsideList == true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Types/Content.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Content.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Content` type.
14 | @Suite("Content Tests")
15 | @MainActor
16 | struct ContentTests {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/Padding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Padding.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Padding` modifier.
14 | @Suite("Padding Tests")
15 | @MainActor
16 | struct PaddingTests {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/EmptyErrorPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyErrorPage.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A default error page that does nothing.
9 | public struct EmptyErrorPage: ErrorPage {
10 |
11 | public init() {}
12 |
13 | public var title: String {
14 | ""
15 | }
16 |
17 | public var body: some BodyElement {
18 | EmptyHTML()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/Position.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Position.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Position` modifier.
14 | @Suite("Position Tests")
15 | @MainActor
16 | struct PositionTests {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Ignite/Types/SpacingAmount.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpacingAmount.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Adaptive spacing amounts that are used by Bootstrap to provide consistency
9 | /// in site design.
10 | public enum SpacingAmount: Int, CaseIterable, Sendable {
11 | case none = 0
12 | case xSmall
13 | case small
14 | case medium
15 | case large
16 | case xLarge
17 | }
18 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Carousel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Carousel.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Carousel` element.
14 | @Suite("Carousel Tests")
15 | @MainActor
16 | class CarouselTests: IgniteTestSuite {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/HoverEffect.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HoverEffect.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `HoverEffect` modifier.
14 | @Suite("HoverEffect Tests")
15 | @MainActor
16 | struct HoverEffectTests {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Actions/ShowAlert.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShowAlert.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `ShowAlert` action.
14 | @Suite("ShowAlert Tests")
15 | @MainActor
16 | class ShowAlertTests: IgniteTestSuite {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Actions/ShowModal.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShowModal.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `ShowModal` action.
14 | @Suite("ShowModal Tests")
15 | @MainActor
16 | class ShowModalTests: IgniteTestSuite {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Framework/AnimatableData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnimatableData.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for `AnimatableData`.
14 | @Suite("AnimatableData Tests")
15 | @MainActor
16 | struct AnimatableDataTests {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/LazyLoadable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LazyLoadable.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `LazyLoadable` modifier.
14 | @Suite("LazyLoadable Tests")
15 | @MainActor
16 | struct LazyLoadableTests {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Types/ContentLoader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentLoader.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `ContentLoader` type.
14 | @Suite("ContentLoader Tests")
15 | @MainActor
16 | struct ContentLoaderTests {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Ignite/Rendering/Category/AllTagsCategory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AllTagsCategory.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | // All tags applied to the articles of this site.
9 | public struct AllTagsCategory: Category {
10 | /// The name of the category, which defaults to "All Tags".
11 | public var name = "All Tags"
12 | /// All of the articles that have a tag.
13 | public var articles: [Article]
14 | }
15 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Strikethrough.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Strikethrough.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Strikethrough` element.
14 | @Suite("Strikethrough Tests")
15 | @MainActor
16 | struct StrikethroughTests {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/LetterSpacing.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LetterSpacing.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `LetterSpacing` modifier.
14 | @Suite("LetterSpacing Tests")
15 | @MainActor
16 | struct LetterSpacingTests {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Actions/HideElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HideElement.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `HideElement` action.
14 | @Suite("HideElement Tests")
15 | @MainActor
16 | class HideElementTests: IgniteTestSuite {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Actions/ShowElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShowElement.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `ShowElement` action.
14 | @Suite("ShowElement Tests")
15 | @MainActor
16 | class ShowElementTests: IgniteTestSuite {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Actions/SwitchTheme.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwitchTheme.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `SwitchTheme` action.
14 | @Suite("SwitchTheme Tests")
15 | @MainActor
16 | class SwitchThemeTests: IgniteTestSuite {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/ButtonGroup.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ButtonGroup.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `ButtonGroup` element.
14 | @Suite("ButtonGroup Tests")
15 | @MainActor
16 | class ButtonGroupTests: IgniteTestSuite {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Actions/DismissModal.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DismissModal.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `DismissModal` action.
14 | @Suite("DismissModal Tests")
15 | @MainActor
16 | class DismissModalTests: IgniteTestSuite {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/AccessibilityLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityLabel.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | public extension HTML {
9 | /// Adds a label to an arbitrary element. Specific types override this in places
10 | /// where accessibility labels need exact forms, e.g. alt text for images.
11 | func accessibilityLabel(_ label: String) -> some HTML {
12 | self.aria(.label, label)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Actions/ClickModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClickModifier.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `ClickModifier` modifier.
14 | @Suite("ClickModifier Tests")
15 | @MainActor
16 | class ClickModifierTests: IgniteTestSuite {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Actions/HoverModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HoverModifier.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `HoverModifier` modifier.
14 | @Suite("HoverModifier Tests")
15 | @MainActor
16 | class HoverModifierTests: IgniteTestSuite {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/ArticlePreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentPreview.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `ArticlePreview` element.
14 | @Suite("ArticlePreview Tests")
15 | @MainActor
16 | class ArticlePreviewTests: IgniteTestSuite {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Framework/HighlighterLanguage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HighlighterLanguage.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for `HighlighterLanguage`.
14 | @Suite("HighlighterLanguage Tests")
15 | @MainActor
16 | struct HighlighterLanguageTests {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/IgnorePageGutters.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IgnorePageGutters.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `IgnorePageGutters` modifier.
14 | @Suite("IgnorePageGutters Tests")
15 | @MainActor
16 | struct IgnorePageGuttersTests {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/Array-ContainsLocation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array-ContainsLocation.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | extension Array where Element == Location {
9 | /// An extension that lets us determine whether one path is contained inside
10 | /// An array of `Location` objects.
11 | func contains(_ path: String) -> Bool {
12 | self.contains {
13 | $0.path == path
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/AccessibilityLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityLabel.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `AccessibilityLabel` modifier.
14 | @Suite("AccessibilityLabel Tests")
15 | @MainActor
16 | struct AccessibilityLabelTests {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/ElementTypes/Document.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Document.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A protocol that defines an HTML document.
9 | /// - Warning: Do not conform to this protocol directly.
10 | public protocol Document: MarkupElement {
11 | /// The main content section of the document.
12 | var body: Body { get }
13 |
14 | /// The metadata section of the document.
15 | var head: Head { get }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/FixedSize.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FixedSize.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | public extension HTML {
9 | /// Forces an element to be sized based on its content rather than expanding to fill its container.
10 | /// - Returns: A modified copy of the element with fixed sizing applied
11 | func fixedSize() -> some HTML {
12 | Section(self)
13 | .style(.display, "inline-block")
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/URL-RemovingWWW.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL-RemovingWWW.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | extension URL {
11 | /// Removes "www" from a URL host if it exists. Useful for social sharing,
12 | /// where "Read more on example.com" reads better than "Read more on
13 | /// www.example.com".
14 | var removingWWW: String? {
15 | host()?.replacing(#/^www\./#, with: "")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/NavigationBarVisibility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationBarVisibility.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Determines how a navigation item responds to different viewport sizes.
9 | public enum NavigationBarVisibility {
10 | /// Automatically collapses the item into a menu at small screen sizes.
11 | case automatic
12 | /// Ensures visibility in the navigation bar at all viewport sizes.
13 | case always
14 | }
15 |
--------------------------------------------------------------------------------
/.github/workflows/swift.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Swift project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift
3 |
4 | name: Swift
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: macos-15
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: Build
20 | run: swift build
21 | - name: Run tests
22 | run: swift test -q
23 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/String-TitleCase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String-TitleCase.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | extension String {
9 | /// Converts a string from PascalCase to Title Case with spaces
10 | func titleCase() -> String {
11 | self.replacing(
12 | #/([a-z0-9]|[A-Z])([A-Z][a-z]|[A-Z])/#,
13 | with: { match in
14 | "\(match.1) \(match.2)"
15 | }
16 | )
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/String-EscapedForJavaScript.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String-EscapedForJavaScript.swift
3 | // IgniteSamples
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | public extension String {
11 | /// Allows user strings to be used inside generated JavaScript event code.
12 | func escapedForJavascript() -> String {
13 | self
14 | .replacing("'", with: "\\'")
15 | .replacing("\"", with: """)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Actions/DoubleClickModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DoubleClickModifier.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `DoubleClickModifier` modifier.
14 | @Suite("DoubleClickModifier Tests")
15 | @MainActor
16 | class DoubleClickModifierTests: IgniteTestSuite {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/ContainerRelativeFrame.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContainerRelativeFrame.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `ContainerRelativeFrame` modifier.
14 | @Suite("ContainerRelativeFrame Tests")
15 | @MainActor
16 | struct ContainerRelativeFrameTests {
17 | @Test("ExampleTest")
18 | func example() async throws {
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/DefaultLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultLayout.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// The layout you assigned to `Site`'s `layout` property.
9 | public struct DefaultLayout: Layout {
10 | public var body: some Document {
11 | let layout = PublishingContext.shared.site.layout
12 | let head = layout.body.head
13 | let body = layout.body.body
14 | return PlainDocument(head: head, body: body)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Ignite/Themes/AutoTheme.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AutoTheme.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A theme that automatically switches between light and dark modes based on system preferences.
9 | ///
10 | /// AutoTheme uses the default Bootstrap light theme values but allows JavaScript to
11 | /// dynamically switch between light and dark modes based on the user's system preferences.
12 | struct AutoTheme: Theme {
13 | var colorScheme: ColorScheme = .light
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Animations/Animatable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Animatable.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import OrderedCollections
9 |
10 | /// A protocol that defines the core animation capabilities for Ignite's animation system.
11 | protocol Animatable: Hashable, Sendable {}
12 |
13 | extension Animatable {
14 | var id: String {
15 | Self.id
16 | }
17 |
18 | static var id: String {
19 | String(describing: self).truncatedHash
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/ControlLabelStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ControlLabelStyle.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Controls how form labels are displayed
9 | public enum ControlLabelStyle: CaseIterable, Sendable {
10 | /// Labels appear above their fields
11 | case top
12 | /// Labels appear to the left of their fields
13 | case leading
14 | /// Labels float when the field has content
15 | case floating
16 | /// No labels are shown
17 | case hidden
18 | }
19 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Table.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Table.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | @Suite("Table Tests")
14 | @MainActor
15 | class TableTests: IgniteTestSuite {
16 | @Test func simpleTable() async throws {
17 | let element = Table { }
18 | let output = element.markupString()
19 | #expect(output == "")
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/TestElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestElement.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | @testable import Ignite
9 |
10 | private struct TestSubElement: HTML {
11 | var body: some HTML {
12 | Text("Test Heading!")
13 | }
14 | }
15 |
16 | /// A custom element that modifiers can test.
17 | struct TestElement: HTML {
18 | var body: some HTML {
19 | TestSubElement()
20 | Text("Test Subheading")
21 | ControlLabel("Test Label")
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/Array-LocalizedContains.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array-LocalizedContains.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | extension Array where Element == String {
9 | /// Searches a string array to see whether it contains one particular string,
10 | /// each time using the `localizedStandardContains()` method
11 | /// for smarter checks.
12 | func localizedContains(_ string: String) -> Bool {
13 | self.contains { $0.localizedStandardContains(string) }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/Ignite/Types/FillMode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FillMode.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Specifies how values persist before and after an animation
9 | public enum FillMode: String, Hashable, Sendable {
10 | /// No fill mode needed
11 | case none
12 | /// Retains the values set by the last keyframe
13 | case forwards
14 | /// Retains the values set by the first keyframe
15 | case backwards
16 | /// Retains values before and after animation
17 | case both
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Animations/ColorArea.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorArea.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Specifies which color property to animate
9 | public enum ColorArea: Sendable {
10 | /// The foreground color
11 | case foreground
12 | /// The background color
13 | case background
14 |
15 | var property: AnimatableProperty {
16 | switch self {
17 | case .foreground: .color
18 | case .background: .backgroundColor
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/String-SplitAndTrim.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String-SplitAndTrim.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String {
11 | /// Splits a single string into an array of strings, breaking on commas, and
12 | /// automatically trimming whitespace.
13 | func splitAndTrim() -> [String] {
14 | self.split(separator: ",", omittingEmptySubsequences: true)
15 | .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/Ignite/Types/SpacingType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpacingType.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A type that represents spacing values in either exact pixels or semantic spacing amounts.
9 | enum SpacingType: Equatable {
10 | /// An exact spacing value in pixels.
11 | case exact(Int)
12 |
13 | /// A semantic spacing value that adapts based on context.
14 | case semantic(SpacingAmount)
15 |
16 | /// The spacing value appropriate for the given context.
17 | case automatic
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/ArticleLoader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Content.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | @MainActor
9 | public struct ArticleLoader {
10 | public var all: [Article]
11 |
12 | init(content: [Article]) {
13 | all = content
14 | }
15 |
16 | public func typed(_ type: String) -> [Article] {
17 | all.filter { $0.type == type }
18 | }
19 |
20 | public func tagged(_ tag: String) -> [Article] {
21 | all.filter { $0.tags?.contains(tag) == true }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Ignite/Publishing/CSS/ImportRule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImportRule.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Represents a CSS @import rule
9 | struct ImportRule {
10 | let source: URL
11 |
12 | init(_ source: URL) {
13 | self.source = source
14 | }
15 |
16 | func render() -> String {
17 | "@import url('\(source.absoluteString)');"
18 | }
19 | }
20 |
21 | extension ImportRule: CustomStringConvertible {
22 | var description: String {
23 | render()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Ignite/Rendering/ResponseErrors/HTTPError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorPageStatus.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A type that represents an HTTP status code error that can be displayed as an error page.
9 | public protocol HTTPError: Sendable {
10 |
11 | /// The status code of the HTTP error.
12 | var statusCode: Int { get }
13 |
14 | /// The title of the error.
15 | var title: String { get }
16 |
17 | /// The description of the error.
18 | var description: String { get }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/LazyLoadable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LazyLoadable.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A protocol that determines which elements can be loaded lazily.
9 | public protocol LazyLoadable {}
10 |
11 | public extension HTML where Self: LazyLoadable {
12 | /// Enables lazy loading for this element.
13 | /// - Returns: A modified copy of the element with lazy loading enabled
14 | func lazy() -> some HTML {
15 | self.customAttribute(name: "loading", value: "lazy")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/Ignite/Rendering/Category/Category.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Category.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// The category by which articles on your site can be grouped.
9 | public protocol Category: CustomStringConvertible, Sendable {
10 | /// The name of the category.
11 | var name: String { get }
12 | /// An array of articles that belongs to this category.
13 | var articles: [Article] { get }
14 | }
15 |
16 | public extension Category {
17 | var description: String {
18 | name
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Divider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Divider.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Divider` element.
14 | @Suite("Divider Tests")
15 | @MainActor
16 | class DividerTests: IgniteTestSuite {
17 | @Test("A single divider")
18 | func singleDivider() async throws {
19 | let element = Divider()
20 | let output = element.markupString()
21 | #expect(output == " ")
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/TestWebsitePackage/Sources/TestSitePublisher.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestSitePublisher.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Ignite
10 |
11 | /// A test publisher for ``TestSite``.
12 | ///
13 | /// It helps to run `TestSite/publish` with a correct path of the file that triggered the build.
14 | @MainActor
15 | struct TestSitePublisher {
16 | var site: any Site = TestSite()
17 |
18 | mutating func publish() async throws {
19 | try await site.publish()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/IgniteCLI/QR Generation/QRCodeError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QRCodeError.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Errors that can occur during QR code generation.
9 | enum QRCodeError: Error {
10 | /// A generic error when QR code generation fails.
11 | case cannotGenerateQRCode
12 |
13 | /// When text cannot be encoded using the specified encoding.
14 | case unableToConvertTextToRequestedEncoding
15 |
16 | /// When the CoreImage filter fails to produce an output image.
17 | case cannotGenerateImage
18 | }
19 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Include.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Include.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Include` element.
14 | @Suite("Include Tests")
15 | @MainActor
16 | class IncludeTests: IgniteTestSuite {
17 | @Test("Basic Include")
18 | func basicInclude() async throws {
19 | let element = Include("important.html")
20 | let output = element.markupString()
21 |
22 | #expect(output == "")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Types/Rotation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Rotation.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Defines the possible directions for 3D rotations
9 | public enum Rotation: Sendable {
10 | /// Rotates 360 degrees around the Y axis (left to right)
11 | case right
12 |
13 | /// Rotates -360 degrees around the Y axis (right to left)
14 | case left
15 |
16 | /// Rotates -360 degrees around the X axis (bottom to top)
17 | case up
18 |
19 | /// Rotates 360 degrees around the X axis (top to bottom)
20 | case down
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Spacer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Spacer.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Spacer` element.
14 | @Suite("Spacer Tests")
15 | @MainActor
16 | class SpacerTests: IgniteTestSuite {
17 | @Test("SpacerTest")
18 | func basicSpacerTest() async throws {
19 | let element = Spacer()
20 | let output = element.markupString()
21 |
22 | #expect(output == "
")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Code.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Code.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Code` element.
14 | @Suite("Code Tests")
15 | @MainActor
16 | class CodeTests: IgniteTestSuite {
17 | @Test("Inline code formatting")
18 | func inlineCode() async throws {
19 | let element = Code("background-color")
20 | let output = element.markupString()
21 | #expect(output == "background-color")
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/URL-Relative.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL-Relative.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | extension URL {
11 | /// Creates a relative URL by using another URL as its base.
12 | /// - Parameter other: The base URL to compare against.
13 | /// - Returns: A relative URL.
14 | func relative(to other: URL) -> String {
15 | let basePath = other.path()
16 | let thisPath = path()
17 | let result = thisPath.trimmingPrefix(basePath)
18 |
19 | return String(result)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Ignite/Types/UnorderedListMarkerStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UnorderedListStyle.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | public enum UnorderedListMarkerStyle: String, Sendable {
9 | /// Lists are shown with filled circle bullet points.
10 | case automatic = "disc"
11 |
12 | /// Lists are shown with hollow circle bullet points.
13 | case circle = "circle"
14 |
15 | /// Lists are shown with filled square bullet points.
16 | case square = "square"
17 |
18 | /// Lists are shown with a custom symbol
19 | case custom = "custom"
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/URL-Unwrapped.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL-Unwrapped.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | public extension URL {
11 | /// Creates URLs from static strings, which will only fail if you have made
12 | /// a significant typing error.
13 | init(static string: StaticString) {
14 | if let created = URL(string: String(describing: string)) {
15 | self = created
16 | } else {
17 | fatalError("Attempted to create a URL from an invalid string: \(string)")
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/String.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for Strings (aka Plain Text)
14 | @Suite("String Tests")
15 | @MainActor
16 | class StringTests: IgniteTestSuite {
17 | @Test("Single Element", arguments: ["This is a test", ""])
18 | func singleElement(element: String) async throws {
19 | let element = element
20 | let output = element.markupString()
21 |
22 | #expect(output == element)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/String-KebabCased.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String-Kebab.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String {
11 | /// Converts string to kebab-case (e.g. "camelCase" -> "camel-case")
12 | func kebabCased() -> String {
13 | self
14 | .replacing(
15 | #/([a-z0-9])([A-Z])/#,
16 | with: { match in
17 | "\(match.1)-\(match.2)"
18 | }
19 | )
20 | .lowercased()
21 | .replacing(/\s+/, with: "-")
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Ignite/Elements/Input.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Input.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// An input element for use in form controls.
9 | struct Input: InlineElement {
10 | /// The content and behavior of this HTML.
11 | var body: some InlineElement { self }
12 |
13 | /// The standard set of control attributes for HTML elements.
14 | var attributes = CoreAttributes()
15 |
16 | /// Whether this HTML belongs to the framework.
17 | var isPrimitive: Bool { true }
18 |
19 | func markup() -> Markup {
20 | Markup(" ")
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/Date-RFC822.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Date-RFC822.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Date {
11 | /// Converts `Date` objects to RFC-822 format, which is used by RSS.
12 | public func asRFC822(timeZone: TimeZone? = nil) -> String {
13 | let formatter = DateFormatter()
14 | formatter.locale = Locale(identifier: "en_US_POSIX")
15 | formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z"
16 | if let timeZone { formatter.timeZone = timeZone }
17 | return formatter.string(from: self)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Queries/OrientationQuery.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OrientationQuery.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Applies styles based on the device orientation.
9 | public enum OrientationQuery: String, Query, CaseIterable {
10 | /// Portrait orientation
11 | case portrait = "orientation: portrait"
12 | /// Landscape orientation
13 | case landscape = "orientation: landscape"
14 |
15 | public var condition: String { rawValue }
16 | }
17 |
18 | extension OrientationQuery: MediaFeature {
19 | public var description: String {
20 | rawValue
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Queries/ColorSchemeQuery.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorSchemeQuery.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Applies styles based on the user's preferred color scheme.
9 | public enum ColorSchemeQuery: String, Query, CaseIterable {
10 | /// Dark mode preference
11 | case dark = "prefers-color-scheme: dark"
12 | /// Light mode preference
13 | case light = "prefers-color-scheme: light"
14 |
15 | public var condition: String { rawValue }
16 | }
17 |
18 | extension ColorSchemeQuery: MediaFeature {
19 | public var description: String {
20 | rawValue
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/FixedSize.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FixedSize.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `FixedSize` modifier.
14 | @Suite("FixedSize Tests")
15 | @MainActor
16 | class FixedSizeTests: IgniteTestSuite {
17 | @Test("FixedSize Modifier")
18 | func fixedSizeModifier() async throws {
19 | let element = Text("Hello").fixedSize()
20 | let output = element.markupString()
21 | #expect(output == "")
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Queries/MotionQuery.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MotionQuery.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Applies styles based on the user's motion preferences.
9 | public enum MotionQuery: String, Query, CaseIterable {
10 | /// Reduced motion preference
11 | case reduced = "prefers-reduced-motion: reduce"
12 | /// Standard motion preference
13 | case allowed = "prefers-reduced-motion: no-preference"
14 |
15 | public var condition: String { rawValue }
16 | }
17 |
18 | extension MotionQuery: MediaFeature {
19 | public var description: String {
20 | rawValue
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/ListItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListItem.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `ListItem` element.
14 | @Suite("ListItem Tests")
15 | @MainActor
16 | class ListItemTests: IgniteTestSuite {
17 | @Test("Standalone ListItem")
18 | func standAlone() async throws {
19 | let element = ListItem {
20 | "Standalone List Item"
21 | }
22 | let output = element.markupString()
23 |
24 | #expect(output == " Standalone List Item ")
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Ignite/Elements/String.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A small String extension that allows strings to be used directly inside HTML.
9 | /// Useful when you don't want your text to be wrapped in a paragraph or similar.
10 | extension String: InlineElement, FormItem {
11 | /// The content and behavior of this HTML.
12 | public var body: some InlineElement { self }
13 |
14 | /// Renders this element using publishing context passed in.
15 | /// - Returns: The HTML for this element.
16 | public func markup() -> Markup {
17 | Markup(self)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/FormFieldLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FormFieldLabel.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `FormFieldLabel` element.
14 | @Suite("Form Field Label Tests")
15 | @MainActor
16 | class FormFieldLabelTests: IgniteTestSuite {
17 | @Test("Basic Label")
18 | func basicLabel() async throws {
19 | let element = ControlLabel("This is a text for label")
20 | let output = element.markupString()
21 |
22 | #expect(output == "This is a text for label ")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/Sources/Ignite/Resources/js/prism-objectivec.js:
--------------------------------------------------------------------------------
1 | Prism.languages.objectivec=Prism.languages.extend("c",{string:{pattern:/@?"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"/,greedy:!0},keyword:/\b(?:asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|in|inline|int|long|register|return|self|short|signed|sizeof|static|struct|super|switch|typedef|typeof|union|unsigned|void|volatile|while)\b|(?:@interface|@end|@implementation|@protocol|@class|@public|@protected|@private|@property|@try|@catch|@finally|@throw|@synthesize|@dynamic|@selector)\b/,operator:/-[->]?|\+\+?|!=?|<=?|>>?=?|==?|&&?|\|\|?|[~^%?*\/@]/}),delete Prism.languages.objectivec["class-name"],Prism.languages.objc=Prism.languages.objectivec;
2 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/SmallCaps.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SmallCaps.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `SmallCaps` modifier.
14 | @Suite("SmallCaps Tests")
15 | @MainActor
16 | class SmallCapsTests: IgniteTestSuite {
17 | @Test("SmallCaps Modifier")
18 | func htmlSmallCaps() async throws {
19 | let element = Span("Hello, World!").smallCaps()
20 | let output = element.markupString()
21 |
22 | #expect(output == "Hello, World! ")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/ElementTypes/PassthroughElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PassthroughHTML.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A protocol that enables HTML elements to pass their content and attributes through to their children.
9 | ///
10 | /// Elements that conform to `PassthroughHTML` act as transparent containers,
11 | /// allowing their content and styling to flow through to their child elements.
12 | @MainActor protocol PassthroughElement: MarkupElement {
13 | associatedtype Content: MarkupElement & Sequence
14 | /// The child elements contained within this HTML element.
15 | var items: Content { get }
16 | }
17 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Emphasis.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Emphasis.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Emphasis` element.
14 | @Suite("Emphasis Tests")
15 | @MainActor
16 | class EmphasisTests: IgniteTestSuite {
17 | @Test("Emphasis")
18 | func simpleEmphasis() async throws {
19 | let element = Emphasis("Although Markdown is still easier, to be honest! ")
20 | let output = element.markupString()
21 |
22 | #expect(output == "Although Markdown is still easier, to be honest! ")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/TransitionModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TransitionModifier.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | public extension HTML {
9 | /// Applies a transition animation to an HTML element.
10 | /// - Parameters:
11 | /// - transition: The transition animation to apply.
12 | /// - trigger: The event that triggers this animation (.hover, .click, or .appear).
13 | /// - Returns: A modified HTML element with the animation applied.
14 | func transition(_ transition: Transition, on trigger: AnimationTrigger) -> some HTML {
15 | AnimatedHTML(self, animation: transition, trigger: trigger)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/Ignite/Resources/js/prism-clike.js:
--------------------------------------------------------------------------------
1 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/};
2 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Analytics/AnalyticsPlausibleMeasurement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnalyticsPlausibleMeasurement.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Analytics {
11 | /// Defines the available measurement options for Plausible Analytics
12 | public enum PlausibleMeasurement: String, Hashable, CaseIterable {
13 | case fileDownloads = "file-downloads"
14 | case hash
15 | case outboundLinks = "outbound-links"
16 | case pageviewProps = "pageview-props"
17 | case revenue
18 | case taggedEvents = "tagged-events"
19 | case track404 = "404"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Queries/TransparencyQuery.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Untitled.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Applies styles based on the user's transparency preferences.
9 | public enum TransparencyQuery: String, Query, CaseIterable {
10 | /// Reduced transparency preference
11 | case reduced = "prefers-reduced-transparency: reduce"
12 | /// Standard transparency preference
13 | case normal = "prefers-reduced-transparency: no-preference"
14 |
15 | public var condition: String { rawValue }
16 | }
17 |
18 | extension TransparencyQuery: MediaFeature {
19 | public var description: String {
20 | rawValue
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/Shadow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Shadow.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Shadow` modifier.
14 | @Suite("Shadow Tests")
15 | @MainActor
16 | class ShadowTests: IgniteTestSuite {
17 | @Test("Shadow Modifier", arguments: [5, 20])
18 | func shadowRadius(radius: Int) async throws {
19 | let element = Span("Hello").shadow(radius: radius)
20 | let output = element.markupString()
21 |
22 | #expect(output == "Hello ")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Types/Axis.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Axis.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Describes the axes of a coordinate system.
9 | public struct Axis: OptionSet, Sendable {
10 | public let rawValue: Int
11 |
12 | public init(rawValue: Int) {
13 | self.rawValue = rawValue
14 | }
15 |
16 | /// The horizontal axis.
17 | public static let horizontal = Axis(rawValue: 1 << 0)
18 |
19 | /// The vertical axis.
20 | public static let vertical = Axis(rawValue: 1 << 1)
21 | }
22 |
23 | public extension Axis {
24 | /// A set containing both horizontal and vertical axes.
25 | static let all: Axis = [.horizontal, .vertical]
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Ignite/Publishing/SiteMapGenerator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SiteMapGenerator.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | @MainActor
9 | struct SiteMapGenerator {
10 | var context: PublishingContext
11 |
12 | func generateSiteMap() -> String {
13 | let locations = context.siteMap.map {
14 | "\(context.site.url.absoluteString)\($0.path) \($0.priority) "
15 | }.joined()
16 |
17 | return """
18 | \
19 | \
20 | \(locations)\
21 |
22 | """
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Components/IgniteFooter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IgniteFooter.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Displays "Created in Swift with Ignite", with a link back to the Ignite project on GitHub.
11 | /// Including this is definitely not required for your site, but it's most appreciated 🙌
12 | public struct IgniteFooter: HTML {
13 | public init() {}
14 |
15 | public var body: some HTML {
16 | Text {
17 | "Created in Swift with "
18 | Link("Ignite", target: URL(static: "https://github.com/twostraws/Ignite"))
19 | }
20 | .horizontalAlignment(.center)
21 | .margin(.top, .xLarge)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Queries/ContrastQuery.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContrastQuery.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Applies styles based on the user's contrast preferences.
9 | public enum ContrastQuery: String, Query, CaseIterable {
10 | /// Standard contrast preference
11 | case normal = "prefers-contrast: no-preference"
12 | /// High contrast preference
13 | case high = "prefers-contrast: more"
14 | /// Low contrast preference
15 | case low = "prefers-contrast: less"
16 |
17 | public var condition: String { rawValue }
18 | }
19 |
20 | extension ContrastQuery: MediaFeature {
21 | public var description: String {
22 | rawValue
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Animations/AnimationDirection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnimationDirection.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Specifies the direction of an animation's playback.
9 | public enum AnimationDirection: String, Hashable, Sendable {
10 | /// Plays the animation normally from start to finish.
11 | case automatic = "normal"
12 |
13 | /// Plays the animation in reverse from end to start.
14 | case reverse = "reverse"
15 |
16 | /// Alternates between forward and reverse playback on each iteration.
17 | case alternate = "alternate"
18 |
19 | /// Alternates between reverse and forward playback on each iteration.
20 | case alternateReverse = "alternate-reverse"
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/FontWeightModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontWeightModifier.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `FontWeightModifier` modifier.
14 | @Suite("FontWeightModifier Tests")
15 | @MainActor
16 | class FontWeightModifierTests: IgniteTestSuite {
17 | @Test("Font Weight Modifier", arguments: Font.Weight.allCases)
18 | func fontWeight(weight: Font.Weight) async throws {
19 | let element = Text("Hello").fontWeight(weight)
20 | let output = element.markupString()
21 | #expect(output == "Hello
")
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PREFIX_DIR := /usr/local/bin
2 |
3 | build:
4 | @echo "Building the Ignite command-line tool...\\n"
5 | @swift build -c release --product IgniteCLI
6 |
7 | install:
8 | @echo "Installing the Ignite command-line tool...\\n"
9 | @mkdir -p $(PREFIX_DIR) 2> /dev/null || ( echo "❌ Unable to create install directory \`$(PREFIX_DIR)\`. You might need to run \`sudo make\`\\n"; exit 126 )
10 | @(install .build/release/IgniteCLI $(PREFIX_DIR)/ignite && \
11 | install ./server.py $(PREFIX_DIR)/ignite-server.py && \
12 | chmod +x $(PREFIX_DIR)/ignite && \
13 | (echo \\n✅ Success! Run \`ignite\` to get started.)) || \
14 | (echo \\n❌ Installation failed. You might need to run \`sudo make\` instead.\\n)
15 |
16 | clean:
17 | @echo "Cleaning the Ignite build folder...\\n"
18 | @rm -rf .build/
19 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/TestSubsite.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestSubsite.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Ignite
9 |
10 | /// An example site used in tests.
11 | struct TestSubsite: Site {
12 | var name = "My Test Subsite"
13 | var titleSuffix = " - My Test Subsite"
14 | var url = URL(static: "https://www.example.com/subsite")
15 |
16 | var builtInIconsEnabled: BootstrapOptions = .localBootstrap
17 |
18 | var homePage = TestSubsitePage()
19 | var layout = EmptyLayout()
20 | }
21 |
22 | /// An example page used in tests.
23 | struct TestSubsitePage: StaticPage {
24 | var title = "Subsite Home"
25 |
26 | var body: some HTML {
27 | Text("Example subsite text")
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Ignite/Types/Angle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Angle.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Represents an angle that can be expressed in different units
9 | public enum Angle: Sendable {
10 | /// Angle in degrees (e.g., 180 degrees)
11 | case degrees(Double)
12 |
13 | /// Angle in radians (e.g., π radians)
14 | case radians(Double)
15 |
16 | /// Angle in turns (e.g., 0.5 turns = 180 degrees)
17 | case turns(Double)
18 |
19 | /// The CSS value for the angle
20 | var value: String {
21 | switch self {
22 | case .degrees(let value): "\(value)deg"
23 | case .radians(let value): "\(value)rad"
24 | case .turns(let value): "\(value)turn"
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/Ignite/Rendering/PageMetadata.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Page.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | /// A single flattened page from any source – static or dynamic – ready to be
11 | /// passed through a theme.
12 | public struct PageMetadata: Sendable {
13 | private(set) public var title: String
14 | private(set) public var description: String
15 | private(set) public var url: URL
16 | private(set) public var image: URL?
17 | }
18 |
19 | extension PageMetadata {
20 | /// Creates an empty page for use as a default value
21 | @MainActor static let empty = PageMetadata(
22 | title: "",
23 | description: "",
24 | url: URL(string: "about:blank")!
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Ignite/Actions/ShowAlert.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShowAlert.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Shows a browser alert dialog with an OK button.
9 | public struct ShowAlert: Action {
10 | /// The text to show inside the alert
11 | var message: String
12 |
13 | /// Creates a new ShowAlert action with its message string.
14 | /// - Parameter message: The message text to show in the alert.
15 | public init(message: String) {
16 | self.message = message
17 | }
18 |
19 | /// Renders this action using publishing context passed in.
20 | /// - Returns: The JavaScript for this action.
21 | public func compile() -> String {
22 | "alert('\(message.escapedForJavascript())')"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Elements/EmptyHTML.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyHTML.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A placeholder HTML element that renders nothing
9 | /// Used as a default or fallback when no content is needed
10 | public struct EmptyHTML: HTML {
11 | /// Creates a new empty HTML element
12 | public nonisolated init() {}
13 |
14 | /// Returns self as the body content since this is an empty element
15 | public var body: some HTML { self }
16 |
17 | /// Whether this HTML belongs to the framework.
18 | public var isPrimitive: Bool { true }
19 |
20 | /// Renders this element as an empty string
21 | /// - Returns: An empty string
22 | public func markup() -> Markup {
23 | Markup()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/ElementTypes/BodyElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BodyElement.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// An element that can exist in the `` of an HTML page.
9 | /// - Warning: Do not conform to this type directly.
10 | public protocol BodyElement: MarkupElement {
11 | /// Whether this HTML belongs to the framework.
12 | var isPrimitive: Bool { get }
13 | }
14 |
15 | public extension BodyElement {
16 | /// The default status as a primitive element.
17 | var isPrimitive: Bool { false }
18 |
19 | /// A collection of styles, classes, and attributes.
20 | var attributes: CoreAttributes {
21 | get { CoreAttributes() }
22 | set {} // swiftlint:disable:this unused_setter_value
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/SmallCaps.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SmallCaps.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | public extension HTML {
9 | /// Converts lowercase letters to small capitals while leaving uppercase letters unchanged.
10 | /// - Returns: A modified copy of the element with small caps applied
11 | func smallCaps() -> some HTML {
12 | self.style(.fontVariant, "small-caps")
13 | }
14 | }
15 |
16 | public extension InlineElement {
17 | /// Converts lowercase letters to small capitals while leaving uppercase letters unchanged.
18 | /// - Returns: A modified copy of the element with small caps applied
19 | func smallCaps() -> some InlineElement {
20 | self.style(.fontVariant, "small-caps")
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Label.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Label.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Label` element.
14 | @Suite("Label Tests")
15 | @MainActor
16 | class LabelTests: IgniteTestSuite {
17 | @Test("Basic Label")
18 | func basicLabel() async throws {
19 | let element = Label("Logo", image: "/images/logo.png")
20 | let output = element.markupString()
21 |
22 | #expect(output == """
23 | \
24 | \
25 | Logo\
26 |
27 | """)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Framework/Robots/RobotsConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RobotsConfiguration.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for `RobotsConfiguration`.
14 | @Suite("RobotsConfiguration Tests")
15 | @MainActor
16 | struct RobotsConfigurationTests {
17 | @Test("Assert that mock conforms to protocol")
18 | func assertMockConformsToProtocol() async throws {
19 | let mock: Any = RobotsConfigurationMock()
20 | #expect(mock is RobotsConfiguration)
21 | }
22 | }
23 |
24 | // MARK: - RobotsConfigurationMock
25 |
26 | final class RobotsConfigurationMock: RobotsConfiguration {
27 | var disallowRules: [DisallowRule] = []
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Queries/ThemeQuery.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThemeQuery.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Applies styles based on the current theme.
9 | public struct ThemeQuery: Query {
10 | /// The theme identifier
11 | let theme: any Theme.Type
12 |
13 | public init(_ theme: any Theme.Type) {
14 | self.theme = theme
15 | }
16 |
17 | public var condition: String {
18 | "data-bs-theme^=\"\(theme.idPrefix)\""
19 | }
20 |
21 | nonisolated public func hash(into hasher: inout Hasher) {
22 | hasher.combine(theme.idPrefix)
23 | }
24 |
25 | nonisolated public static func == (lhs: ThemeQuery, rhs: ThemeQuery) -> Bool {
26 | lhs.theme.idPrefix == rhs.theme.idPrefix
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/IgniteFooter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IgniteFooter.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `IgniteFooter` element.
14 | @Suite("IgniteFooter Tests")
15 | @MainActor
16 | class IgniteFooterTests: IgniteTestSuite {
17 | @Test("Default Ignite Footer")
18 | func defaultIgniteFooter() async throws {
19 | let element = IgniteFooter()
20 | let output = element.markupString()
21 |
22 | #expect(output == """
23 | \
24 | Created in Swift with \
25 | Ignite \
26 |
27 | """)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Animations/AnimationTrigger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnimationTrigger.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Defines the events that can trigger an animation on an HTML element.
9 | public enum AnimationTrigger: String, Hashable, CaseIterable, Sendable {
10 | /// Animation triggers when the element is clicked/tapped
11 | /// - Note: Adds cursor: pointer and click event listener
12 | case click
13 |
14 | /// Animation triggers when the mouse hovers over the element
15 | /// - Note: Adds cursor: pointer and :hover CSS selector
16 | case hover
17 |
18 | /// Animation triggers when the element first appears in the viewport
19 | /// - Note: Uses IntersectionObserver for viewport detection
20 | case appear
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/TextDecoration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextDecoration.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `TextDecoration` modifier.
14 | @Suite("TextDecoration Tests")
15 | @MainActor
16 | class TextDecorationModifierTests: IgniteTestSuite {
17 | @Test("Text Decoration Modifier", arguments: TextDecoration.allCases)
18 | func textDecorationNone(_ decoration: TextDecoration) async throws {
19 | let element = Span("Hello, World!").textDecoration(decoration)
20 | let output = element.markupString()
21 |
22 | #expect(output == "Hello, World! ")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Rendering/ResponseErrors/PageNotFoundError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PageNotFoundError.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// An HTTP error that represents a page not found.
9 | public struct PageNotFoundError: HTTPError {
10 | public let statusCode: Int
11 | public let title: String
12 | public let description: String
13 |
14 | public init() {
15 | self.statusCode = 404
16 | self.title = "Page Not Found"
17 | self.description = "The page you are looking for could not be found."
18 | }
19 | }
20 |
21 | public extension HTTPError where Self == PageNotFoundError {
22 | /// An HTTP error that represents a page not found.
23 | static var pageNotFound: HTTPError {
24 | PageNotFoundError()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Ignite/Actions/ToggleElementVisibility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToggleElementVisibility.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Toggles an element's visibility by adding or removing the "d-none" CSS class.
11 | public struct ToggleElementVisibility: Action {
12 | /// The unique identifier of the element we're trying to show/hide.
13 | var id: String
14 |
15 | /// Creates a new ToggleElementVisibility action from a specific page element ID.
16 | /// - Parameter id: The unique identifier of the element we're trying to show/hide.
17 | public init(_ id: String) {
18 | self.id = id
19 | }
20 |
21 | public func compile() -> String {
22 | "document.getElementById('\(id)').classList.toggle('d-none')"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/BorderStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BorderStyle.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Defines the style of a border.
9 | public enum BorderStyle: String {
10 | /// Specifies no border
11 | case none
12 | /// A series of dots
13 | case dotted
14 | /// A series of dashes
15 | case dashed
16 | /// A single solid line
17 | case solid
18 | /// Two parallel solid lines
19 | case double
20 | /// A 3D grooved effect that depends on the border color
21 | case groove
22 | /// A 3D ridged effect that depends on the border color
23 | case ridge
24 | /// A 3D inset effect that depends on the border color
25 | case inset
26 | /// A 3D outset effect that depends on the border color
27 | case outset
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/EmptyInlineElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyInlineElement.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A placeholder element that renders nothing
9 | /// Used as a default or fallback when no content is needed
10 | public struct EmptyInlineElement: InlineElement {
11 | /// Creates a new empty element
12 | public nonisolated init() {}
13 |
14 | /// Returns self as the body content since this is an empty element
15 | public var body: some InlineElement { self }
16 |
17 | /// Whether this element belongs to the framework.
18 | public var isPrimitive: Bool { true }
19 |
20 | /// Renders this element as an empty string
21 | /// - Returns: An empty string
22 | public func markup() -> Markup {
23 | Markup()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Ignite/Actions/CustomAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomAction.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Allows the user to inject hand-written JavaScript into an event. The code you provide
9 | /// will automatically be escaped.
10 | public struct CustomAction: Action {
11 | /// The JavaScript code to execute.
12 | var code: String
13 |
14 | /// Creates a new CustomAction action from the provided JavaScript code.
15 | /// - Parameter code: The code to execute.
16 | public init(_ code: String) {
17 | self.code = code
18 | }
19 |
20 | /// Renders this action using publishing context passed in.
21 | /// - Returns: The JavaScript for this action.
22 | public func compile() -> String {
23 | code.escapedForJavascript()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Ignite/Themes/BootstrapConstants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BootstrapConstants.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | extension Bootstrap {
9 | // Breakpoints
10 | static let smallBreakpoint: LengthUnit = .px(576)
11 | static let mediumBreakpoint: LengthUnit = .px(768)
12 | static let largeBreakpoint: LengthUnit = .px(992)
13 | static let xLargeBreakpoint: LengthUnit = .px(1200)
14 | static let xxLargeBreakpoint: LengthUnit = .px(1400)
15 |
16 | // Container sizes
17 | static let smallContainer: LengthUnit = .px(540)
18 | static let mediumContainer: LengthUnit = .px(720)
19 | static let largeContainer: LengthUnit = .px(960)
20 | static let xLargeContainer: LengthUnit = .px(1140)
21 | static let xxLargeContainer: LengthUnit = .px(1320)
22 | }
23 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Framework/Robots/KnownRobot.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KnownRobot.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for `KnownRobot`.
14 | @Suite("KnownRobot Tests")
15 | @MainActor
16 | struct KnownRobotTests {
17 | @Test("All KnownRobot cases", arguments: zip(
18 | KnownRobot.allCases, [
19 | "Applebot",
20 | "baiduspider",
21 | "bingbot",
22 | "GPTBot",
23 | "CCBot",
24 | "Googlebot",
25 | "slurp",
26 | "yandex"
27 | ]
28 | ))
29 | func allKnowRobots(robot: KnownRobot, expectedName: String) async throws {
30 | #expect(robot.rawValue == expectedName)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/Ignite/Types/ImageFit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageFit.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Represents CSS object-fit values for images
9 | public enum ImageFit: String {
10 | /// Stretches the image to fill the container without maintaining aspect ratio
11 | case fill = "fill"
12 |
13 | /// Scales the image to fit within the container while maintaining aspect ratio
14 | case fit = "contain"
15 |
16 | /// Scales the image to cover the container while maintaining aspect ratio, potentially cropping the image
17 | case cover = "cover"
18 |
19 | /// Similar to `fit` but never scales the image larger than its original size
20 | case scaleDown = "scale"
21 |
22 | /// Displays the image at its original size without scaling
23 | case none = "none"
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Actions/HideElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HideElement.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Hides a page element by appending the "d-none" CSS class.
9 | public struct HideElement: Action {
10 | /// The unique identifier of the element we're trying to hide.
11 | var id: String
12 |
13 | /// Creates a new HideElement action from a specific page element ID.
14 | /// - Parameter id: The unique identifier of the element we're trying to hide.
15 | public init(_ id: String) {
16 | self.id = id
17 | }
18 |
19 | /// Renders this action using publishing context passed in.
20 | /// - Returns: The JavaScript for this action.
21 | public func compile() -> String {
22 | "document.getElementById('\(id)').classList.add('d-none')"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Actions/ShowElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShowElement.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Shows a page element by removing the "d-none" CSS class.
9 | public struct ShowElement: Action {
10 | /// The unique identifier of the element we're trying to hide.
11 | var id: String
12 |
13 | /// Creates a new ShowElement action from a specific page element ID.
14 | /// - Parameter id: The unique identifier of the element we're trying to hide.
15 | public init(_ id: String) {
16 | self.id = id
17 | }
18 |
19 | /// Renders this action using publishing context passed in.
20 | /// - Returns: The JavaScript for this action.
21 | public func compile() -> String {
22 | "document.getElementById('\(id)').classList.remove('d-none')"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Robots/DisallowRule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DisallowRule.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A rule that disallows one specific robot from one or more paths on your site.
9 | public struct DisallowRule {
10 | var name: String
11 | var paths: [String]
12 |
13 | public init(name: String, paths: [String]) {
14 | self.name = name
15 | self.paths = paths
16 | }
17 |
18 | public init(name: String) {
19 | self.name = name
20 | self.paths = ["*"]
21 | }
22 |
23 | public init(robot: KnownRobot, paths: [String]) {
24 | self.name = robot.rawValue
25 | self.paths = paths
26 | }
27 |
28 | public init(robot: KnownRobot) {
29 | self.name = robot.rawValue
30 | self.paths = ["*"]
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/Ignite/Publishing/RobotsGenerator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RobotsGenerator.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | @MainActor
9 | struct RobotsGenerator {
10 | var site: any Site
11 |
12 | func generateRobots() -> String {
13 | let disallowRules = site.robotsConfiguration.disallowRules.map { rule in
14 | var ruleText = "User-agent: \(rule.name)\n"
15 |
16 | ruleText += rule.paths.map {
17 | "Disallow: \($0)\n"
18 | }.joined()
19 |
20 | return "\(ruleText)\n"
21 | }.joined()
22 |
23 | let result = """
24 | \(disallowRules)\
25 | User-agent: *
26 | Allow: /
27 |
28 | Sitemap: \(site.url.absoluteString)/sitemap.xml
29 | """
30 |
31 | return result
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/Ignite/Actions/SwitchTheme.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwitchTheme.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// An action that switches between themes by updating data-bs-theme
9 | /// and data-theme-state attributes on the document root.
10 | public struct SwitchTheme: Action {
11 | let themeID: String
12 |
13 | /// Creates a new theme switching action
14 | /// - Parameter theme: The ID of the theme to switch to (will be automatically sanitized)
15 | @MainActor public init(_ theme: any Theme) {
16 | self.themeID = theme.cssID
17 | }
18 |
19 | /// Compiles the action into JavaScript code that calls the switchTheme function
20 | /// - Returns: JavaScript code to execute the theme switch
21 | public func compile() -> String {
22 | "igniteSwitchTheme('\(themeID)');"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Analytics/AnalyticsService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnalyticsService.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Analytics {
11 | /// Defines the different types of analytics services supported
12 | public enum Service {
13 | /// Google Analytics 4
14 | case googleAnalytics(measurementID: String)
15 | /// Plausible Analytics
16 | case plausible(domain: String, measurements: Set = [])
17 | /// Fathom Analytics
18 | case fathom(siteID: String)
19 | /// Clicky Analytics
20 | case clicky(siteID: String)
21 | /// TelemetryDeck Analytics
22 | case telemetryDeck(siteID: String)
23 | /// Custom analytics script
24 | case custom(_ code: String)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Ignite/Publishing/BootstrapOptions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BootstrapOptions.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Configuration option including a remote or local version of Bootstrap or none at all
9 | public enum BootstrapOptions: Sendable {
10 | /// Use a local copy of Bootstrap. This will copy Bootstrap's CSS and JavaScript to the
11 | /// generated code and add references in the generated pages
12 | case localBootstrap
13 | /// Use a remote copy of Bootstrap. This will add references to the Bootstrap in the
14 | /// generated pages and reference Bootstrap's CDN. Local copies of Bootstrap
15 | /// will not be copied over
16 | case remoteBootstrap
17 | /// Don't add any rereferences to Bootstrap in the generated pages and don't copy any
18 | /// files over
19 | case none
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/FontStyleModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontStyleModifier.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `FontStyleModifier` modifier.
14 | @Suite("FontStyleModifier Tests")
15 | @MainActor
16 | class FontStyleModifierTests: IgniteTestSuite {
17 | private static let tagBasedStyles: [Font.Style] = [
18 | .title1, .title2, .title3, .title4, .title5, .title6, .body
19 | ]
20 |
21 | @Test("Font Style", arguments: await tagBasedStyles)
22 | func fontStyle(style: Font.Style) async throws {
23 | let element = Text("Hello").font(style)
24 | let output = element.markupString()
25 | #expect(output == "<\(style.description)>Hello\(style.description)>")
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/PrivacySensitive.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrivacySensitive.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `PrivacySensitive` modifier.
14 | @Suite("PrivacySensitive Tests")
15 | @MainActor
16 | struct PrivacySensitiveTests {
17 | @Test("Privacy Sensitive Modifier",
18 | arguments: [PrivacyEncoding.urlOnly, PrivacyEncoding.urlAndDisplay])
19 | func privacySensitive(encoding: PrivacyEncoding) async throws {
20 | let element = Link("Go Home", target: "/").privacySensitive(encoding)
21 | let output = element.markupString()
22 |
23 | #expect(output.contains("privacy-sensitive=\"\(encoding.rawValue)\""))
24 | #expect(output.contains("protected-link"))
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/LayoutContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Layoutable.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A protocol that allows pages of any type to use a layout.
9 | @MainActor
10 | public protocol LayoutContent: Sendable {
11 | /// The type of HTML content this element contains.
12 | associatedtype Body: BodyElement
13 |
14 | /// The content and behavior of this `HTML` element.
15 | @HTMLBuilder var body: Body { get }
16 |
17 | /// The type of layout you want this page to use.
18 | associatedtype LayoutType: Layout
19 |
20 | /// The page layout this content should use.
21 | var layout: LayoutType { get }
22 | }
23 |
24 | public extension LayoutContent {
25 | /// Defaults to the main layout defined in `Site`.
26 | var layout: DefaultLayout {
27 | DefaultLayout()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/ArticlePage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentLayout.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Article pages describe custom page structures for articles. You can provide
9 | /// one page in your site to use that for all articles, or create custom pages and
10 | /// assign them uniquely to individual articles.
11 | ///
12 | /// ```swift
13 | /// struct MyArticle: ArticlePage {
14 | /// var body: HTML {
15 | /// Text(content.title)
16 | /// .font(.title1)
17 | /// Text(content.description)
18 | /// }
19 | /// }
20 | /// ```
21 | @MainActor
22 | public protocol ArticlePage: LayoutContent {}
23 |
24 | public extension ArticlePage {
25 | /// The current Markdown content being rendered.
26 | var article: Article {
27 | PublishingContext.shared.environment.article
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Robots/KnownRobot.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KnownRobot.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A collection of known robots.
9 | public enum KnownRobot: String, CaseIterable, Sendable {
10 | /// Apple's robot.
11 | case apple = "Applebot"
12 |
13 | /// Baidu's robot.
14 | case baidu = "baiduspider"
15 |
16 | /// Bing's robot.
17 | case bing = "bingbot"
18 |
19 | /// ChatGPT's robot.
20 | case chatGPT = "GPTBot"
21 |
22 | /// The Common Crawl robot, which is used to make websites available
23 | /// to researchers. Sadly also used by OpenAI to train ChatGPT.
24 | case commonCrawl = "CCBot"
25 |
26 | /// Google's robot.
27 | case google = "Googlebot"
28 |
29 | /// Yahoo's robot.
30 | case yahoo = "slurp"
31 |
32 | /// Yandex's robot.
33 | case yandex = "yandex"
34 | }
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Short description**
11 | A clear and concise description of what the bug is.
12 |
13 | **Steps to Reproduce**
14 | Steps to reproduce the behavior – exact code or commands used, ideally as a minimal reproducible example.
15 |
16 | **Expected behavior**
17 | A clear and concise description of what you expected to happen.
18 |
19 | **Build information**
20 | - OS: [e.g. macOS 26.1]
21 | - Editor: [e.g. Xcode 26.1]
22 | - Swift version: [e.g. Swift 6.2]
23 | - Ignite version: [e.g. 0.6]
24 | - Tested browsers: [e.g. Safari 26.0.1]
25 |
26 | **Screenshots**
27 | If applicable, add screenshots to help explain your problem.
28 |
29 | **Additional context**
30 | Add any other context about the problem here, such as any attempts you've made to identify or debug the issue.
31 |
--------------------------------------------------------------------------------
/Sources/Ignite/Elements/Divider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Divider.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A horizontal divider for your page, that can also be used to divide elements
9 | /// in a dropdown.
10 | public struct Divider: HTML, DropdownItem {
11 | /// The content and behavior of this HTML.
12 | public var body: some HTML { self }
13 |
14 | /// The standard set of control attributes for HTML elements.
15 | public var attributes = CoreAttributes()
16 |
17 | /// Whether this HTML belongs to the framework.
18 | public var isPrimitive: Bool { true }
19 |
20 | /// Creates a new divider.
21 | public init() {}
22 |
23 | /// Renders this element using publishing context passed in.
24 | /// - Returns: The HTML for this element.
25 | public func markup() -> Markup {
26 | Markup(" ")
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Ignite/Components/FeedLink.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FeedLink.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Displays a link to your RSS feed, if enabled.
9 | public struct FeedLink: HTML {
10 |
11 | @Environment(\.builtInIconsEnabled) private var builtInIconsEnabled
12 | @Environment(\.feedConfiguration) private var feedConfig
13 |
14 | public var body: some HTML {
15 | if let feedConfig {
16 | Text {
17 | if builtInIconsEnabled != .none {
18 | Image(systemName: "rss-fill")
19 | .foregroundStyle("#f26522")
20 | .margin(.trailing, .px(10))
21 | }
22 |
23 | Link("RSS Feed", target: feedConfig.path)
24 | EmptyInlineElement()
25 | }
26 | .horizontalAlignment(.center)
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/ControlSize.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ControlSize.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// The size of form controls and labels
9 | public enum ControlSize: Sendable, CaseIterable {
10 | case small
11 | case medium
12 | case large
13 |
14 | var controlClass: String? {
15 | switch self {
16 | case .small: "form-control-sm"
17 | case .large: "form-control-lg"
18 | case .medium: nil
19 | }
20 | }
21 |
22 | var labelClass: String? {
23 | switch self {
24 | case .small: "col-form-label-sm"
25 | case .large: "col-form-label-lg"
26 | case .medium: nil
27 | }
28 | }
29 |
30 | var buttonClass: String? {
31 | switch self {
32 | case .small: "btn-sm"
33 | case .large: "btn-lg"
34 | case .medium: nil
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/List.swift:
--------------------------------------------------------------------------------
1 | //
2 | // List.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `List` element.
14 | @Suite("List Tests")
15 | @MainActor
16 | class ListTests: IgniteTestSuite {
17 | @Test("Basic Rendering")
18 | func testEmptyListRendering() async throws {
19 | let list = List {}
20 | let output = list.markupString()
21 | #expect(output == "")
22 | }
23 |
24 | @Test("Basic Unordered List")
25 | func unorderedList() async throws {
26 | let list = List {
27 | "Veni"
28 | "Vidi"
29 | "Vici"
30 | }
31 | let output = list.markupString()
32 |
33 | #expect(output == "")
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Span.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Span.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Span` element.
14 | @Suite("Span Tests")
15 | @MainActor class SpanTests: IgniteTestSuite {
16 | @Test("Single Element", arguments: ["This is a test", "Another test"])
17 | func singleElement(spanText: String) async throws {
18 | let element = Span(spanText)
19 | let output = element.markupString()
20 |
21 | #expect(output == "\(spanText) ")
22 | }
23 |
24 | @Test("Builder", arguments: ["This is a test", "Another test"])
25 | func builder(spanText: String) async throws {
26 | let element = Span { spanText }
27 | let output = element.markupString()
28 |
29 | #expect(output == "\(spanText) ")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/TagPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TagPage.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Tag pages show all articles on your site that match a specific tag,
9 | /// or all articles period if `tag` is nil. You get to decide what is shown
10 | /// on those pages by making a custom type that conforms to this protocol.
11 | ///
12 | /// ```swift
13 | /// struct MyTagPage: TagPage {
14 | /// var body: some HTML {
15 | /// Text(tag.name)
16 | /// .font(.title1)
17 | ///
18 | /// List(tag.articles) { article in
19 | /// Link(article)
20 | /// }
21 | /// }
22 | /// }
23 | /// ```
24 | @MainActor
25 | public protocol TagPage: LayoutContent {}
26 |
27 | extension TagPage {
28 | /// The current tag during page generation.
29 | public var tag: any Category {
30 | PublishingContext.shared.environment.category
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/Ignite/Resources/js/prism-go.js:
--------------------------------------------------------------------------------
1 | Prism.languages.go=Prism.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"|`[^`]*`/,lookbehind:!0,greedy:!0},keyword:/\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,boolean:/\b(?:_|false|iota|nil|true)\b/,number:[/\b0(?:b[01_]+|o[0-7_]+)i?\b/i,/\b0x(?:[a-f\d_]+(?:\.[a-f\d_]*)?|\.[a-f\d_]+)(?:p[+-]?\d+(?:_\d+)*)?i?(?!\w)/i,/(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?[\d_]+)?i?(?!\w)/i],operator:/[*\/%^!=]=?|\+[=+]?|-[=-]?|\|[=|]?|&(?:=|&|\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\.\.\./,builtin:/\b(?:append|bool|byte|cap|close|complex|complex(?:64|128)|copy|delete|error|float(?:32|64)|u?int(?:8|16|32|64)?|imag|len|make|new|panic|print(?:ln)?|real|recover|rune|string|uintptr)\b/}),Prism.languages.insertBefore("go","string",{char:{pattern:/'(?:\\.|[^'\\\r\n]){0,10}'/,greedy:!0}}),delete Prism.languages.go["class-name"];
2 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Body.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTMLBody.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `title` element.
14 | @Suite("Body Tests")
15 | @MainActor class BodyTests: IgniteTestSuite {
16 | static let sites: [any Site] = [TestSite(), TestSubsite()]
17 |
18 | @Test("Simple Body Test", arguments: await Self.sites)
19 | func simpleBody(for site: any Site) async throws {
20 | let element = Body()
21 | let output = element.markupString()
22 | let path = publishingContext.path(for: URL(string: "/js")!)
23 |
24 | #expect(output == """
25 | \
26 | \
27 | \
28 |
29 | """)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Types/Angle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Angle.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Angle` type.
14 | @Suite("Angle Tests")
15 | @MainActor
16 | struct AngleTests {
17 | @Test("Degrees")
18 | func degreesTest() async throws {
19 | let angle = Angle.degrees(180)
20 | let output = angle.value
21 | #expect(output == "180.0deg")
22 | }
23 |
24 | @Test("Radians")
25 | func radiansTest() async throws {
26 | let angle = Angle.radians(.pi)
27 | let output = angle.value
28 | #expect(output == "\(Double.pi)rad")
29 | }
30 |
31 | @Test("Turns")
32 | func turnsTest() async throws {
33 | let angle = Angle.turns(0.5)
34 | let output = angle.value
35 | #expect(output == "0.5turn")
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/Ignite/Rendering/SiteMetadata.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SiteMetadata.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// The core metadata of your website, such as name, description, and URL.
9 | public struct SiteMetadata: Sendable {
10 | /// The name of the site
11 | private(set) public var name: String
12 |
13 | /// A string to append to the end of page titles
14 | private(set) public var titleSuffix: String
15 |
16 | /// An optional description for the site
17 | private(set) public var description: String?
18 |
19 | /// The base URL for the site
20 | private(set) public var url: URL
21 | }
22 |
23 | extension SiteMetadata {
24 | /// Creates an empty page for use as a default value
25 | @MainActor static let empty = SiteMetadata(
26 | name: "",
27 | titleSuffix: "",
28 | description: "",
29 | url: URL(string: "about:blank")!
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Embed.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Embed.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Embed` element.
14 | @Suite("Embed Tests")
15 | @MainActor
16 | class EmbedTests: IgniteTestSuite {
17 | @Test("Basic Embed")
18 | func basicEmbed() async throws {
19 | let element = Embed(youTubeID: "dQw4w9WgXcQ", title: "There was only ever going to be one video used here.")
20 | let output = element.markupString()
21 |
22 | #expect(output == """
23 |
27 | """)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Title.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Title.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `title` element.
14 | @Suite("Title Tests")
15 | @MainActor
16 | class TitleTests: IgniteTestSuite {
17 | @Test("Empty Title", arguments: [""])
18 | func empty(emptyTitleText: String) async throws {
19 | let element = Title(emptyTitleText)
20 | let output = element.markupString()
21 |
22 | #expect(output == "\(emptyTitleText) - My Test Site ")
23 | }
24 |
25 | @Test("Builder", arguments: ["Example Page", "Another Example Page"])
26 | func builder(titleText: String) async throws {
27 | let element = Title(titleText)
28 | let output = element.markupString()
29 |
30 | #expect(output == "\(titleText) - My Test Site ")
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/TestWebsitePackage/Sources/TestSiteWithErrorPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestSiteWithErrorPage.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Ignite
10 |
11 | /// A test site that shows an error page.
12 | struct TestSiteWithErrorPage: Site {
13 | var name = "My Error Page Test Site"
14 | var titleSuffix = " - My Test Site"
15 | var url = URL(static: "https://www.example.com")
16 | var timeZone: TimeZone?
17 | var language: Language = .english
18 |
19 | var homePage = TestPage()
20 | var layout = EmptyLayout()
21 |
22 | var errorPage = TestErrorPage()
23 |
24 | var articlePages: [any ArticlePage] = [
25 | TestStory()
26 | ]
27 |
28 | var staticPages: [any StaticPage] = [
29 | TestPage()
30 | ]
31 |
32 | init() {}
33 |
34 | init(errorPage: ErrorPageType) {
35 | self.errorPage = errorPage
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/ImageFitModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageFit-Modifier.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | public extension Image {
9 | /// Applies sizing and positioning behavior to an image.
10 | /// - Parameters:
11 | /// - fit: The scaling behavior to apply to the image. Defaults to `.cover`
12 | /// - anchor: The position within the container where the image should be anchored. Defaults to `.center`
13 | /// - Returns: A modified image with the specified fit and anchor point applied
14 | func imageFit(
15 | _ fit: ImageFit = .cover,
16 | anchor: UnitPoint = .center
17 | ) -> some InlineElement {
18 | let xPercent = Int(anchor.x * 100)
19 | let yPercent = Int(anchor.y * 100)
20 |
21 | return self
22 | .class("w-100 h-100 object-fit-\(fit.rawValue)")
23 | .style(.objectPosition, "\(xPercent)% \(yPercent)%")
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/CodeBlock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodeBlock.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `CodeBlock` element.
14 | @Suite("CodeBlock Tests")
15 | @MainActor
16 | class CodeBlockTests: IgniteTestSuite {
17 | @Test("Rendering a code block")
18 | func codeBlockTest() {
19 | let element = CodeBlock { """
20 | import Foundation
21 | struct CodeBlockTest {
22 | let name: String
23 | }
24 | let test = CodeBlockTest(name: "Swift")
25 | """ }
26 |
27 | let output = element.markupString()
28 |
29 | #expect(output == """
30 | import Foundation
31 | struct CodeBlockTest {
32 | let name: String
33 | }
34 | let test = CodeBlockTest(name: "Swift")
35 | """)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Strong.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Strong.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Strong` element.
14 | @Suite("Strong Tests")
15 | @MainActor class StrongTests: IgniteTestSuite {
16 | @Test("Single Element", arguments: ["This is a test", "Another test", ""])
17 | func singleElement(strongText: String) async throws {
18 | let element = Strong(strongText)
19 | let output = element.markupString()
20 |
21 | #expect(output == "\(strongText) ")
22 | }
23 |
24 | @Test("Builder", arguments: ["This is a test", "Another test", ""])
25 | func builder(strongText: String) async throws {
26 | let element = Strong { strongText }
27 | let output = element.markupString()
28 |
29 | #expect(output == "\(strongText) ")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Framework/Robots/DefaultRobotsConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Untitled.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for `DefaultRobotsConfiguration`.
14 | @Suite("DefaultRobotsConfiguration Tests")
15 | @MainActor
16 | struct DefaultRobotsConfigurationTests {
17 | @Test("Assert `disallowRules` is empty by default")
18 | func assertMockConformsToProtocol() async throws {
19 | let configuration = DefaultRobotsConfiguration()
20 | #expect(configuration.disallowRules.isEmpty)
21 | }
22 |
23 | @Test("Assert `disallowRules` reflects updates")
24 | mutating func assertDisallowRules() async throws {
25 | var configuration = DefaultRobotsConfiguration()
26 | configuration.disallowRules = [DisallowRule(name: "example")]
27 | #expect(!configuration.disallowRules.isEmpty)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Layout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Layout.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Layouts allow you to have complete control over the HTML used to generate
9 | /// your pages.
10 | ///
11 | /// Example:
12 | /// ```swift
13 | /// struct BlogLayout: Layout {
14 | /// var body: some Document {
15 | /// Body {
16 | /// content
17 | /// Footer()
18 | /// }
19 | /// }
20 | /// }
21 | /// ```
22 | @MainActor
23 | public protocol Layout {
24 | /// The type of Document content this element contains.
25 | associatedtype Content: Document
26 | /// The main content of the layout.
27 | @DocumentBuilder var body: Content { get }
28 | }
29 |
30 | public extension Layout {
31 | /// The current page being rendered.
32 | var content: some HTML {
33 | Section(PublishingContext.shared.environment.pageContent)
34 | .class("ig-main-content")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/TextSelection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextSelection.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Controls whether the user can select the text inside this element or not.
9 | public enum TextSelection: String {
10 | case automatic = "auto"
11 | case all
12 | case none
13 | }
14 |
15 | public extension HTML {
16 | /// Adjusts whether the user can select the text inside this element. You of course
17 | /// welcome to use this how you see fit, but please exercise restraint – not only
18 | /// does disabling selection annoy some people, but it can cause a genuine
19 | /// accessibility problem if you aren't careful.
20 | /// - Parameter selection: The new text selection value.
21 | /// - Returns: A copy of the current element with the updated text selection value.
22 | func textSelection(_ selection: TextSelection) -> some HTML {
23 | self.class("user-select-\(selection)")
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Ignite/Types/ColumnWidth.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColumnWidth.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Controls how many columns a given block element takes up in a `Section`.
9 | struct ColumnWidth: Equatable, Sendable {
10 | /// The Bootstrap class name for the column width.
11 | var className: String
12 |
13 | /// The column will expand to fill available space, distributing evenly with other `.uniform` columns.
14 | /// For example, if there are three `.uniform` elements in a 12-column section,
15 | /// each will automatically take up four columns.
16 | static let uniform = ColumnWidth(className: "col")
17 |
18 | /// The columns should be sized based on its content.
19 | static let intrinsic = ColumnWidth(className: "col-auto")
20 |
21 | /// This element should take up a precise number of columns.
22 | static func count(_ width: Int) -> Self {
23 | .init(className: "col-md-\(width)")
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/IgniteTestSuite.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestContext.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | @testable import Ignite
10 |
11 | /// A base class for Ignite tests that manages the lifecycle of `TestSite`'s publishing context.
12 | ///
13 | /// This class automatically initializes the publishing context with a test site during initialization
14 | /// and cleanup, ensuring each test starts with a fresh context.
15 | ///
16 | /// - Important: Subclassing is required for suites that test `HTML` elements or modifiers.
17 | @MainActor
18 | class IgniteTestSuite {
19 | let site: any Site = TestSite()
20 |
21 | var publishingContext: PublishingContext {
22 | PublishingContext.shared
23 | }
24 |
25 | /// Creates a new test instance and initializes the publishing context for `TestSite`.
26 | init() throws {
27 | try PublishingContext.initialize(for: TestSite(), from: #filePath)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Underline.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Underline.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Underline` element.
14 | @Suite("Underline Tests")
15 | @MainActor
16 | class UnderlineTests: IgniteTestSuite {
17 | @Test("Single Element Test", arguments: ["This is a test", "Another test", ""])
18 | func singleElement(underlineText: String) async throws {
19 | let element = Underline(underlineText)
20 | let output = element.markupString()
21 |
22 | #expect(output == "\(underlineText) ")
23 | }
24 |
25 | @Test("Builder", arguments: ["This is a test", "Another test", ""])
26 | func builder(underlineText: String) async throws {
27 | let element = Underline { underlineText }
28 | let output = element.markupString()
29 |
30 | #expect(output == "\(underlineText) ")
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/Ignite/Actions/Event Modifiers/ClickModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClickModifier.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | public extension HTML {
9 | /// Adds an "onclick" JavaScript event to this element.
10 | /// - Parameter actions: A closure that returns the actions to execute when clicked.
11 | /// - Returns: A modified HTML element with the click event handler attached.
12 | func onClick(@ActionBuilder actions: () -> [Action]) -> some HTML {
13 | self.onEvent(.click, actions())
14 | }
15 | }
16 |
17 | public extension InlineElement {
18 | /// Adds an "onclick" JavaScript event to this inline element.
19 | /// - Parameter actions: A closure that returns the actions to execute when clicked.
20 | /// - Returns: A modified inline HTML element with the click event handler attached.
21 | func onClick(@ActionBuilder actions: () -> [Action]) -> some InlineElement {
22 | self.onEvent(.click, actions())
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Elements/UnderlineProminence.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UnderlineProminence.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Defines the prominence of the underline decoration for links,
9 | /// allowing customization of both base and hover styles.
10 | public enum UnderlineProminence: Int, CustomStringConvertible, Equatable {
11 | /// No underline style with an opacity of 0%.
12 | case none = 0
13 | /// A faint underline style with an opacity of 10%.
14 | case faint = 10
15 | /// A light underline style with an opacity of 25%.
16 | case light = 25
17 | /// A medium underline style with an opacity of 50%.
18 | case medium = 50
19 | /// A bold underline style with an opacity of 75%.
20 | case bold = 75
21 | /// A fully opaque underline style with an opacity of 100%.
22 | case heavy = 100
23 |
24 | /// The Bootstrap opacity suffix.
25 | public var description: String {
26 | rawValue.formatted()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/IgnorePageGutters.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScreenFrame.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | public extension HTML {
9 | /// Determines whether this element should observe the site
10 | /// width or extend from one edge of the screen to the other.
11 | /// - Parameters:
12 | /// - ignore: Whether this HTML should ignore the page gutters. Defaults to `true`.
13 | /// - - Returns: A modified element that either obeys or ignores the page gutters.
14 | func ignorePageGutters(_ ignore: Bool = true) -> some HTML {
15 | AnyHTML(ignorePageGuttersModifer(ignore))
16 | }
17 | }
18 |
19 | private extension HTML {
20 | func ignorePageGuttersModifer(_ shouldIgnore: Bool = true) -> any HTML {
21 | if shouldIgnore {
22 | self.style(.init(.width, value: "100vw"), .init(.marginInline, value: "calc(50% - 50vw)"))
23 | } else {
24 | self.class("container")
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/String-CSStoJS.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String-SplitAndTrim.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String {
11 | /// Converts CSS names to JavaScript names, e.g. box-shadow
12 | /// becomes boxShadow, background-color becomes backgroundColor.
13 | /// - Returns: The string with CSS names replaced with JS names
14 | func convertingCSSNamesToJS() -> String {
15 | // Split the CSS property name by -.
16 | let parts = self.split(separator: "-")
17 |
18 | // Capitalize each part except the first.
19 | let camelCased = parts.enumerated().map { index, part in
20 | if index == 0 {
21 | String(part)
22 | } else {
23 | part.prefix(1).uppercased() + part.dropFirst()
24 | }
25 | }
26 |
27 | // Send back the full, rejoined string as camel case.
28 | return camelCased.joined()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/NavigationItemConfigurable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationItemConfigurable.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A protocol that allows elements to be configured for placement in a navigation bar.
9 | protocol NavigationItemConfigurable: BodyElement {
10 | /// Whether this element is configured as a navigation item.
11 | var isNavigationItem: Bool { get set }
12 |
13 | /// Returns a new instance of the element configured for use in a navigation bar.
14 | func configuredAsNavigationItem(_ isNavItem: Bool) -> Self
15 | }
16 |
17 | extension NavigationItemConfigurable {
18 | /// Configures this element to be placed inside a `NavigationBar`.
19 | /// - Returns: A new element instance suitable for placement
20 | /// inside a `NavigationBar`.
21 | func configuredAsNavigationItem(_ isNavItem: Bool = true) -> Self {
22 | var copy = self
23 | copy.isNavigationItem = isNavItem
24 | return copy
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Queries/DisplayModeQuery.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DisplayModeQuery.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Applies styles based on the web application's display mode.
9 | public enum DisplayModeQuery: String, Query, CaseIterable {
10 | /// Standard browser mode
11 | case browser = "display-mode: browser"
12 | /// Full screen mode
13 | case fullscreen = "display-mode: fullscreen"
14 | /// Minimal UI mode
15 | case minimalUI = "display-mode: minimal-ui"
16 | /// Picture-in-picture mode
17 | case pip = "display-mode: picture-in-picture"
18 | /// Standalone application mode
19 | case standalone = "display-mode: standalone"
20 | /// Window controls overlay mode
21 | case windowControlsOverlay = "display-mode: window-controls-overlay"
22 |
23 | public var condition: String { rawValue }
24 | }
25 |
26 | extension DisplayModeQuery: MediaFeature {
27 | public var description: String {
28 | rawValue
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/Ignite/Actions/DismissModal.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DismissModal.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Dismiss a modal dialog with the content of the page element identified by ID
9 | public struct DismissModal: Action {
10 | /// The unique identifier of the element of the modal we're trying to dismiss.
11 | var id: String
12 |
13 | /// Creates a new DismissModal action from a specific page element ID.
14 | /// - Parameter id: The unique identifier of the element of the modal we're trying to dismiss.
15 | public init(id: String) {
16 | self.id = id
17 | }
18 |
19 | /// Renders this action using publishing context passed in.
20 | /// - Returns: The JavaScript for this action.
21 | public func compile() -> String {
22 | """
23 | const modal = document.getElementById('\(id)');
24 | const modalInstance = bootstrap.Modal.getInstance(modal);
25 | if (modalInstance) { modalInstance.hide(); }
26 | """
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Alert.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Alert.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Alert` element.
14 | @Suite("Alert Tests")
15 | @MainActor
16 | class AlertTests: IgniteTestSuite {
17 | @Test("All Alert roles are correctly set", arguments: zip(Role.standardRoles, [
18 | "alert-primary",
19 | "alert-secondary",
20 | "alert-success",
21 | "alert-danger",
22 | "alert-warning",
23 | "alert-info",
24 | "alert-light",
25 | "alert-dark"]))
26 | func allRolesForAlertVariant(role: Role, cssAppliedClass: String) async throws {
27 | let element = Alert {
28 | Text("This is not an exercice")
29 | }.role(role)
30 |
31 | let output = element.markup()
32 |
33 | #expect(output.string == "")
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Attribute.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Attribute.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A simple key-value pair of strings that is able to store custom attributes.
9 | struct Attribute: Hashable, Equatable, Sendable, Comparable, CustomStringConvertible {
10 | /// The attribute's name, e.g. "target" or "rel".
11 | var name: String
12 |
13 | /// The attribute's value, e.g. "myFrame" or "stylesheet".
14 | var value: String?
15 |
16 | init(name: String, value: String) {
17 | self.name = name
18 | self.value = value
19 | }
20 |
21 | init(_ name: String) {
22 | self.name = name
23 | self.value = nil
24 | }
25 |
26 | public var description: String {
27 | if let value {
28 | "\(name)=\"\(value)\""
29 | } else {
30 | name
31 | }
32 | }
33 |
34 | public static func < (lhs: Attribute, rhs: Attribute) -> Bool {
35 | lhs.description < rhs.description
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/Ignite/Resources/js/prism-markuptemplating.js:
--------------------------------------------------------------------------------
1 | !function(e){function n(e,n){return"___"+e.toUpperCase()+n+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(t,a,r,o){if(t.language===a){var c=t.tokenStack=[];t.code=t.code.replace(r,(function(e){if("function"==typeof o&&!o(e))return e;for(var r,i=c.length;-1!==t.code.indexOf(r=n(a,i));)++i;return c[i]=e,r})),t.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(t,a){if(t.language===a&&t.tokenStack){t.grammar=e.languages[a];var r=0,o=Object.keys(t.tokenStack);!function c(i){for(var u=0;u=o.length);u++){var g=i[u];if("string"==typeof g||g.content&&"string"==typeof g.content){var l=o[r],s=t.tokenStack[l],f="string"==typeof g?g:g.content,p=n(a,l),k=f.indexOf(p);if(k>-1){++r;var m=f.substring(0,k),d=new e.Token(a,e.tokenize(s,t.grammar),"language-"+a,s),h=f.substring(k+p.length),v=[];m&&v.push.apply(v,c([m])),v.push(d),h&&v.push.apply(v,c([h])),"string"==typeof g?i.splice.apply(i,[u,1].concat(v)):g.content=v}}else g.content&&c(g.content)}return i}(t.tokens)}}}})}(Prism);
2 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/TestWebsitePackage/Sources/TestSite.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestSite.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Ignite
10 |
11 | /// An example site used in tests.
12 | struct TestSite: Site {
13 | var name = "My Test Site"
14 | var titleSuffix = " - My Test Site"
15 | var url = URL(static: "https://www.example.com")
16 | var timeZone: TimeZone?
17 | var language: Language = .english
18 |
19 | var homePage = TestPage()
20 | var layout = EmptyLayout()
21 | var builtInIconsEnabled: BootstrapOptions = .localBootstrap
22 |
23 | var feedConfiguration = FeedConfiguration(
24 | mode: .descriptionOnly,
25 | contentCount: 20,
26 | image: .init(url: "path/to/image.png", width: 100, height: 100)
27 | )
28 |
29 | var articlePages: [any ArticlePage] = [
30 | TestStory()
31 | ]
32 |
33 | init() {}
34 |
35 | init(timeZone: TimeZone) {
36 | self.timeZone = timeZone
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/Ignite/Elements/ControlLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Label.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A form label with support for various styles
9 | struct ControlLabel: InlineElement {
10 | /// The content and behavior of this HTML.
11 | var body: some InlineElement { self }
12 |
13 | /// The standard set of control attributes for HTML elements.
14 | var attributes = CoreAttributes()
15 |
16 | /// Whether this HTML belongs to the framework.
17 | var isPrimitive: Bool { true }
18 |
19 | /// The text content of the label
20 | private let text: any InlineElement
21 |
22 | /// Creates a new control label with the specified text content.
23 | /// - Parameter text: The inline element to display within the label.
24 | init(_ text: any InlineElement) {
25 | self.text = text
26 | }
27 |
28 | func markup() -> Markup {
29 | let textHTML = text.markupString()
30 | return Markup("\(textHTML) ")
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/IgniteCLI/IgniteCLI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IgniteCLI.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import ArgumentParser
9 | import Foundation
10 |
11 | /// The main entry point for our tool. This points users to one
12 | /// of the subcommands.
13 | @main
14 | struct IgniteCLI: ParsableCommand {
15 | static let discussion = """
16 | Example usages:
17 | ignite new MySite – create a new site called MySite.
18 | ignite build - flattens the current site to HTML.
19 | ignite run – runs the current site in a local web server.
20 | ignite run --preview – runs the current site in a local web server, and opens it in your web browser.
21 | """
22 |
23 | static let configuration = CommandConfiguration(
24 | commandName: "ignite",
25 | abstract: "A command-line tool for manipulating Ignite sites.",
26 | discussion: discussion,
27 | version: "0.6.0",
28 | subcommands: [NewCommand.self, BuildCommand.self, RunCommand.self]
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/Ignite/Types/VerticalAlignment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VerticalAlignment.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Defines how content should be aligned vertically within its container
9 | public enum VerticalAlignment: Equatable, Sendable {
10 | /// Align content to the top
11 | case top
12 | /// Center content vertically
13 | case center
14 | /// Align content to the bottom
15 | case bottom
16 |
17 | /// The Bootstrap class for the container with this alignment
18 | var containerAlignmentClass: String {
19 | switch self {
20 | case .top: "align-items-start"
21 | case .center: "align-items-center"
22 | case .bottom: "align-items-end"
23 | }
24 | }
25 |
26 | /// The Bootstrap class applied to items inside a container
27 | var itemAlignmentClass: String {
28 | switch self {
29 | case .top: "align-self-start"
30 | case .center: "align-self-center"
31 | case .bottom: "align-self-end"
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/Ignite/Actions/Event Modifiers/DoubleClickModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DoubleClickModifier.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | public extension HTML {
9 | /// Adds an "ondblclick" JavaScript event to this element.
10 | /// - Parameter actions: A closure that returns the actions to execute when double-clicked.
11 | /// - Returns: A modified HTML element with the double click event handler attached.
12 | func onDoubleClick(@ActionBuilder actions: () -> [Action]) -> some HTML {
13 | self.onEvent(.doubleClick, actions())
14 | }
15 | }
16 |
17 | public extension InlineElement {
18 | /// Adds an "ondblclick" JavaScript event to this inline element.
19 | /// - Parameter actions: A closure that returns the actions to execute when double-clicked.
20 | /// - Returns: A modified inline HTML element with the double click event handler attached.
21 | func onDoubleClick(@ActionBuilder actions: () -> [Action]) -> some InlineElement {
22 | self.onEvent(.doubleClick, actions())
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/String-AbsoluteLinks.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String-AbsoluteLinks.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String {
11 | /// Converts links and image sources from relative links to absolute.
12 | /// - Parameter url: The base URL, which is usually your web domain.
13 | /// - Returns: The adjusted string, where all relative links are absolute.
14 | func makingAbsoluteLinks(relativeTo url: URL) -> String {
15 | var absolute = self
16 |
17 | // Fix images.
18 | absolute.replace(#/src="(?\/[^"]+)/#) { match in
19 | let fullURL = url.appending(path: match.output.path).absoluteString
20 | return "src=\"\(fullURL)"
21 | }
22 |
23 | // Fix links.
24 | absolute.replace(#/href="(?\/[^"]+)/#) { match in
25 | let fullURL = url.appending(path: match.output.path).absoluteString
26 | return "href=\"\(fullURL)"
27 | }
28 |
29 | return absolute
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/Clipped.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Clipped.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Clipped` modifier.
14 | @Suite("Clipped Tests")
15 | @MainActor
16 | class ClippedTests: IgniteTestSuite {
17 | @Test("Clipped Modifier")
18 | func clippedModifier() async throws {
19 | let element = Text("Hello").clipped()
20 | let output = element.markupString()
21 | #expect(output == "Hello
")
22 | }
23 |
24 | @Test("Clipped Modifier on Custom Element")
25 | func clippedModifier_onCustomElement() async throws {
26 | let element = TestElement().clipped()
27 | let output = element.markupString()
28 | #expect(output == """
29 | \
30 |
Test Heading!
\
31 |
Test Subheading
\
32 |
Test Label \
33 |
34 | """)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Subsite/IgniteSubsiteTestSuite.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IgniteSubsiteTestSuite.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | @testable import Ignite
10 |
11 | /// A base class for Ignite tests that manages the lifecycle of `TestSubsite`'s publishing context.
12 | /// - Important: Subclassing is required for suites that test `HTML` elements or modifiers.
13 | @MainActor
14 | class IgniteSubsiteTestSuite {
15 | let site: any Site = TestSubsite()
16 |
17 | var publishingContext: PublishingContext {
18 | PublishingContext.shared
19 | }
20 |
21 | /// Creates a new test instance and initializes the publishing context for `TestSubsite`.
22 | init() throws {
23 | try PublishingContext.initialize(for: TestSubsite(), from: #filePath)
24 | }
25 |
26 | /// Resets the publishing context when the test is deallocated.
27 | deinit {
28 | Task { @MainActor in
29 | try? PublishingContext.initialize(for: TestSite(), from: #filePath)
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/Ignite/Elements/Title.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Title.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Provides the title for a given page, which is rendered in the browser and also
9 | /// appears in search engine results.
10 | public struct Title: HeadElement {
11 | /// The standard set of control attributes for HTML elements.
12 | public var attributes = CoreAttributes()
13 |
14 | /// Whether this HTML belongs to the framework.
15 | public var isPrimitive: Bool { true }
16 |
17 | /// A plain-text string for the page title.
18 | var text: String
19 |
20 | /// Creates a new page title using the plain-text string provided.
21 | /// - Parameter text: The title to use for this page.
22 | public init(_ text: String) {
23 | self.text = text
24 | }
25 |
26 | /// Renders this element using publishing context passed in.
27 | /// - Returns: The HTML for this element.
28 | public func markup() -> Markup {
29 | Markup("\(text)\(publishingContext.site.titleSuffix) ")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/TestWebsitePackage/Sources/TestErrorPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestErrorPage.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Ignite
10 |
11 | struct TestErrorPage: ErrorPage {
12 |
13 | var title: String = "Test Error Page"
14 | var description: String = "Test Error Page Description"
15 |
16 | let errorChecker: (HTTPError) -> Void
17 |
18 | init(
19 | title: String = "Test Error Page",
20 | description: String = "Test Error Page Description",
21 | errorChecker: @escaping (HTTPError) -> Void = { _ in }
22 | ) {
23 | self.title = title
24 | self.description = description
25 | self.errorChecker = errorChecker
26 | }
27 |
28 | var body: some HTML {
29 | ErrorChecker { errorChecker(error) }
30 | }
31 |
32 | struct ErrorChecker: HTML {
33 | init(handler: @escaping () -> Void) {
34 | handler()
35 | }
36 |
37 | var body: some HTML {
38 | EmptyHTML()
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Event.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Event.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// One event that can trigger a series of actions, such as
9 | /// an onClick event hiding an element on the page.
10 | struct Event: Sendable, Hashable, Comparable {
11 | var name: String
12 | var actions: [any Action]
13 |
14 | static func == (lhs: Event, rhs: Event) -> Bool {
15 | rhs.name == lhs.name &&
16 | rhs.actions.map { $0.compile() } == lhs.actions.map { $0.compile() }
17 | }
18 |
19 | static func < (lhs: Event, rhs: Event) -> Bool {
20 | lhs.name < rhs.name
21 | }
22 |
23 | init(_ type: EventType, actions: [any Action]) {
24 | self.name = type.rawValue
25 | self.actions = actions
26 | }
27 |
28 | init(name: String, actions: [any Action]) {
29 | self.name = name
30 | self.actions = actions
31 | }
32 |
33 | func hash(into hasher: inout Hasher) {
34 | hasher.combine(name)
35 | hasher.combine(actions.map { $0.compile() })
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/LineSpacing.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LineSpacing.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `LineSpacing` modifier.
14 | @Suite("LineSpacing Tests")
15 | @MainActor
16 | struct LineSpacingTests {
17 | @Test("Custom Line Spacing", arguments: zip([2.5, 0.0, -2.0], ["2.5", "0", "-2"]))
18 | func lineSpacing(value: Double, expected: String) async throws {
19 | let element = Text("Hello, world!").lineSpacing(value)
20 | let output = element.markupString()
21 |
22 | #expect(output == "Hello, world!
")
23 | }
24 |
25 | @Test("Preset Line Spacing", arguments: LineSpacing.allCases)
26 | func lineSpacing(spacing: LineSpacing) async throws {
27 | let element = Text("Hello, world!").lineSpacing(spacing)
28 | let output = element.markupString()
29 |
30 | #expect(output == "Hello, world!
")
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/PrivacySensitive.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrivacySensitive.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Specifies what content should be encoded for privacy protection
9 | public enum PrivacyEncoding: String, Sendable {
10 | /// Only encode the URL (default)
11 | case urlOnly
12 |
13 | /// Encode both the URL and display text
14 | case urlAndDisplay
15 | }
16 |
17 | public extension Link {
18 | /// Marks this link as containing sensitive content that should be protected from scraping.
19 | /// Use this to protect email addresses, phone numbers, or other sensitive URLs from being
20 | /// scraped by bots while still remaining clickable for real users.
21 | ///
22 | /// - Parameter encoding: Controls what content gets encoded for privacy protection
23 | /// - Returns: A copy of the current link with protection enabled
24 | func privacySensitive(_ encoding: PrivacyEncoding = .urlOnly) -> some InlineElement {
25 | self.customAttribute(name: "privacy-sensitive", value: encoding.rawValue)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Paul Hudson and other authors.
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.
--------------------------------------------------------------------------------
/Sources/Ignite/Rendering/ResultBuilders/DocumentElementBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DocumentElementBuilder.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A result builder for populating the children of a `Document`.
9 | @MainActor
10 | @resultBuilder
11 | public struct DocumentElementBuilder {
12 | /// Creates a tuple containing the provided head and body elements.
13 | /// - Parameters:
14 | /// - head: The document's head section containing metadata.
15 | /// - body: The document's body section containing main content.
16 | /// - Returns: A tuple containing the provided head and body elements.
17 | public static func buildBlock(_ head: Head, _ body: Body) -> (Head, Body) {
18 | (head, body)
19 | }
20 |
21 | /// Creates a tuple containing a default head and the provided body element.
22 | /// - Parameter body: The document's body section containing main content.
23 | /// - Returns: A tuple containing a default head and the provided body element.
24 | public static func buildBlock(_ body: Body) -> (Head, Body) {
25 | (Head(), body)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Environment/Environment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Environment.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A property wrapper that provides access to values from the environment.
9 | ///
10 | /// Use `Environment` to read values that are propagated through your site's view hierarchy. For example:
11 | ///
12 | /// ```swift
13 | /// struct ContentView: HTMLRootElement {
14 | /// @Environment(\.themes) var themes
15 | /// }
16 | /// ```
17 | @MainActor
18 | @propertyWrapper public struct Environment {
19 | /// The key path to the desired environment value.
20 | private let keyPath: KeyPath
21 |
22 | /// The current value from the environment store.
23 | public var wrappedValue: Value {
24 | PublishingContext.shared.environment[keyPath: keyPath]
25 | }
26 |
27 | /// Creates an environment property with the given key path.
28 | /// - Parameter keyPath: A key path to a specific value in the environment.
29 | public init(_ keyPath: KeyPath) {
30 | self.keyPath = keyPath
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/Ignite/Resources/js/prism-wasm.js:
--------------------------------------------------------------------------------
1 | Prism.languages.wasm={comment:[/\(;[\s\S]*?;\)/,{pattern:/;;.*/,greedy:!0}],string:{pattern:/"(?:\\[\s\S]|[^"\\])*"/,greedy:!0},keyword:[{pattern:/\b(?:align|offset)=/,inside:{operator:/=/}},{pattern:/\b(?:(?:f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|neg?|nearest|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|sqrt|store(?:8|16|32)?|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))?|memory\.(?:grow|size))\b/,inside:{punctuation:/\./}},/\b(?:anyfunc|block|br(?:_if|_table)?|call(?:_indirect)?|data|drop|elem|else|end|export|func|get_(?:global|local)|global|if|import|local|loop|memory|module|mut|nop|offset|param|result|return|select|set_(?:global|local)|start|table|tee_local|then|type|unreachable)\b/],variable:/\$[\w!#$%&'*+\-./:<=>?@\\^`|~]+/,number:/[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/,punctuation:/[()]/};
2 |
--------------------------------------------------------------------------------
/Sources/Ignite/Resources/js/prism-css.js:
--------------------------------------------------------------------------------
1 | !function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism);
2 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/Cursor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cursor.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Cursor` modifier.
14 | @Suite("Cursor Tests")
15 | @MainActor
16 | class CursorTests: IgniteTestSuite {
17 | @Test("Cursor Modifier", arguments: Cursor.allCases)
18 | func cursorModifier(_ cursor: Cursor) async throws {
19 | let element = Span("Hello, World!").cursor(cursor)
20 | let output = element.markupString()
21 |
22 | #expect(output == "Hello, World! ")
23 | }
24 |
25 | @Test("Cursor Modifier on Custom Element", arguments: Cursor.allCases)
26 | func cursorModifier_onCustomElement(_ cursor: Cursor) async throws {
27 | let element = TestElement().cursor(cursor)
28 | let output = element.markupString()
29 | #expect(output == """
30 | \
31 |
Test Heading!
\
32 |
Test Subheading
\
33 |
Test Label \
34 |
35 | """)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/FontWeightModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontWeight.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | public extension HTML {
9 | /// Adjusts the font weight (boldness) of this font.
10 | /// - Parameter weight: The new font weight.
11 | /// - Returns: A new `Text` instance with the updated weight.
12 | func fontWeight(_ weight: Font.Weight) -> some HTML {
13 | self.style(.fontWeight, weight.rawValue.formatted())
14 | }
15 | }
16 |
17 | public extension InlineElement {
18 | /// Adjusts the font weight (boldness) of this font.
19 | /// - Parameter weight: The new font weight.
20 | /// - Returns: A new `Text` instance with the updated weight.
21 | func fontWeight(_ weight: Font.Weight) -> some InlineElement {
22 | self.style(.fontWeight, weight.rawValue.formatted())
23 | }
24 | }
25 |
26 | public extension StyledHTML {
27 | /// Adjusts the font weight (boldness) of this font.
28 | /// - Parameter weight: The new font weight.
29 | /// - Returns: A new instance with the updated weight.
30 | func fontWeight(_ weight: Font.Weight) -> Self {
31 | self.style(.fontWeight, weight.description)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/Animations/KeyframeBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyframeBuilder.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A result builder that enables a declarative syntax for creating keyframe animations.
9 | @resultBuilder
10 | public struct KeyframeBuilder {
11 | /// Combines multiple keyframes into an array.
12 | public static func buildBlock(_ components: Keyframe...) -> [Keyframe] {
13 | components
14 | }
15 |
16 | /// Converts a single keyframe into its base type.
17 | public static func buildExpression(_ expression: Keyframe) -> Keyframe {
18 | expression
19 | }
20 |
21 | /// Handles optional keyframes by providing nil as fallback.
22 | public static func buildOptional(_ component: Keyframe?) -> Keyframe? {
23 | component
24 | }
25 |
26 | /// Handles conditional keyframes by returning the first branch.
27 | public static func buildEither(first component: Keyframe) -> Keyframe {
28 | component
29 | }
30 |
31 | /// Handles conditional keyframes by returning the second branch.
32 | public static func buildEither(second component: Keyframe) -> Keyframe {
33 | component
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/Ignite/Publishing/CSS/FontFaceRule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontFace.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Represents a CSS @font-face rule
9 | struct FontFaceRule: Hashable, Equatable, Sendable {
10 | let family: String
11 | let source: URL
12 | let weight: String
13 | let style: String
14 | let display: String
15 |
16 | init(
17 | family: String,
18 | source: URL,
19 | weight: String = "normal",
20 | style: String = "normal",
21 | display: String = "swap"
22 | ) {
23 | self.family = family
24 | self.source = source
25 | self.weight = weight
26 | self.style = style
27 | self.display = display
28 | }
29 |
30 | func render() -> String {
31 | """
32 | @font-face {
33 | font-family: '\(family)';
34 | src: url('\(source.absoluteString)');
35 | font-weight: \(weight);
36 | font-style: \(style);
37 | font-display: \(display);
38 | }
39 | """
40 | }
41 | }
42 |
43 | extension FontFaceRule: CustomStringConvertible {
44 | var description: String {
45 | render()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/Ignite/Extensions/String-TruncatedHash.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String-TruncatedHash.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Creates a 5-character hash string that persists between app launches (e.g. "Hello World" -> "5eb21")
11 | extension String {
12 | var truncatedHash: String {
13 | let hash = strHash(self)
14 |
15 | // Create a deterministic string from the hash
16 | let charset = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
17 | var result = ""
18 | var remainingHash = hash
19 |
20 | // Generate exactly 5 characters
21 | for _ in 0..<5 {
22 | let index = Int(remainingHash % UInt64(charset.count))
23 | result.append(charset[index])
24 | remainingHash /= UInt64(charset.count)
25 | }
26 |
27 | return result
28 | }
29 |
30 | private func strHash(_ str: String) -> UInt64 {
31 | var result = UInt64(5381)
32 | let characters = [UInt8](str.utf8)
33 |
34 | for character in characters {
35 | result = 127 * (result & 0x00ffffffffffffff) + UInt64(character)
36 | }
37 |
38 | return result
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/TextSelection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextSelection.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `TextSelection` modifier.
14 | @Suite("TextSelection Tests")
15 | @MainActor
16 | struct TextSelectionTests {
17 | @Test("Automatic Text Selection")
18 | func automaticTextSelection() async throws {
19 | let element = Text("Hello").textSelection(.automatic)
20 | let output = element.markupString()
21 |
22 | #expect(output == "Hello
")
23 | }
24 |
25 | @Test("All Text Selection")
26 | func allTextSelection() async throws {
27 | let element = Text("Hello").textSelection(.all)
28 | let output = element.markupString()
29 |
30 | #expect(output == "Hello
")
31 | }
32 |
33 | @Test("None Text Selection")
34 | func noneTextSelection() async throws {
35 | let element = Text("Hello").textSelection(.none)
36 | let output = element.markupString()
37 |
38 | #expect(output == "Hello
")
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/Ignite/Resources/js/prism-typescript.js:
--------------------------------------------------------------------------------
1 | !function(e){e.languages.typescript=e.languages.extend("javascript",{"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/}),e.languages.typescript.keyword.push(/\b(?:abstract|declare|is|keyof|readonly|require)\b/,/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,/\btype\b(?=\s*(?:[\{*]|$))/),delete e.languages.typescript.parameter,delete e.languages.typescript["literal-property"];var s=e.languages.extend("typescript",{});delete s["class-name"],e.languages.typescript["class-name"].inside=s,e.languages.insertBefore("typescript","function",{decorator:{pattern:/@[$\w\xA0-\uFFFF]+/,inside:{at:{pattern:/^@/,alias:"operator"},function:/^[\s\S]+/}},"generic-function":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/,greedy:!0,inside:{function:/^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:s}}}}),e.languages.ts=e.languages.typescript}(Prism);
2 |
--------------------------------------------------------------------------------
/Sources/Ignite/Rendering/TabFocus.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabFocus.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// An enum representing the `tabindex` attribute for controlling the tab order and focus behavior of HTML elements.
9 | public enum TabFocus {
10 | /// The element is focusable but not part of the tab order (equivalent to `tabindex="-1"`).
11 | case focusable
12 |
13 | /// The element is focusable and part of the natural tab order (equivalent to `tabindex="0"`).
14 | case auto
15 |
16 | /// The element is focusable and has a custom tab order specified by the associated integer
17 | /// value (equivalent to `tabindex` with a positive integer).
18 | /// - Parameter index: A positive integer specifying the custom tab order.
19 | case custom(Int)
20 |
21 | /// The `tabindex` value corresponding to the enum case.
22 | var value: String {
23 | switch self {
24 | case .focusable:
25 | return "-1"
26 | case .auto:
27 | return "0"
28 | case .custom(let index):
29 | return "\(index)"
30 | }
31 | }
32 |
33 | /// The html name for this attribute
34 | var htmlName: String {
35 | "tabindex"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Section.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Section.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Section` element.
14 | @Suite("Section Tests")
15 | @MainActor
16 | class SectionTests: IgniteTestSuite {
17 | @Test("Section")
18 | func section() async throws {
19 | let element = Section {
20 | Span("Hello, World!")
21 | Span("Goodbye, World!")
22 | }
23 |
24 | let output = element.markupString()
25 | #expect(output == "Hello, World! Goodbye, World!
")
26 | }
27 |
28 | @Test("Section with Header")
29 | func sectionWithHeader() async throws {
30 | let element = Section("Greetings") {
31 | Span("Hello, World!")
32 | Span("Goodbye, World!")
33 | }
34 | .headerProminence(.title3)
35 |
36 | let output = element.markupString()
37 |
38 | #expect(output == """
39 | \
40 | Greetings \
41 | Hello, World! \
42 | Goodbye, World! \
43 |
44 | """)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/StaticPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StaticPage.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | /// One static page in your site, where the content is entirely standalone rather
11 | /// than being produced in conjunction with an external Markdown file.
12 | @MainActor
13 | public protocol StaticPage: LayoutContent {
14 | /// All pages have a default path generated for them by Ignite, but you can
15 | /// override that here if you wish.
16 | var path: String { get }
17 |
18 | /// The title for this page.
19 | var title: String { get }
20 |
21 | /// The image for sharing the page
22 | var image: URL? { get }
23 |
24 | /// A plain-text description for this page. Defaults to an empty string.
25 | var description: String { get }
26 | }
27 |
28 | public extension StaticPage {
29 | /// A default description for this page, which is just an empty string.
30 | var description: String { "" }
31 |
32 | /// Auto-generates a path for this page using its Swift type name.
33 | var path: String {
34 | "/" + String(describing: Self.self).convertedToSlug()
35 | }
36 |
37 | /// Defaults to no sharing image
38 | var image: URL? { nil }
39 | }
40 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/ZStack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ZStack.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `ZStack` element.
14 | @Suite("ZStack Tests")
15 | @MainActor
16 | class ZStackTests: IgniteTestSuite {
17 | static let alignments: [Alignment] = [
18 | .top, .topLeading, .topTrailing,
19 | .leading, .center, .trailing,
20 | .bottom, .bottomLeading, .bottomTrailing
21 | ]
22 |
23 | @Test("ZStack with elements")
24 | func basicZStack() async throws {
25 | let element = ZStack {
26 | ControlLabel("Top Label")
27 | ControlLabel("Bottom Label")
28 | }
29 | let output = element.markupString()
30 |
31 | #expect(output == """
32 | \
33 | Top Label \
35 | Bottom Label \
37 |
38 | """)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/AspectRatio.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AspectRatio.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `AspectRatio` modifier.
14 | @Suite("AspectRatio Tests")
15 | @MainActor
16 | class AspectRatioTests: IgniteTestSuite {
17 | @Test("Verify AspectRatio Modifiers", arguments: AspectRatio.allCases)
18 | func verifyAspectRatioModifiers(ratio: AspectRatio) async throws {
19 | let element = Text("Hello").aspectRatio(ratio)
20 | let output = element.markupString()
21 |
22 | #expect(output == "Hello
")
23 | }
24 |
25 | @Test("Verify Content Modes", arguments: AspectRatio.allCases, ContentMode.allCases)
26 | func verifyContentModes(ratio: AspectRatio, mode: ContentMode) async throws {
27 | let element = Image("/images/example.jpg").aspectRatio(ratio, contentMode: mode)
28 | let output = element.markupString()
29 |
30 | #expect(output == """
31 | \
32 |
33 | """)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/Border.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Border.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `BorderModifier` modifier.
14 | @Suite("BorderModifier Tests")
15 | @MainActor
16 | class BorderModifierTests: IgniteTestSuite {
17 | @Test("Border Modifier with All Edges")
18 | func borderWithAllEdges() async throws {
19 | let element = Text("Hello").border(.red, width: 2.0, style: .solid, edges: .all)
20 | let output = element.markupString()
21 | #expect(output == "Hello
")
22 | }
23 |
24 | @Test("Border Modifier with Specific Edges")
25 | func borderWithSpecificEdges() async throws {
26 | let element = Text("Hello").border(.blue, width: 1.0, style: .dotted, edges: [.top, .bottom])
27 | let output = element.markupString()
28 | #expect(output.contains("border-top: 1.0px dotted rgb(0 0 255 / 100%)"))
29 | #expect(output.contains("border-bottom: 1.0px dotted rgb(0 0 255 / 100%)"))
30 | #expect(!output.contains("border-left"))
31 | #expect(!output.contains("border-right"))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/InlineStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Declaration.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A simple property-value pair of strings that is able to store inline styles
9 | struct InlineStyle: Hashable, Equatable, Sendable, Comparable, CustomStringConvertible {
10 | /// The property, e.g. `\.color`.
11 | var property: String
12 |
13 | /// The declaration's value, e.g. "blue".
14 | var value: String
15 |
16 | init(_ property: Property, value: String) {
17 | self.property = property.rawValue
18 | self.value = value
19 | }
20 |
21 | init(_ property: String, value: String) {
22 | self.property = property
23 | self.value = value
24 | }
25 |
26 | /// The full declaration, e.g. "color: blue""
27 | var description: String {
28 | property + ": " + value
29 | }
30 |
31 | static func < (lhs: InlineStyle, rhs: InlineStyle) -> Bool {
32 | lhs.description < rhs.description
33 | }
34 |
35 | func important(_ isImportant: Bool = true) -> InlineStyle {
36 | if isImportant {
37 | InlineStyle(self.property, value: self.value + " !important")
38 | } else {
39 | self
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/Ignite/Themes/Fonts/FontWeight.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontWeight.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | public typealias FontWeight = Font.Weight
9 |
10 | /// The list of standard font weights supported by HTML. This
11 | /// is designed to match the same order provided by SwiftUI.
12 | /// Note: Bootstrap provides its own font weights as classes,
13 | /// but these are less close to both regular CSS and SwiftUI.
14 | public extension Font {
15 | enum Weight: Int, Defaultable, CaseIterable, Sendable, CustomStringConvertible {
16 | case ultraLight = 100
17 | case thin = 200
18 | case light = 300
19 | case regular = 400
20 | case medium = 500
21 | case semibold = 600
22 | case bold = 700
23 | case heavy = 800
24 | case black = 900
25 |
26 | /// Special value indicating default
27 | static var `default`: Self { .regular }
28 |
29 | /// Indicates whether this unit represents a default value
30 | var isDefault: Bool {
31 | if self == .default { return true }
32 | return false
33 | }
34 |
35 | public var description: String {
36 | rawValue.formatted()
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/IgniteCLI/QR Generation/ErrorCorrection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorCorrection.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// The level of error correction to use in a QR code.
9 | ///
10 | /// QR codes can incorporate different levels of error correction, allowing the code
11 | /// to be read even when partially damaged or obscured.
12 | enum ErrorCorrection: Int, CaseIterable, Sendable {
13 | /// Low level error correction. Approximately 7% of data can be restored.
14 | case low = 0
15 |
16 | /// Medium level error correction. Approximately 15% of data can be restored.
17 | case medium = 1
18 |
19 | /// Quantize level error correction. Approximately 25% of data can be restored.
20 | case quantize = 2
21 |
22 | /// High level error correction. Approximately 30% of data can be restored.
23 | case high = 3
24 |
25 | /// The default error correction level (`.quantize`).
26 | static let `default` = ErrorCorrection.quantize
27 |
28 | /// The CoreImage error correction string representation.
29 | var level: String {
30 | switch self {
31 | case .low: return "L"
32 | case .medium: return "M"
33 | case .quantize: return "Q"
34 | case .high: return "H"
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/FontStyleModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontStyleModifier.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | @MainActor private func fontStyleModifier(
9 | _ style: Font.Style,
10 | content: any HTML
11 | ) -> any HTML {
12 | if content.isText {
13 | content.fontStyle(style)
14 | } else {
15 | content.class(style.sizeClass)
16 | }
17 | }
18 |
19 | @MainActor private func fontStyleModifier(
20 | _ style: Font.Style,
21 | content: any InlineElement
22 | ) -> any InlineElement {
23 | content.fontStyle(style)
24 | }
25 |
26 | public extension HTML {
27 | /// Adjusts the heading level of this text.
28 | /// - Parameter style: The new heading level.
29 | /// - Returns: A new `Text` instance with the updated font style.
30 | func font(_ style: Font.Style) -> some HTML {
31 | AnyHTML(fontStyleModifier(style, content: self))
32 | }
33 | }
34 |
35 | public extension InlineElement {
36 | /// Adjusts the heading level of this text.
37 | /// - Parameter style: The new heading level.
38 | /// - Returns: A new `Text` instance with the updated font style.
39 | func font(_ style: Font.Style) -> some InlineElement {
40 | AnyInlineElement(fontStyleModifier(style, content: self))
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/BadgeModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BadgeModifier.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `BadgeModifier` modifier.
14 | @Suite("BadgeModifier Tests")
15 | @MainActor
16 | class BadgeModifierTests: IgniteTestSuite {
17 | @Test("Badge Modifier for InlineHTML")
18 | func badgeModifierForInlineHTML() async throws {
19 | let element = Text("Notifications").badge(Badge("3"))
20 |
21 | let output = element.markupString()
22 |
23 | #expect(output == """
24 | \
25 | Notifications
\
26 | 3 \
27 |
28 | """)
29 | }
30 |
31 | @Test("Badge Modifier for ListItem")
32 | func badgeModifierForListItem() async throws {
33 | let element = ListItem {
34 | Text("Messages")
35 | }.badge(Badge("5"))
36 |
37 | let output = element.markupString()
38 |
39 | #expect(output == """
40 | \
41 | Messages
\
42 |
43 | """)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/Ignite/Rendering/Markdown/ArticleRenderer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArticleRenderer.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | /// A protocol defining the basic information we need to get good
11 | /// article parsing. This is implemented by the default
12 | /// MarkdownToHTML parser included with Ignite, but users
13 | /// can override that default in their `Site` conformance to
14 | /// get a custom parser if needed.
15 | public protocol ArticleRenderer {
16 | /// The title of this document.
17 | var title: String { get }
18 |
19 | /// The description of this document, which is the first paragraph.
20 | var description: String { get }
21 |
22 | /// The body text of this file, which includes its title by default.
23 | var body: String { get }
24 |
25 | /// Whether to remove the article's title from its body. This only applies
26 | /// to the first heading.
27 | var removeTitleFromBody: Bool { get }
28 |
29 | /// Parses Markdown provided as a direct input string.
30 | /// - Parameters:
31 | /// - markdown: The Markdown to parse.
32 | /// - removeTitleFromBody: `true` if the first title should be removed
33 | /// from the final `text` property.
34 | init(markdown: String, removeTitleFromBody: Bool) throws
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/Position.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Position.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Specific values that can be used to position this element.
9 | public enum Position: String {
10 | /// No specific position.
11 | case `default` = ""
12 |
13 | /// This element stays fixed to the top of the screen.
14 | case fixedTop = "fixed-top"
15 |
16 | /// This element stays fixed to the bottom of the screen.
17 | case fixedBottom = "fixed-bottom"
18 |
19 | /// This element is rendered in its natural location until it reaches
20 | /// the top of the screen, at which point it stays fixed.
21 | case stickyTop = "sticky-top"
22 |
23 | /// This element is rendered in its natural location until it reaches
24 | /// the bottom of the screen, at which point it stays fixed.
25 | case stickyBottom = "sticky-bottom"
26 | }
27 |
28 | public extension HTML {
29 | /// Adjusts the rendering position for this element, using a handful of
30 | /// specific, known position values.
31 | /// - Parameter newPopositionsition: A `Position` case to use for this element.
32 | /// - Returns: A copy of this element with the new position applied.
33 | func position(_ position: Position) -> some HTML {
34 | self.class(position.rawValue)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/Ignite/Resources/css/prism-tomorrow.css:
--------------------------------------------------------------------------------
1 | /* PrismJS 1.29.0 */
2 | code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:var(--bs-font-monospace,Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace);font-size:var(--code-block-font-size, 1em);text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
3 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Audio.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Audio.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Audio` element.
14 | @Suite("Audio Tests")
15 | @MainActor
16 | class AudioTests: IgniteTestSuite {
17 | @Test("Lone File Audio", arguments: ["/audio/example.mp3"])
18 | func loneFileAudio(audioFile: String) async throws {
19 | let element = Audio(audioFile)
20 | let output = element.markupString()
21 |
22 | #expect(output == """
23 | \
24 | Your browser does not support the audio element.\
25 |
26 | """)
27 | }
28 |
29 | @Test("Multiple File Audio", arguments: ["/audio/example1.mp3"], ["/audio/example1.wav"])
30 | func multiFileAudio(audioFile1: String, audioFile2: String) async throws {
31 | let element = Audio(audioFile1, audioFile2)
32 | let output = element.markupString()
33 |
34 | #expect(output == """
35 | \
36 | \
37 | Your browser does not support the audio element.\
38 |
39 | """)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/StyleModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StyleModifier.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | @MainActor private func styleModifier(
9 | _ style: any Style,
10 | content: any HTML
11 | ) -> any HTML {
12 | StyleManager.shared.registerStyle(style)
13 | return content.class(style.className)
14 | }
15 |
16 | @MainActor private func styleModifier(
17 | _ style: any Style,
18 | content: any InlineElement
19 | ) -> any InlineElement {
20 | StyleManager.shared.registerStyle(style)
21 | return content.class(style.className)
22 | }
23 |
24 | public extension HTML {
25 | /// Applies a custom style to the element.
26 | /// - Parameter style: The style to apply, conforming to the `Style` protocol
27 | /// - Returns: A modified copy of the element with the style applied
28 | func style(_ style: any Style) -> some HTML {
29 | AnyHTML(styleModifier(style, content: self))
30 | }
31 | }
32 |
33 | public extension InlineElement {
34 | /// Applies a custom style to the element.
35 | /// - Parameter style: The style to apply, conforming to the `Style` protocol
36 | /// - Returns: A modified copy of the element with the style applied
37 | func style(_ style: any Style) -> some InlineElement {
38 | AnyInlineElement(styleModifier(style, content: self))
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/Ignite/Framework/LinkTarget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LinkTarget.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Controls where this link should be opened, e.g. in the current browser
11 | /// window or in a new window.
12 | public enum LinkTarget: Equatable {
13 | /// No location is specified, which usually means the link opens in
14 | /// the current browser window.
15 | case `default`
16 |
17 | /// The page should be opened in a new window.
18 | case blank
19 |
20 | /// The page should be opened in a new window. (same as `.blank`)`
21 | case newWindow
22 |
23 | /// The page should be opened in the parent window.
24 | case parent
25 |
26 | /// The page should be opened at the top-most level in the user's
27 | /// browser. Used when your page is displayed inside a frame.
28 | case top
29 |
30 | /// Target a specific, named location.
31 | case custom(String)
32 |
33 | /// Converts enum cases to the matching HTML.
34 | var name: String? {
35 | switch self {
36 | case .default:
37 | nil
38 | case .blank, .newWindow:
39 | "_blank"
40 | case .parent:
41 | "_parent"
42 | case .top:
43 | "_top"
44 | case .custom(let name):
45 | name
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Video.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Video.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Video` element.
14 | @Suite("Video Tests")
15 | @MainActor class VideoTests: IgniteTestSuite {
16 | @Test("Lone File Video", arguments: ["/videos/example.mp4"])
17 | func loneFileVideo(videoFile: String) async throws {
18 | let element = Video(videoFile)
19 | let output = element.markupString()
20 |
21 | #expect(output == """
22 | \
23 | \
24 | Your browser does not support the video tag.\
25 |
26 | """)
27 | }
28 |
29 | @Test("Multi-file Video", arguments: ["/videos/example1.mp4"], ["/videos/example1.mov"])
30 | func multiFileVideo(videoFile1: String, videoFile2: String) async throws {
31 | let element = Video(videoFile1, videoFile2)
32 | let output = element.markupString()
33 |
34 | #expect(output == """
35 | \
36 | \
37 | \
38 | Your browser does not support the video tag.\
39 |
40 | """)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Types/AnchorPoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnchorPoint.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `AnchorPoint` type.
14 | @Suite("AnchorPoint Tests")
15 | @MainActor
16 | struct AnchorPointTests {
17 | static let anchorPoints: [AnchorPoint] = [
18 | .center, .topLeft, .topRight,
19 | .bottomLeft, .bottomRight, .top,
20 | .bottom, .left, .right
21 | ]
22 |
23 | static let anchorPointCSSValues = [
24 | "center", "top left", "top right",
25 | "bottom left", "bottom right", "top",
26 | "bottom", "left", "right"
27 | ]
28 |
29 | @Test("Anchor point", arguments: await zip(anchorPoints, anchorPointCSSValues))
30 | func anchorPoint(_ anchorPoint: AnchorPoint, css: String) async throws {
31 | #expect(anchorPoint.value == css)
32 | }
33 |
34 | static let xCoordinates = ["1", "2", "3", "4", "5"]
35 | static let yCoordinates = ["6", "7", "8", "9", "10"]
36 |
37 | @Test("Custom anchor point", arguments: zip(await Self.xCoordinates, await Self.yCoordinates))
38 | func customAnchorPoint(xCoord: String, yCoord: String) async throws {
39 | let element = AnchorPoint.custom(x: xCoord, y: yCoord)
40 | #expect(element.value == "\(xCoord) \(yCoord)")
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Quote.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Quote.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Quote` element.
14 | @Suite("Quote Tests")
15 | @MainActor
16 | class QuoteTests: IgniteTestSuite {
17 | @Test("Plain Quote", arguments: ["It is a truth universally acknowledged..."])
18 | func plainQuoteTest(quoteText: String) async throws {
19 | let element = Quote {
20 | Text(quoteText)
21 | }
22 |
23 | let output = element.markupString()
24 |
25 | #expect(output == """
26 | \
27 | \(quoteText)
\
28 |
29 | """)
30 | }
31 |
32 | @Test("Quote With Caption", arguments: ["Programming is an art."], ["Paul Hudson"])
33 | func quoteWithCaptionTest(quoteText: String, captionText: String) async throws {
34 | let element = Quote {
35 | Text(quoteText)
36 | } caption: {
37 | captionText
38 | }
39 |
40 | let output = element.markupString()
41 |
42 | #expect(output == """
43 | \
44 | \(quoteText)
\
45 | \
46 |
47 | """)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/Cursor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cursor.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | public enum Cursor: String, CaseIterable, Sendable {
9 | /// The cursor to display based on the current context. E.g., equivalent to text when hovering text.
10 | case auto
11 |
12 | /// Default cursor. Typically an arrow.
13 | case `default`
14 |
15 | /// No cursor is rendered.
16 | case pointer
17 |
18 | /// Something can be zoomed (magnified) in.
19 | case zoomIn = "zoom-in"
20 |
21 | /// Something can be zoomed (magnified) out.
22 | case zoomOut = "zoom-out"
23 | }
24 |
25 | public extension HTML {
26 | /// Changes the cursor appearance of the element when hovering over the element.
27 | /// - Parameter cursor: The desired cursor style.
28 | /// - Returns: A modified copy of the element with cursor styling applied
29 | func cursor(_ cursor: Cursor) -> some HTML {
30 | self.style(.cursor, cursor.rawValue)
31 | }
32 | }
33 |
34 | public extension InlineElement {
35 | /// Changes the cursor appearance of the element when hovering over the element.
36 | /// - Parameter cursor: The desired cursor style.
37 | /// - Returns: A modified copy of the element with cursor styling applied
38 | func cursor(_ cursor: Cursor) -> some InlineElement {
39 | self.style(.cursor, cursor.rawValue)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/Ignite/Resources/css/prism-okaidia.min.css:
--------------------------------------------------------------------------------
1 | /* PrismJS 1.29.0 */
2 | code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:var(--bs-font-monospace,Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace);font-size:var(--code-block-font-size, 1em);text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
3 |
--------------------------------------------------------------------------------
/Sources/Ignite/Modifiers/Attribute Modifiers/IDModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IDModifier.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | @MainActor
9 | private func idModifier(_ id: String, content: any HTML) -> any HTML {
10 | guard !id.isEmpty else { return content }
11 | var copy: any HTML = content.isPrimitive ? content : Section(content)
12 | copy.attributes.id = id
13 | return copy
14 | }
15 |
16 | @MainActor
17 | private func idModifier(_ id: String, content: any InlineElement) -> any InlineElement {
18 | guard !id.isEmpty else { return content }
19 | var copy: any InlineElement = content.isPrimitive ? content : Span(content)
20 | copy.attributes.id = id
21 | return copy
22 | }
23 |
24 | public extension HTML {
25 | /// Sets the `HTML` id attribute of the element.
26 | /// - Parameter id: The HTML ID value to set
27 | /// - Returns: A modified copy of the element with the HTML id added
28 | func id(_ id: String) -> some HTML {
29 | AnyHTML(idModifier(id, content: self))
30 | }
31 | }
32 |
33 | public extension InlineElement {
34 | /// Sets the `HTML` id attribute of the element.
35 | /// - Parameter id: The HTML ID value to set
36 | /// - Returns: A modified copy of the element with the HTML ID added
37 | func id(_ id: String) -> some InlineElement {
38 | AnyInlineElement(idModifier(id, content: self))
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/Ignite/Styles/StyledHTML.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StyledHTML.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// A concrete type used for style resolution that only holds attributes
9 | @MainActor public struct StyledHTML {
10 | /// A collection of styles, classes, and attributes.
11 | var attributes = CoreAttributes()
12 |
13 | /// Adds inline styles to the element.
14 | /// - Parameter values: Variable number of `InlineStyle` objects
15 | /// - Returns: The modified `HTML` element
16 | public func style(_ property: Property, _ value: String) -> Self {
17 | var copy = self
18 | copy.attributes.append(styles: .init(property, value: value))
19 | return copy
20 | }
21 | }
22 |
23 | extension StyledHTML {
24 | /// Adds inline styles to the element.
25 | /// - Parameter values: An array of `InlineStyle` objects
26 | /// - Returns: The modified `HTML` element
27 | func style(_ styles: [InlineStyle]) -> Self {
28 | var copy = self
29 | copy.attributes.append(styles: styles)
30 | return copy
31 | }
32 |
33 | /// Adds inline styles to the element.
34 | /// - Parameter values: An array of `InlineStyle` objects
35 | /// - Returns: The modified `HTML` element
36 | func style(_ styles: InlineStyle...) -> Self {
37 | var copy = self
38 | copy.attributes.append(styles: styles)
39 | return copy
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Modifiers/Hidden.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Hidden.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Hidden` modifier.
14 | @Suite("Hidden Tests")
15 | @MainActor
16 | struct HiddenTests {
17 | @Test("Hidden Modifier for Text")
18 | func hiddenForText() async throws {
19 | let element = Text("Hello world!").hidden()
20 | let output = element.markupString()
21 |
22 | #expect(output == "Hello world!
")
23 | }
24 |
25 | @Test("MediaQuery based hidden Modifier for Text")
26 | func hiddenMediaQueryForText() async throws {
27 | let className = CSSManager.shared.registerStyles(.init(small: true))
28 | let element = Text("Hello world!").hidden(.responsive(small: true))
29 | let output = element.markupString()
30 |
31 | #expect(output == "Hello world!
")
32 | }
33 |
34 | @Test("Hidden Modifier for Column")
35 | func hiddenForColumn() async throws {
36 | let element = Column {
37 | ControlLabel("Left Label")
38 | ControlLabel("Right Label")
39 | }.hidden()
40 | let output = element.markupString()
41 |
42 | #expect(output == "Left Label Right Label ")
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/Ignite/Elements/Row.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Row.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// One row inside a `Table`.
9 | public struct Row: HTML {
10 | /// The content and behavior of this HTML.
11 | public var body: some HTML { self }
12 |
13 | /// The standard set of control attributes for HTML elements.
14 | public var attributes = CoreAttributes()
15 |
16 | /// Whether this HTML belongs to the framework.
17 | public var isPrimitive: Bool { true }
18 |
19 | /// The columns to display inside this row.
20 | private var columns: HTMLCollection
21 |
22 | /// Create a new `Row` using a page element builder that returns the
23 | /// array of columns to use in this row.
24 | /// - Parameter columns: The columns to use in this row.
25 | public init(@HTMLBuilder columns: () -> some BodyElement) {
26 | self.columns = HTMLCollection(columns)
27 | }
28 |
29 | /// Renders this element using publishing context passed in.
30 | /// - Returns: The HTML for this element.
31 | public func markup() -> Markup {
32 | let output = columns.map { column in
33 | if column is Column {
34 | column.markup()
35 | } else {
36 | Markup("\(column.markupString()) ")
37 | }
38 | }.joined()
39 | .string
40 |
41 | return Markup("\(output) ")
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/Row.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Row.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `Row` element.
14 | @Suite("Row Tests")
15 | @MainActor
16 | struct RowTests {
17 | @Test("Row with Multiple Columns")
18 | func rowWithMultipleColumns() async throws {
19 | let row = Row {
20 | Text("Column 1")
21 | Text("Column 2")
22 | }
23 | let output = row.markupString()
24 |
25 | #expect(output == "Column 1
Column 2
")
26 | }
27 |
28 | @Test("Row with Column Elements")
29 | func rowWithColumnElements() async throws {
30 | let row = Row {
31 | Column { Text("Column 1") }
32 | Column { Text("Column 2") }
33 | }
34 | let output = row.markupString()
35 |
36 | #expect(output == "Column 1
Column 2
")
37 | }
38 |
39 | @Test("Row with Mixed Content")
40 | func rowWithMixedContent() async throws {
41 | let row = Row {
42 | Text("Column 1")
43 | Column { Text("Column 2") }
44 | }
45 | let output = row.markupString()
46 |
47 | #expect(output == "Column 1
Column 2
")
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Tests/IgniteTesting/Elements/VStack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VStack.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | import Foundation
9 | import Testing
10 |
11 | @testable import Ignite
12 |
13 | /// Tests for the `VStack` element.
14 | @Suite("VStack Tests")
15 | @MainActor
16 | class VStackTests: IgniteTestSuite {
17 | @Test("VStack with elements")
18 | func basicVStack() async throws {
19 | let element = VStack {
20 | ControlLabel("Top Label")
21 | ControlLabel("Bottom Label")
22 | }
23 | let output = element.markupString()
24 |
25 | #expect(output == """
26 | \
27 | Top Label \
28 | Bottom Label \
29 |
30 | """)
31 | }
32 |
33 | @Test("VStack with elements and spacing")
34 | func elementsWithSpacingWithinVStack() async throws {
35 | let element = VStack(spacing: 10) {
36 | ControlLabel("Top Label")
37 | ControlLabel("Bottom Label")
38 | }
39 | let output = element.markupString()
40 |
41 | #expect(output == """
42 | \
43 | Top Label \
44 | Bottom Label \
45 |
46 | """)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/Ignite/Types/Edge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Edge.swift
3 | // Ignite
4 | // https://www.github.com/twostraws/Ignite
5 | // See LICENSE for license information.
6 | //
7 |
8 | /// Describes edges on an element, e.g. top or leading, along
9 | /// with groups of edges such as "horizontal" (leading *and* trailing).
10 | public struct Edge: OptionSet, Sendable {
11 | /// The internal value used to represent this edge.
12 | public let rawValue: Int
13 |
14 | /// Creates a new `Edge` instance from a raw value integer.
15 | /// - Parameter rawValue: The internal value for this edge.
16 | public init(rawValue: Int) {
17 | self.rawValue = rawValue
18 | }
19 |
20 | /// The top edge of an element,
21 | public static let top = Edge(rawValue: 1 << 0)
22 |
23 | /// The leading edge of an element, i.e. left in left-to-right languages.
24 | public static let leading = Edge(rawValue: 1 << 1)
25 |
26 | /// The bottom edge of an element.
27 | public static let bottom = Edge(rawValue: 1 << 2)
28 |
29 | /// The trailing edge of an element, i.e. right in left-to-right languages.
30 | public static let trailing = Edge(rawValue: 1 << 3)
31 |
32 | /// The leading and trailing edges of an element.
33 | public static let horizontal: Edge = [.leading, .trailing]
34 |
35 | /// The top and bottom edges of an element.
36 | public static let vertical: Edge = [.top, .bottom]
37 |
38 | /// All edges of an element.
39 | public static let all: Edge = [.horizontal, .vertical]
40 | }
41 |
--------------------------------------------------------------------------------