From 432a877c4d6fc707348527c31b3a1de77bafed5c Mon Sep 17 00:00:00 2001 From: leogdion Date: Tue, 14 Jul 2026 16:31:48 -0400 Subject: [PATCH 1/5] Update subrepo parent refs after squash [skip ci] --- Sources/Plot/API/HTMLAnchorRelationship.swift | 2 +- Sources/Plot/API/HTMLAudioFormat.swift | 2 +- Sources/Plot/API/HTMLVideoFormat.swift | 2 +- Sources/Plot/API/Indentation.swift | 4 ++-- Sources/Plot/API/PodcastType.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Plot/API/HTMLAnchorRelationship.swift b/Sources/Plot/API/HTMLAnchorRelationship.swift index dd29479..d5bf783 100644 --- a/Sources/Plot/API/HTMLAnchorRelationship.swift +++ b/Sources/Plot/API/HTMLAnchorRelationship.swift @@ -9,7 +9,7 @@ import Foundation /// An enum that defines various values for an HTML anchor's `rel` /// attribute, which specifies the relationship that the anchor has /// to the URL that it's linking to. -public struct HTMLAnchorRelationship: RawRepresentable, Identifiable, ExpressibleByStringLiteral { +public struct HTMLAnchorRelationship: RawRepresentable, Identifiable, ExpressibleByStringLiteral, Sendable { public var id: String { rawValue } public var rawValue: String diff --git a/Sources/Plot/API/HTMLAudioFormat.swift b/Sources/Plot/API/HTMLAudioFormat.swift index 0a5d4dc..cb2592d 100644 --- a/Sources/Plot/API/HTMLAudioFormat.swift +++ b/Sources/Plot/API/HTMLAudioFormat.swift @@ -7,7 +7,7 @@ import Foundation /// Enum that defines various audio formats supported by most browsers. -public enum HTMLAudioFormat: String, Codable { +public enum HTMLAudioFormat: String, Codable, Sendable { case mp3 = "mpeg" case wav case ogg diff --git a/Sources/Plot/API/HTMLVideoFormat.swift b/Sources/Plot/API/HTMLVideoFormat.swift index 6c36add..d895937 100644 --- a/Sources/Plot/API/HTMLVideoFormat.swift +++ b/Sources/Plot/API/HTMLVideoFormat.swift @@ -7,7 +7,7 @@ import Foundation /// Enum that defines various video formats supported by most browsers. -public enum HTMLVideoFormat: String, Codable { +public enum HTMLVideoFormat: String, Codable, Sendable { case mp4 case webM = "webm" case ogg diff --git a/Sources/Plot/API/Indentation.swift b/Sources/Plot/API/Indentation.swift index 6f65e59..0cb32f7 100644 --- a/Sources/Plot/API/Indentation.swift +++ b/Sources/Plot/API/Indentation.swift @@ -7,7 +7,7 @@ import Foundation /// A representation of a kind of indentation at a given level. -public struct Indentation: Codable, Equatable { +public struct Indentation: Codable, Equatable, Sendable { /// The kind of the indentation (see `Kind`). public var kind: Kind /// The level of the indentation (0 = root). @@ -22,7 +22,7 @@ public struct Indentation: Codable, Equatable { public extension Indentation { /// Enum defining various kinds of indentation that a document /// can be rendered using. - enum Kind: Equatable { + enum Kind: Equatable, Sendable { /// Each level should be indented by a given number of tabs. case tabs(Int) /// Each level should be indented by a given number of spaces. diff --git a/Sources/Plot/API/PodcastType.swift b/Sources/Plot/API/PodcastType.swift index 7138ba3..cedd4a2 100644 --- a/Sources/Plot/API/PodcastType.swift +++ b/Sources/Plot/API/PodcastType.swift @@ -7,7 +7,7 @@ import Foundation /// Enum describing various podcast types supported by Apple Podcasts. -public enum PodcastType: String, Codable { +public enum PodcastType: String, Codable, Sendable { case episodic case serial } From c9f1cd46a2bca9cd9044ba15603ac1f07fd3bbd8 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Thu, 16 Jul 2026 11:47:09 -0400 Subject: [PATCH 2/5] Update subrepo parent refs after squash [skip ci] --- .mise.toml | 16 + .periphery.yml | 3 + .swift-format | 70 + .swift-version | 1 + .swiftlint.yml | 149 ++ CLAUDE.md | 12 + Package.swift | 2 +- Scripts/header.sh | 104 + Scripts/lint.sh | 101 + Sources/Plot/API/Attribute+HTML.swift | 173 ++ Sources/Plot/API/Attribute+HTMLForms.swift | 195 ++ Sources/Plot/API/Attribute+HTMLLinks.swift | 190 ++ Sources/Plot/API/Attribute+HTMLMedia.swift | 167 ++ Sources/Plot/API/Attribute+Podcast.swift | 28 + Sources/Plot/API/Attribute+XML.swift | 21 + Sources/Plot/API/Attribute.swift | 95 +- Sources/Plot/API/AudioPlayer.swift | 82 + Sources/Plot/API/Component+Attributes.swift | 113 + Sources/Plot/API/Component.swift | 224 +- Sources/Plot/API/ComponentAttributes.swift | 99 - Sources/Plot/API/ComponentBuilder.swift | 55 +- Sources/Plot/API/ComponentContainer.swift | 52 +- Sources/Plot/API/ComponentGroup.swift | 34 +- Sources/Plot/API/ControlFlow.swift | 60 - Sources/Plot/API/Directionality.swift | 14 +- Sources/Plot/API/Document.swift | 59 +- Sources/Plot/API/DocumentEncoding.swift | 4 +- Sources/Plot/API/DocumentFormat.swift | 18 + Sources/Plot/API/Element+XML.swift | 19 + Sources/Plot/API/Element.swift | 79 +- Sources/Plot/API/ElementClosingMode.swift | 16 +- Sources/Plot/API/ElementComponent.swift | 17 +- Sources/Plot/API/ElementDefinition.swift | 12 +- Sources/Plot/API/ElementDefinitions.swift | 191 ++ Sources/Plot/API/EmptyComponent.swift | 8 +- Sources/Plot/API/EnvironmentKey.swift | 114 +- Sources/Plot/API/EnvironmentValue.swift | 20 +- Sources/Plot/API/Form.swift | 51 + Sources/Plot/API/HTML.swift | 313 ++- Sources/Plot/API/HTMLAnchorRelationship.swift | 106 +- Sources/Plot/API/HTMLAnchorTarget.swift | 21 +- Sources/Plot/API/HTMLAttributes.swift | 633 ----- Sources/Plot/API/HTMLAudioFormat.swift | 6 +- Sources/Plot/API/HTMLButtonType.swift | 6 +- Sources/Plot/API/HTMLComponents.swift | 906 ------- Sources/Plot/API/HTMLContext.swift | 8 + Sources/Plot/API/HTMLDimensionContext.swift | 9 + Sources/Plot/API/HTMLDividableContext.swift | 8 + Sources/Plot/API/HTMLElements.swift | 533 ---- Sources/Plot/API/HTMLFormContentType.swift | 12 +- Sources/Plot/API/HTMLFormMethod.swift | 8 +- .../Plot/API/HTMLImageContainerContext.swift | 8 + Sources/Plot/API/HTMLInputType.swift | 44 +- Sources/Plot/API/HTMLIntegrityContext.swift | 9 + Sources/Plot/API/HTMLLinkRelationship.swift | 40 +- Sources/Plot/API/HTMLLinkableContext.swift | 9 + Sources/Plot/API/HTMLListStyle.swift | 96 +- Sources/Plot/API/HTMLMediaContext.swift | 9 + Sources/Plot/API/HTMLNamableContext.swift | 9 + Sources/Plot/API/HTMLOptionListContext.swift | 9 + Sources/Plot/API/HTMLScriptableContext.swift | 9 + Sources/Plot/API/HTMLSourceContext.swift | 9 + Sources/Plot/API/HTMLSourceListContext.swift | 12 + Sources/Plot/API/HTMLStylableContext.swift | 9 + Sources/Plot/API/HTMLTypeContext.swift | 9 + Sources/Plot/API/HTMLValueContext.swift | 9 + Sources/Plot/API/HTMLVideoFormat.swift | 6 +- Sources/Plot/API/HTMLViewportFitMode.swift | 16 +- Sources/Plot/API/HTMLViewportWidthMode.swift | 26 +- Sources/Plot/API/IFrame.swift | 47 + Sources/Plot/API/Image.swift | 38 + Sources/Plot/API/Indentation.swift | 133 +- Sources/Plot/API/Input.swift | 57 + Sources/Plot/API/InputComponent.swift | 22 + Sources/Plot/API/Label.swift | 42 + Sources/Plot/API/Language.swift | 401 +-- Sources/Plot/API/Link.swift | 49 + Sources/Plot/API/List.swift | 56 + Sources/Plot/API/Node+Conformances.swift | 82 + Sources/Plot/API/Node+ControlFlow.swift | 76 + Sources/Plot/API/Node+HTMLBody.swift | 194 ++ Sources/Plot/API/Node+HTMLBody2.swift | 189 ++ Sources/Plot/API/Node+HTMLComponents.swift | 145 ++ Sources/Plot/API/Node+HTMLElements.swift | 101 + Sources/Plot/API/Node+HTMLElementsMisc.swift | 143 ++ Sources/Plot/API/Node+Podcast.swift | 126 + Sources/Plot/API/Node+PodcastComponents.swift | 38 + Sources/Plot/API/Node+PodcastItem.swift | 130 + Sources/Plot/API/Node+RSS.swift | 167 ++ Sources/Plot/API/Node+RSSAttributes.swift | 32 + Sources/Plot/API/Node+SiteMap.swift | 78 + Sources/Plot/API/Node.swift | 389 ++- Sources/Plot/API/NodeConvertible.swift | 21 +- Sources/Plot/API/Optional+Component.swift | 7 +- Sources/Plot/API/PodcastAttributes.swift | 25 - Sources/Plot/API/PodcastCategoryContext.swift | 8 + Sources/Plot/API/PodcastComponents.swift | 34 - Sources/Plot/API/PodcastContentContext.swift | 8 + Sources/Plot/API/PodcastElements.swift | 214 -- Sources/Plot/API/PodcastEpisodeType.swift | 6 +- Sources/Plot/API/PodcastFeed.swift | 88 +- Sources/Plot/API/PodcastMediaType.swift | 4 +- Sources/Plot/API/PodcastNameableContext.swift | 8 + Sources/Plot/API/PodcastType.swift | 4 +- Sources/Plot/API/RSS.swift | 123 +- Sources/Plot/API/RSSAttributes.swift | 28 - Sources/Plot/API/RSSBasedDocumentFormat.swift | 11 + Sources/Plot/API/RSSChannelContext.swift | 11 + Sources/Plot/API/RSSContentContext.swift | 8 + Sources/Plot/API/RSSElements.swift | 140 -- Sources/Plot/API/RSSFeedContext.swift | 11 + Sources/Plot/API/RSSItemContext.swift | 8 + Sources/Plot/API/RSSRootContext.swift | 9 + Sources/Plot/API/Renderable.swift | 15 +- Sources/Plot/API/SiteMap.swift | 55 +- Sources/Plot/API/SiteMapChangeFrequency.swift | 14 +- Sources/Plot/API/SiteMapElements.swift | 71 - Sources/Plot/API/SubmitButton.swift | 34 + Sources/Plot/API/Table.swift | 73 + Sources/Plot/API/TableRow.swift | 48 + Sources/Plot/API/Text.swift | 58 + Sources/Plot/API/TextArea.swift | 59 + Sources/Plot/API/TextField.swift | 49 + Sources/Plot/API/Time.swift | 33 + Sources/Plot/API/TwitterCardType.swift | 8 +- Sources/Plot/API/URLRepresentable.swift | 4 +- Sources/Plot/API/XML.swift | 47 +- Sources/Plot/API/XMLAttributes.swift | 19 - Sources/Plot/API/XMLElements.swift | 16 - Sources/Plot/API/XMLRootContext.swift | 9 + Sources/Plot/Internal/AnyAttribute.swift | 14 +- Sources/Plot/Internal/AnyElement.swift | 6 +- .../Plot/Internal/AnyEnvironmentValue.swift | 2 +- Sources/Plot/Internal/AnyNode.swift | 2 +- .../Internal/ElementRenderingBuffer.swift | 113 +- Sources/Plot/Internal/ElementWrapper.swift | 23 +- Sources/Plot/Internal/Environment.swift | 32 +- Sources/Plot/Internal/ModifiedComponent.swift | 8 +- Sources/Plot/Internal/Renderer.swift | 265 +- Sources/Plot/Internal/String+Escaping.swift | 81 +- Tests/PlotTests/Assertions.swift | 397 +-- Tests/PlotTests/ControlFlowTests.swift | 56 +- Tests/PlotTests/Date+Stubbing.swift | 76 +- Tests/PlotTests/DocumentTests.swift | 181 +- Tests/PlotTests/HTMLComponentTests.swift | 1193 ++++----- Tests/PlotTests/HTMLTests.swift | 2192 ++++++++++------- Tests/PlotTests/IndentationTests.swift | 56 +- Tests/PlotTests/NodeTests.swift | 161 +- Tests/PlotTests/PodcastFeedTests.swift | 520 ++-- Tests/PlotTests/RSSTests.swift | 282 ++- Tests/PlotTests/Require.swift | 26 - Tests/PlotTests/RequireError.swift | 28 + Tests/PlotTests/SiteMapTests.swift | 102 +- Tests/PlotTests/XMLTests.swift | 79 +- codecov.yml | 2 + 155 files changed, 8695 insertions(+), 6900 deletions(-) create mode 100644 .mise.toml create mode 100644 .periphery.yml create mode 100644 .swift-format create mode 100644 .swift-version create mode 100644 .swiftlint.yml create mode 100644 CLAUDE.md create mode 100755 Scripts/header.sh create mode 100755 Scripts/lint.sh create mode 100644 Sources/Plot/API/Attribute+HTML.swift create mode 100644 Sources/Plot/API/Attribute+HTMLForms.swift create mode 100644 Sources/Plot/API/Attribute+HTMLLinks.swift create mode 100644 Sources/Plot/API/Attribute+HTMLMedia.swift create mode 100644 Sources/Plot/API/Attribute+Podcast.swift create mode 100644 Sources/Plot/API/Attribute+XML.swift create mode 100644 Sources/Plot/API/AudioPlayer.swift create mode 100644 Sources/Plot/API/Component+Attributes.swift delete mode 100644 Sources/Plot/API/ComponentAttributes.swift delete mode 100644 Sources/Plot/API/ControlFlow.swift create mode 100644 Sources/Plot/API/DocumentFormat.swift create mode 100644 Sources/Plot/API/Element+XML.swift create mode 100644 Sources/Plot/API/ElementDefinitions.swift create mode 100644 Sources/Plot/API/Form.swift delete mode 100644 Sources/Plot/API/HTMLAttributes.swift delete mode 100644 Sources/Plot/API/HTMLComponents.swift create mode 100644 Sources/Plot/API/HTMLContext.swift create mode 100644 Sources/Plot/API/HTMLDimensionContext.swift create mode 100644 Sources/Plot/API/HTMLDividableContext.swift delete mode 100644 Sources/Plot/API/HTMLElements.swift create mode 100644 Sources/Plot/API/HTMLImageContainerContext.swift create mode 100644 Sources/Plot/API/HTMLIntegrityContext.swift create mode 100644 Sources/Plot/API/HTMLLinkableContext.swift create mode 100644 Sources/Plot/API/HTMLMediaContext.swift create mode 100644 Sources/Plot/API/HTMLNamableContext.swift create mode 100644 Sources/Plot/API/HTMLOptionListContext.swift create mode 100644 Sources/Plot/API/HTMLScriptableContext.swift create mode 100644 Sources/Plot/API/HTMLSourceContext.swift create mode 100644 Sources/Plot/API/HTMLSourceListContext.swift create mode 100644 Sources/Plot/API/HTMLStylableContext.swift create mode 100644 Sources/Plot/API/HTMLTypeContext.swift create mode 100644 Sources/Plot/API/HTMLValueContext.swift create mode 100644 Sources/Plot/API/IFrame.swift create mode 100644 Sources/Plot/API/Image.swift create mode 100644 Sources/Plot/API/Input.swift create mode 100644 Sources/Plot/API/InputComponent.swift create mode 100644 Sources/Plot/API/Label.swift create mode 100644 Sources/Plot/API/Link.swift create mode 100644 Sources/Plot/API/List.swift create mode 100644 Sources/Plot/API/Node+Conformances.swift create mode 100644 Sources/Plot/API/Node+ControlFlow.swift create mode 100644 Sources/Plot/API/Node+HTMLBody.swift create mode 100644 Sources/Plot/API/Node+HTMLBody2.swift create mode 100644 Sources/Plot/API/Node+HTMLComponents.swift create mode 100644 Sources/Plot/API/Node+HTMLElements.swift create mode 100644 Sources/Plot/API/Node+HTMLElementsMisc.swift create mode 100644 Sources/Plot/API/Node+Podcast.swift create mode 100644 Sources/Plot/API/Node+PodcastComponents.swift create mode 100644 Sources/Plot/API/Node+PodcastItem.swift create mode 100644 Sources/Plot/API/Node+RSS.swift create mode 100644 Sources/Plot/API/Node+RSSAttributes.swift create mode 100644 Sources/Plot/API/Node+SiteMap.swift delete mode 100644 Sources/Plot/API/PodcastAttributes.swift create mode 100644 Sources/Plot/API/PodcastCategoryContext.swift delete mode 100644 Sources/Plot/API/PodcastComponents.swift create mode 100644 Sources/Plot/API/PodcastContentContext.swift delete mode 100644 Sources/Plot/API/PodcastElements.swift create mode 100644 Sources/Plot/API/PodcastNameableContext.swift delete mode 100644 Sources/Plot/API/RSSAttributes.swift create mode 100644 Sources/Plot/API/RSSBasedDocumentFormat.swift create mode 100644 Sources/Plot/API/RSSChannelContext.swift create mode 100644 Sources/Plot/API/RSSContentContext.swift delete mode 100644 Sources/Plot/API/RSSElements.swift create mode 100644 Sources/Plot/API/RSSFeedContext.swift create mode 100644 Sources/Plot/API/RSSItemContext.swift create mode 100644 Sources/Plot/API/RSSRootContext.swift delete mode 100644 Sources/Plot/API/SiteMapElements.swift create mode 100644 Sources/Plot/API/SubmitButton.swift create mode 100644 Sources/Plot/API/Table.swift create mode 100644 Sources/Plot/API/TableRow.swift create mode 100644 Sources/Plot/API/Text.swift create mode 100644 Sources/Plot/API/TextArea.swift create mode 100644 Sources/Plot/API/TextField.swift create mode 100644 Sources/Plot/API/Time.swift delete mode 100644 Sources/Plot/API/XMLAttributes.swift delete mode 100644 Sources/Plot/API/XMLElements.swift create mode 100644 Sources/Plot/API/XMLRootContext.swift delete mode 100644 Tests/PlotTests/Require.swift create mode 100644 Tests/PlotTests/RequireError.swift create mode 100644 codecov.yml diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..87bc056 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,16 @@ +[settings] +# Enable experimental features (required for the spm backend) +experimental = true + +[tools] +# Swift development tools (managed via mise) +# 604.x aligns swift-format (and its swift-syntax) with the Swift 6.4 toolchain. +# Only one 604 tag exists so far; bump when a newer 604 prerelease/release lands. +"spm:swiftlang/swift-format" = "604.0.0-prerelease-2025-12-17" +"aqua:realm/SwiftLint" = "0.63.2" +"spm:peripheryapp/periphery" = "3.7.4" + +[tasks] +swift-format = "swift-format" +swiftlint = "swiftlint" +periphery = "periphery" diff --git a/.periphery.yml b/.periphery.yml new file mode 100644 index 0000000..b0e0637 --- /dev/null +++ b/.periphery.yml @@ -0,0 +1,3 @@ +retain_public: true +targets: +- Plot diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..257f555 --- /dev/null +++ b/.swift-format @@ -0,0 +1,70 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "fileprivate" + }, + "indentation" : { + "spaces" : 2 + }, + "indentConditionalCompilationBlocks" : true, + "indentSwitchCaseLabels" : false, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : false, + "lineBreakBeforeEachGenericRequirement" : false, + "lineLength" : 100, + "maximumBlankLines" : 1, + "multiElementCollectionTrailingCommas" : true, + "noAssignmentInExpressions" : { + "allowedFunctions" : [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether" : false, + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : true, + "AlwaysUseLiteralForEmptyCollectionInit" : false, + "AlwaysUseLowerCamelCase" : true, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : false, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : true, + "NeverUseForceTry" : true, + "NeverUseImplicitlyUnwrappedOptionals" : true, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : true, + "NoParensAroundConditions" : true, + "NoPlaygroundLiterals" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : false, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "TypeNamesShouldBeCapitalized" : true, + "UseEarlyExits" : false, + "UseExplicitNilCheckInConditions" : true, + "UseLetInEveryBoundCaseVariable" : true, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : true, + "UseSynthesizedInitializer" : true, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : true, + "ValidateDocumentationComments" : true + }, + "spacesAroundRangeFormationOperators" : false, + "tabWidth" : 2, + "version" : 1 +} diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..944c7e2 --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +6.4.x-snapshot diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..4ccdb85 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,149 @@ +opt_in_rules: + - array_init + - closure_body_length + - closure_end_indentation + - closure_spacing + - collection_alignment + - conditional_returns_on_newline + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - discouraged_object_literal + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - expiring_todo + - explicit_acl + - explicit_init + - explicit_top_level_acl + # - fallthrough + - fatal_error_message + - file_name + - file_name_no_space + - file_types_order + - first_where + - flatmap_over_map_reduce + - force_unwrapping +# - function_default_parameter_at_end + - ibinspectable_in_extension + - identical_operands + - implicit_return + - implicitly_unwrapped_optional + - indentation_width + - joined_default_parameter + - last_where + - legacy_multiple + - legacy_random + - literal_expression_end_indentation + - lower_acl_than_parent + - missing_docs + - modifier_order + - multiline_arguments + - multiline_arguments_brackets + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - nimble_operator + - nslocalizedstring_key + - nslocalizedstring_require_bundle + - number_separator + - object_literal + - one_declaration_per_file + - operator_usage_whitespace + - optional_enum_case_matching + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init + - private_action + - private_outlet + - prohibited_interface_builder + - prohibited_super_call + - quick_discouraged_call + - quick_discouraged_focused_test + - quick_discouraged_pending_test + - reduce_into + - redundant_nil_coalescing + - redundant_type_annotation + - required_enum_case + - single_test_class + - sorted_first_last + - sorted_imports + - static_operator + - strong_iboutlet + - toggle_bool +# - trailing_closure + - type_contents_order + - unavailable_function + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - xct_specific_matcher + - yoda_condition +analyzer_rules: + - unused_import + - unused_declaration +cyclomatic_complexity: + - 6 + - 12 +file_length: + warning: 225 + error: 300 +function_body_length: + - 50 + - 76 +function_parameter_count: 8 +line_length: + - 108 + - 200 +closure_body_length: + - 50 + - 60 +type_name: + min_length: 3 + max_length: + warning: 50 + error: 60 +identifier_name: + excluded: + - id + - no +excluded: + - DerivedData + - .build + - .swiftpm + - "*/.swiftpm" + - "*/.build" + - Mint + - Examples + - Packages + - Package.swift +indentation_width: + indentation_width: 2 +file_name: + severity: error +fatal_error_message: + severity: error +disabled_rules: + - nesting + - implicit_getter + - switch_case_alignment + - closure_parameter_position + - trailing_comma + - opening_brace + - optional_data_string_conversion + - pattern_matching_keywords +custom_rules: + no_unchecked_sendable: + name: "No Unchecked Sendable" + regex: '@unchecked\s+Sendable' + message: "Use proper Sendable conformance instead of @unchecked Sendable to maintain strict concurrency safety" + severity: error diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1c91f5b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,12 @@ +# CLAUDE.md — Plot + +Vendored package in the BrightDigit Publish stack (`Packages/Publish/Plot`), +maintained in-tree on plain Swift 6.4 (`// swift-tools-version:6.4`), macOS 15+. + +- Strict concurrency is mandatory. Resolve every diagnostic properly + (Sendable / isolation / `Synchronization.Mutex`); never use + `@unchecked Sendable` — a SwiftLint `no_unchecked_sendable` rule enforces this. +- Lint: `LINT_MODE=STRICT ./Scripts/lint.sh` — full BrightDigit house style + (swift-format + SwiftLint + build) gates; periphery is local-only (skipped + when `$CI` is set). +- Tests use XCTest (kept as-is). diff --git a/Package.swift b/Package.swift index ea8d804..e6992c9 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.4 +// swift-tools-version:6.4 /** * Plot diff --git a/Scripts/header.sh b/Scripts/header.sh new file mode 100755 index 0000000..c571c18 --- /dev/null +++ b/Scripts/header.sh @@ -0,0 +1,104 @@ +#!/bin/bash + +# Function to print usage +usage() { + echo "Usage: $0 -d directory -c creator -o company -p package [-y year]" + echo " -d directory Directory to read from (including subdirectories)" + echo " -c creator Name of the creator" + echo " -o company Name of the company with the copyright" + echo " -p package Package or library name" + echo " -y year Copyright year (optional, defaults to current year)" + exit 1 +} + +# Get the current year if not provided +current_year=$(date +"%Y") + +# Default values +year="$current_year" + +# Parse arguments +while getopts ":d:c:o:p:y:" opt; do + case $opt in + d) directory="$OPTARG" ;; + c) creator="$OPTARG" ;; + o) company="$OPTARG" ;; + p) package="$OPTARG" ;; + y) year="$OPTARG" ;; + *) usage ;; + esac +done + +# Check for mandatory arguments +if [ -z "$directory" ] || [ -z "$creator" ] || [ -z "$company" ] || [ -z "$package" ]; then + usage +fi + +# Define the header template +header_template="// +// %s +// %s +// +// Created by %s. +// Copyright © %s %s. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the \"Software\"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +//" + +# Loop through each Swift file in the specified directory and subdirectories +find "$directory" -type f -name "*.swift" | while read -r file; do + # Skip files in the Generated directory + if [[ "$file" == *"/Generated/"* ]]; then + echo "Skipping $file (generated file)" + continue + fi + + # Check if the first line is the swift-format-ignore indicator + first_line=$(head -n 1 "$file") + if [[ "$first_line" == "// swift-format-ignore-file" ]]; then + echo "Skipping $file due to swift-format-ignore directive." + continue + fi + + # Create the header with the current filename + filename=$(basename "$file") + header=$(printf "$header_template" "$filename" "$package" "$creator" "$year" "$company") + + # Remove all consecutive lines at the beginning which start with "// ", contain only whitespace, or only "//" + awk ' + BEGIN { skip = 1 } + { + if (skip && ($0 ~ /^\/\/ / || $0 ~ /^\/\/$/ || $0 ~ /^$/)) { + next + } + skip = 0 + print + }' "$file" > temp_file + + # Add the header to the cleaned file + (echo "$header"; echo; cat temp_file) > "$file" + + # Remove the temporary file + rm temp_file +done + +echo "Headers added or files skipped appropriately across all Swift files in the directory and subdirectories." diff --git a/Scripts/lint.sh b/Scripts/lint.sh new file mode 100755 index 0000000..3d72311 --- /dev/null +++ b/Scripts/lint.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +# Remove set -e to allow script to continue running +# set -e # Exit on any error + +ERRORS=0 + +run_command() { + "$@" || ERRORS=$((ERRORS + 1)) +} + +if [ "$LINT_MODE" = "INSTALL" ]; then + exit +fi + +echo "LintMode: $LINT_MODE" + +# More portable way to get script directory +if [ -z "$SRCROOT" ]; then + SCRIPT_DIR=$(dirname "$(readlink -f "$0")") + PACKAGE_DIR="${SCRIPT_DIR}/.." +else + PACKAGE_DIR="${SRCROOT}" +fi + +# Detect if mise is available +# Check common installation paths for mise +MISE_PATHS=( + "/opt/homebrew/bin/mise" + "/usr/local/bin/mise" + "$HOME/.local/bin/mise" +) + +MISE_BIN="" +for mise_path in "${MISE_PATHS[@]}"; do + if [ -x "$mise_path" ]; then + MISE_BIN="$mise_path" + break + fi +done + +# Fallback to PATH lookup +if [ -z "$MISE_BIN" ] && command -v mise &> /dev/null; then + MISE_BIN="mise" +fi + +if [ -n "$MISE_BIN" ]; then + TOOL_CMD="$MISE_BIN exec --" +else + echo "Error: mise is not installed" + echo "Install mise: https://mise.jdx.dev/getting-started.html" + echo "Checked paths: ${MISE_PATHS[*]}" + exit 1 +fi + +if [ "$LINT_MODE" = "NONE" ]; then + exit +elif [ "$LINT_MODE" = "STRICT" ]; then + SWIFTFORMAT_LINT_OPTIONS="--strict" + SWIFTLINT_OPTIONS="--strict" +else + SWIFTFORMAT_LINT_OPTIONS="" + SWIFTLINT_OPTIONS="" +fi + +pushd $PACKAGE_DIR + +# Bootstrap tools (mise will install based on .mise.toml) +run_command "$MISE_BIN" install + +if [ -z "$CI" ]; then + run_command $TOOL_CMD swift-format format --configuration .swift-format --recursive --parallel --in-place Sources Tests + run_command $TOOL_CMD swiftlint --fix +fi + +if [ -z "$FORMAT_ONLY" ]; then + run_command $TOOL_CMD swift-format lint --configuration .swift-format --recursive --parallel $SWIFTFORMAT_LINT_OPTIONS Sources Tests + run_command $TOOL_CMD swiftlint lint $SWIFTLINT_OPTIONS + # Check for compilation errors + run_command swift build --build-tests +fi + +# header.sh rewrites file headers in place, so it only runs locally — never in CI. +if [ -z "$CI" ]; then + $PACKAGE_DIR/Scripts/header.sh -d $PACKAGE_DIR/Sources -c "Leo Dion" -o "BrightDigit" -p "Plot" +fi + +if [ -z "$CI" ]; then + run_command $TOOL_CMD periphery scan $PERIPHERY_OPTIONS --disable-update-check +fi + +popd + +# Exit with error code if any errors occurred +if [ $ERRORS -gt 0 ]; then + echo "Linting completed with $ERRORS error(s)" + exit 1 +else + echo "Linting completed successfully" + exit 0 +fi diff --git a/Sources/Plot/API/Attribute+HTML.swift b/Sources/Plot/API/Attribute+HTML.swift new file mode 100644 index 0000000..6a09ba5 --- /dev/null +++ b/Sources/Plot/API/Attribute+HTML.swift @@ -0,0 +1,173 @@ +/** +* Plot +* Copyright (c) John Sundell 2019 +* MIT license, see LICENSE file for details +*/ + +extension Attribute where Context: HTMLContext { + /// Assign an ID to the current element. + /// - parameter id: The ID to assign. + /// - Returns: The created attribute. + public static func id(_ id: String) -> Attribute { + Attribute(name: "id", value: id) + } + + /// Assign a class name to the current element. May also be a list of + /// space-separated class names. + /// - parameter className: The class or list of classes to assign. + /// - Returns: The created attribute. + public static func `class`(_ className: String) -> Attribute { + Attribute(name: "class", value: className) + } + + /// Add a `data-` attribute to the current element. + /// - Parameters: + /// - name: The name of the attribute to add. The name will + /// be prefixed with `data-`. + /// - value: The attribute's string value. + /// - Returns: The created attribute. + public static func data(named name: String, value: String) -> Attribute { + Attribute(name: "data-\(name)", value: value) + } + + /// Assign whether operating system level spell checking should be enabled. + /// - parameter isEnabled: Whether spell checking should be enabled. + /// - Returns: The created attribute. + public static func spellcheck(_ isEnabled: Bool) -> Attribute { + Attribute(name: "spellcheck", value: String(isEnabled)) + } + + /// Specify a title for the element. + /// - parameter title: The title to assign to the element. + /// - Returns: The created attribute. + public static func title(_ title: String) -> Attribute { + Attribute(name: "title", value: title) + } + + /// Specify a directionality for the element. + /// - parameter directionality: The directionality to assign to the element. + /// - Returns: The created attribute. + public static func dir(_ directionality: Directionality) -> Attribute { + Attribute(name: "dir", value: directionality.rawValue) + } +} + +extension Node where Context: HTMLContext { + /// Assign an ID to the current element. + /// - parameter id: The ID to assign. + /// - Returns: The created node. + public static func id(_ id: String) -> Node { + .attribute(named: "id", value: id) + } + + /// Assign a class name to the current element. May also be a list of + /// space-separated class names. + /// - parameter className: The class or list of classes to assign. + /// - Returns: The created node. + public static func `class`(_ className: String) -> Node { + .attribute(named: "class", value: className) + } + + /// Add a `data-` attribute to the current element. + /// - Parameters: + /// - name: The name of the attribute to add. The name will + /// be prefixed with `data-`. + /// - value: The attribute's string value. + /// - Returns: The created node. + public static func data(named name: String, value: String) -> Node { + .attribute(named: "data-\(name)", value: value) + } + + /// Assign whether operating system level spell checking should be enabled. + /// - parameter isEnabled: Whether spell checking should be enabled. + /// - Returns: The created node. + public static func spellcheck(_ isEnabled: Bool) -> Node { + .attribute(named: "spellcheck", value: String(isEnabled)) + } + + /// Specify a title for the element. + /// - parameter title: The title to assign to the element. + /// - Returns: The created node. + public static func title(_ title: String) -> Node { + .attribute(named: "title", value: title) + } + + /// Assign whether the element should be hidden. + /// - parameter isHidden: Whether the element should be hidden or not. + /// - Returns: The created node. + public static func hidden(_ isHidden: Bool) -> Node { + isHidden ? .attribute(named: "hidden") : .empty + } + + /// Specify a directionality for the element. + /// - parameter directionality: The directionality to assign to the element. + /// - Returns: The created node. + public static func dir(_ directionality: Directionality) -> Node { + .attribute(named: "dir", value: directionality.rawValue) + } +} + +extension Attribute where Context: HTMLNamableContext { + /// Assign a name to the element. + /// - parameter name: The name to assign. + /// - Returns: The created attribute. + public static func name(_ name: String) -> Attribute { + Attribute(name: "name", value: name) + } +} + +extension Node where Context: HTMLNamableContext { + /// Assign a name to the element. + /// - parameter name: The name to assign. + /// - Returns: The created node. + public static func name(_ name: String) -> Node { + .attribute(named: "name", value: name) + } +} + +extension Attribute where Context == HTML.MetaContext { + /// Assign a property to the element. + /// - parameter property: The property to assign. + /// - Returns: The created attribute. + public static func property(_ property: String) -> Attribute { + Attribute(name: "property", value: property) + } +} + +extension Attribute where Context: HTMLTypeContext { + /// Assign a type string to this element. + /// - parameter type: The name of the type to assign. + /// - Returns: The created attribute. + public static func type(_ type: String) -> Attribute { + Attribute(name: "type", value: type) + } +} + +extension Attribute where Context: HTMLValueContext { + /// Assign a string value to the element. + /// - parameter value: The value to assign. + /// - Returns: The created attribute. + public static func value(_ value: String) -> Attribute { + Attribute(name: "value", value: value) + } +} + +extension Node where Context: HTMLValueContext { + /// Assign a string value to the element. + /// - parameter value: The value to assign. + /// - Returns: The created node. + public static func value(_ value: String) -> Node { + .attribute(named: "value", value: value) + } +} + +// MARK: - Document + +extension Node where Context == HTML.DocumentContext { + /// Specify the language of the HTML document's content. + /// - parameter language: The language to specify. + /// - Returns: The created node. + public static func lang(_ language: Language) -> Node { + .attribute(named: "lang", value: language.rawValue) + } +} diff --git a/Sources/Plot/API/Attribute+HTMLForms.swift b/Sources/Plot/API/Attribute+HTMLForms.swift new file mode 100644 index 0000000..cb61a17 --- /dev/null +++ b/Sources/Plot/API/Attribute+HTMLForms.swift @@ -0,0 +1,195 @@ +/** +* Plot +* Copyright (c) John Sundell 2019 +* MIT license, see LICENSE file for details +*/ + +extension Node where Context == HTML.FormContext { + /// Assign a URL that this form should be sent to when submitted. + /// - parameter url: The action URL that the form should be sent to. + /// - Returns: The created node. + public static func action(_ url: URLRepresentable) -> Node { + .attribute(named: "action", value: url.string) + } + + /// Assign a specific content type to the form. + /// - Parameter type: The content type to assign. + /// - Returns: The created node. + public static func enctype(_ type: HTMLFormContentType) -> Node { + .attribute(named: "enctype", value: type.rawValue) + } + + /// Assign a specific HTTP request method that the form should + /// be submitted using. + /// - Parameter method: The HTTP request method to use. + /// - Returns: The created node. + public static func method(_ method: HTMLFormMethod) -> Node { + .attribute(named: "method", value: method.rawValue) + } + + /// Add the `novalidate` attribute to the form, which + /// disables any native browser validation on the form. + /// - parameter isOn: Whether validation should be disabled. + /// - Returns: The created node. + public static func novalidate(_ isOn: Bool = true) -> Node { + isOn ? .attribute(named: "novalidate") : .empty + } +} + +extension Node where Context == HTML.LabelContext { + /// Assign which input control that this label is for. + /// - parameter target: The target input control's name. + /// - Returns: The created node. + public static func `for`(_ target: String) -> Node { + .attribute(named: "for", value: target) + } +} + +extension Attribute where Context == HTML.InputContext { + /// Assign an input type to the element. + /// - parameter type: The input type to assign. + /// - Returns: The created attribute. + public static func type(_ type: HTMLInputType) -> Attribute { + Attribute(name: "type", value: type.rawValue) + } + + /// Assign a placeholder to the input field. + /// - parameter placeholder: The placeholder to assign. + /// - Returns: The created attribute. + public static func placeholder(_ placeholder: String) -> Attribute { + Attribute(name: "placeholder", value: placeholder) + } + + /// Assign whether the element should have autocomplete turned on or off. + /// - parameter isOn: Whether autocomplete should be turned on. + /// - Returns: The created attribute. + public static func autocomplete(_ isOn: Bool) -> Attribute { + Attribute(name: "autocomplete", value: isOn ? "on" : "off") + } + + /// Assign whether the element is required before submitting the form. + /// - parameter isRequired: Whether the element is required. + /// - Returns: The created attribute. + public static func required(_ isRequired: Bool) -> Attribute { + isRequired ? Attribute(name: "required", value: nil, ignoreIfValueIsEmpty: false) : .empty + } + + /// Assign whether the element should be autofocused when the page loads. + /// - parameter isOn: Whether autofocus should be turned on. + /// - Returns: The created attribute. + public static func autofocus(_ isOn: Bool) -> Attribute { + isOn ? Attribute(name: "autofocus", value: nil, ignoreIfValueIsEmpty: false) : .empty + } + + /// Assign whether the element should be read-only. + /// - parameter isReadonly: Whether the input is read-only. + /// - Returns: The created attribute. + public static func readonly(_ isReadonly: Bool) -> Attribute { + isReadonly ? Attribute(name: "readonly", value: nil, ignoreIfValueIsEmpty: false) : .empty + } + + /// Assign whether the element should be disabled. + /// - parameter isDisabled: Whether the input is disabled. + /// - Returns: The created attribute. + public static func disabled(_ isDisabled: Bool) -> Attribute { + isDisabled ? Attribute(name: "disabled", value: nil, ignoreIfValueIsEmpty: false) : .empty + } + + /// Assign whether the element should allow the selection of multiple values. + /// - parameter isEnabled: Whether multiple values are allowed. + /// - Returns: The created attribute. + public static func multiple(_ isEnabled: Bool) -> Attribute { + isEnabled ? Attribute(name: "multiple", value: nil, ignoreIfValueIsEmpty: false) : .empty + } + + /// Assign whether a checkbox or radio input element has an active state. + /// - parameter isChecked: Whether the element has an active state. + /// - Returns: The created attribute. + public static func checked(_ isChecked: Bool) -> Attribute { + isChecked ? Attribute(name: "checked", value: nil, ignoreIfValueIsEmpty: false) : .empty + } +} + +extension Node where Context == HTML.ButtonContext { + /// Assign a button type to the element. + /// - parameter type: The button type to assign. + /// - Returns: The created node. + public static func type(_ type: HTMLButtonType) -> Node { + .attribute(named: "type", value: type.rawValue) + } +} + +extension Node where Context == HTML.TextAreaContext { + /// Specify the number of columns that the text area should contain. + /// - parameter columns: The number of columns to specify. + /// - Returns: The created node. + public static func cols(_ columns: Int) -> Node { + .attribute(named: "cols", value: String(columns)) + } + + /// Specify the number of text rows that should be visible within the text area. + /// - parameter rows: The number of rows to specify. + /// - Returns: The created node. + public static func rows(_ rows: Int) -> Node { + .attribute(named: "rows", value: String(rows)) + } + + /// Assign a placeholder to the text area. + /// - parameter placeholder: The placeholder to assign. + /// - Returns: The created node. + public static func placeholder(_ placeholder: String) -> Node { + .attribute(named: "placeholder", value: placeholder) + } + + /// Assign whether the element is required before submitting the form. + /// - parameter isRequired: Whether the element is required. + /// - Returns: The created node. + public static func required(_ isRequired: Bool) -> Node { + isRequired ? .attribute(named: "required") : .empty + } + + /// Assign whether the element should be autofocused when the page loads. + /// - parameter isOn: Whether autofocus should be turned on. + /// - Returns: The created node. + public static func autofocus(_ isOn: Bool) -> Node { + isOn ? .attribute(named: "autofocus") : .empty + } + + /// Assign whether the element should be read-only. + /// - parameter isReadonly: Whether the input is read-only. + /// - Returns: The created node. + public static func readonly(_ isReadonly: Bool) -> Node { + isReadonly ? .attribute(named: "readonly") : .empty + } + + /// Assign whether the element should be disabled. + /// - parameter isDisabled: Whether the input is disabled. + /// - Returns: The created node. + public static func disabled(_ isDisabled: Bool) -> Node { + isDisabled ? .attribute(named: "disabled") : .empty + } +} + +extension Attribute where Context == HTML.OptionContext { + /// Specify whether the option should be initially selected. + /// - parameter isSelected: Whether the option should be selected. + /// - Returns: The created attribute. + public static func isSelected(_ isSelected: Bool) -> Attribute { + guard isSelected else { + return .empty + } + + return Attribute( + name: "selected", + value: nil, + ignoreIfValueIsEmpty: false + ) + } + + /// Assign a label to the given option. + /// - parameter label: The user displayed value of the option + /// - Returns: The created attribute. + public static func label(_ label: String) -> Attribute { + Attribute(name: "label", value: label, ignoreIfValueIsEmpty: false) + } +} diff --git a/Sources/Plot/API/Attribute+HTMLLinks.swift b/Sources/Plot/API/Attribute+HTMLLinks.swift new file mode 100644 index 0000000..2a2b995 --- /dev/null +++ b/Sources/Plot/API/Attribute+HTMLLinks.swift @@ -0,0 +1,190 @@ +/** +* Plot +* Copyright (c) John Sundell 2019 +* MIT license, see LICENSE file for details +*/ + +extension Attribute where Context == HTML.LinkContext { + /// Assign a relationship to the link. See `HTMLLinkRelationship` for more info. + /// - parameter relationship: The relationship to assign. + /// - Returns: The created attribute. + public static func rel(_ relationship: HTMLLinkRelationship) -> Attribute { + Attribute(name: "rel", value: relationship.rawValue) + } + + /// Assign an `hreflang` attribute to this element. + /// - parameter language: The language to assign. + /// - Returns: The created attribute. + public static func hreflang(_ language: Language) -> Attribute { + Attribute(name: "hreflang", value: language.rawValue) + } + + /// Assign an icon sizes string to this element. + /// - parameter sizes: The icon sizes string to assign. + /// - Returns: The created attribute. + public static func sizes(_ sizes: String) -> Attribute { + Attribute(name: "sizes", value: sizes) + } + + /// Assign an icon color string to this element. + /// - parameter color: The icon color string to assign. + /// - Returns: The created attribute. + public static func color(_ color: String) -> Attribute { + Attribute(name: "color", value: color) + } + + /// Assign whether the link should have the crossorigin attribute. + /// - parameter isEnabled: Whether crossorigin should be enabled. + /// - Returns: The created attribute. + public static func crossorigin(_ isEnabled: Bool) -> Attribute { + isEnabled ? Attribute(name: "crossorigin", value: nil, ignoreIfValueIsEmpty: false) : .empty + } +} + +extension Attribute where Context: HTMLLinkableContext { + /// Assign a URL to link the element to, using its `href` attribute. + /// - parameter url: The URL to assign. + /// - Returns: The created attribute. + public static func href(_ url: URLRepresentable) -> Attribute { + Attribute(name: "href", value: url.string) + } +} + +extension Node where Context: HTMLLinkableContext { + /// Assign a URL to link the element to, using its `href` attribute. + /// - parameter url: The URL to assign. + /// - Returns: The created node. + public static func href(_ url: URLRepresentable) -> Node { + .attribute(named: "href", value: url.string) + } +} + +extension Node where Context == HTML.AnchorContext { + /// Assign a target to the anchor, specifying how its URL should be opened. + /// - parameter target: The target to assign. See `HTMLAnchorTarget`. + /// - Returns: The created node. + public static func target(_ target: HTMLAnchorTarget) -> Node { + .attribute(named: "target", value: target.rawValue) + } + + /// Assign a relationship to the anchor, using its `rel` attribute. + /// - parameter relationship: The relationship to assign. See + /// `HTMLAnchorRelationship` for more info. + /// - Returns: The created node. + public static func rel(_ relationship: HTMLAnchorRelationship) -> Node { + .attribute(named: "rel", value: relationship.rawValue) + } +} + +// MARK: - DateTime + +extension Node where Context == HTML.TimeContext { + /// Attach a datetime to the time element, to translate the element into + /// a machine readable format for browsers. + /// - parameter datetime: The datetime string to attach. See the datetime + /// reference for valid `datetime` values: + /// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time#valid_datetime_values + /// - Returns: The created node. + public static func datetime(_ datetime: String) -> Node { + .attribute(named: "datetime", value: datetime) + } +} + +// MARK: - Interactive elements + +extension Node where Context == HTML.DetailsContext { + /// Assign whether the details element is opened/expanded. + /// - parameter isOpen: Whether the element should be displayed as open. + /// - Returns: The created node. + public static func open(_ isOpen: Bool) -> Node { + isOpen ? .attribute(named: "open") : .empty + } +} + +// MARK: - Sources and media + +extension Attribute where Context: HTMLSourceContext { + /// Assign a source to the element, using its `src` attribute. + /// - parameter url: The source URL to assign. + /// - Returns: The created attribute. + public static func src(_ url: URLRepresentable) -> Attribute { + Attribute(name: "src", value: url.string) + } +} + +extension Node where Context: HTMLSourceContext { + /// Assign a source to the element, using its `src` attribute. + /// - parameter url: The source URL to assign. + /// - Returns: The created node. + public static func src(_ url: URLRepresentable) -> Node { + .attribute(named: "src", value: url.string) + } +} + +extension Node where Context: HTMLMediaContext { + /// Assign whether the element's media controls should be enabled. + /// - parameter enableControls: Whether controls should be shown. + /// - Returns: The created node. + public static func controls(_ enableControls: Bool) -> Node { + enableControls ? .attribute(named: "controls") : .empty + } +} + +extension Attribute where Context == HTML.ObjectContext { + /// Assign an external resource to the element, using its `data` attribute. + /// - parameter url: The data URL to assign. + /// - Returns: The created attribute. + public static func data(_ url: URLRepresentable) -> Attribute { + Attribute(name: "data", value: url.string) + } +} + +extension Node where Context == HTML.ObjectContext { + /// Assign an external resource to the element, using its `data` attribute. + /// - parameter url: The data URL to assign. + /// - Returns: The created node. + public static func data(_ url: URLRepresentable) -> Node { + .attribute(named: "data", value: url.string) + } +} + +extension Attribute where Context == HTML.AudioSourceContext { + /// Assign a type to this audio source. See `HTMLAudioFormat` for more info. + /// - parameter format: The audio format to assign. + /// - Returns: The created attribute. + public static func type(_ format: HTMLAudioFormat) -> Attribute { + Attribute(name: "type", value: "audio/" + format.rawValue) + } +} + +extension Attribute where Context == HTML.VideoSourceContext { + /// Assign a type to this video source. See `HTMLVideoFormat` for more info. + /// - parameter format: The video format to assign. + /// - Returns: The created attribute. + public static func type(_ format: HTMLVideoFormat) -> Attribute { + Attribute(name: "type", value: "video/" + format.rawValue) + } +} + +extension Attribute where Context == HTML.PictureSourceContext { + /// Assign a string describing a set of sources, using the `srcset` attribute. + /// - parameter set: The set of sources that this element should point to. + /// - Returns: The created attribute. + public static func srcset(_ set: String) -> Attribute { + Attribute(name: "srcset", value: set) + } + + /// Assign a media query that determines whether this source should be used. + /// - parameter query: The media query that the browser should evaluate. + /// - Returns: The created attribute. + public static func media(_ query: String) -> Attribute { + Attribute(name: "media", value: query) + } + + /// Assign a string describing the MIME type, using the `type` attribute. + /// - parameter type: The type (MIME type) for this element. + /// - Returns: The created attribute. + public static func type(_ type: String) -> Attribute { + Attribute(name: "type", value: type) + } +} diff --git a/Sources/Plot/API/Attribute+HTMLMedia.swift b/Sources/Plot/API/Attribute+HTMLMedia.swift new file mode 100644 index 0000000..501118d --- /dev/null +++ b/Sources/Plot/API/Attribute+HTMLMedia.swift @@ -0,0 +1,167 @@ +/** +* Plot +* Copyright (c) John Sundell 2019 +* MIT license, see LICENSE file for details +*/ + +extension Attribute where Context: HTMLDimensionContext { + /// Assign a given width to the element. + /// - parameter width: The width to assign. + /// - Returns: The created attribute. + public static func width(_ width: Int) -> Attribute { + Attribute(name: "width", value: String(width)) + } + + /// Assign a given height to the element + /// - parameter height: The height to assign. + /// - Returns: The created attribute. + public static func height(_ height: Int) -> Attribute { + Attribute(name: "height", value: String(height)) + } +} + +extension Node where Context: HTMLStylableContext { + /// Assign inline CSS to the element, using its `style` attribute. + /// - parameter css: The CSS string to assign. + /// - Returns: The created node. + public static func style(_ css: String) -> Node { + .attribute(named: "style", value: css) + } +} + +// MARK: - Metadata + +extension Attribute where Context == HTML.MetaContext { + /// Assign an encoding to the element, using its `charset` attribute. + /// - parameter encoding: The encoding to assign. See `DocumentEncoding`. + /// - Returns: The created attribute. + public static func charset(_ encoding: DocumentEncoding) -> Attribute { + Attribute(name: "charset", value: encoding.rawValue) + } + + /// Assign a content string to the element. + /// - parameter content: The content value to assign. + /// - Returns: The created attribute. + public static func content(_ content: String) -> Attribute { + Attribute(name: "content", value: content) + } +} + +// MARK: - iFrames + +extension Attribute where Context == HTML.IFrameContext { + /// Assign whether the iframe should display a border or not. + /// - parameter isOn: Whether a border should be displayed. + /// - Returns: The created attribute. + public static func frameborder(_ isOn: Bool) -> Attribute { + Attribute(name: "frameborder", value: isOn ? "1" : "0") + } + + /// Assign what sort of features that the iframe should be allowed to access. + /// - parameter features: A list of feature names to allow. + /// - Returns: The created attribute. + public static func allow(_ features: String...) -> Attribute { + Attribute(name: "allow", value: features.joined(separator: "; ")) + } + + /// Assign whether to grant the iframe full screen capabilities. + /// - parameter allow: Whether the iframe should be allowed to go full screen. + /// - Returns: The created attribute. + public static func allowfullscreen(_ allow: Bool) -> Attribute { + allow ? Attribute(name: "allowfullscreen", value: nil, ignoreIfValueIsEmpty: false) : .empty + } +} + +// MARK: - Images + +extension Attribute where Context == HTML.ImageContext { + /// Assign an alternative text to the image. This is important both for + /// accessibility, and in case the referenced image can't be rendered. + /// - parameter text: The alternative text to use. + /// - Returns: The created attribute. + public static func alt(_ text: String) -> Attribute { + Attribute(name: "alt", value: text) + } +} + +// MARK: - Accessibility + +extension Node where Context: HTML.BodyContext { + /// Assign an accessibility label to the element, which is used by + /// assistive technologies to get a text representation of it. + /// - parameter label: The label to assign. + /// - Returns: The created node. + public static func ariaLabel(_ label: String) -> Node { + .attribute(named: "aria-label", value: label) + } + + /// Assign an accessibility attribute to an element, + /// to establish a parent -> child relationship + /// - parameter child: The child to assign to the parent + /// - Returns: The created node. + public static func ariaControls(_ child: String) -> Node { + .attribute(named: "aria-controls", value: child) + } + + /// Assign an accessibility attribute to an element, + /// which describes whether the element is expanded or not + /// - parameter isExpanded: Whether the element is expanded or not + /// - Returns: The created node. + public static func ariaExpanded(_ isExpanded: Bool) -> Node { + .attribute(named: "aria-expanded", value: isExpanded ? "true" : "false") + } + + /// Assign an accessibility attribute to an element, + /// which removes an element from the accessibility tree + /// - parameter isHidden: Whether the element is hidden or not + /// - Returns: The created node. + public static func ariaHidden(_ isHidden: Bool) -> Node { + .attribute(named: "aria-hidden", value: isHidden ? "true" : "false") + } +} + +// MARK: - Subresource Integrity + +extension Attribute where Context: HTMLIntegrityContext { + /// Assign a subresouce integrity hash to the element, using its `integrity` attribute. + /// - parameter hash: base64-encoded cryptographic hash + /// - Returns: The created attribute. + public static func integrity(_ hash: String) -> Attribute { + Attribute(name: "integrity", value: hash) + } +} + +extension Node where Context: HTMLIntegrityContext { + /// Assign a subresouce integrity hash to the element, using its `integrity` attribute. + /// - parameter hash: base64-encoded cryptographic hash + /// - Returns: The created node. + public static func integrity(_ hash: String) -> Node { + .attribute(named: "integrity", value: hash) + } +} + +// MARK: - Scripts + +extension Node where Context == HTML.ScriptContext { + /// Assign that the element's script should be loaded in `async` mode. + public static func async() -> Node { + .attribute(named: "async", value: nil, ignoreIfValueIsEmpty: false) + } + + /// Assign that the element's script should be loaded in `defer` mode. + public static func `defer`() -> Node { + .attribute(named: "defer", value: nil, ignoreIfValueIsEmpty: false) + } +} + +// MARK: - Javascript + +extension Node where Context: HTML.BodyContext { + /// Add a script to execute when the user clicks the current element. + /// - parameter script: The script to execute when the user clicks on the node. + /// Usually prefixed with `javascript:`. + /// - Returns: The created node. + public static func onclick(_ script: String) -> Node { + .attribute(named: "onclick", value: script) + } +} diff --git a/Sources/Plot/API/Attribute+Podcast.swift b/Sources/Plot/API/Attribute+Podcast.swift new file mode 100644 index 0000000..2c1bbe4 --- /dev/null +++ b/Sources/Plot/API/Attribute+Podcast.swift @@ -0,0 +1,28 @@ +/** +* Plot +* Copyright (c) John Sundell 2019 +* MIT license, see LICENSE file for details +*/ + +extension Attribute where Context == PodcastFeed.EnclosureContext { + /// Assign an URL from which the enclosure's file can be downloaded. + /// - parameter url: The URL to assign. + /// - Returns: The created attribute. + public static func url(_ url: URLRepresentable) -> Attribute { + Attribute(name: "url", value: url.string) + } + + /// Assign a length to the enclosure, in terms of its file size. + /// - parameter byteCount: The file's size in bytes. + /// - Returns: The created attribute. + public static func length(_ byteCount: Int) -> Attribute { + Attribute(name: "length", value: String(byteCount)) + } + + /// Assign a MIME type to the enclosure. + /// - parameter mimeType: The MIME type to assign. + /// - Returns: The created attribute. + public static func type(_ mimeType: String) -> Attribute { + Attribute(name: "type", value: mimeType) + } +} diff --git a/Sources/Plot/API/Attribute+XML.swift b/Sources/Plot/API/Attribute+XML.swift new file mode 100644 index 0000000..c13823a --- /dev/null +++ b/Sources/Plot/API/Attribute+XML.swift @@ -0,0 +1,21 @@ +/** +* Plot +* Copyright (c) John Sundell 2019 +* MIT license, see LICENSE file for details +*/ + +extension Attribute where Context == XML.DeclarationContext { + /// Declare the XML version used to define the document. + /// - parameter version: The XML document's version. + /// - Returns: The created attribute. + public static func version(_ version: Double) -> Attribute { + Attribute(name: "version", value: String(version)) + } + + /// Declare the encoding used to define the document's content. + /// - parameter encoding: The XML document's encoding. + /// - Returns: The created attribute. + public static func encoding(_ encoding: DocumentEncoding) -> Attribute { + Attribute(name: "encoding", value: encoding.rawValue) + } +} diff --git a/Sources/Plot/API/Attribute.swift b/Sources/Plot/API/Attribute.swift index 932b24e..40b3b70 100644 --- a/Sources/Plot/API/Attribute.swift +++ b/Sources/Plot/API/Attribute.swift @@ -11,63 +11,66 @@ import Foundation /// but rather use Plot's various DSL APIs to create them, for example by using /// the `id()` or `class()` modifier on an HTML element. public struct Attribute { - /// The name of the attribute - public var name: String - /// The attribute's value - public var value: String? - /// Whether the attribute's value should replace any existing one that has - /// already been added to a given element for the same attribute name. - public var replaceExisting: Bool - /// Whether the attribute should be completely ignored if it has no value. - public var ignoreIfValueIsEmpty: Bool + /// The name of the attribute + public var name: String + /// The attribute's value + public var value: String? + /// Whether the attribute's value should replace any existing one that has + /// already been added to a given element for the same attribute name. + public var replaceExisting: Bool + /// Whether the attribute should be completely ignored if it has no value. + public var ignoreIfValueIsEmpty: Bool - /// Create a new `Attribute` instance with a name and a value, and optionally - /// opt out of ignoring the attribute if its value is empty, and decide whether the - /// attribute should replace any existing one that's already been added to an element - /// for the same name. - public init(name: String, - value: String?, - replaceExisting: Bool = true, - ignoreIfValueIsEmpty: Bool = true) { - self.name = name - self.value = value - self.replaceExisting = replaceExisting - self.ignoreIfValueIsEmpty = ignoreIfValueIsEmpty - } + /// Create a new `Attribute` instance with a name and a value, and optionally + /// opt out of ignoring the attribute if its value is empty, and decide whether the + /// attribute should replace any existing one that's already been added to an element + /// for the same name. + public init( + name: String, + value: String?, + replaceExisting: Bool = true, + ignoreIfValueIsEmpty: Bool = true + ) { + self.name = name + self.value = value + self.replaceExisting = replaceExisting + self.ignoreIfValueIsEmpty = ignoreIfValueIsEmpty + } } -public extension Attribute { - /// Create a completely empty attribute, that's ignored during rendering. - /// This is useful in contexts where you need to return an `Attribute`, but - /// your logic determines that nothing should be added. - static var empty: Attribute { - Attribute(name: "", value: nil) - } +extension Attribute { + /// Create a completely empty attribute, that's ignored during rendering. + /// This is useful in contexts where you need to return an `Attribute`, but + /// your logic determines that nothing should be added. + public static var empty: Attribute { + Attribute(name: "", value: nil) + } - /// Create an attribute with a given name and value. This is the recommended - /// way of creating completely custom attributes, or ones that Plot does not - /// yet support, when within an attribute context. - static func attribute(named name: String, value: String?) -> Self { - Attribute(name: name, value: value) - } + /// Create an attribute with a given name and value. This is the recommended + /// way of creating completely custom attributes, or ones that Plot does not + /// yet support, when within an attribute context. + public static func attribute(named name: String, value: String?) -> Self { + Attribute(name: name, value: value) + } } -internal extension Attribute where Context == Any { - static func any(name: String, value: String) -> Attribute { - Attribute(name: name, value: value) - } +extension Attribute where Context == Any { + internal static func any(name: String, value: String) -> Attribute { + Attribute(name: name, value: value) + } } extension Attribute: NodeConvertible { - public var node: Node { .attribute(self) } + /// The node representation of this attribute. + public var node: Node { .attribute(self) } } extension Attribute: AnyAttribute { - func render() -> String { - guard let value = nonEmptyValue else { - return ignoreIfValueIsEmpty ? "" : name - } - - return "\(name)=\"\(value)\"" + internal func render() -> String { + guard let value = nonEmptyValue else { + return ignoreIfValueIsEmpty ? "" : name } + + return "\(name)=\"\(value)\"" + } } diff --git a/Sources/Plot/API/AudioPlayer.swift b/Sources/Plot/API/AudioPlayer.swift new file mode 100644 index 0000000..eaf5fc2 --- /dev/null +++ b/Sources/Plot/API/AudioPlayer.swift @@ -0,0 +1,82 @@ +/** +* Plot +* Copyright (c) John Sundell 2019 +* MIT license, see LICENSE file for details +*/ + +/// Component used to render an `