File: JSONEncodingRenderNodeWriter.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (118 lines) | stat: -rw-r--r-- 5,521 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/*
 This source file is part of the Swift.org open source project

 Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
 Licensed under Apache License v2.0 with Runtime Library Exception

 See https://swift.org/LICENSE.txt for license information
 See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation
import SwiftDocC

/// An object that writes render nodes, as JSON files, into a target folder.
///
/// The render node writer writes the JSON files into a hierarchy of folders and subfolders based on the relative URL for each node.
class JSONEncodingRenderNodeWriter {
    private let renderNodeURLGenerator: NodeURLGenerator
    private let targetFolder: URL
    private let transformForStaticHostingIndexHTML: URL?
    private let fileManager: FileManagerProtocol
    private let renderReferenceCache = RenderReferenceCache([:])
    
    /// Creates a writer object that write render node JSON into a given folder.
    ///
    /// - Parameters:
    ///   - targetFolder: The folder to which the writer object writes the files.
    ///   - fileManager: The file manager with which the writer object writes data to files.
    init(targetFolder: URL, fileManager: FileManagerProtocol, transformForStaticHostingIndexHTML: URL?) {
        self.renderNodeURLGenerator = NodeURLGenerator(
            baseURL: targetFolder.appendingPathComponent("data", isDirectory: true)
        )
        self.targetFolder = targetFolder
        self.transformForStaticHostingIndexHTML = transformForStaticHostingIndexHTML
        self.fileManager = fileManager
    }
    
    // The already created directories on disk
    let directoryIndex = Synchronized(Set<URL>())
    
    /// Writes a render node to a JSON file at a location based on the node's relative URL.
    ///
    /// If the target path to the JSON file includes intermediate folders that don't exist, the writer object will ask the file manager, with which it was created, to
    /// create those intermediate folders before writing the JSON file.
    ///
    /// - Parameters:
    ///   - renderNode: The node which the writer object writes to a JSON file.
    ///   - encoder: The encoder to serialize the render node with.
    func write(_ renderNode: RenderNode, encoder: JSONEncoder) throws {
        let fileSafePath = NodeURLGenerator.fileSafeReferencePath(
            renderNode.identifier,
            lowercased: true
        )
        
        // The path on disk to write the render node JSON file at.
        let renderNodeTargetFileURL = renderNodeURLGenerator
            .urlForReference(
                renderNode.identifier,
                fileSafePath: fileSafePath
            )
            .appendingPathExtension("json")
        
        let renderNodeTargetFolderURL = renderNodeTargetFileURL.deletingLastPathComponent()
        
        // On Linux sometimes it takes a moment for the directory to be created and that leads to
        // errors when trying to write files concurrently in the same target location.
        // We keep an index in `directoryIndex` and create new sub-directories as needed.
        // When the symbol's directory already exists no code is executed during the lock below
        // besides the set lookup.
        try directoryIndex.sync { directoryIndex in
            let (insertedRenderNodeTargetFolderURL, _) = directoryIndex.insert(renderNodeTargetFolderURL)
            if insertedRenderNodeTargetFolderURL {
                try fileManager.createDirectory(
                    at: renderNodeTargetFolderURL,
                    withIntermediateDirectories: true,
                    attributes: nil
                )
            }
        }
        
        let data = try renderNode.encodeToJSON(with: encoder, renderReferenceCache: renderReferenceCache)
        try fileManager.createFile(at: renderNodeTargetFileURL, contents: data, options: nil)
        
        guard let indexHTML = transformForStaticHostingIndexHTML else {
            return
        }
        
        let htmlTargetFolderURL = targetFolder.appendingPathComponent(
            fileSafePath,
            isDirectory: true
        )
        let htmlTargetFileURL = htmlTargetFolderURL.appendingPathComponent(
            HTMLTemplate.indexFileName.rawValue,
            isDirectory: false
        )
        
        // Note that it doesn't make sense to use the above-described `directoryIndex` for this use
        // case since we expect every 'index.html' file to require the creation of
        // its own unique parent directory.
        try fileManager.createDirectory(
            at: htmlTargetFolderURL,
            withIntermediateDirectories: true,
            attributes: nil
        )
        
        do {
            try fileManager.copyItem(at: indexHTML, to: htmlTargetFileURL)
        } catch let error as NSError where error.code == NSFileWriteFileExistsError {
            // We already have an 'index.html' file at this path. This could be because
            // we're writing to an output directory that already contains built documentation
            // or because we we're given bad input such that multiple documentation pages
            // have the same path on the filesystem. Either way, we don't want this to error out
            // so just remove the destination item and try the copy operation again.
            try fileManager.removeItem(at: htmlTargetFileURL)
            try fileManager.copyItem(at: indexHTML, to: htmlTargetFileURL)
        }
    }
}