Skip to content

Commit 39bcd8a

Browse files
committed
Added support for exports and for docc archives, and return both additions and removals.
1 parent 098dc89 commit 39bcd8a

File tree

3 files changed

+267
-118
lines changed

3 files changed

+267
-118
lines changed

Sources/SwiftDocCUtilities/Action/Actions/Init/CatalogTemplateKind.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,25 @@ extension CatalogTemplateKind {
8787
"""
8888
]
8989
}
90+
91+
/// Content of the 'changeLog' template
92+
static func changeLogTemplateFiles(_ frameworkName: String) -> [String: String] {
93+
[
94+
"\(frameworkName)_ChangeLog.md": """
95+
# \(frameworkName)
96+
97+
<!--- Metadata configuration to make appear this documentation page as a top-level page -->
98+
99+
@Metadata {
100+
@TechnologyRoot
101+
}
102+
103+
Add a single sentence or sentence fragment, which DocC uses as the page’s abstract or summary.
104+
105+
## Overview
106+
107+
Add one or more paragraphs that introduce your content overview.
108+
"""
109+
]
110+
}
90111
}
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import ArgumentParser
12+
import Foundation
13+
import SwiftDocC
14+
15+
extension Docc.ProcessArchive {
16+
17+
struct DiffDocCArchive: ParsableCommand {
18+
19+
// MARK: - Content and Configuration
20+
21+
/// Command line configuration.
22+
static var configuration = CommandConfiguration(
23+
commandName: "diff-docc-archive",
24+
abstract: "Produce a list of symbols added in the newer DocC Archive that did not exist in the initial DocC Archive.",
25+
shouldDisplay: true)
26+
27+
/// Content of the 'changeLog' template.
28+
static func changeLogTemplateFileContent(
29+
frameworkName: String,
30+
initialDocCArchiveName: String,
31+
newerDocCArchiveName: String,
32+
additionLinks: String,
33+
removalLinks: String
34+
) -> [String : String] {
35+
[
36+
"\(frameworkName.localizedCapitalized)_ChangeLog.md": """
37+
# \(frameworkName.localizedCapitalized) Updates
38+
39+
@Metadata { @PageColor(yellow) }
40+
41+
Learn about important changes to \(frameworkName.localizedCapitalized).
42+
43+
## Overview
44+
45+
Browse notable changes in \(frameworkName.localizedCapitalized).
46+
47+
## Version: Diff between \(initialDocCArchiveName) and \(newerDocCArchiveName)
48+
49+
50+
### Change Log
51+
52+
#### Additions
53+
_New symbols added in \(newerDocCArchiveName) that did not previously exist in \(initialDocCArchiveName)._
54+
55+
\(additionLinks)
56+
57+
58+
#### Removals
59+
_Old symbols that existed in \(initialDocCArchiveName) that no longer exist in \(newerDocCArchiveName)._
60+
61+
\(removalLinks)
62+
63+
"""
64+
]
65+
}
66+
67+
// MARK: - Command Line Options & Arguments
68+
69+
@Argument(
70+
help: ArgumentHelp(
71+
"The name of the initial DocC Archive to be compared.",
72+
valueName: "initialDocCArchiveName"))
73+
var initialDocCArchiveName: String
74+
75+
@Argument(
76+
help: ArgumentHelp(
77+
"The path to the initial DocC Archive to be compared.",
78+
valueName: "initialDocCArchive"),
79+
transform: URL.init(fileURLWithPath:))
80+
var initialDocCArchivePath: URL
81+
82+
@Argument(
83+
help: ArgumentHelp(
84+
"The name of the newer DocC Archive to be compared.",
85+
valueName: "newerDocCArchiveName"))
86+
var newerDocCArchiveName: String
87+
88+
@Argument(
89+
help: ArgumentHelp(
90+
"The path to the newer DocC Archive to be compared.",
91+
valueName: "newerDocCArchive"),
92+
transform: URL.init(fileURLWithPath:))
93+
var newerDocCArchivePath: URL
94+
95+
// MARK: - Execution
96+
97+
public mutating func run() throws {
98+
let initialDocCArchiveAPIs: [URL] = try findAllSymbolLinks(initialPath: initialDocCArchivePath)
99+
let newDocCArchiveAPIs: [URL] = try findAllSymbolLinks(initialPath: newerDocCArchivePath)
100+
101+
print("\nInitial DocC Archive: ")
102+
printAllSymbols(symbols: initialDocCArchiveAPIs)
103+
104+
print("\nNew DocC Archive: ")
105+
printAllSymbols(symbols: newDocCArchiveAPIs)
106+
107+
let initialSet = Set(initialDocCArchiveAPIs.map { $0 })
108+
let newSet = Set(newDocCArchiveAPIs.map { $0 })
109+
110+
// Compute additions and removals to both sets
111+
let additionsToNewSet = newSet.subtracting(initialSet)
112+
let removedFromOldSet = initialSet.subtracting(newSet)
113+
114+
print("\nAdditions to New DocC Archive:")
115+
printAllSymbols(symbols: Array(additionsToNewSet))
116+
print("\nRemovals from Initial DocC Archive:")
117+
printAllSymbols(symbols: Array(removedFromOldSet))
118+
119+
// Map identifier urls in differences to external urls
120+
let additionsExternalURLs = Set(additionsToNewSet.map { findExternalLink(identifierURL: $0) })
121+
let removalsExternalURLs = Set(removedFromOldSet.map { findExternalLink(identifierURL: $0) })
122+
123+
// The framework name is the path component after "/documentation/".
124+
var frameworkName: String = "No_Framework_Name"
125+
var potentialFrameworkName = try findFrameworkName(initialPath: initialDocCArchivePath)
126+
if potentialFrameworkName == nil {
127+
potentialFrameworkName = try findFrameworkName(initialPath: newerDocCArchivePath)
128+
}
129+
130+
if potentialFrameworkName != nil {
131+
frameworkName = potentialFrameworkName ?? "No_Framework_Name"
132+
}
133+
134+
var additionLinks: String = ""
135+
for addition in additionsExternalURLs {
136+
additionLinks.append("\n- <\(addition)>")
137+
}
138+
139+
var removalLinks: String = ""
140+
for removal in removalsExternalURLs {
141+
removalLinks.append("\n- <\(removal)>")
142+
}
143+
144+
// Create markdown file with changes in the newer DocC Archive that do not exist in the initial DocC Archive.
145+
for fileNameAndContent in Docc.ProcessArchive.DiffDocCArchive.changeLogTemplateFileContent(frameworkName: frameworkName, initialDocCArchiveName: initialDocCArchiveName, newerDocCArchiveName: newerDocCArchiveName, additionLinks: additionLinks, removalLinks: removalLinks) {
146+
let fileName = fileNameAndContent.key
147+
let content = fileNameAndContent.value
148+
try FileManager.default.createFile(at: initialDocCArchivePath.deletingLastPathComponent().appendingPathComponent(fileName), contents: Data(content.utf8))
149+
}
150+
}
151+
152+
/// Pretty print all symbols' url identifiers into a pretty format, with a new line between each symbol.
153+
func printAllSymbols(symbols: [URL]) {
154+
for symbol in symbols {
155+
print(symbol)
156+
}
157+
}
158+
159+
/// The framework name is the path component after "/documentation/".
160+
func findFrameworkName(initialPath: URL) throws -> String? {
161+
guard let enumerator = FileManager.default.enumerator(
162+
at: initialPath,
163+
includingPropertiesForKeys: [],
164+
options: .skipsHiddenFiles,
165+
errorHandler: nil
166+
) else {
167+
return nil
168+
}
169+
170+
var frameworkName: String?
171+
for case let filePath as URL in enumerator {
172+
let pathComponents = filePath.pathComponents
173+
var isFrameworkName = false
174+
for pathComponent in pathComponents {
175+
if isFrameworkName {
176+
frameworkName = pathComponent
177+
return frameworkName
178+
}
179+
180+
if pathComponent == "documentation" {
181+
isFrameworkName = true
182+
}
183+
}
184+
}
185+
186+
return frameworkName
187+
}
188+
189+
/// Given the identifier url, cut off everything preceding /documentation/ and append this resulting string to doc:
190+
func findExternalLink(identifierURL: URL) -> String {
191+
var resultantURL = identifierURL.absoluteString
192+
var shouldAppend = false
193+
for pathComponent in identifierURL.pathComponents {
194+
if pathComponent == "documentation" {
195+
resultantURL = "doc:"
196+
shouldAppend = true
197+
}
198+
if shouldAppend {
199+
resultantURL.append(pathComponent + "/")
200+
}
201+
}
202+
return resultantURL
203+
}
204+
205+
/// Given a URL, return each of the symbols by their unique identifying links
206+
func findAllSymbolLinks(initialPath: URL) throws -> [URL] {
207+
guard let enumerator = FileManager.default.enumerator(
208+
at: initialPath,
209+
includingPropertiesForKeys: [],
210+
options: .skipsHiddenFiles,
211+
errorHandler: nil
212+
) else {
213+
return []
214+
}
215+
216+
var returnSymbolLinks: [URL] = []
217+
for case let filePath as URL in enumerator {
218+
if filePath.lastPathComponent.hasSuffix(".json") {
219+
let symbolLink = try findSymbolLink(symbolPath: filePath)
220+
if symbolLink != nil {
221+
returnSymbolLinks.append(symbolLink!)
222+
}
223+
}
224+
}
225+
226+
return returnSymbolLinks
227+
}
228+
229+
func findSymbolLink(symbolPath: URL) throws -> URL? {
230+
struct ContainerWithTopicReferenceIdentifier: Codable {
231+
var identifier: ResolvedTopicReference
232+
}
233+
234+
let renderJSONData = try Data(contentsOf: symbolPath)
235+
let decoder = RenderJSONDecoder.makeDecoder()
236+
237+
do {
238+
let identifier = try decoder.decode(ContainerWithTopicReferenceIdentifier.self, from: renderJSONData).identifier
239+
return identifier.url
240+
} catch {
241+
return nil
242+
}
243+
}
244+
245+
}
246+
}

Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/File.swift

Lines changed: 0 additions & 118 deletions
This file was deleted.

0 commit comments

Comments
 (0)