├── 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 == "

    Hello

    ") 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 == "") 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 | Logo\ 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") 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 == "

    This is not an exercice

    ") 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 | \ 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 | \ 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 | \ 35 | \ 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 | 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 | 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 | 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 | 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 |
    \(captionText)
    \ 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 == "") 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 | \ 28 | \ 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 | \ 44 | \ 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 | --------------------------------------------------------------------------------