File: NavigatorIndex.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 (1349 lines) | stat: -rw-r--r-- 66,640 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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
/*
 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 Crypto

/// A protocol to provide data to be indexed.
public protocol RenderNodeProvider {
    /// Get an instance of `RenderNode` to be processed by the index.
    /// - Note: Returning `nil` will end the indexing process.
    func getRenderNode() -> RenderNode?
    
    /// Returns an array of `Problem` indicating which problems the `Provider` encountered.
    func getProblems() -> [Problem]
}



/**
 A `NavigatorIndex` contains all the necessary information to display the data inside a navigator.
 The data ranges from the tree to the necessary pieces of information to filter the content and perform actions in a fast way.
 A navigator index is created per bundle and needs a bundle identifier to correctly work. Anonymous bundles are allowed, but they limit
 the functionalities of the index.
 
 A `NavigatorIndex` is composed by two main components:
    - A navigator tree reflecting the content curation
    - An availability index storing the information about for which platform and SDK a given symbol is available and they map USR to document's path
 
 The two mentioned components are generated by using a `NavigatorIndex.Builder` instance, which indexes the content accordingly to the desired configuration.
 A `NavigatorIndex` can be stored on disk to be later loaded. Loading an index can be performed in a single operation (synchronous) or asynchronously.
 This option is extremely useful in case an application needs to load a very large amount of data while updating the UI to let the user navigate the loaded content,
 while the remaining is loaded in a background thread and presented later in time.
 
 There are few important pieces information a `NavigatorIndex` requires to properly work:
    - A bundle identifier
    - A valid LMDB database for storing availability information
    - A valid navigator tree
 
 Building an index with one of the mentioned components is not supported.
 */
public class NavigatorIndex {
    
    /// A string indicating an unknown bundle identifier.
    public static let UnknownBundleIdentifier = ""
    
    /// The key used to store the name of the bundle inside the database.
    public static let bundleKey = "bundleIdentifier"
    
    /// The key used to store the name of path hasher inside the database.
    public static let pathHasherKey = "pathHasher"
    
    /// The key used to store the number of indexed items.
    public static let itemsIndexKey = "itemsIndex"
    
    /// A specific error to describe issues when processing a `NavigatorIndex`.
    public enum Error: Swift.Error, DescribedError {
        
        /// Missing bundle identifier.
        case missingBundleIdentifier
        
        @available(*, deprecated, renamed: "missingBundleIdentifier", message: "Use 'missingBundleIdentifier' instead. This deprecated API will be removed after 5.11 is released")
        case missingBundleIndentifier
        
        /// A RenderNode has no title and won't be indexed.
        case missingTitle(description: String)
        
        /// The navigator index has not been initialized.
        case navigatorIndexIsNil
        
        public var errorDescription: String {
            switch self {
            case .missingBundleIdentifier, .missingBundleIndentifier:
                return "A navigator index requires a bundle identifier, which is missing."
            case .missingTitle:
                return "The page has no valid title available."
            case .navigatorIndexIsNil:
                return "The NavigatorIndex is Nil and can't be processed."
            }
        }
    }
    
    /// The url of the index.
    public let url: URL
    
    /// The LMDB environment.
    var environment: LMDB.Environment?
    
    /// The path hasher.
    ///
    /// The hasher is used to make paths, like "/documentation/mykit/myclass/mysymbol", shorter for storage inside the LMDB database,
    /// avoiding storing very long strings, multiple times that will cause an index to use unnecessary space on disk.
    var pathHasher: PathHasher = .md5
    
    /// The index database in LMDB.
    private var database: LMDB.Database?
    
    /// The information dedicated database to store data such as the bundle identifier or the number of items indexed.
    private var information: LMDB.Database?
    
    /// The availability dedicated database.
    private var availability: LMDB.Database?
    
    /// The navigator tree.
    public let navigatorTree: NavigatorTree
    
    /// The availability index.
    public let availabilityIndex: AvailabilityIndex
    
    /// Bundle Identifier.
    public var bundleIdentifier: String = NavigatorIndex.UnknownBundleIdentifier
    
    /// A presentation identifier used to disambiguate content in presentation contexts.
    public let presentationIdentifier: String?
    
    /// The available languages in the index.
    public lazy var languages: [String] = {
        return self.availabilityIndex.interfaceLanguages.map{ $0.name }
    }()
    
    /// The mapping from a single language mask to its interface language type.
    public lazy var languageMaskToLanguage: [UInt8: InterfaceLanguage] = {
        var value = [UInt8: InterfaceLanguage]()
        for language in Array(availabilityIndex.interfaceLanguages) {
            value[language.mask] = language
        }
        return value
    }()
    
    /// The number of item indexed.
    public lazy var count: Int = {
        return self.information?.get(type: Int.self, forKey: NavigatorIndex.itemsIndexKey) ?? 0
    }()
    
    /**
     Initializes a `NavigatorIndex` from a given path on disk.
     
     Most uses should be made using just the url parameter:
     ```swift
    let indexFilePath = URL(string: "file://path/to/index/on/disk")
    let index = NavigatorIndex.readNavigatorIndex(url: indexFilePath)
     ```
     
     - Parameters:
        - url: The URL pointing to the path from which the index should be read.
        - bundleIdentifier: The name of the bundle the index is referring to.
        - readNavigatorTree: Indicates if the init the navigator tree should be read from the disk now or later, if false, then `readNavigatorTree` needs to be called later. Default: `true`.
        - presentationIdentifier: Indicates if the index has an identifier useful for presentation contexts.
        - onNodeRead: An action to perform after reading a node. This allows clients to perform arbitrary actions on the node while it is being read from disk. This is useful for clients wanting to attach data to ``NavigatorTree/Node/attributes``.
     
     - Throws: A `NavigatorIndex.Error` describing the nature of the problem.
     
     - Note: The index powered by LMDB opens in `readOnly` mode to avoid performing a filesystem lock which fails without writing permissions. As this initializer opens a built index, write permission is not expected.
     */
    public static func readNavigatorIndex(
        url: URL,
        bundleIdentifier: String? = nil,
        readNavigatorTree: Bool = true,
        presentationIdentifier: String? = nil,
        onNodeRead: ((NavigatorTree.Node) -> Void)? = nil
    ) throws -> NavigatorIndex {
        // To avoid performing a filesystem lock which might fail without write permission, we pass `.readOnly` and `.noLock` to open the index.
        let environment = try LMDB.Environment(path: url.path, flags: [.readOnly, .noLock], maxDBs: 4, mapSize: 100 * 1024 * 1024) // mapSize = 100MB
        let database = try environment.openDatabase(named: "index", flags: [])
        let availability = try environment.openDatabase(named: "availability", flags: [])
        
        let information = try environment.openDatabase(named: "information", flags: [])
        
        let data = try Data(contentsOf: url.appendingPathComponent("availability.index", isDirectory: false))
        let plistDecoder = PropertyListDecoder()
        let availabilityIndex = try plistDecoder.decode(AvailabilityIndex.self, from: data)
        let bundleIdentifier = bundleIdentifier ?? information.get(type: String.self, forKey: NavigatorIndex.bundleKey) ?? NavigatorIndex.UnknownBundleIdentifier
        
        guard bundleIdentifier != NavigatorIndex.UnknownBundleIdentifier else {
            throw Error.missingBundleIdentifier
        }
        
        // Use `.fnv1` by default if no path hasher is set for compatibility reasons.
        let pathHasher = PathHasher(rawValue: information.get(type: String.self, forKey: NavigatorIndex.pathHasherKey) ?? "") ?? .fnv1
        
        let navigatorTree: NavigatorTree
        if readNavigatorTree {
            navigatorTree = try NavigatorTree.read(
                from: url.appendingPathComponent("navigator.index", isDirectory: false),
                bundleIdentifier: bundleIdentifier,
                interfaceLanguages: availabilityIndex.interfaceLanguages,
                presentationIdentifier: presentationIdentifier,
                onNodeRead: onNodeRead)
        } else {
            navigatorTree = NavigatorTree()
        }
        
        return NavigatorIndex(
            url: url,
            presentationIdentifier: presentationIdentifier,
            bundleIdentifier: bundleIdentifier,
            environment: environment,
            database: database,
            availability: availability,
            information: information,
            availabilityIndex: availabilityIndex,
            pathHasher: pathHasher,
            navigatorTree: navigatorTree
        )
    }
    
    fileprivate init(
        url: URL,
        presentationIdentifier: String?,
        bundleIdentifier: String,
        environment: LMDB.Environment,
        database: LMDB.Database,
        availability: LMDB.Database,
        information: LMDB.Database,
        availabilityIndex: AvailabilityIndex,
        pathHasher: PathHasher,
        navigatorTree: NavigatorTree
    ) {
        self.url = url
        self.presentationIdentifier = presentationIdentifier
        self.bundleIdentifier = bundleIdentifier
        self.environment = environment
        self.database = database
        self.availability = availability
        self.information = information
        self.availabilityIndex = availabilityIndex
        self.pathHasher = pathHasher
        self.navigatorTree = navigatorTree
    }
    
    /**
     Initialize an `NavigatorIndex` from a given path with an empty tree.
     
     - Parameter url: The URL pointing to the path from which the index should be read.
     - Parameter bundleIdentifier: The name of the bundle the index is referring to.
     
     - Note: Don't exposed this initializer as it's used **ONLY** for building an index.
     */
    fileprivate init(withEmptyTree url: URL, bundleIdentifier: String) throws {
        self.url = url
        self.bundleIdentifier = bundleIdentifier
        self.presentationIdentifier = nil
        self.navigatorTree = NavigatorTree(root: NavigatorTree.rootNode(bundleIdentifier: bundleIdentifier))
        self.availabilityIndex = AvailabilityIndex()
        
        guard self.bundleIdentifier != NavigatorIndex.UnknownBundleIdentifier else {
            throw Error.missingBundleIdentifier
        }
    }
    
    /// Indicates the page type of a given item inside the tree.
    /// - Note: This information is stored as `UInt8` to decrease the required size to store it and make the comparison faster between types.
    public enum PageType: UInt8 {
        case root = 0
        case article = 1
        case tutorial = 2
        case section = 3
        case learn = 4
        case overview = 5
        case resources = 6
        case symbol = 7 // This indicates a generic symbol
        
        // Symbol specialization
        case framework = 10
        case `class` = 20
        case structure = 21
        case `protocol` = 22
        case enumeration = 23
        case function = 24
        case `extension` = 25
        case localVariable = 26
        case globalVariable = 27
        case typeAlias = 28
        case associatedType = 29
        case `operator` = 30
        case macro = 31
        case union = 32
        case enumerationCase = 33
        case initializer = 34
        case instanceMethod = 35
        case instanceProperty = 36
        case instanceVariable = 37
        case `subscript` = 38
        case typeMethod =  39
        case typeProperty = 40
    
        // Data entities:
        case buildSetting = 42
        case propertyListKey = 43

        // Other:
        case sampleCode = 44

        // REST entities:
        case httpRequest = 45
        case dictionarySymbol = 46

        // A property list key.
        case propertyListKeyReference = 47

        // C++ symbols
        case namespace = 48
        
        // Special items
        case languageGroup = 127
        case container = 254
        case groupMarker = 255 // UInt8.max
                
        /// Initialize a page type from a `role` and a `symbolKind` returning the Symbol type.
        init(symbolKind: String) {
            // Prioritize the SymbolKind first
            switch symbolKind.lowercased() {
            case "module": self = .framework
            case "cl", "class": self = .class
            case "struct", "tag": self = .structure
            case "intf", "protocol": self = .protocol
            case "enum": self = .enumeration
            case "func", "function": self = .function
            case "extension": self = .extension
            case "data", "var": self = .globalVariable
            case "tdef", "typealias": self = .typeAlias
            case "intftdef", "associatedtype": self = .associatedType
            case "op", "opfunc", "intfopfunc", "func.op": self = .operator
            case "macro": self = .macro
            case "union": self = .union
            case "enumelt", "econst", "enum.case", "case": self = .enumerationCase
            case "enumctr", "structctr", "instctr", "intfctr", "constructor", "initializer", "init": self = .initializer
            case "enumm", "structm", "instm", "intfm", "method": self = .instanceMethod
            case "enump", "structp", "instp", "intfp", "unionp", "pseudo", "variable", "property": self = .instanceProperty
            case "enumdata", "structdata", "cldata", "clconst", "intfdata", "type.property", "typeConstant": self = .instanceVariable
            case "enumsub", "structsub", "instsub", "intfsub", "subscript": self = .subscript
            case "enumcm", "structcm", "clm", "intfcm", "type.method": self = .typeMethod
            case "httpget", "httpput", "httppost", "httppatch", "httpdelete": self = .httpRequest
            case "dict": self = .dictionarySymbol
            case "namespace": self = .namespace
            default: self = .symbol
            }
        }
        
        init(role: String) {
            switch role.lowercased() {
            case "symbol", "containersymbol": self = .symbol
            case "restrequestsymbol": self = .httpRequest
            case "dictionarysymbol": self = .dictionarySymbol
            case "pseudosymbol": self = .symbol
            case "pseudocollection": self = .framework
            case "collection": self = .framework
            case "collectiongroup": self = .symbol
            case "article": self = .article
            case "samplecode": self = .sampleCode
            default: self = .article
            }
        }

        /// Whether this page kind references a symbol.
        var isSymbolKind: Bool {
            switch self {
            case .root, .article, .tutorial, .section, .learn, .overview, .resources, .framework,
                    .buildSetting, .sampleCode, .languageGroup, .container, .groupMarker:
                return false
            case .symbol, .class, .structure, .protocol, .enumeration, .function, .extension,
                    .localVariable, .globalVariable, .typeAlias, .associatedType, .operator, .macro,
                    .union, .enumerationCase, .initializer, .instanceMethod, .instanceProperty,
                    .instanceVariable, .subscript, .typeMethod, .typeProperty, .propertyListKey,
                    .httpRequest, .dictionarySymbol, .propertyListKeyReference, .namespace:
                return true
            }
        }
    }
    
    // MARK: - Read Navigator Tree
    
    /**
    Read a tree on disk from a given path.
    The read is atomically performed, which means it reads all the content of the file from the disk and process the tree from loaded data.
    The queue is used to load the data for a given timeout period, after that, the queue is used to schedule another read after a given delay.
    This approach ensures that the used  queue doesn't stall while loading the content from the disk keeping the used queue responsive.
    
    - Parameters:
       - timeout: The amount of time we can load a batch of items from data, once the timeout time pass,
                  the reading process will reschedule asynchronously using the given queue.
       - delay: The delay to wait before schedule the next read. Default: 0.01 seconds.
       - queue: The queue to use.
       - broadcast: The callback to update get updates of the current process.
     
    - Note: Do not access the navigator tree root node or the map from identifier to node from a different thread than the one the queue is using while the read is performed,
     this may cause data inconsistencies. For that please use the broadcast callback that notifies which items have been loaded.
    */
    public func readNavigatorTree(timeout: TimeInterval, delay: TimeInterval = 0.01, queue: DispatchQueue, broadcast: NavigatorTree.BroadcastCallback?) throws {
        let indexURL = url.appendingPathComponent("navigator.index")
        try navigatorTree.read(from: indexURL, bundleIdentifier: bundleIdentifier, interfaceLanguages: availabilityIndex.interfaceLanguages, timeout: timeout, delay: delay, queue: queue, broadcast: broadcast)
    }
    
    // MARK: - Data Query
    
    /// Returns an array of availabilities based on a single id.
    public func availabilities(for id: UInt64) -> [AvailabilityIndex.Info] {
        let array = availability?.get(type: [Int].self, forKey: id)
        return array?.compactMap{ availabilityIndex.info(for: $0) } ?? []
    }
    
    /// Returns the path of a given USR if existing.
    /// - Parameters:
    ///   - usr: The full USR or a hashed USR.
    ///   - language: The interface language to look the USR for.
    ///   - hashed: A boolean indicating if the USR is hashed or not.
    /// - Returns: The path of a given USR, if available.
    public func path(for usr: String, language: InterfaceLanguage = .swift, hashed: Bool = false) -> String? {
        let usrKey = language.name + "-" + ((hashed) ? usr : ExternalIdentifier.usr(usr).hash)
        guard let nodeID = database?.get(type: UInt32.self, forKey: usrKey) else { return nil }
        return path(for: nodeID)
    }
    
    /// If available, returns the path from the numeric ID inside the navigator tree.
    public func path(for id: UInt32) -> String? {
        guard var path = database?.get(type: String.self, forKey: id) else { return nil }
        // Remove the language prefix.
        if let slashRange = path.range(of: "/") {
            path.removeSubrange(path.startIndex..<slashRange.lowerBound)
        }
        return path
    }
    
    /// If available, returns the ID of a path for the given language.
    public func id(for path: String, with interfaceLanguage: InterfaceLanguage) -> UInt32? {
        // The fullPath needs to account for the language.
        let fullPath = interfaceLanguage.name.lowercased() + path
        return database?.get(type: UInt32.self, forKey: pathHasher.hash(fullPath))
    }
}

extension ResolvedTopicReference {
    func normalizedNavigatorIndexIdentifier(
        forLanguage languageIdentifier: InterfaceLanguage.ID
    ) -> NavigatorIndex.Identifier {
        let normalizedPath = NodeURLGenerator.fileSafeReferencePath(self, lowercased: true)
        
        return NavigatorIndex.Identifier(
            bundleIdentifier: bundleIdentifier.lowercased(),
            path: "/" + normalizedPath,
            fragment: fragment,
            languageIdentifier: languageIdentifier
        )
    }
}

extension NavigatorIndex {
    /// A unique identifier for navigator index items.
    ///
    /// Used to identify relationships in the navigator index during the index build process.
    public struct Identifier: Hashable {
        let bundleIdentifier: String
        let path: String
        let fragment: String?
        let languageIdentifier: InterfaceLanguage.ID
        
        init(
            bundleIdentifier: String,
            path: String,
            fragment: String? = nil,
            languageIdentifier: InterfaceLanguage.ID
        ) {
            self.bundleIdentifier = bundleIdentifier
            self.path = path
            self.fragment = fragment
            self.languageIdentifier = languageIdentifier
        }
    }
    
    /**
     A `Builder` is a utility class to build a navigator index.
     
     The builder generates an index for content navigation, but also maps important information to filter content based on availability, symbol type, platform and some others.
     
     - Note: The builder is not thread safe and therefore, calling `index(renderNode:)` requires external synchronization in case the process is performed on different threads.
     */
    open class Builder {
        
        /// The data provider.
        public let renderNodeProvider: RenderNodeProvider?
        
        /// The output URL.
        public let outputURL: URL
        
        /// The bundle name.
        public let bundleIdentifier: String
        
        /// Indicates if the root children must be sorted by title.
        public let sortRootChildrenByName: Bool
        
        /// Indicates if the children need to be grouped by languages.
        public let groupByLanguage: Bool
        
        /// The navigator index.
        public private(set) var navigatorIndex: NavigatorIndex?
        
        /// An array holding all problems encountered during the index build.
        public private(set) var problems = [Problem]()
        
        /// The number of items processed during a build.
        public private(set) var counter = 0
        
        /// Indicates if a building process has been completed.
        public private(set) var isCompleted = false
        
        /// The map of identifier to navigation item.
        private var identifierToNode = [Identifier: NavigatorTree.Node]()
        
        /// The map of identifier to children.
        private var identifierToChildren = [Identifier: [Identifier]]()
        
        /// A temporary list of pending references that are waiting for their parent to be indexed.
        private var pendingUncuratedReferences = Set<Identifier>()
        
        /// A map with all nodes that are curated multiple times in the tree and need to be processed at the very end.
        private var multiCurated = [Identifier: NavigatorTree.Node]()
        
        /// A set with all nodes that are curated multiple times, but still have to be visited.
        private var multiCuratedUnvisited = Set<Identifier>()
        
        /// A set with all nodes that are curated.
        private var curatedIdentifiers = Set<Identifier>()
        
        /// Maps an arbitrary InterfaceLanguage string to an InterfaceLanguage.
        private var idToLanguage = [String: InterfaceLanguage]()
        
        /// Maps an arbitrary Platform name string to a Platform.Name instance.
        private var nameToPlatform = [String: Platform.Name]()
        
        // availabilityIDs and availabilityToID serve as a bidirectional map to lookup entries in the
        // availabilityIndex of the NavigatorIndex. NavigatorItem stores an availabilityID that corresponds
        // to the key of availabilityIDs. The associated value for that availabilityID corresponds
        // to a list of IDs which is used to get the availability information from the availability index.
        // availabilityToID is used to reuse the same availabilityID if two NavigatorItem have the same
        // availability information.
        
        private static let availabilityIDWithNoAvailabilities = 0
        
        /// The map of the availabilities from a single availabilityID to an array of availabilities inside the availability index.
        /// This approach gives us the opportunity to map multiple availabilities with the same entries using a single ID.
        /// Ex. An item with: iOS 13.0 and macOS 10.15 can share the same ID with other items having exactly the same availability.
        /// We use the `0` value to indicate that there are no associated availabilityIndex entries.
        private var availabilityIDs: [Int: [Int]] = [availabilityIDWithNoAvailabilities: []]
        
        /// The map of the availabilities to their ID, the opposite of `availabilityIDs`.
        /// Conversely we make sure that an empty list of IDs into the availabilityIndex maps back to the `0` value.
        private var availabilityToID: [[Int]: Int] = [[]: availabilityIDWithNoAvailabilities]
        
        /// Indicates if the path component inside the navigator item needs to be persisted or not.
        private let writePathsOnDisk: Bool
        
        /// Indicates if the page title should be used instead of the navigator title.
        private let usePageTitle: Bool
        
        
        /// Maps the icon render references in the navigator items created by this builder
        /// to their image references.
        ///
        /// Use the `NavigatorItem.icon` render reference to look up the full image reference
        /// for any custom icons used in this navigator index.
        var iconReferences = [String : ImageReference]()
        
        
        /// Create a new a builder with the given data provider and output URL.
        /// - Parameters:
        ///    - renderNodeProvider: The `RenderNode` provider to use.
        ///    - outputURL: The location where the builder will write the the built navigator index.
        ///    - bundleIdentifier: The bundle identifier of the documentation that the builder builds a navigator index for.
        ///    - sortRootChildrenByName: Configure the builder to sort root's children by name.
        ///    - groupByLanguage: Configure the builder to group the entries by language.
        ///    - writePathsOnDisk: Configure the builder to write each navigator item's path components to the location.
        ///    - usePageTitle: Configure the builder to use the "page title" instead of the "navigator title" as the title for each entry.
        public init(renderNodeProvider: RenderNodeProvider? = nil, outputURL: URL, bundleIdentifier: String, sortRootChildrenByName: Bool = false, groupByLanguage: Bool = false, writePathsOnDisk: Bool = true, usePageTitle: Bool = false) {
            self.renderNodeProvider = renderNodeProvider
            self.outputURL = outputURL
            self.bundleIdentifier = bundleIdentifier
            self.sortRootChildrenByName = sortRootChildrenByName
            self.groupByLanguage = groupByLanguage
            self.writePathsOnDisk = writePathsOnDisk
            self.usePageTitle = usePageTitle
        }
        
        /// Setup the builder to process render nodes.
        public func setup() {
            // If setup has been called already, skip.
            guard navigatorIndex == nil && isCompleted == false else { return }
            
            do {
                // The folder in which the environment, if existing, will be overwritten.
                if FileManager.default.fileExists(atPath: outputURL.path) {
                    try FileManager.default.removeItem(at: outputURL)
                }
                try FileManager.default.createDirectory(at: outputURL, withIntermediateDirectories: true, attributes: nil)
                
                navigatorIndex = try NavigatorIndex(withEmptyTree: outputURL, bundleIdentifier: bundleIdentifier)
            } catch {
                problems.append(error.problem(source: outputURL,
                                              severity: .error,
                                              summaryPrefix: "The folder couldn't be processed correctly."))
            }
            
            // Setup the default known values for Platforms and Languages
            for language in InterfaceLanguage.apple {
                idToLanguage[language.id.lowercased()] = language
            }
            
            for platformName in Platform.Name.apple {
                nameToPlatform[platformName.name.lowercased()] = platformName
            }
        }
        
        /// Index a single render `RenderNode`.
        /// - Parameter renderNode: The render node to be indexed.
        /// - Parameter ignoringLanguage: Whether language variants should be ignored when indexing this render node.
        public func index(renderNode: RenderNode, ignoringLanguage: Bool = false) throws {
            // Always index the main render node representation
            let language = try index(renderNode, traits: nil)
            
            // Additionally, for Swift want to also index the Objective-C variant, if there is any.
            guard !ignoringLanguage && language == .swift else {
                return
            }
            
            // Check if the render node has an Objective-C representation
            guard let objCVariantTrait = renderNode.variants?.flatMap(\.traits).first(where: { trait in
                switch trait {
                case .interfaceLanguage(let language):
                    return InterfaceLanguage.from(string: language) == .objc
                }
            }) else {
                return
            }
            
            // A render node is structured differently depending on if it was created by "rendering" a documentation node 
            // or if it was deserialized from a documentation archive.
            //
            // If it was created by rendering a documentation node, all variant information is stored in each individual variant collection and the variant overrides are nil.
            // If it was deserialized from a documentation archive, all variant information is stored in the variant overrides and the variant collections are empty.
            
            // Operating on the variant override is _significantly_ slower, so we only take that code path if we have to.
            // The only reason why this code path still exists is to support the `docc process-archive index` command, which creates an navigation index from an already build documentation archive.
            if let overrides = renderNode.variantOverrides, !overrides.isEmpty {
                // This code looks peculiar and very inefficient because it is.
                // I didn't write it and I really wanted to remove it, but it's the only way to support the `docc process-archive index` command for now.
                // rdar://128050800 Tracks fixing the inefficiencies with this code, to make `docc process-archive index` command as fast as indexing during a `docc convert` command.
                //
                // First, it encodes the render node, which was read from a file, back to data; because that's what the overrides applier operates on
                let encodedRenderNode = try renderNode.encodeToJSON()
                // Second, the overrides applier will decode that data into an abstract JSON representation of arrays, dictionaries, string, numbers, etc.
                // After that the overrides applier loops over all the JSON patches and applies them to the abstract JSON representation.
                // With all the patches applies, the overrides applier encodes the abstract JSON representation into data again and returns it.
                let transformedData = try RenderNodeVariantOverridesApplier().applyVariantOverrides(in: encodedRenderNode, for: [objCVariantTrait])
                // Third, this code decodes the render node from the transformed data. If you count reading the render node from the documentation archive, 
                // this is the fifth time that the same node is either encoded or decoded.
                let variantRenderNode = try RenderNode.decode(fromJSON: transformedData)
                // Finally, the decoded node is in a way flattened, so that it only contains its Objective-C content. That's why we pass `nil` instead of `[objCVariantTrait]` to this call.
                _ = try index(variantRenderNode, traits: nil)
            }
            
            // If this render node was created by rendering a documentation node, we create a "view" into its Objective-C specific data and index that.
            let objVariantView = RenderNodeVariantView(wrapped: renderNode, traits: [objCVariantTrait])
            _ = try index(objVariantView, traits: [objCVariantTrait])
        }
        
        // The private index implementation which indexes a given render node representation
        private func index(_ renderNode: any NavigatorIndexableRenderNodeRepresentation, traits: [RenderNode.Variant.Trait]?) throws -> InterfaceLanguage? {
            guard let navigatorIndex else {
                throw Error.navigatorIndexIsNil
            }
            
            // Process the language
            let interfaceLanguage = renderNode.identifier.sourceLanguage
            let interfaceLanguageID = interfaceLanguage.id.lowercased()
            
            let language: InterfaceLanguage
            if InterfaceLanguage.from(string: interfaceLanguageID) != .undefined {
                language = InterfaceLanguage.from(string: interfaceLanguageID)
            } else if let storedLanguage = idToLanguage[interfaceLanguageID] {
                language = storedLanguage
            } else {
                // It's a new language, create a new instance.
                language = InterfaceLanguage(interfaceLanguage.name, id: interfaceLanguage.id, mask: idToLanguage.count)
                idToLanguage[interfaceLanguageID] = language
            }

            let normalizedIdentifier = renderNode
                .identifier
                .normalizedNavigatorIndexIdentifier(forLanguage: language.mask)
            
            guard identifierToNode[normalizedIdentifier] == nil else {
                return nil // skip as item exists already.
            }
            
            guard let title = usePageTitle ? renderNode.metadata.title : renderNode.navigatorTitle() else {
                throw Error.missingTitle(description: "\(renderNode.identifier.absoluteString.singleQuoted) has an empty title and so can't have a usable entry in the index.")
            }
            
            // Get the identifier path
            let identifierPath = normalizedIdentifier.path
            
            // Store the language inside the availability index.
            navigatorIndex.availabilityIndex.add(language: language)
            
            // Process the availability and platform ID
            var platformID: Platform.Name.ID = 0
            var availabilityID: Int = 0
            
            if let platforms = renderNode.metadata.platforms {
                var entryIDs = [Int]()
                for availability in platforms {
                    if let name = availability.name {
                        let platformName: Platform.Name
                        if let existing = nameToPlatform[name.lowercased()] {
                            platformName = existing
                        } else { // Create a new one if non existing.
                            platformName = Platform.Name(name, id: nameToPlatform.count)
                            nameToPlatform[name.lowercased()] = platformName
                        }
                        if language != .undefined {
                            navigatorIndex.availabilityIndex.add(platform: platformName, for: language)
                        }
                        let introduced = Platform.Version(string: availability.introduced ?? "")
                        let deprecated = Platform.Version(string: availability.deprecated ?? "")
                        let info = AvailabilityIndex.Info(platformName: platformName, introduced: introduced, deprecated: deprecated)
                        
                        // Append a single availability ID
                        if let id = navigatorIndex.availabilityIndex.id(for: info, createIfMissing: true) {
                            entryIDs.append(id)
                        }
                        
                        // Add the mask to the platform ID
                        platformID += platformName.mask
                    }
                }
                
                // Sort the IDs so multiple entries with the same availabilities
                // will generate the same hash. In this way we can find them in the dictionary.
                entryIDs.sort()
                
                if let existing = availabilityToID[entryIDs] {
                    availabilityID = existing
                } else {
                    let newID = availabilityIDs.count
                    availabilityToID[entryIDs] = newID
                    availabilityIDs[newID] = entryIDs
                    availabilityID = newID
                }
            }
            
            
            if let icon = renderNode.icon,
                let iconRenderReference = renderNode.references[icon.identifier] as? ImageReference
            {
                iconReferences[icon.identifier] = iconRenderReference
            }
            
            let navigationItem = NavigatorItem(
                pageType: renderNode.navigatorPageType().rawValue,
                languageID: language.mask,
                title: title,
                platformMask: platformID,
                availabilityID: UInt64(availabilityID),
                icon: renderNode.icon
            )
            navigationItem.path = identifierPath
            
            // Index the USR for the given identifier
            if let usr = renderNode.metadata.externalID {
                navigationItem.usrIdentifier =  language.name + "-" + ExternalIdentifier.usr(usr).hash // We pair the hash and the language name
            }
            
            let navigatorNode = NavigatorTree.Node(item: navigationItem, bundleIdentifier: bundleIdentifier)
            
            // Process the children
            var children = [Identifier]()
            for (index, child) in renderNode.navigatorChildren(for: traits).enumerated() {
                let groupIdentifier: Identifier?
                
                if let title = child.name {
                    let fragment = "\(title)#\(index)".addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
                    
                    let identifier = Identifier(
                        bundleIdentifier: normalizedIdentifier.bundleIdentifier,
                        path: identifierPath,
                        fragment: fragment,
                        languageIdentifier: language.mask
                    )
                    
                    let groupItem = NavigatorItem(
                        pageType: UInt8(PageType.groupMarker.rawValue),
                        languageID: language.mask,
                        title: title,
                        platformMask: platformID,
                        availabilityID: UInt64(Self.availabilityIDWithNoAvailabilities)
                    )
                    
                    groupItem.path = identifier.path + "#" + fragment
                    
                    let navigatorGroup = NavigatorTree.Node(item: groupItem, bundleIdentifier: bundleIdentifier)
                    
                    identifierToNode[identifier] = navigatorGroup
                    children.append(identifier)
                    
                    groupIdentifier = identifier
                } else {
                    groupIdentifier = nil
                }
                
                let identifiers = child.references.map { reference in
                    return Identifier(
                        bundleIdentifier: bundleIdentifier.lowercased(),
                        path: reference.url.lowercased(),
                        languageIdentifier: language.mask
                    )
                }
                
                var nestedChildren = [Identifier]()
                for identifier in identifiers {
                    if child.referencesAreNested {
                        nestedChildren.append(identifier)
                    } else {
                        children.append(identifier)
                    }
                    
                    // If a topic has been already curated and has a valid node processed, flag it as multi-curated.
                    if curatedIdentifiers.contains(identifier) && pendingUncuratedReferences.contains(identifier) {
                        multiCurated[identifier] = identifierToNode[identifier]
                    } else if curatedIdentifiers.contains(identifier) { // In case we have no node, then keep track.
                        multiCuratedUnvisited.insert(identifier)
                    } else { // Otherwise keep track for later.
                        curatedIdentifiers.insert(identifier)
                    }
                }
                
                if let groupIdentifier, !nestedChildren.isEmpty {
                    identifierToChildren[groupIdentifier] = nestedChildren
                }
            }
            
            // Keep track of the node
            identifierToNode[normalizedIdentifier] = navigatorNode
            identifierToChildren[normalizedIdentifier] = children
            pendingUncuratedReferences.insert(normalizedIdentifier)
            
            // Track a multiple curated node
            if multiCuratedUnvisited.remove(normalizedIdentifier) != nil {
                multiCurated[normalizedIdentifier] = navigatorNode
            }
            
            // Bump the nodes counter.
            counter += 1
            
            return language
        }
        
        /// An internal struct to store data about a single navigator entry.
        struct Record {
            let nodeMapping: (UInt32, String)
            let curationMapping: (String, UInt32)
            let usrMapping: (String, UInt32)?
        }
        
        /// Finalize the process by writing the content on disk.
        ///
        /// By default this function writes out the navigator index to disk as an LMDB database
        /// but emitting a JSON representation of the index is also supported.
        ///
        /// - Parameters:
        ///   - estimatedCount: An estimate of the number of nodes in the navigator index.
        ///
        ///   - emitJSONRepresentation: Whether or not a JSON representation of the index should
        ///     be written to disk.
        ///
        ///     Defaults to `false`.
        ///
        ///   - emitLMDBRepresentation: Whether or not an LMDB representation of the index should
        ///     written to disk.
        ///
        ///     Defaults to `true`.
        public func finalize(
            estimatedCount: Int? = nil,
            emitJSONRepresentation: Bool = true,
            emitLMDBRepresentation: Bool = true
        ) {
            precondition(!isCompleted, "Finalizing an already completed index build multiple times is not possible.")
            
            guard let navigatorIndex else {
                preconditionFailure("The navigatorIndex instance has not been initialized.")
            }
            
            let root = navigatorIndex.navigatorTree.root
            root.bundleIdentifier = bundleIdentifier

            let allReferences = pendingUncuratedReferences
            
            // Assign the children to the parents, starting with multi curated nodes
            var nodesMultiCurated = multiCurated.map { ($0, $1) }
            
            while !nodesMultiCurated.isEmpty {
                // The children of the multicurated nodes. These need to be tracked so we can multicurate them as well.
                var nodesMultiCuratedChildren: [(Identifier, NavigatorTree.Node)] = []
                
                for index in 0..<nodesMultiCurated.count {
                    let (nodeID, parent) = nodesMultiCurated[index]
                    let placeholders = identifierToChildren[nodeID]!
                    for reference in placeholders {
                        if let child = identifierToNode[reference] {
                            parent.add(child: child)
                            pendingUncuratedReferences.remove(reference)
                            if !multiCurated.keys.contains(reference) && reference.fragment == nil {
                                // As the children of a multi-curated node is itself curated multiple times
                                // we need to process it as well, ignoring items with fragments as those are sections.
                                nodesMultiCuratedChildren.append((reference, child))
                                multiCurated[reference] = child
                            }
                        }
                    }
                    // Once assigned, placeholders can be removed as we use copy later.
                    identifierToChildren[nodeID]!.removeAll()
                }
                
                nodesMultiCurated = nodesMultiCuratedChildren
            }
                
            for (nodeIdentifier, placeholders) in identifierToChildren {
                for reference in placeholders {
                    let parent = identifierToNode[nodeIdentifier]!
                    if let child = identifierToNode[reference] {
                        let needsCopy = multiCurated[reference] != nil
                        parent.add(child: (needsCopy) ? child.copy() : child)
                        pendingUncuratedReferences.remove(reference)
                    }
                }
            }
            
            var languageMaskToNode = [InterfaceLanguage.ID: NavigatorTree.Node]()
            if groupByLanguage {
                for language in navigatorIndex.availabilityIndex.interfaceLanguages {
                    let languageNode = NavigatorTree.Node(item: NavigatorItem(pageType: PageType.languageGroup.rawValue,
                                                                              languageID: language.mask,
                                                                              title: language.name,
                                                                              platformMask: Platform.Name.any.mask,
                                                                              availabilityID: UInt64(Self.availabilityIDWithNoAvailabilities)),
                                                          bundleIdentifier: bundleIdentifier)
                    languageMaskToNode[language.mask] = languageNode
                    root.add(child: languageNode)
                }
            }

            let curatedReferences = allReferences.subtracting(pendingUncuratedReferences)
            
            // The rest have no parent, so they need to be under the root.
            for nodeID in pendingUncuratedReferences {
                // Don't add symbol nodes to the root; if they have been dropped by automatic
                // curation, then they should not be in the navigator. In addition, treat unknown
                // page types as symbol nodes on the assumption that an unknown page type is a
                // symbol kind added in a future version of Swift-DocC.
                if let node = identifierToNode[nodeID], PageType(rawValue: node.item.pageType)?.isSymbolKind == false {

                    // If an uncurated page has been curated in another language, don't add it to the top-level.
                    if curatedReferences.contains(where: { curatedNodeID in
                        // Compare all the identifier's properties for equality, except for its language.
                        curatedNodeID.bundleIdentifier == nodeID.bundleIdentifier
                            && curatedNodeID.path == nodeID.path
                            && curatedNodeID.fragment == nodeID.fragment
                    }) {
                        continue
                    }

                    if groupByLanguage {
                        // Force unwrap is safe as we mapped this before
                        let languageNode = languageMaskToNode[node.item.languageID]!
                        languageNode.add(child: node)
                    } else {
                        root.add(child: node)
                    }
                }
            }
            
            // A list of items without curation, but still indexed.
            var fallouts = [NavigatorTree.Node]()

            if sortRootChildrenByName {
                root.children.sort(by: \.item.title)
                if groupByLanguage {
                    root.children.forEach { (languageGroup) in
                        languageGroup.children.sort(by: \.item.title)
                    }
                }
            }
            
            // If a set of supported languages is passed, merge the others.
            if groupByLanguage {
                if !fallouts.isEmpty {
                    fallouts.sort(by: { $0.item.title > $1.item.title })
                    let otherNode = NavigatorTree.Node(item: NavigatorItem(pageType: PageType.languageGroup.rawValue,
                                                                           languageID: InterfaceLanguage.undefined.mask,
                                                                           title: "Other",
                                                                           platformMask: Platform.Name.any.mask,
                                                                           availabilityID: UInt64(Self.availabilityIDWithNoAvailabilities),
                                                                           path: ""),
                                                                           bundleIdentifier: bundleIdentifier)
                    languageMaskToNode[InterfaceLanguage.any.mask] = otherNode
                    fallouts.forEach { (node) in
                        root.children.removeAll(where: { $0 == node})
                        node.children.forEach { otherNode.add(child: $0) }
                    }
                    root.add(child: otherNode)
                }
            }
            
            if emitJSONRepresentation {
                let renderIndex = RenderIndex.fromNavigatorIndex(navigatorIndex, with: self)
                
                let jsonEncoder = RenderJSONEncoder.makeEncoder(
                    prettyPrint: shouldPrettyPrintOutputJSON,
                    assetPrefixComponent: bundleIdentifier.split(separator: "/").joined(separator: "-")
                )
                jsonEncoder.outputFormatting.insert(.sortedKeys)
                
                let jsonNavigatorIndexURL = outputURL.appendingPathComponent("index.json")
                do {
                    let renderIndexData = try jsonEncoder.encode(renderIndex)
                    try renderIndexData.write(to: jsonNavigatorIndexURL)
                } catch {
                    self.problems.append(
                        error.problem(
                            source: nil,
                            severity: .error,
                            summaryPrefix: "Failed to write render index JSON to '\(jsonNavigatorIndexURL)': "
                        )
                    )
                }
                
            }
            
            guard emitLMDBRepresentation else {
                return
            }
            
            let environment: LMDB.Environment
            if let alreadyDefinedEnvironment = navigatorIndex.environment {
                environment = alreadyDefinedEnvironment
            } else {
                do {
                    environment = try LMDB.Environment(
                        path: navigatorIndex.url.path,
                        flags: [.noLock],
                        maxDBs: 4, mapSize: 100 * 1024 * 1024 // mapSize = 100MB
                    )
                    navigatorIndex.environment = environment
                } catch {
                    problems.append(
                        error.problem(
                            source: nil,
                            severity: .error,
                            summaryPrefix: "Failed to create navigator index LMDB environment: "
                        )
                    )
                    
                    return
                }
            }

            defer { environment.close() }

            let database: LMDB.Database
            if let alreadyDefinedDatabase = navigatorIndex.database {
                database = alreadyDefinedDatabase
            } else {
                do {
                    database = try environment.openDatabase(named: "index", flags: [.create])
                    navigatorIndex.database = database
                } catch {
                    problems.append(
                        error.problem(
                            source: nil,
                            severity: .error,
                            summaryPrefix: "Failed to create navigator index LMDB database: "
                        )
                    )
                    
                    return
                }
            }
            
            let information: LMDB.Database
            if let alreadyDefinedInformation = navigatorIndex.information {
                information = alreadyDefinedInformation
            } else {
                do {
                    information = try environment.openDatabase(named: "information", flags: [.create])
                    navigatorIndex.information = information
                } catch {
                    problems.append(
                        error.problem(
                            source: nil,
                            severity: .error,
                            summaryPrefix: "Failed to create navigator index LMDB information database: "
                        )
                    )
                    
                    return
                }
            }
            
            let availability: LMDB.Database
            if let alreadyDefinedAvailability = navigatorIndex.availability {
                availability = alreadyDefinedAvailability
            } else {
                do {
                    availability = try environment.openDatabase(named: "availability", flags: [.create])
                    navigatorIndex.availability = availability
                } catch {
                    problems.append(
                        error.problem(
                            source: nil,
                            severity: .error,
                            summaryPrefix: "Failed to navigator index LMDB availability database: "
                        )
                    )
                    
                    return
                }
            }
            
            do {
                for (newID, entryIDs) in availabilityIDs {
                    try availability.put(key: newID, value: entryIDs)
                }
            } catch {
                problems.append(
                    error.problem(
                        source: nil,
                        severity: .error,
                        summaryPrefix: "Failed to write navigator index availability information: "
                    )
                )
            }
            
            do {
                var records = [Record]()
                if let estimatedCount {
                    records.reserveCapacity(estimatedCount)
                }
                
                try navigatorIndex.navigatorTree.write(to: outputURL.appendingPathComponent("navigator.index"), writePaths: writePathsOnDisk) { node in
                    // Skip the nodes that have no content to present.
                    guard let pageType = PageType(rawValue: node.item.pageType) else { return }
                    guard !Set<PageType>([.root, .groupMarker, .languageGroup]).contains(pageType) else { return }
                    
                    // Retrieve the language, if possible.
                    guard let interfaceLanguage = self.navigatorIndex?.languageMaskToLanguage[node.item.languageID] else { return }
                    
                    // The fullPath needs to account for the language.
                    let fullPath = interfaceLanguage.name.lowercased() + node.item.path
                    
                    // Create the database records and store them in `records` for the time being.
                    records.append(
                        Record(
                            // Store the node to path mapping.
                            nodeMapping: (node.id!, fullPath),
                            // As we might have the same path curated in multiple places, we store only the first one found in the tree.
                            curationMapping: (navigatorIndex.pathHasher.hash(fullPath), node.id!),
                            // Store the USR to node relationship.
                            usrMapping: node.item.usrIdentifier.map({ ($0, node.id!) })
                        )
                    )
                }
                
                do {
                    // Write all records to disk in a single transaction.
                    try database.put(records: records)
                }
                // `put(records:)` throws only `LMDB.Database.NodeError.errorForPath`
                catch LMDB.Database.NodeError.errorForPath(let path, let error) {
                    if (error as? LMDB.Error) == LMDB.Error.keyExists {
                        self.problems.append(error.problem(source: self.outputURL,
                                                           severity: .information,
                                                           summaryPrefix: "Duplicated path found for \(path)"))
                    } else {
                        self.problems.append(error.problem(source: self.outputURL,
                                                           severity: .warning,
                                                           summaryPrefix: "The navigator index failed to map the data: \(error.localizedDescription)"))
                    }
                }

            } catch {
                problems.append(error.problem(source: outputURL,
                                              severity: .warning,
                                              summaryPrefix: "Couldn't write the navigator tree to the disk"))
            }
            
            // Write the availability index to the disk
            do {
                let plistEncoder = PropertyListEncoder()
                let encoded = try plistEncoder.encode(navigatorIndex.availabilityIndex)
                try encoded.write(to: outputURL.appendingPathComponent("availability.index"))
            } catch {
                problems.append(error.problem(source: outputURL,
                                              severity: .warning,
                                              summaryPrefix: "Couldn't write the availability index to the disk"))
            }
            
            // Insert the data about bundle identifier and items processed.
            do {
                let txn = environment.transaction()
                try txn.begin()
                try txn.put(key: NavigatorIndex.bundleKey, value: bundleIdentifier, in: information)
                try txn.put(key: NavigatorIndex.pathHasherKey, value: navigatorIndex.pathHasher.rawValue, in: information)
                try txn.put(key: NavigatorIndex.itemsIndexKey, value: counter, in: information)
                try txn.commit()
            } catch {
                problems.append(error.problem(source: outputURL,
                                              severity: .error,
                                              summaryPrefix: "LMDB failed to store the content"))
            }
                        
            var diagnostic = Diagnostic(source: outputURL,
                                             severity: .information,
                                             range: nil,
                                             identifier: "org.swift.docc.index",
                                             summary: "Indexed \(counter) entities")
            var problem = Problem(diagnostic: diagnostic, possibleSolutions: [])
            problems.append(problem)
            
            let availabilities = navigatorIndex.availabilityIndex.indexed
            diagnostic = Diagnostic(source: outputURL,
                                         severity: .information,
                                         range: nil,
                                         identifier: "org.swift.docc.index",
                                         summary: "Created index with \(availabilities)")
            problem = Problem(diagnostic: diagnostic, possibleSolutions: [])
            problems.append(problem)
            
            let treeString = root.dumpTree()
            diagnostic = Diagnostic(source: outputURL,
                                         severity: .information,
                                         range: nil,
                                         identifier: "org.swift.docc.index",
                                         summary: "Index tree:\n\(treeString)")
            problem = Problem(diagnostic: diagnostic, possibleSolutions: [])
            problems.append(problem)
        }
        
        /**
         Build the index using the passed instance of `RenderNodeProvider` if available.
         - Returns: A list containing all the problems encountered during indexing.
         - Note: If a provider is not available, this method would generate a fatal error.
         */
        public func build() -> [Problem] {
            precondition(renderNodeProvider != nil, "Calling build without a renderNodeProvider set is not permitted.")
            
            setup()
            
            while let renderNode = renderNodeProvider!.getRenderNode() {
                do {
                    try index(renderNode: renderNode)
                } catch {
                    problems.append(error.problem(source: renderNode.identifier.url,
                                                  severity: .warning,
                                                  summaryPrefix: "RenderNode indexing process failed"))
                }
            }
            
            finalize()
            
            return problems
        }
        
        func availabilityEntryIDs(for availabilityID: UInt64) -> [Int]? {
            return availabilityIDs[Int(availabilityID)]
        }
    }
    
}

fileprivate extension Error {
    
    /// Returns a problem from an `Error`.
    func problem(source: URL?, severity: DiagnosticSeverity, summaryPrefix: String = "") -> Problem {
        let diagnostic = Diagnostic(source: source,
                                         severity: severity,
                                         range: nil,
                                         identifier: "org.swift.docc.index",
                                         summary: "\(summaryPrefix) \(localizedDescription)")
        return Problem(diagnostic: diagnostic, possibleSolutions: [])
    }
}


extension LMDB.Database {
    enum NodeError: Error {
        /// A database error that includes the path of a specific node and the original database error.
        case errorForPath(String, Error)
    }
    
    /**
    Insert records into a database.
    
    - Parameters:
       - records: Key/value pairs to insert into the database.
       - flags: The list of `WriteFlags` to use for the put action.
    - Throws: An error in case a read-only transaction has been used, an invalid parameter has been specified, the database is full or the transaction has too many dirty pages to complete.
    > Warning: Wrap all thrown errors inside `NodeError` so the node path information is preserved.
    */
    func put(records: [NavigatorIndex.Builder.Record], flags: WriteFlags = []) throws {
        try LMDB.Transaction(environment: environment).run { database in
            try records.forEach { record in
                do {
                    try database.put(key: record.nodeMapping.0, value: record.nodeMapping.1, in: self, flags: flags)
                    try database.put(key: record.curationMapping.0, value: record.curationMapping.1, in: self, flags: flags)
                    if let usrMapping = record.usrMapping {
                        try database.put(key: usrMapping.0, value: usrMapping.1, in: self, flags: flags)
                    }
                } catch {
                    throw NodeError.errorForPath(record.nodeMapping.1, error)
                }
            }
        }
    }
}

/// A path hasher representation, used only internally in the navigator index to hash paths avoiding to persist long paths multiple times.
enum PathHasher: String {
    /// The `FNV-1` implementation.
    case fnv1 = "FNV-1"
    
    /// The `MD5` implementation, using the first 12 characters.
    case md5 = "MD5"
    
    /// Hashes a path using the selected strategy.
    func hash(_ value: String) -> String {
        switch self {
        case .fnv1: return value.stableHashString
        case .md5:
            let digest = Insecure.MD5.hash(data: Data(value.utf8)).map {
                String(format: "%02hhx", $0)
            }.joined().prefix(12)
            return String(digest)
        }
    }
}