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
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>