jazzy 0.14.4 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/Tests.yml +6 -5
  3. data/.rubocop.yml +13 -0
  4. data/CHANGELOG.md +40 -0
  5. data/CONTRIBUTING.md +1 -1
  6. data/Gemfile.lock +62 -47
  7. data/README.md +115 -5
  8. data/bin/sourcekitten +0 -0
  9. data/js/package-lock.json +6 -6
  10. data/lib/jazzy/config.rb +156 -24
  11. data/lib/jazzy/doc.rb +2 -2
  12. data/lib/jazzy/doc_builder.rb +55 -29
  13. data/lib/jazzy/doc_index.rb +185 -0
  14. data/lib/jazzy/docset_builder/info_plist.mustache +1 -1
  15. data/lib/jazzy/docset_builder.rb +44 -13
  16. data/lib/jazzy/extensions/katex/css/katex.min.css +1 -1
  17. data/lib/jazzy/extensions/katex/js/katex.min.js +1 -1
  18. data/lib/jazzy/gem_version.rb +1 -1
  19. data/lib/jazzy/grouper.rb +130 -0
  20. data/lib/jazzy/podspec_documenter.rb +1 -1
  21. data/lib/jazzy/source_declaration/type.rb +10 -2
  22. data/lib/jazzy/source_declaration.rb +69 -8
  23. data/lib/jazzy/source_document.rb +5 -1
  24. data/lib/jazzy/source_module.rb +13 -11
  25. data/lib/jazzy/sourcekitten.rb +231 -237
  26. data/lib/jazzy/symbol_graph/ext_key.rb +37 -0
  27. data/lib/jazzy/symbol_graph/ext_node.rb +23 -6
  28. data/lib/jazzy/symbol_graph/graph.rb +31 -19
  29. data/lib/jazzy/symbol_graph/relationship.rb +21 -3
  30. data/lib/jazzy/symbol_graph/sym_node.rb +10 -22
  31. data/lib/jazzy/symbol_graph/symbol.rb +28 -0
  32. data/lib/jazzy/symbol_graph.rb +19 -16
  33. data/lib/jazzy/themes/apple/assets/css/jazzy.css.scss +10 -7
  34. data/lib/jazzy/themes/apple/assets/js/typeahead.jquery.js +3 -2
  35. data/lib/jazzy/themes/apple/templates/doc.mustache +8 -1
  36. data/lib/jazzy/themes/fullwidth/assets/css/jazzy.css.scss +5 -5
  37. data/lib/jazzy/themes/fullwidth/assets/js/typeahead.jquery.js +3 -2
  38. data/lib/jazzy/themes/fullwidth/templates/doc.mustache +8 -1
  39. data/lib/jazzy/themes/jony/assets/css/jazzy.css.scss +6 -5
  40. data/lib/jazzy/themes/jony/templates/doc.mustache +9 -2
  41. data/spec/integration_spec.rb +8 -1
  42. metadata +5 -2
@@ -13,6 +13,8 @@ require 'jazzy/highlighter'
13
13
  require 'jazzy/source_declaration'
14
14
  require 'jazzy/source_mark'
15
15
  require 'jazzy/stats'
16
+ require 'jazzy/grouper'
17
+ require 'jazzy/doc_index'
16
18
 
17
19
  ELIDED_AUTOLINK_TOKEN = '36f8f5912051ae747ef441d6511ca4cb'
18
20
 
@@ -60,83 +62,9 @@ module Jazzy
60
62
  ).freeze
61
63
  end
62
64
 
63
- # Group root-level docs by custom categories (if any) and type
64
- def self.group_docs(docs)
65
- custom_categories, docs = group_custom_categories(docs)
66
- unlisted_prefix = Config.instance.custom_categories_unlisted_prefix
67
- type_categories, uncategorized = group_type_categories(
68
- docs, custom_categories.any? ? unlisted_prefix : ''
69
- )
70
- custom_categories + merge_categories(type_categories) + uncategorized
71
- end
72
-
73
- def self.group_custom_categories(docs)
74
- group = Config.instance.custom_categories.map do |category|
75
- children = category['children'].flat_map do |name|
76
- docs_with_name, docs = docs.partition { |doc| doc.name == name }
77
- if docs_with_name.empty?
78
- warn 'WARNING: No documented top-level declarations match ' \
79
- "name \"#{name}\" specified in categories file"
80
- end
81
- docs_with_name
82
- end
83
- # Category config overrides alphabetization
84
- children.each.with_index { |child, i| child.nav_order = i }
85
- make_group(children, category['name'], '')
86
- end
87
- [group.compact, docs]
88
- end
89
-
90
- def self.group_type_categories(docs, type_category_prefix)
91
- group = SourceDeclaration::Type.all.map do |type|
92
- children, docs = docs.partition { |doc| doc.type == type }
93
- make_group(
94
- children,
95
- type_category_prefix + type.plural_name,
96
- "The following #{type.plural_name.downcase} are available globally.",
97
- type_category_prefix + type.plural_url_name,
98
- )
99
- end
100
- [group.compact, docs]
101
- end
102
-
103
- # Join categories with the same name (eg. ObjC and Swift classes)
104
- def self.merge_categories(categories)
105
- merged = []
106
- categories.each do |new_category|
107
- if existing = merged.find { |c| c.name == new_category.name }
108
- existing.children += new_category.children
109
- else
110
- merged.append(new_category)
111
- end
112
- end
113
- merged
114
- end
115
-
116
- def self.make_group(group, name, abstract, url_name = nil)
117
- group.reject! { |doc| doc.name.empty? }
118
- unless group.empty?
119
- SourceDeclaration.new.tap do |sd|
120
- sd.type = SourceDeclaration::Type.overview
121
- sd.name = name
122
- sd.url_name = url_name
123
- sd.abstract = Markdown.render(abstract)
124
- sd.children = group
125
- end
126
- end
127
- end
128
-
129
- # Merge consecutive sections with the same mark into one section
130
- def self.merge_consecutive_marks(docs)
131
- prev_mark = nil
132
- docs.each do |doc|
133
- if prev_mark&.can_merge?(doc.mark)
134
- doc.mark = prev_mark
135
- end
136
- prev_mark = doc.mark
137
- merge_consecutive_marks(doc.children)
138
- end
139
- end
65
+ #
66
+ # URL assignment
67
+ #
140
68
 
141
69
  def self.sanitize_filename(doc)
142
70
  unsafe_filename = doc.docs_filename
@@ -149,7 +77,7 @@ module Jazzy
149
77
  end
150
78
 
151
79
  # rubocop:disable Metrics/MethodLength
152
- # Generate doc URL by prepending its parents URLs
80
+ # Generate doc URL by prepending its parents' URLs
153
81
  # @return [Hash] input docs with URLs
154
82
  def self.make_doc_urls(docs)
155
83
  docs.each do |doc|
@@ -189,18 +117,50 @@ module Jazzy
189
117
  # Guides in the root for back-compatibility.
190
118
  # Declarations under outer namespace type (Structures, Classes, etc.)
191
119
  def self.subdir_for_doc(doc)
192
- return [] if doc.type.markdown?
193
-
194
- top_level_decl = doc.namespace_path.first
195
- if top_level_decl.type.name
196
- [top_level_decl.type.plural_url_name] +
197
- doc.namespace_ancestors.map(&:name)
120
+ if Config.instance.multiple_modules?
121
+ subdir_for_doc_multi_module(doc)
198
122
  else
199
- # Category - in the root
200
- []
123
+ # Back-compatibility layout version
124
+ subdir_for_doc_single_module(doc)
201
125
  end
202
126
  end
203
127
 
128
+ # Pre-multi-module site layout, does not allow for
129
+ # types with the same name.
130
+ def self.subdir_for_doc_single_module(doc)
131
+ # Guides + Groups in the root
132
+ return [] if doc.type.markdown? || doc.type.overview?
133
+
134
+ [doc.namespace_path.first.type.plural_url_name] +
135
+ doc.namespace_ancestors.map(&:name)
136
+ end
137
+
138
+ # Multi-module site layout, separate each module that
139
+ # is being documented.
140
+ def self.subdir_for_doc_multi_module(doc)
141
+ # Guides + Groups in the root
142
+ return [] if doc.type.markdown? || doc.type.overview?
143
+
144
+ root_decl = doc.namespace_path.first
145
+
146
+ # Extensions need an extra dir to allow for extending
147
+ # ExternalModule1.TypeName and ExternalModule2.TypeName
148
+ namespace_subdir =
149
+ if root_decl.type.swift_extension?
150
+ ['Extensions', root_decl.module_name]
151
+ else
152
+ [doc.namespace_path.first.type.plural_url_name]
153
+ end
154
+
155
+ [root_decl.doc_module_name] +
156
+ namespace_subdir +
157
+ doc.namespace_ancestors.map(&:name)
158
+ end
159
+
160
+ #
161
+ # CLI argument calculation
162
+ #
163
+
204
164
  # returns all subdirectories of specified path
205
165
  def self.rec_path(path)
206
166
  path.children.collect do |child|
@@ -210,42 +170,42 @@ module Jazzy
210
170
  end.select { |x| x }.flatten(1)
211
171
  end
212
172
 
213
- def self.use_spm?(options)
214
- options.swift_build_tool == :spm ||
215
- (!options.swift_build_tool_configured &&
173
+ def self.use_spm?(module_config)
174
+ module_config.swift_build_tool == :spm ||
175
+ (!module_config.swift_build_tool_configured &&
216
176
  Dir['*.xcodeproj', '*.xcworkspace'].empty? &&
217
- !options.build_tool_arguments.include?('-project') &&
218
- !options.build_tool_arguments.include?('-workspace'))
177
+ !module_config.build_tool_arguments.include?('-project') &&
178
+ !module_config.build_tool_arguments.include?('-workspace'))
219
179
  end
220
180
 
221
181
  # Builds SourceKitten arguments based on Jazzy options
222
- def self.arguments_from_options(options)
182
+ def self.arguments_from_options(module_config)
223
183
  arguments = ['doc']
224
- if options.objc_mode
225
- arguments += objc_arguments_from_options(options)
184
+ if module_config.objc_mode
185
+ arguments += objc_arguments_from_options(module_config)
226
186
  else
227
- arguments += ['--spm'] if use_spm?(options)
228
- unless options.module_name.empty?
229
- arguments += ['--module-name', options.module_name]
187
+ arguments += ['--spm'] if use_spm?(module_config)
188
+ unless module_config.module_name.empty?
189
+ arguments += ['--module-name', module_config.module_name]
230
190
  end
231
191
  arguments += ['--']
232
192
  end
233
193
 
234
- arguments + options.build_tool_arguments
194
+ arguments + module_config.build_tool_arguments
235
195
  end
236
196
 
237
- def self.objc_arguments_from_options(options)
197
+ def self.objc_arguments_from_options(module_config)
238
198
  arguments = []
239
- if options.build_tool_arguments.empty?
240
- arguments += ['--objc', options.umbrella_header.to_s, '--', '-x',
199
+ if module_config.build_tool_arguments.empty?
200
+ arguments += ['--objc', module_config.umbrella_header.to_s, '--', '-x',
241
201
  'objective-c', '-isysroot',
242
- `xcrun --show-sdk-path --sdk #{options.sdk}`.chomp,
243
- '-I', options.framework_root.to_s,
202
+ `xcrun --show-sdk-path --sdk #{module_config.sdk}`.chomp,
203
+ '-I', module_config.framework_root.to_s,
244
204
  '-fmodules']
245
205
  end
246
206
  # add additional -I arguments for each subdirectory of framework_root
247
- unless options.framework_root.nil?
248
- rec_path(Pathname.new(options.framework_root.to_s)).collect do |child|
207
+ unless module_config.framework_root.nil?
208
+ rec_path(Pathname.new(module_config.framework_root.to_s)).collect do |child|
249
209
  if child.directory?
250
210
  arguments += ['-I', child.to_s]
251
211
  end
@@ -270,6 +230,10 @@ module Jazzy
270
230
  output
271
231
  end
272
232
 
233
+ #
234
+ # SourceDeclaration generation
235
+ #
236
+
273
237
  def self.make_default_doc_info(declaration)
274
238
  # @todo: Fix these
275
239
  declaration.abstract = ''
@@ -352,18 +316,10 @@ module Jazzy
352
316
  end
353
317
  end
354
318
 
355
- # Call things undocumented if they were compiled properly
356
- # and came from our module.
357
- def self.should_mark_undocumented(declaration)
358
- declaration.usr &&
359
- (declaration.modulename.nil? ||
360
- declaration.modulename == Config.instance.module_name)
361
- end
362
-
363
319
  def self.process_undocumented_token(doc, declaration)
364
320
  make_default_doc_info(declaration)
365
321
 
366
- if !declaration.swift? || should_mark_undocumented(declaration)
322
+ if declaration.mark_undocumented?
367
323
  @stats.add_undocumented(declaration)
368
324
  return nil if @skip_undocumented
369
325
 
@@ -621,7 +577,15 @@ module Jazzy
621
577
  declaration.file = Pathname(doc['key.filepath']) if doc['key.filepath']
622
578
  declaration.usr = doc['key.usr']
623
579
  declaration.type_usr = doc['key.typeusr']
624
- declaration.modulename = doc['key.modulename']
580
+ declaration.module_name =
581
+ if declaration.swift?
582
+ # Filter out Apple sub-framework implementation names
583
+ doc['key.modulename']&.sub(/\..*$/, '')
584
+ else
585
+ # ObjC best effort, category original module is unavailable
586
+ @current_module_name
587
+ end
588
+ declaration.doc_module_name = @current_module_name
625
589
  declaration.name = documented_name
626
590
  declaration.mark = current_mark
627
591
  declaration.access_control_level =
@@ -665,6 +629,10 @@ module Jazzy
665
629
  Regexp.last_match[1].gsub(/\s+/, ' ')
666
630
  end
667
631
 
632
+ #
633
+ # SourceDeclaration generation - extension management
634
+ #
635
+
668
636
  # Expands extensions of nested types declared at the top level into
669
637
  # a tree so they can be deduplicated properly
670
638
  def self.expand_extensions(decls)
@@ -689,7 +657,8 @@ module Jazzy
689
657
  SourceDeclaration.new.tap do |decl|
690
658
  make_default_doc_info(decl)
691
659
  decl.name = name
692
- decl.modulename = extension.modulename
660
+ decl.module_name = extension.module_name
661
+ decl.doc_module_name = extension.doc_module_name
693
662
  decl.type = extension.type
694
663
  decl.mark = extension.mark
695
664
  decl.usr = candidates.first.usr unless candidates.empty?
@@ -720,9 +689,10 @@ module Jazzy
720
689
 
721
690
  # Returns true if an Objective-C declaration is mergeable.
722
691
  def self.mergeable_objc?(decl, root_decls)
723
- decl.type.objc_class? \
724
- || (decl.type.objc_category? \
725
- && name_match(decl.objc_category_name[0], root_decls))
692
+ decl.type.objc_class? ||
693
+ (decl.type.objc_category? &&
694
+ (category_classname = decl.objc_category_name[0]) &&
695
+ root_decls.any? { _1.name == category_classname })
726
696
  end
727
697
 
728
698
  # Returns if a Swift declaration is mergeable.
@@ -733,22 +703,45 @@ module Jazzy
733
703
  decl.type.swift_typealias?
734
704
  end
735
705
 
706
+ # Normally merge all extensions into their types and each other.
707
+ #
708
+ # :none means only merge within a module -- so two extensions to
709
+ # some type get merged, but an extension to a type from
710
+ # another documented module does not get merged into that type
711
+ # :extensions means extensions of documented modules get merged,
712
+ # but if we're documenting ModA and ModB, and they both provide
713
+ # extensions to Swift.String, then those two extensions still
714
+ # appear separately.
715
+ #
716
+ # (The USR part of the dedup key means ModA.Foo and ModB.Foo do not
717
+ # get merged.)
718
+ def self.module_deduplication_key(decl)
719
+ if (Config.instance.merge_modules == :none) ||
720
+ (Config.instance.merge_modules == :extensions &&
721
+ decl.extension_of_external_type?)
722
+ decl.doc_module_name
723
+ else
724
+ ''
725
+ end
726
+ end
727
+
736
728
  # Two declarations get merged if they have the same deduplication key.
737
729
  def self.deduplication_key(decl, root_decls)
730
+ mod_key = module_deduplication_key(decl)
738
731
  # Swift extension of objc class
739
732
  if decl.swift_objc_extension?
740
- [decl.swift_extension_objc_name, :objc_class_and_categories]
733
+ [decl.swift_extension_objc_name, :objc_class_and_categories, mod_key]
741
734
  # Swift type or Swift extension of Swift type
742
735
  elsif mergeable_swift?(decl)
743
- [decl.usr, decl.name]
736
+ [decl.usr, decl.name, mod_key]
744
737
  # Objc categories and classes
745
738
  elsif mergeable_objc?(decl, root_decls)
746
739
  # Using the ObjC name to match swift_objc_extension.
747
740
  name, _ = decl.objc_category_name || decl.objc_name
748
- [name, :objc_class_and_categories]
741
+ [name, :objc_class_and_categories, mod_key]
749
742
  # Non-mergable declarations (funcs, typedefs etc...)
750
743
  else
751
- [decl.usr, decl.name, decl.type.kind]
744
+ [decl.usr, decl.name, decl.type.kind, '']
752
745
  end
753
746
  end
754
747
 
@@ -904,81 +897,45 @@ module Jazzy
904
897
  # declaration: public protocol conformances and, for top-level extensions,
905
898
  # further conditional extensions of the same type.
906
899
  def self.merge_code_declaration(decls)
907
- first = decls.first
908
-
909
900
  declarations = decls[1..].select do |decl|
910
901
  decl.type.swift_extension? &&
911
902
  (decl.other_inherited_types?(@inaccessible_protocols) ||
912
- (first.type.swift_extension? && decl.constrained_extension?))
913
- end.map(&:declaration)
914
-
915
- unless declarations.empty?
916
- first.declaration = declarations.prepend(first.declaration).uniq.join
917
- end
918
- end
919
-
920
- # Apply filtering based on the "included" and "excluded" flags.
921
- def self.filter_files(json)
922
- json = filter_included_files(json) if Config.instance.included_files.any?
923
- json = filter_excluded_files(json) if Config.instance.excluded_files.any?
924
- json.map do |doc|
925
- key = doc.keys.first
926
- doc[key]
927
- end.compact
928
- end
929
-
930
- # Filter based on the "included" flag.
931
- def self.filter_included_files(json)
932
- included_files = Config.instance.included_files
933
- json.map do |doc|
934
- key = doc.keys.first
935
- doc if included_files.detect do |include|
936
- File.fnmatch?(include, key)
903
+ (decls.first.type.swift_extension? && decl.constrained_extension?))
904
+ end.prepend(decls.first)
905
+
906
+ html_declaration = ''
907
+ until declarations.empty?
908
+ module_decls, declarations = next_doc_module_group(declarations)
909
+ first = module_decls.first
910
+ if need_doc_module_note?(first, html_declaration)
911
+ html_declaration += "<span class='declaration-note'>From #{first.doc_module_name}:</span>"
937
912
  end
938
- end.compact
939
- end
913
+ html_declaration += module_decls.map(&:declaration).uniq.join
914
+ end
940
915
 
941
- # Filter based on the "excluded" flag.
942
- def self.filter_excluded_files(json)
943
- excluded_files = Config.instance.excluded_files
944
- json.map do |doc|
945
- key = doc.keys.first
946
- doc unless excluded_files.detect do |exclude|
947
- File.fnmatch?(exclude, key)
948
- end
949
- end.compact
916
+ # Must preserve `nil` for edge cases
917
+ decls.first.declaration = html_declaration unless html_declaration.empty?
950
918
  end
951
919
 
952
- def self.name_match(name_part, docs)
953
- return nil unless name_part
954
-
955
- wildcard_expansion = Regexp.escape(name_part)
956
- .gsub('\.\.\.', '[^)]*')
957
- .gsub(/<.*>/, '')
958
-
959
- whole_name_pat = /\A#{wildcard_expansion}\Z/
960
- docs.find do |doc|
961
- whole_name_pat =~ doc.name
962
- end
920
+ # Grab all the extensions from the same doc module
921
+ def self.next_doc_module_group(decls)
922
+ decls.partition { _1.doc_module_name == decls.first.doc_module_name }
963
923
  end
964
924
 
965
- # Find the first ancestor of doc whose name matches name_part.
966
- def self.ancestor_name_match(name_part, doc)
967
- doc.namespace_ancestors.reverse_each do |ancestor|
968
- if match = name_match(name_part, ancestor.children)
969
- return match
970
- end
971
- end
972
- nil
925
+ # Does this extension/type need a note explaining which doc module it is from?
926
+ # Only for extensions, if there actually are multiple modules.
927
+ # Last condition avoids it for simple 'extension Array'.
928
+ def self.need_doc_module_note?(decl, html_declaration)
929
+ Config.instance.multiple_modules? &&
930
+ decl.type.swift_extension? &&
931
+ !(html_declaration.empty? &&
932
+ !decl.constrained_extension? &&
933
+ !decl.inherited_types?)
973
934
  end
974
935
 
975
- def self.name_traversal(name_parts, doc)
976
- while doc && !name_parts.empty?
977
- next_part = name_parts.shift
978
- doc = name_match(next_part, doc.children)
979
- end
980
- doc
981
- end
936
+ #
937
+ # Autolinking
938
+ #
982
939
 
983
940
  # Links recognized top-level declarations within
984
941
  # - inlined code within docs
@@ -987,85 +944,97 @@ module Jazzy
987
944
  # The `after_highlight` flag is used to differentiate between the two modes.
988
945
  #
989
946
  # DocC link format - follow Xcode and don't display slash-separated parts.
990
- # rubocop:disable Metrics/MethodLength
991
- def self.autolink_text(text, doc, root_decls, after_highlight: false)
947
+ def self.autolink_text(text, doc, after_highlight: false)
992
948
  text.autolink_block(doc.url, '[^\s]+', after_highlight) do |raw_name|
993
949
  sym_name =
994
950
  (raw_name[/^<doc:(.*)>$/, 1] || raw_name).sub(/(?<!^)-.+$/, '')
995
951
 
996
- parts = sym_name
997
- .sub(/^@/, '') # ignore for custom attribute ref
998
- .split(%r{(?<!\.)[/.](?!\.)}) # dot or slash, but not '...'
999
- .reject(&:empty?)
1000
-
1001
- # First dot-separated component can match any ancestor or top-level doc
1002
- first_part = parts.shift
1003
- name_root = ancestor_name_match(first_part, doc) ||
1004
- name_match(first_part, root_decls)
1005
-
1006
- # Traverse children via subsequent components, if any
1007
- [name_traversal(parts, name_root), sym_name.sub(%r{^.*/}, '')]
952
+ [@doc_index.lookup(sym_name, doc), sym_name.sub(%r{^.*/}, '')]
1008
953
  end.autolink_block(doc.url, '[+-]\[\w+(?: ?\(\w+\))? [\w:]+\]',
1009
954
  after_highlight) do |raw_name|
1010
- match = raw_name.match(/([+-])\[(\w+(?: ?\(\w+\))?) ([\w:]+)\]/)
1011
-
1012
- # Subject component can match any ancestor or top-level doc
1013
- subject = match[2].delete(' ')
1014
- name_root = ancestor_name_match(subject, doc) ||
1015
- name_match(subject, root_decls)
1016
-
1017
- if name_root
1018
- # Look up the verb in the subject’s children
1019
- [name_match(match[1] + match[3], name_root.children), raw_name]
1020
- end
955
+ [@doc_index.lookup(raw_name, doc), raw_name]
1021
956
  end.autolink_block(doc.url, '[+-]\w[\w:]*', after_highlight) do |raw_name|
1022
- [name_match(raw_name, doc.children), raw_name]
957
+ [@doc_index.lookup(raw_name, doc), raw_name]
1023
958
  end
1024
959
  end
1025
- # rubocop:enable Metrics/MethodLength
1026
960
 
1027
961
  AUTOLINK_TEXT_FIELDS = %w[return
1028
962
  abstract
1029
963
  unavailable_message
1030
964
  deprecation_message].freeze
1031
965
 
1032
- def self.autolink_text_fields(doc, root_decls)
966
+ def self.autolink_text_fields(doc)
1033
967
  AUTOLINK_TEXT_FIELDS.each do |field|
1034
968
  if text = doc.send(field)
1035
- doc.send(field + '=', autolink_text(text, doc, root_decls))
969
+ doc.send(field + '=', autolink_text(text, doc))
1036
970
  end
1037
971
  end
1038
972
 
1039
973
  (doc.parameters || []).each do |param|
1040
974
  param[:discussion] =
1041
- autolink_text(param[:discussion], doc, root_decls)
975
+ autolink_text(param[:discussion], doc)
1042
976
  end
1043
977
  end
1044
978
 
1045
979
  AUTOLINK_HIGHLIGHT_FIELDS = %w[declaration
1046
980
  other_language_declaration].freeze
1047
981
 
1048
- def self.autolink_highlight_fields(doc, root_decls)
982
+ def self.autolink_highlight_fields(doc)
1049
983
  AUTOLINK_HIGHLIGHT_FIELDS.each do |field|
1050
984
  if text = doc.send(field)
1051
985
  doc.send(field + '=',
1052
- autolink_text(text, doc, root_decls, after_highlight: true))
986
+ autolink_text(text, doc, after_highlight: true))
1053
987
  end
1054
988
  end
1055
989
  end
1056
990
 
1057
- def self.autolink(docs, root_decls)
1058
- @autolink_root_decls = root_decls
991
+ def self.autolink(docs)
1059
992
  docs.each do |doc|
1060
- doc.children = autolink(doc.children, root_decls)
1061
- autolink_text_fields(doc, root_decls)
1062
- autolink_highlight_fields(doc, root_decls)
993
+ doc.children = autolink(doc.children)
994
+ autolink_text_fields(doc)
995
+ autolink_highlight_fields(doc)
1063
996
  end
1064
997
  end
1065
998
 
1066
999
  # For autolinking external markdown documents
1067
1000
  def self.autolink_document(html, doc)
1068
- autolink_text(html, doc, @autolink_root_decls || [])
1001
+ autolink_text(html, doc)
1002
+ end
1003
+
1004
+ #
1005
+ # Entrypoint and misc filtering
1006
+ #
1007
+
1008
+ # Apply filtering based on the "included" and "excluded" flags.
1009
+ def self.filter_files(json)
1010
+ json = filter_included_files(json) if Config.instance.included_files.any?
1011
+ json = filter_excluded_files(json) if Config.instance.excluded_files.any?
1012
+ json.map do |doc|
1013
+ key = doc.keys.first
1014
+ doc[key]
1015
+ end.compact
1016
+ end
1017
+
1018
+ # Filter based on the "included" flag.
1019
+ def self.filter_included_files(json)
1020
+ included_files = Config.instance.included_files
1021
+ json.map do |doc|
1022
+ key = doc.keys.first
1023
+ doc if included_files.detect do |include|
1024
+ File.fnmatch?(include, key)
1025
+ end
1026
+ end.compact
1027
+ end
1028
+
1029
+ # Filter based on the "excluded" flag.
1030
+ def self.filter_excluded_files(json)
1031
+ excluded_files = Config.instance.excluded_files
1032
+ json.map do |doc|
1033
+ key = doc.keys.first
1034
+ doc unless excluded_files.detect do |exclude|
1035
+ File.fnmatch?(exclude, key)
1036
+ end
1037
+ end.compact
1069
1038
  end
1070
1039
 
1071
1040
  def self.reject_objc_types(docs)
@@ -1083,26 +1052,51 @@ module Jazzy
1083
1052
  end
1084
1053
  end
1085
1054
 
1055
+ # Remove top-level enum cases because it means they have an ACL lower
1056
+ # than min_acl
1057
+ def self.reject_swift_types(docs)
1058
+ docs.reject { _1.type.swift_enum_element? }
1059
+ end
1060
+
1061
+ # Spot and mark any categories on classes not declared in these docs
1062
+ def self.mark_objc_external_categories(docs)
1063
+ class_names = docs.select { _1.type.objc_class? }.to_set(&:name)
1064
+
1065
+ docs.map do |doc|
1066
+ if (names = doc.objc_category_name) && !class_names.include?(names.first)
1067
+ doc.module_name = '(Imported)'
1068
+ end
1069
+ doc
1070
+ end
1071
+ end
1072
+
1086
1073
  # Parse sourcekitten STDOUT output as JSON
1087
1074
  # @return [Hash] structured docs
1088
- def self.parse(sourcekitten_output, min_acl, skip_undocumented, inject_docs)
1089
- @min_acl = min_acl
1090
- @skip_undocumented = skip_undocumented
1075
+ def self.parse(sourcekitten_output, options, inject_docs)
1076
+ @min_acl = options.min_acl
1077
+ @skip_undocumented = options.skip_undocumented
1091
1078
  @stats = Stats.new
1092
1079
  @inaccessible_protocols = []
1093
- sourcekitten_json = filter_files(JSON.parse(sourcekitten_output).flatten)
1094
- docs = make_source_declarations(sourcekitten_json).concat inject_docs
1080
+
1081
+ # Process each module separately to inject the source module name
1082
+ docs = sourcekitten_output.zip(options.module_names).map do |json, name|
1083
+ @current_module_name = name
1084
+ sourcekitten_dicts = filter_files(JSON.parse(json).flatten)
1085
+ make_source_declarations(sourcekitten_dicts)
1086
+ end.flatten + inject_docs
1087
+
1095
1088
  docs = expand_extensions(docs)
1096
1089
  docs = deduplicate_declarations(docs)
1097
1090
  docs = reject_objc_types(docs)
1098
- # Remove top-level enum cases because it means they have an ACL lower
1099
- # than min_acl
1100
- docs = docs.reject { |doc| doc.type.swift_enum_element? }
1101
- ungrouped_docs = docs
1102
- docs = group_docs(docs)
1103
- merge_consecutive_marks(docs)
1091
+ docs = reject_swift_types(docs)
1092
+ docs = mark_objc_external_categories(docs)
1093
+
1094
+ @doc_index = DocIndex.new(docs)
1095
+
1096
+ docs = Grouper.group_docs(docs, @doc_index)
1097
+
1104
1098
  make_doc_urls(docs)
1105
- autolink(docs, ungrouped_docs)
1099
+ autolink(docs)
1106
1100
  [docs, @stats]
1107
1101
  end
1108
1102
  end