jazzy 0.14.4 → 0.15.1

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/Tests.yml +6 -5
  3. data/.rubocop.yml +19 -0
  4. data/CHANGELOG.md +56 -0
  5. data/CONTRIBUTING.md +1 -1
  6. data/Gemfile.lock +66 -49
  7. data/README.md +115 -5
  8. data/bin/sourcekitten +0 -0
  9. data/jazzy.gemspec +1 -1
  10. data/js/package-lock.json +6 -6
  11. data/lib/jazzy/config.rb +156 -24
  12. data/lib/jazzy/doc.rb +2 -2
  13. data/lib/jazzy/doc_builder.rb +55 -29
  14. data/lib/jazzy/doc_index.rb +185 -0
  15. data/lib/jazzy/docset_builder/info_plist.mustache +1 -1
  16. data/lib/jazzy/docset_builder.rb +44 -13
  17. data/lib/jazzy/extensions/katex/css/katex.min.css +1 -1
  18. data/lib/jazzy/extensions/katex/js/katex.min.js +1 -1
  19. data/lib/jazzy/gem_version.rb +1 -1
  20. data/lib/jazzy/grouper.rb +130 -0
  21. data/lib/jazzy/podspec_documenter.rb +1 -1
  22. data/lib/jazzy/source_declaration/type.rb +10 -2
  23. data/lib/jazzy/source_declaration.rb +69 -8
  24. data/lib/jazzy/source_document.rb +5 -1
  25. data/lib/jazzy/source_module.rb +13 -11
  26. data/lib/jazzy/sourcekitten.rb +232 -236
  27. data/lib/jazzy/symbol_graph/ext_key.rb +37 -0
  28. data/lib/jazzy/symbol_graph/ext_node.rb +23 -6
  29. data/lib/jazzy/symbol_graph/graph.rb +31 -19
  30. data/lib/jazzy/symbol_graph/relationship.rb +21 -3
  31. data/lib/jazzy/symbol_graph/sym_node.rb +10 -22
  32. data/lib/jazzy/symbol_graph/symbol.rb +28 -0
  33. data/lib/jazzy/symbol_graph.rb +19 -16
  34. data/lib/jazzy/themes/apple/assets/css/jazzy.css.scss +10 -7
  35. data/lib/jazzy/themes/apple/assets/js/typeahead.jquery.js +3 -2
  36. data/lib/jazzy/themes/apple/templates/doc.mustache +8 -1
  37. data/lib/jazzy/themes/fullwidth/assets/css/jazzy.css.scss +5 -5
  38. data/lib/jazzy/themes/fullwidth/assets/js/typeahead.jquery.js +3 -2
  39. data/lib/jazzy/themes/fullwidth/templates/doc.mustache +8 -1
  40. data/lib/jazzy/themes/jony/assets/css/jazzy.css.scss +6 -5
  41. data/lib/jazzy/themes/jony/templates/doc.mustache +9 -2
  42. data/spec/integration_spec.rb +8 -1
  43. metadata +16 -7
@@ -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? { |d| d.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,47 @@ 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
920
+ # Grab all the extensions from the same doc module
921
+ def self.next_doc_module_group(decls)
922
+ decls.partition do |decl|
923
+ decl.doc_module_name == decls.first.doc_module_name
962
924
  end
963
925
  end
964
926
 
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
927
+ # Does this extension/type need a note explaining which doc module it is from?
928
+ # Only for extensions, if there actually are multiple modules.
929
+ # Last condition avoids it for simple 'extension Array'.
930
+ def self.need_doc_module_note?(decl, html_declaration)
931
+ Config.instance.multiple_modules? &&
932
+ decl.type.swift_extension? &&
933
+ !(html_declaration.empty? &&
934
+ !decl.constrained_extension? &&
935
+ !decl.inherited_types?)
973
936
  end
974
937
 
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
938
+ #
939
+ # Autolinking
940
+ #
982
941
 
983
942
  # Links recognized top-level declarations within
984
943
  # - inlined code within docs
@@ -987,85 +946,97 @@ module Jazzy
987
946
  # The `after_highlight` flag is used to differentiate between the two modes.
988
947
  #
989
948
  # 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)
949
+ def self.autolink_text(text, doc, after_highlight: false)
992
950
  text.autolink_block(doc.url, '[^\s]+', after_highlight) do |raw_name|
993
951
  sym_name =
994
952
  (raw_name[/^<doc:(.*)>$/, 1] || raw_name).sub(/(?<!^)-.+$/, '')
995
953
 
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{^.*/}, '')]
954
+ [@doc_index.lookup(sym_name, doc), sym_name.sub(%r{^.*/}, '')]
1008
955
  end.autolink_block(doc.url, '[+-]\[\w+(?: ?\(\w+\))? [\w:]+\]',
1009
956
  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
957
+ [@doc_index.lookup(raw_name, doc), raw_name]
1021
958
  end.autolink_block(doc.url, '[+-]\w[\w:]*', after_highlight) do |raw_name|
1022
- [name_match(raw_name, doc.children), raw_name]
959
+ [@doc_index.lookup(raw_name, doc), raw_name]
1023
960
  end
1024
961
  end
1025
- # rubocop:enable Metrics/MethodLength
1026
962
 
1027
963
  AUTOLINK_TEXT_FIELDS = %w[return
1028
964
  abstract
1029
965
  unavailable_message
1030
966
  deprecation_message].freeze
1031
967
 
1032
- def self.autolink_text_fields(doc, root_decls)
968
+ def self.autolink_text_fields(doc)
1033
969
  AUTOLINK_TEXT_FIELDS.each do |field|
1034
970
  if text = doc.send(field)
1035
- doc.send(field + '=', autolink_text(text, doc, root_decls))
971
+ doc.send(field + '=', autolink_text(text, doc))
1036
972
  end
1037
973
  end
1038
974
 
1039
975
  (doc.parameters || []).each do |param|
1040
976
  param[:discussion] =
1041
- autolink_text(param[:discussion], doc, root_decls)
977
+ autolink_text(param[:discussion], doc)
1042
978
  end
1043
979
  end
1044
980
 
1045
981
  AUTOLINK_HIGHLIGHT_FIELDS = %w[declaration
1046
982
  other_language_declaration].freeze
1047
983
 
1048
- def self.autolink_highlight_fields(doc, root_decls)
984
+ def self.autolink_highlight_fields(doc)
1049
985
  AUTOLINK_HIGHLIGHT_FIELDS.each do |field|
1050
986
  if text = doc.send(field)
1051
987
  doc.send(field + '=',
1052
- autolink_text(text, doc, root_decls, after_highlight: true))
988
+ autolink_text(text, doc, after_highlight: true))
1053
989
  end
1054
990
  end
1055
991
  end
1056
992
 
1057
- def self.autolink(docs, root_decls)
1058
- @autolink_root_decls = root_decls
993
+ def self.autolink(docs)
1059
994
  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)
995
+ doc.children = autolink(doc.children)
996
+ autolink_text_fields(doc)
997
+ autolink_highlight_fields(doc)
1063
998
  end
1064
999
  end
1065
1000
 
1066
1001
  # For autolinking external markdown documents
1067
1002
  def self.autolink_document(html, doc)
1068
- autolink_text(html, doc, @autolink_root_decls || [])
1003
+ autolink_text(html, doc)
1004
+ end
1005
+
1006
+ #
1007
+ # Entrypoint and misc filtering
1008
+ #
1009
+
1010
+ # Apply filtering based on the "included" and "excluded" flags.
1011
+ def self.filter_files(json)
1012
+ json = filter_included_files(json) if Config.instance.included_files.any?
1013
+ json = filter_excluded_files(json) if Config.instance.excluded_files.any?
1014
+ json.map do |doc|
1015
+ key = doc.keys.first
1016
+ doc[key]
1017
+ end.compact
1018
+ end
1019
+
1020
+ # Filter based on the "included" flag.
1021
+ def self.filter_included_files(json)
1022
+ included_files = Config.instance.included_files
1023
+ json.map do |doc|
1024
+ key = doc.keys.first
1025
+ doc if included_files.detect do |include|
1026
+ File.fnmatch?(include, key)
1027
+ end
1028
+ end.compact
1029
+ end
1030
+
1031
+ # Filter based on the "excluded" flag.
1032
+ def self.filter_excluded_files(json)
1033
+ excluded_files = Config.instance.excluded_files
1034
+ json.map do |doc|
1035
+ key = doc.keys.first
1036
+ doc unless excluded_files.detect do |exclude|
1037
+ File.fnmatch?(exclude, key)
1038
+ end
1039
+ end.compact
1069
1040
  end
1070
1041
 
1071
1042
  def self.reject_objc_types(docs)
@@ -1083,26 +1054,51 @@ module Jazzy
1083
1054
  end
1084
1055
  end
1085
1056
 
1057
+ # Remove top-level enum cases because it means they have an ACL lower
1058
+ # than min_acl
1059
+ def self.reject_swift_types(docs)
1060
+ docs.reject { |doc| doc.type.swift_enum_element? }
1061
+ end
1062
+
1063
+ # Spot and mark any categories on classes not declared in these docs
1064
+ def self.mark_objc_external_categories(docs)
1065
+ class_names = docs.select { |doc| doc.type.objc_class? }.to_set(&:name)
1066
+
1067
+ docs.map do |doc|
1068
+ if (names = doc.objc_category_name) && !class_names.include?(names.first)
1069
+ doc.module_name = '(Imported)'
1070
+ end
1071
+ doc
1072
+ end
1073
+ end
1074
+
1086
1075
  # Parse sourcekitten STDOUT output as JSON
1087
1076
  # @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
1077
+ def self.parse(sourcekitten_output, options, inject_docs)
1078
+ @min_acl = options.min_acl
1079
+ @skip_undocumented = options.skip_undocumented
1091
1080
  @stats = Stats.new
1092
1081
  @inaccessible_protocols = []
1093
- sourcekitten_json = filter_files(JSON.parse(sourcekitten_output).flatten)
1094
- docs = make_source_declarations(sourcekitten_json).concat inject_docs
1082
+
1083
+ # Process each module separately to inject the source module name
1084
+ docs = sourcekitten_output.zip(options.module_names).map do |json, name|
1085
+ @current_module_name = name
1086
+ sourcekitten_dicts = filter_files(JSON.parse(json).flatten)
1087
+ make_source_declarations(sourcekitten_dicts)
1088
+ end.flatten + inject_docs
1089
+
1095
1090
  docs = expand_extensions(docs)
1096
1091
  docs = deduplicate_declarations(docs)
1097
1092
  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)
1093
+ docs = reject_swift_types(docs)
1094
+ docs = mark_objc_external_categories(docs)
1095
+
1096
+ @doc_index = DocIndex.new(docs)
1097
+
1098
+ docs = Grouper.group_docs(docs, @doc_index)
1099
+
1104
1100
  make_doc_urls(docs)
1105
- autolink(docs, ungrouped_docs)
1101
+ autolink(docs)
1106
1102
  [docs, @stats]
1107
1103
  end
1108
1104
  end