jazzy 0.14.4 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
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