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
data/lib/jazzy/config.rb CHANGED
@@ -13,16 +13,17 @@ module Jazzy
13
13
  # rubocop:disable Naming/AccessorMethodName
14
14
  class Attribute
15
15
  attr_reader :name, :description, :command_line, :config_file_key,
16
- :default, :parse
16
+ :default, :parse, :per_module
17
17
 
18
18
  def initialize(name, description: nil, command_line: nil,
19
- default: nil, parse: ->(x) { x })
19
+ default: nil, parse: ->(x) { x }, per_module: false)
20
20
  @name = name.to_s
21
21
  @description = Array(description)
22
22
  @command_line = Array(command_line)
23
23
  @default = default
24
24
  @parse = parse
25
25
  @config_file_key = full_command_line_name || @name
26
+ @per_module = per_module
26
27
  end
27
28
 
28
29
  def get(config)
@@ -134,22 +135,26 @@ module Jazzy
134
135
  config_attr :objc_mode,
135
136
  command_line: '--[no-]objc',
136
137
  description: 'Generate docs for Objective-C.',
137
- default: false
138
+ default: false,
139
+ per_module: true
138
140
 
139
141
  config_attr :umbrella_header,
140
142
  command_line: '--umbrella-header PATH',
141
143
  description: 'Umbrella header for your Objective-C framework.',
142
- parse: ->(uh) { expand_path(uh) }
144
+ parse: ->(uh) { expand_path(uh) },
145
+ per_module: true
143
146
 
144
147
  config_attr :framework_root,
145
148
  command_line: '--framework-root PATH',
146
149
  description: 'The root path to your Objective-C framework.',
147
- parse: ->(fr) { expand_path(fr) }
150
+ parse: ->(fr) { expand_path(fr) },
151
+ per_module: true
148
152
 
149
153
  config_attr :sdk,
150
154
  command_line: '--sdk [iphone|watch|appletv][os|simulator]|macosx',
151
155
  description: 'The SDK for which your code should be built.',
152
- default: 'macosx'
156
+ default: 'macosx',
157
+ per_module: true
153
158
 
154
159
  config_attr :hide_declarations,
155
160
  command_line: '--hide-declarations [objc|swift] ',
@@ -175,6 +180,13 @@ module Jazzy
175
180
  command_line: ['-b', '--build-tool-arguments arg1,arg2,…argN', Array],
176
181
  description: 'Arguments to forward to xcodebuild, swift build, or ' \
177
182
  'sourcekitten.',
183
+ default: [],
184
+ per_module: true
185
+
186
+ config_attr :modules,
187
+ command_line: ['--modules Mod1,Mod2,…ModN', Array],
188
+ description: 'List of modules to document. Use the config file to set per-module ' \
189
+ "build flags, see 'Documenting multiple modules' in the README.",
178
190
  default: []
179
191
 
180
192
  alias_config_attr :xcodebuild_arguments, :build_tool_arguments,
@@ -185,19 +197,23 @@ module Jazzy
185
197
  command_line: ['-s', '--sourcekitten-sourcefile filepath1,…filepathN',
186
198
  Array],
187
199
  description: 'File(s) generated from sourcekitten output to parse',
188
- parse: ->(paths) { [paths].flatten.map { |path| expand_path(path) } }
200
+ parse: ->(paths) { [paths].flatten.map { |path| expand_path(path) } },
201
+ default: [],
202
+ per_module: true
189
203
 
190
204
  config_attr :source_directory,
191
205
  command_line: '--source-directory DIRPATH',
192
206
  description: 'The directory that contains the source to be documented',
193
207
  default: Pathname.pwd,
194
- parse: ->(sd) { expand_path(sd) }
208
+ parse: ->(sd) { expand_path(sd) },
209
+ per_module: true
195
210
 
196
211
  config_attr :symbolgraph_directory,
197
212
  command_line: '--symbolgraph-directory DIRPATH',
198
213
  description: 'A directory containing a set of Swift Symbolgraph files ' \
199
214
  'representing the module to be documented',
200
- parse: ->(sd) { expand_path(sd) }
215
+ parse: ->(sd) { expand_path(sd) },
216
+ per_module: true
201
217
 
202
218
  config_attr :excluded_files,
203
219
  command_line: ['-e', '--exclude filepath1,filepath2,…filepathN', Array],
@@ -261,7 +277,8 @@ module Jazzy
261
277
  config_attr :module_name,
262
278
  command_line: ['-m', '--module MODULE_NAME'],
263
279
  description: 'Name of module being documented. (e.g. RealmSwift)',
264
- default: ''
280
+ default: '',
281
+ per_module: true
265
282
 
266
283
  config_attr :version,
267
284
  command_line: '--module-version VERSION',
@@ -284,6 +301,10 @@ module Jazzy
284
301
  description: 'The path to a markdown README file',
285
302
  parse: ->(rp) { expand_path(rp) }
286
303
 
304
+ config_attr :readme_title,
305
+ command_line: '--readme-title TITLE',
306
+ description: 'The title for the README in the generated documentation'
307
+
287
308
  config_attr :documentation_glob,
288
309
  command_line: '--documentation GLOB',
289
310
  description: 'Glob that matches available documentation',
@@ -317,6 +338,13 @@ module Jazzy
317
338
  command_line: '--docset-path DIRPATH',
318
339
  description: 'The relative path for the generated docset'
319
340
 
341
+ config_attr :docset_title,
342
+ command_line: '--docset-title TITLE',
343
+ description: 'The title of the generated docset. A simplified version ' \
344
+ 'is used for the filenames associated with the docset. If the ' \
345
+ 'option is not set then the name of the module being documented is ' \
346
+ 'used as the docset title.'
347
+
320
348
  # ──────── URLs ────────
321
349
 
322
350
  config_attr :root_url,
@@ -488,6 +516,23 @@ module Jazzy
488
516
  '--min-acl is set to `public` or `open`.',
489
517
  default: false
490
518
 
519
+ MERGE_MODULES = %w[all extensions none].freeze
520
+
521
+ config_attr :merge_modules,
522
+ command_line: "--merge-modules #{MERGE_MODULES.join(' | ')}",
523
+ description: 'Control how to display declarations from multiple ' \
524
+ 'modules. `all`, the default, places all declarations of the ' \
525
+ "same kind together. `none` keeps each module's declarations " \
526
+ 'separate. `extensions` is like `none` but merges ' \
527
+ 'cross-module extensions into their extended type.',
528
+ default: 'all',
529
+ parse: ->(merge) do
530
+ return merge.to_sym if MERGE_MODULES.include?(merge)
531
+
532
+ raise "Unsupported merge_modules #{merge}, " \
533
+ "supported values: #{MERGE_MODULES.join(', ')}"
534
+ end
535
+
491
536
  # rubocop:enable Layout/ArgumentAlignment
492
537
 
493
538
  def initialize
@@ -507,12 +552,7 @@ module Jazzy
507
552
  config.parse_config_file
508
553
  PodspecDocumenter.apply_config_defaults(config.podspec, config)
509
554
 
510
- if config.root_url
511
- config.dash_url ||= URI.join(
512
- config.root_url,
513
- "docsets/#{config.module_name}.xml",
514
- )
515
- end
555
+ config.set_module_configs
516
556
 
517
557
  config.validate
518
558
 
@@ -569,11 +609,13 @@ module Jazzy
569
609
  puts "Using config file #{config_path}"
570
610
  config_file = read_config_file(config_path)
571
611
 
572
- attrs_by_conf_key, attrs_by_name = %i[config_file_key name].map do |prop|
573
- self.class.all_config_attrs.group_by(&prop)
574
- end
612
+ attrs_by_conf_key, attrs_by_name = grouped_attributes
575
613
 
576
- config_file.each do |key, value|
614
+ parse_config_hash(config_file, attrs_by_conf_key, attrs_by_name)
615
+ end
616
+
617
+ def parse_config_hash(hash, attrs_by_conf_key, attrs_by_name, override: false)
618
+ hash.each do |key, value|
577
619
  unless attr = attrs_by_conf_key[key]
578
620
  message = "Unknown config file attribute #{key.inspect}"
579
621
  if matching_name = attrs_by_name[key]
@@ -583,11 +625,19 @@ module Jazzy
583
625
  warning message
584
626
  next
585
627
  end
586
-
587
- attr.first.set_if_unconfigured(self, value)
628
+ setter = override ? :set : :set_if_unconfigured
629
+ attr.first.method(setter).call(self, value)
588
630
  end
631
+ end
589
632
 
590
- self.base_path = nil
633
+ # Find keyed versions of the attributes, by config file key and then name-in-code
634
+ # Optional block allows filtering/overriding of attribute list.
635
+ def grouped_attributes
636
+ attrs = self.class.all_config_attrs
637
+ attrs = yield attrs if block_given?
638
+ %i[config_file_key name].map do |property|
639
+ attrs.group_by(&property)
640
+ end
591
641
  end
592
642
 
593
643
  def validate
@@ -598,6 +648,19 @@ module Jazzy
598
648
  '`source_host_url` or `source_host_files_url`.'
599
649
  end
600
650
 
651
+ if modules_configured && module_name_configured
652
+ raise 'Options `modules` and `module` are both set which is not supported. ' \
653
+ 'To document multiple modules, use just `modules`.'
654
+ end
655
+
656
+ if modules_configured && podspec_configured
657
+ raise 'Options `modules` and `podspec` are both set which is not supported.'
658
+ end
659
+
660
+ module_configs.each(&:validate_module)
661
+ end
662
+
663
+ def validate_module
601
664
  if objc_mode &&
602
665
  build_tool_arguments_configured &&
603
666
  (framework_root_configured || umbrella_header_configured)
@@ -608,6 +671,76 @@ module Jazzy
608
671
 
609
672
  # rubocop:enable Metrics/MethodLength
610
673
 
674
+ # Module Configs
675
+ #
676
+ # The user can enter module information in three different ways. This
677
+ # consolidates them into one view for the rest of the code.
678
+ #
679
+ # 1) Single module, back-compatible
680
+ # --module Foo etc etc (or not given at all)
681
+ #
682
+ # 2) Multiple modules, simple, sharing build params
683
+ # --modules Foo,Bar,Baz --source-directory Xyz
684
+ #
685
+ # 3) Multiple modules, custom, different build params but
686
+ # inheriting others from the top level.
687
+ # This is config-file only.
688
+ # - modules
689
+ # - module: Foo
690
+ # source_directory: Xyz
691
+ # build_tool_arguments: [a, b, c]
692
+ #
693
+ # After this we're left with `config.module_configs` that is an
694
+ # array of `Config` objects.
695
+
696
+ attr_reader :module_configs
697
+ attr_reader :module_names
698
+
699
+ def set_module_configs
700
+ @module_configs = parse_module_configs
701
+ @module_names = module_configs.map(&:module_name)
702
+ @module_names_set = Set.new(module_names)
703
+ end
704
+
705
+ def module_name?(name)
706
+ @module_names_set.include?(name)
707
+ end
708
+
709
+ def multiple_modules?
710
+ @module_names.count > 1
711
+ end
712
+
713
+ def parse_module_configs
714
+ return [self] unless modules_configured
715
+
716
+ raise 'Config file key `modules` must be an array' unless modules.is_a?(Array)
717
+
718
+ if modules.first.is_a?(String)
719
+ # Massage format (2) into (3)
720
+ self.modules = modules.map { { 'module' => _1 } }
721
+ end
722
+
723
+ # Allow per-module overrides of only some config options
724
+ attrs_by_conf_key, attrs_by_name =
725
+ grouped_attributes { _1.select(&:per_module) }
726
+
727
+ modules.map do |module_hash|
728
+ mod_name = module_hash['module'] || ''
729
+ raise 'Missing `modules.module` config key' if mod_name.empty?
730
+
731
+ dup.tap do |module_config|
732
+ module_config.parse_config_hash(
733
+ module_hash, attrs_by_conf_key, attrs_by_name, override: true
734
+ )
735
+ end
736
+ end
737
+ end
738
+
739
+ # For podspec query
740
+ def module_name_known?
741
+ module_name_configured || modules_configured
742
+ end
743
+
611
744
  def locate_config_file
612
745
  return config_file if config_file
613
746
 
@@ -615,7 +748,6 @@ module Jazzy
615
748
  candidate = dir.join('.jazzy.yaml')
616
749
  return candidate if candidate.exist?
617
750
  end
618
-
619
751
  nil
620
752
  end
621
753
 
data/lib/jazzy/doc.rb CHANGED
@@ -48,9 +48,9 @@ module Jazzy
48
48
  elsif config.version_configured
49
49
  # Fake version for integration tests
50
50
  version = ENV['JAZZY_FAKE_MODULE_VERSION'] || config.version
51
- "#{config.module_name} #{version} Docs"
51
+ "#{config.module_configs.first.module_name} #{version} Docs"
52
52
  else
53
- "#{config.module_name} Docs"
53
+ "#{config.module_configs.first.module_name} Docs"
54
54
  end
55
55
  end
56
56
 
@@ -67,32 +67,32 @@ module Jazzy
67
67
 
68
68
  # Build documentation from the given options
69
69
  # @param [Config] options
70
- # @return [SourceModule] the documented source module
71
70
  def self.build(options)
72
- if options.sourcekitten_sourcefile_configured
73
- stdout = "[#{options.sourcekitten_sourcefile.map(&:read).join(',')}]"
74
- elsif options.podspec_configured
75
- pod_documenter = PodspecDocumenter.new(options.podspec)
76
- stdout = pod_documenter.sourcekitten_output(options)
77
- elsif options.swift_build_tool == :symbolgraph
78
- stdout = SymbolGraph.build(options)
79
- else
80
- stdout = Dir.chdir(options.source_directory) do
81
- arguments = SourceKitten.arguments_from_options(options)
82
- SourceKitten.run_sourcekitten(arguments)
71
+ module_jsons = options.module_configs.map do |module_config|
72
+ if module_config.podspec_configured
73
+ # Config#validate guarantees not multi-module here
74
+ pod_documenter = PodspecDocumenter.new(options.podspec)
75
+ pod_documenter.sourcekitten_output(options)
76
+ elsif !module_config.sourcekitten_sourcefile.empty?
77
+ "[#{module_config.sourcekitten_sourcefile.map(&:read).join(',')}]"
78
+ elsif module_config.swift_build_tool == :symbolgraph
79
+ SymbolGraph.build(module_config)
80
+ else
81
+ Dir.chdir(module_config.source_directory) do
82
+ arguments = SourceKitten.arguments_from_options(module_config)
83
+ SourceKitten.run_sourcekitten(arguments)
84
+ end
83
85
  end
84
86
  end
85
87
 
86
- build_docs_for_sourcekitten_output(stdout, options)
88
+ build_docs_for_sourcekitten_output(module_jsons, options)
87
89
  end
88
90
 
89
91
  # Build & write HTML docs to disk from structured docs array
90
92
  # @param [String] output_dir Root directory to write docs
91
- # @param [Array] docs Array of structured docs
92
- # @param [Config] options Build options
93
- # @param [Array] doc_structure @see #doc_structure_for_docs
94
- def self.build_docs(output_dir, docs, source_module)
95
- each_doc(output_dir, docs) do |doc, path|
93
+ # @param [SourceModule] source_module All info to generate docs
94
+ def self.build_docs(output_dir, source_module)
95
+ each_doc(output_dir, source_module.docs) do |doc, path|
96
96
  prepare_output_dir(path.parent, false)
97
97
  depth = path.relative_path_from(output_dir).each_filename.count - 1
98
98
  path_to_root = '../' * depth
@@ -124,10 +124,13 @@ module Jazzy
124
124
 
125
125
  docs << SourceDocument.make_index(options.readme_path)
126
126
 
127
- source_module = SourceModule.new(options, docs, structure, coverage)
128
-
129
127
  output_dir = options.output
130
- build_docs(output_dir, source_module.docs, source_module)
128
+
129
+ docset_builder = DocsetBuilder.new(output_dir)
130
+
131
+ source_module = SourceModule.new(docs, structure, coverage, docset_builder)
132
+
133
+ build_docs(output_dir, source_module)
131
134
 
132
135
  unless options.disable_search
133
136
  warn 'building search index'
@@ -137,7 +140,7 @@ module Jazzy
137
140
  copy_extensions(source_module, output_dir)
138
141
  copy_theme_assets(output_dir)
139
142
 
140
- DocsetBuilder.new(output_dir, source_module).build!
143
+ docset_builder.build!(source_module.all_declarations)
141
144
 
142
145
  generate_badge(source_module.doc_coverage, options)
143
146
 
@@ -148,14 +151,12 @@ module Jazzy
148
151
  end
149
152
 
150
153
  # Build docs given sourcekitten output
151
- # @param [String] sourcekitten_output Output of sourcekitten command
154
+ # @param [Array<String>] sourcekitten_output Output of sourcekitten command for each module
152
155
  # @param [Config] options Build options
153
- # @return [SourceModule] the documented source module
154
156
  def self.build_docs_for_sourcekitten_output(sourcekitten_output, options)
155
157
  (docs, stats) = SourceKitten.parse(
156
158
  sourcekitten_output,
157
- options.min_acl,
158
- options.skip_undocumented,
159
+ options,
159
160
  DocumentationGenerator.source_docs,
160
161
  )
161
162
 
@@ -249,7 +250,8 @@ module Jazzy
249
250
  doc[:doc_coverage] = source_module.doc_coverage unless
250
251
  Config.instance.hide_documentation_coverage
251
252
  doc[:structure] = source_module.doc_structure
252
- doc[:module_name] = source_module.name
253
+ doc[:readme_title] = source_module.readme_title
254
+ doc[:module_name] = doc[:readme_title]
253
255
  doc[:author_name] = source_module.author_name
254
256
  if source_host = source_module.host
255
257
  doc[:source_host_name] = source_host.name
@@ -259,7 +261,8 @@ module Jazzy
259
261
  doc[:github_url] = doc[:source_host_url]
260
262
  doc[:github_token_url] = doc[:source_host_item_url]
261
263
  end
262
- doc[:dash_url] = source_module.dash_url
264
+ doc[:dash_url] = source_module.dash_feed_url
265
+ doc[:breadcrumbs] = make_breadcrumbs(doc_model)
263
266
  end
264
267
  end
265
268
 
@@ -269,7 +272,7 @@ module Jazzy
269
272
  # @param [String] path_to_root
270
273
  def self.document_markdown(source_module, doc_model, path_to_root)
271
274
  doc = new_document(source_module, doc_model)
272
- name = doc_model.name == 'index' ? source_module.name : doc_model.name
275
+ name = doc_model.readme? ? source_module.readme_title : doc_model.name
273
276
  doc[:name] = name
274
277
  doc[:overview] = render(doc_model, doc_model.content(source_module))
275
278
  doc[:path_to_root] = path_to_root
@@ -447,5 +450,28 @@ module Jazzy
447
450
  doc.render.gsub(ELIDED_AUTOLINK_TOKEN, path_to_root)
448
451
  end
449
452
  # rubocop:enable Metrics/MethodLength
453
+
454
+ # Breadcrumbs for a page - doesn't include the top 'readme' crumb
455
+ def self.make_breadcrumbs(doc_model)
456
+ return [] if doc_model.readme?
457
+
458
+ docs_path = doc_model.docs_path
459
+ breadcrumbs = docs_path.map do |doc|
460
+ {
461
+ name: doc.name,
462
+ url: doc.url,
463
+ last: doc == doc_model,
464
+ }
465
+ end
466
+
467
+ return breadcrumbs if breadcrumbs.count == 1
468
+
469
+ # Add the module name to the outer type if not clear from context
470
+ if docs_path[1].ambiguous_module_name?(docs_path[0].name)
471
+ breadcrumbs[1][:name] = docs_path[1].fully_qualified_module_name
472
+ end
473
+
474
+ breadcrumbs
475
+ end
450
476
  end
451
477
  end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jazzy
4
+ # This class stores an index of symbol names for doing name lookup
5
+ # when resolving custom categories and autolinks.
6
+ class DocIndex
7
+ # A node in the index tree. The root has no decl; its children are
8
+ # per-module indexed by module names. The second level, where each
9
+ # scope is a module, also has no decl; its children are scopes, one
10
+ # for each top-level decl in the module. From the third level onwards
11
+ # the decl is valid.
12
+ class Scope
13
+ attr_reader :decl # SourceDeclaration
14
+ attr_reader :children # String:Scope
15
+
16
+ def initialize(decl, children)
17
+ @decl = decl
18
+ @children = children
19
+ end
20
+
21
+ def self.new_root(module_decls)
22
+ new(nil,
23
+ module_decls.transform_values do |decls|
24
+ Scope.new_decl(nil, decls)
25
+ end)
26
+ end
27
+
28
+ # Decl names in a scope are usually unique. The exceptions
29
+ # are (1) methods and (2) typealias+extension, which historically
30
+ # jazzy does not merge. The logic here and in `merge()` below
31
+ # preserves the historical ambiguity-resolution of (1) and tries
32
+ # to do the best for (2).
33
+ def self.new_decl(decl, child_decls)
34
+ child_scopes = {}
35
+ child_decls.flat_map do |child_decl|
36
+ child_scope = Scope.new_decl(child_decl, child_decl.children)
37
+ child_decl.index_names.map do |name|
38
+ if curr = child_scopes[name]
39
+ curr.merge(child_scope)
40
+ else
41
+ child_scopes[name] = child_scope
42
+ end
43
+ end
44
+ end
45
+ new(decl, child_scopes)
46
+ end
47
+
48
+ def merge(new_scope)
49
+ return unless type = decl&.type
50
+ return unless new_type = new_scope.decl&.type
51
+
52
+ if type.swift_typealias? && new_type.swift_extension?
53
+ @children = new_scope.children
54
+ elsif type.swift_extension? && new_type.swift_typealias?
55
+ @decl = new_scope.decl
56
+ end
57
+ end
58
+
59
+ # Lookup of a name like `Mod.Type.method(arg:)` requires passing
60
+ # an array of name 'parts' eg. ['Mod', 'Type', 'method(arg:)'].
61
+ def lookup(parts)
62
+ return decl if parts.empty?
63
+
64
+ children[parts.first]&.lookup(parts[1...])
65
+ end
66
+
67
+ # Get an array of scopes matching the name parts.
68
+ def lookup_path(parts)
69
+ [self] +
70
+ (children[parts.first]&.lookup_path(parts[1...]) || [])
71
+ end
72
+ end
73
+
74
+ attr_reader :root_scope
75
+
76
+ def initialize(all_decls)
77
+ @root_scope = Scope.new_root(all_decls.group_by(&:module_name))
78
+ end
79
+
80
+ # Look up a name and return the matching SourceDeclaration or nil.
81
+ #
82
+ # `context` is an optional SourceDeclaration indicating where the text
83
+ # was found, affects name resolution - see `lookup_context()` below.
84
+ def lookup(name, context = nil)
85
+ lookup_name = LookupName.new(name)
86
+
87
+ return lookup_fully_qualified(lookup_name) if lookup_name.fully_qualified?
88
+ return lookup_guess(lookup_name) if context.nil?
89
+
90
+ lookup_context(lookup_name, context)
91
+ end
92
+
93
+ private
94
+
95
+ # Look up a fully-qualified name, ie. it starts with the module name.
96
+ def lookup_fully_qualified(lookup_name)
97
+ root_scope.lookup(lookup_name.parts)
98
+ end
99
+
100
+ # Look up a top-level name best-effort, searching for a module that
101
+ # has it before trying the first name-part as a module name.
102
+ def lookup_guess(lookup_name)
103
+ root_scope.children.each_value do |module_scope|
104
+ if result = module_scope.lookup(lookup_name.parts)
105
+ return result
106
+ end
107
+ end
108
+
109
+ lookup_fully_qualified(lookup_name)
110
+ end
111
+
112
+ # Look up a name from a declaration context, approximately how
113
+ # Swift resolves names.
114
+ #
115
+ # 1 - try and resolve with a common prefix, eg. 'B' from 'T.A'
116
+ # can match 'T.B', or 'R' from 'S.T.A' can match 'S.R'.
117
+ # 2 - try and resolve as a top-level symbol from a different module
118
+ # 3 - (affordance for docs writers) resolve as a child of the context,
119
+ # eg. 'B' from 'T.A' can match 'T.A.B' *only if* (1,2) fail.
120
+ # Currently disabled for Swift for back-compatibility.
121
+ def lookup_context(lookup_name, context)
122
+ context_scope_path =
123
+ root_scope.lookup_path(context.fully_qualified_module_name_parts)
124
+
125
+ context_scope = context_scope_path.pop
126
+ context_scope_path.reverse.each do |scope|
127
+ if decl = scope.lookup(lookup_name.parts)
128
+ return decl
129
+ end
130
+ end
131
+
132
+ lookup_guess(lookup_name) ||
133
+ (lookup_name.objc? && context_scope.lookup(lookup_name.parts))
134
+ end
135
+
136
+ # Helper for name lookup, really a cache for information as we
137
+ # try various strategies.
138
+ class LookupName
139
+ attr_reader :name
140
+
141
+ def initialize(name)
142
+ @name = name
143
+ end
144
+
145
+ def fully_qualified?
146
+ name.start_with?('/')
147
+ end
148
+
149
+ def objc?
150
+ name.start_with?('-', '+')
151
+ end
152
+
153
+ def parts
154
+ @parts ||= find_parts
155
+ end
156
+
157
+ private
158
+
159
+ # Turn a name as written into a list of components to
160
+ # be matched.
161
+ # Swift: Strip out odd characters and split
162
+ # ObjC: Compound names look like '+[Class(Category) method:]'
163
+ # and need to become ['Class(Category)', '+method:']
164
+ def find_parts
165
+ if name =~ /([+-])\[(\w+(?: ?\(\w+\))?) ([\w:]+)\]/
166
+ [Regexp.last_match[2],
167
+ Regexp.last_match[1] + Regexp.last_match[3]]
168
+ else
169
+ name
170
+ .sub(%r{^[@\/]}, '') # ignore custom attribute reference, fully-qualified
171
+ .gsub(/<.*?>/, '') # remove generic parameters
172
+ .split(%r{(?<!\.)[/.](?!\.)}) # dot or slash, but not '...'
173
+ .reject(&:empty?)
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ class SourceDeclaration
180
+ # Names for a symbol. Permits function parameters to be omitted.
181
+ def index_names
182
+ [name, name.sub(/\(.*\)/, '(...)')].uniq
183
+ end
184
+ end
185
+ end
@@ -3,7 +3,7 @@
3
3
  <plist version="1.0">
4
4
  <dict>
5
5
  <key>CFBundleIdentifier</key>
6
- <string>com.jazzy.{{lowercase_name}}</string>
6
+ <string>com.jazzy.{{lowercase_safe_name}}</string>
7
7
  <key>CFBundleName</key>
8
8
  <string>{{name}}</string>
9
9
  <key>DocSetPlatformFamily</key>