Skip to content
This repository was archived by the owner on Jun 1, 2023. It is now read-only.

Commit 5f57067

Browse files
Lukas-Stuehrkmattt
andauthored
Group operators (#228)
* Group operators separately. * Display operators differently. * Add Operator icon and style to CSS * Distinguish operators from function implementations * Reorder operations in members component * Diminish generic where clause in operator headings * Add changelog entry for #228 Co-authored-by: Mattt <[email protected]>
1 parent b8a0cd9 commit 5f57067

File tree

15 files changed

+274
-10
lines changed

15 files changed

+274
-10
lines changed

Assets/css/all.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@
183183
--icon-extension: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23eca95b' height='90' rx='8' stroke='%23e89234' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cg fill='%23fff'%3E%3Cpath d='m54.43 81.93h-33.92v-63.86h33.92v12.26h-21.82v13.8h20.45v11.32h-20.45v14.22h21.82z'/%3E%3Cpath d='m68.74 74.58h-.27l-2.78 7.35h-7.28l5.59-12.61-6-12.54h8l2.74 7.3h.27l2.76-7.3h7.64l-6.14 12.54 5.89 12.61h-7.64z'/%3E%3C/g%3E%3C/svg%3E%0A");
184184
--icon-function: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m24.25 75.66a5.47 5.47 0 0 1 5.75-5.73c1.55 0 3.55.41 6.46.41 3.19 0 4.78-1.55 5.46-6.65l1.5-10.14h-9.34a6 6 0 1 1 0-12h11.1l1.09-7.27c1.55-10.89 8.01-16.58 17.73-16.58 6.69 0 11.74 1.77 11.74 6.64a5.47 5.47 0 0 1 -5.74 5.73c-1.55 0-3.55-.41-6.46-.41-3.14 0-4.73 1.51-5.46 6.65l-.78 5.27h11.44a6 6 0 1 1 .05 12h-13.19l-1.78 12.11c-1.59 10.92-8.1 16.61-17.82 16.61-6.7 0-11.75-1.77-11.75-6.64z' fill='%23fff'/%3E%3C/svg%3E%0A");
185185
--icon-method: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%235a98f8' height='90' rx='8' stroke='%232974ed' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m70.61 81.71v-39.6h-.31l-15.69 39.6h-9.22l-15.65-39.6h-.35v39.6h-14.19v-63.42h18.63l16 41.44h.36l16-41.44h18.61v63.42z' fill='%23fff'/%3E%3C/svg%3E%0A");
186+
--icon-operator: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cellipse fill='%23fff' cx='50' cy='50' rx='16' ry='16'/%3E%3C/svg%3E%0A");
186187
--icon-property: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%2389c5e6' height='90' rx='8' stroke='%236bb7e1' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m52.31 18.29c13.62 0 22.85 8.84 22.85 22.46s-9.71 22.37-23.82 22.37h-10.34v18.59h-16.16v-63.42zm-11.31 32.71h7c6.85 0 10.89-3.56 10.89-10.2s-4.08-10.16-10.89-10.16h-7z' fill='%23fff'/%3E%3C/svg%3E%0A");
187188
--icon-protocol: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23ff6682' height='90' rx='8' stroke='%23ff2d55' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cg fill='%23fff'%3E%3Cpath d='m46.28 18.29c11.84 0 20 8.66 20 21.71s-8.44 21.71-20.6 21.71h-10.81v20h-12.09v-63.42zm-11.41 33.05h8.13c6.93 0 11-4 11-11.29s-4-11.25-10.93-11.25h-8.2z'/%3E%3Cpath d='m62 57.45h8v4.77h.16c.84-3.45 2.54-5.12 5.17-5.12a5.06 5.06 0 0 1 1.92.35v7.55a5.69 5.69 0 0 0 -2.39-.51c-3.08 0-4.66 1.74-4.66 5.12v12.1h-8.2z'/%3E%3C/g%3E%3C/svg%3E%0A");
188189
--icon-structure: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23b57edf' height='90' rx='8' stroke='%239454c2' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m38.38 63c.74 4.53 5.62 7.16 11.82 7.16s10.37-2.81 10.37-6.68c0-3.51-2.73-5.31-10.24-6.76l-6.5-1.23c-12.66-2.35-19.21-8.49-19.21-18.21 0-12.22 10.59-20.09 25.18-20.09 16 0 25.36 7.83 25.53 19.91h-15c-.26-4.57-4.57-7.29-10.42-7.29s-9.31 2.63-9.31 6.37c0 3.34 2.9 5.18 9.8 6.5l6.5 1.23c13.56 2.6 19.71 8.09 19.71 18.09 0 12.74-10 20.83-26.72 20.83-15.82 0-26.28-7.3-26.5-19.78z' fill='%23fff'/%3E%3C/svg%3E%0A");
@@ -950,6 +951,10 @@ h1 small {
950951
color: var(--quaternary-label);
951952
}
952953

954+
h3 small {
955+
color: var(--tertiary-label);
956+
}
957+
953958
p code,
954959
dd code,
955960
li code {
@@ -1036,6 +1041,11 @@ nav li[class] {
10361041
--link: var(--system-blue);
10371042
}
10381043

1044+
.operator {
1045+
--background-image: var(--icon-operator);
1046+
--link: var(--system-green);
1047+
}
1048+
10391049
.property {
10401050
--background-image: var(--icon-property);
10411051
--link: var(--system-teal);

Changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Added support for generating documentation for
1313
extensions to external types.
1414
#230 by @Lukas-Stuehrk and @mattt.
15+
- Added support for generating documentation for operators.
16+
#228 by @Lukas-Stuehrk and @mattt.
1517
- Added end-to-end tests for command-line interface.
1618
#199 by @MaxDesiatov and @mattt.
1719
- Added `--minimum-access-level` option to `generate` and `coverage` commands.

Resources/all.min.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/SwiftDoc/API.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ extension Enumeration.Case: API {}
3131

3232
extension Function: API {
3333
public var name: String {
34-
return "\(identifier)(\(signature.input.map { ($0.firstName ?? "_") + ":" }.joined()))"
34+
if self.isOperator {
35+
return identifier
36+
} else {
37+
return "\(identifier)(\(signature.input.map { ($0.firstName ?? "_") + ":" }.joined()))"
38+
}
3539
}
3640
}
3741

Sources/SwiftDoc/Extensions/SwiftSemantics+Extensions.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,18 @@ extension Structure: Type {}
2727
extension Unknown: Type {
2828
public var inheritance: [String] { return [] }
2929
}
30+
31+
// MARK: -
32+
33+
extension Operator.Kind: Comparable {
34+
public static func < (lhs: Operator.Kind, rhs: Operator.Kind) -> Bool {
35+
switch (lhs, rhs) {
36+
case (_, .infix):
37+
return true
38+
case (.postfix, _):
39+
return true
40+
default:
41+
return false
42+
}
43+
}
44+
}

Sources/SwiftDoc/Helpers.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public func route(for symbol: Symbol) -> String {
66

77
public func route(for name: CustomStringConvertible) -> String {
88
return name.description.replacingOccurrences(of: ".", with: "_")
9+
.replacingOccurrences(of: " ", with: "-")
910
}
1011

1112
public func path(for symbol: Symbol, with baseURL: String) -> String {

Sources/SwiftDoc/Interface.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,17 @@ public final class Interface {
1515

1616
self.symbolsGroupedByIdentifier = symbolsGroupedByIdentifier
1717
self.symbolsGroupedByQualifiedName = symbolsGroupedByQualifiedName
18-
self.topLevelSymbols = symbols.filter { $0.api is Type || $0.id.context.isEmpty }
18+
self.topLevelSymbols = symbols.filter { symbol in
19+
if symbol.api is Type || symbol.api is Operator {
20+
return true
21+
}
22+
23+
if let function = symbol.api as? Function, function.isOperator {
24+
return false
25+
}
26+
27+
return symbol.id.pathComponents.isEmpty
28+
}
1929

2030
self.relationships = {
2131
let extensionsByExtendedType: [String: [Extension]] = Dictionary(grouping: symbols.flatMap { $0.context.compactMap { $0 as? Extension } }, by: { $0.extendedType })
@@ -90,6 +100,20 @@ public final class Interface {
90100
return Array(relationships)
91101
}()
92102

103+
self.functionsByOperator = {
104+
var functionsByOperator: [Symbol: Set<Symbol>] = [:]
105+
106+
let functionsGroupedByName = Dictionary(grouping: symbols.filter { $0.api is Function},
107+
by: { $0.api.name })
108+
109+
for `operator` in symbols.filter({ $0.api is Operator }) {
110+
let functions = functionsGroupedByName[`operator`.name] ?? []
111+
functionsByOperator[`operator`] = Set(functions)
112+
}
113+
114+
return functionsByOperator
115+
}()
116+
93117
self.relationshipsBySubject = Dictionary(grouping: relationships, by: { $0.subject.id })
94118
self.relationshipsByObject = Dictionary(grouping: relationships, by: { $0.object.id })
95119
}
@@ -99,6 +123,7 @@ public final class Interface {
99123
public let symbolsGroupedByIdentifier: [Symbol.ID: [Symbol]]
100124
public let symbolsGroupedByQualifiedName: [String: [Symbol]]
101125
public let topLevelSymbols: [Symbol]
126+
public var functionsByOperator: [Symbol: Set<Symbol>]
102127
public var baseClasses: [Symbol] {
103128
symbols.filter { $0.api is Class && typesInherited(by: $0).isEmpty }
104129
}

Sources/SwiftDoc/SourceFile.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ public struct SourceFile: Hashable, Codable {
168168
return .skipChildren
169169
}
170170

171+
override func visit(_ node: OperatorDeclSyntax) -> SyntaxVisitorContinueKind {
172+
push(symbol(Operator.self, node))
173+
return .skipChildren
174+
}
175+
171176
override func visit(_ node: PrecedenceGroupDeclSyntax) -> SyntaxVisitorContinueKind {
172177
push(symbol(PrecedenceGroup.self, node))
173178
return .skipChildren

Sources/SwiftDoc/Symbol.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,24 @@ public final class Symbol {
3434
return api.name
3535
}
3636

37+
public var kind: String {
38+
switch api {
39+
case let function as Function where function.isOperator:
40+
return "Operator"
41+
default:
42+
return String(describing: type(of: api))
43+
}
44+
}
45+
3746
public var isPublic: Bool {
3847
if api is Unknown {
3948
return true
4049
}
4150

51+
if api is Operator {
52+
return true
53+
}
54+
4255
if api.modifiers.contains(where: { $0.name == "public" || $0.name == "open" }) {
4356
return true
4457
}

Sources/swift-doc/Extensions/StringProtocol+Extensions.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import Foundation
2+
13
extension StringProtocol {
24
func indented(by spaces: Int = 2) -> String {
35
return String(repeating: " ", count: spaces) + self
@@ -20,4 +22,30 @@ extension StringProtocol {
2022
// See: https://docs.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax#using-emoji
2123
return self.replacingOccurrences(of: ":", with: ":\u{200B}")
2224
}
25+
26+
var escaped: String {
27+
#if os(macOS)
28+
return (CFXMLCreateStringByEscapingEntities(nil, String(self) as NSString, nil)! as NSString) as String
29+
#else
30+
return [
31+
("&", "&amp;"),
32+
("<", "&lt;"),
33+
(">", "&gt;"),
34+
("'", "&apos;"),
35+
("\"", "&quot;"),
36+
].reduce(String(self)) { (string, element) in
37+
string.replacingOccurrences(of: element.0, with: element.1)
38+
}
39+
#endif
40+
}
41+
42+
func escapingOccurrences<Target>(of target: Target, options: String.CompareOptions = [], range searchRange: Range<String.Index>? = nil) -> String where Target : StringProtocol {
43+
return replacingOccurrences(of: target, with: target.escaped, options: options, range: searchRange)
44+
}
45+
46+
func escapingOccurrences<Target>(of targets: [Target], options: String.CompareOptions = []) -> String where Target : StringProtocol {
47+
return targets.reduce(into: String(self)) { (result, target) in
48+
result = result.escapingOccurrences(of: target, options: options)
49+
}
50+
}
2351
}

Sources/swift-doc/Subcommands/Generate.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ extension SwiftDoc {
7575
pages[route(for: symbol)] = TypePage(module: module, symbol: symbol, baseURL: baseURL, includingChildren: symbolFilter)
7676
case let `typealias` as Typealias:
7777
pages[route(for: `typealias`.name)] = TypealiasPage(module: module, symbol: symbol, baseURL: baseURL)
78+
case is Operator:
79+
pages[route(for: symbol)] = OperatorPage(module: module, symbol: symbol, baseURL: baseURL)
7880
case let function as Function where !function.isOperator:
7981
globals[function.name, default: []] += [symbol]
8082
case let variable as Variable:

Sources/swift-doc/Supporting Types/Components/Members.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ struct Members: Component {
1616
var cases: [Symbol]
1717
var properties: [Symbol]
1818
var methods: [Symbol]
19+
let operatorImplementations: [Symbol]
1920
var genericallyConstrainedMembers: [[GenericRequirement] : [Symbol]]
2021
let defaultImplementations: [Symbol]
2122

@@ -32,7 +33,8 @@ struct Members: Component {
3233
self.initializers = members.filter { $0.api is Initializer }
3334
self.cases = members.filter { $0.api is Enumeration.Case }
3435
self.properties = members.filter { $0.api is Variable }
35-
self.methods = members.filter { $0.api is Function }
36+
self.methods = members.filter { ($0.api as? Function)?.isOperator == false }
37+
self.operatorImplementations = members.filter { ($0.api as? Function)?.isOperator == true }
3638
self.genericallyConstrainedMembers = Dictionary(grouping: members) { $0.`extension`?.genericRequirements ?? [] }.filter { !$0.key.isEmpty }
3739
self.defaultImplementations = module.interface.defaultImplementations(of: symbol).filter(symbolFilter)
3840
}
@@ -44,6 +46,7 @@ struct Members: Component {
4446
("Enumeration Cases", cases),
4547
("Properties", properties),
4648
("Methods", methods),
49+
("Operators", operatorImplementations),
4750
("Default Implementations", defaultImplementations),
4851
].filter { !$0.members.isEmpty }
4952
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import CommonMarkBuilder
2+
import SwiftDoc
3+
import SwiftMarkup
4+
import SwiftSemantics
5+
import HypertextLiteral
6+
7+
struct OperatorImplementations: Component {
8+
var symbol: Symbol
9+
var module: Module
10+
let baseURL: String
11+
12+
var implementations: [Symbol]
13+
14+
init(of symbol: Symbol, in module: Module, baseURL: String, implementations: [Symbol]) {
15+
self.symbol = symbol
16+
self.module = module
17+
self.baseURL = baseURL
18+
self.implementations = implementations
19+
}
20+
21+
22+
// MARK: - Component
23+
24+
var fragment: Fragment {
25+
guard !implementations.isEmpty else { return Fragment { "" } }
26+
27+
return Fragment {
28+
ForEach(in: implementations) { implementation -> BlockConvertible in
29+
Section {
30+
Heading { implementation.name }
31+
32+
Documentation(for: implementation, in: module, baseURL: baseURL)
33+
}
34+
}
35+
}
36+
}
37+
38+
var html: HypertextLiteral.HTML {
39+
let sections = implementations.compactMap { implementation -> HypertextLiteral.HTML? in
40+
guard let `operator` = symbol.api as? Operator,
41+
let function = implementation.api as? Function
42+
else { return nil }
43+
44+
let heading: String
45+
switch `operator`.kind {
46+
case .infix:
47+
guard function.signature.input.count == 2,
48+
let lhs = function.signature.input.first,
49+
let rhs = function.signature.input.last
50+
else {
51+
return nil
52+
}
53+
54+
heading = [lhs.type, function.name, rhs.type].compactMap { $0 }.joined(separator: " ")
55+
case .prefix:
56+
guard function.signature.input.count == 2,
57+
let operand = function.signature.input.first
58+
else {
59+
return nil
60+
}
61+
heading = [function.name, operand.type].compactMap { $0 }.joined(separator: " ")
62+
63+
case .postfix:
64+
guard function.signature.input.count == 2,
65+
let operand = function.signature.input.first
66+
else {
67+
return nil
68+
}
69+
heading = [operand.type, function.name].compactMap { $0 }.joined(separator: " ")
70+
}
71+
72+
return #"""
73+
<div role="article" class="function" id=\#(implementation.id.description.lowercased().replacingOccurrences(of: " ", with: "-"))>
74+
<h3>
75+
\#(heading)
76+
\#(unsafeUnescaped: function.genericWhereClause.map({ #"<small>\#($0.escaped)</small>"# }) ?? "")
77+
</h3>
78+
\#(Documentation(for: implementation, in: module, baseURL: baseURL).html)
79+
</div>
80+
"""#
81+
}
82+
83+
guard !sections.isEmpty else { return "" }
84+
85+
return #"""
86+
<section id="implementations">
87+
<h2>Implementations</h2>
88+
\#(sections)
89+
</section>
90+
"""#
91+
}
92+
}
93+
94+
fileprivate extension Function {
95+
var genericWhereClause: String? {
96+
guard !genericRequirements.isEmpty else { return nil }
97+
return "where \(genericRequirements.map { $0.description }.joined(separator: ", "))"
98+
}
99+
}

0 commit comments

Comments
 (0)