spreen-wiki 0.1.0 → 0.2.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -7
  3. data/exe/spreen +2 -2
  4. data/lib/spreen-wiki.rb +2 -2
  5. data/lib/spreen_wiki/application.rb +149 -0
  6. data/lib/spreen_wiki/cli.rb +157 -0
  7. data/lib/spreen_wiki/configuration.rb +215 -0
  8. data/lib/spreen_wiki/home.rb +100 -0
  9. data/lib/spreen_wiki/sidebar.rb +48 -0
  10. data/lib/spreen_wiki/unknown_wiki_count_list_exporter.rb +59 -0
  11. data/lib/spreen_wiki/unknown_wiki_list_exporter_for_llm.rb +51 -0
  12. data/lib/{spreen/wiki → spreen_wiki}/version.rb +2 -4
  13. data/lib/spreen_wiki.rb +18 -0
  14. data/sig/generated/spreen_wiki/application.rbs +91 -0
  15. data/sig/generated/spreen_wiki/cli.rbs +68 -0
  16. data/sig/generated/spreen_wiki/configuration.rbs +148 -0
  17. data/sig/generated/spreen_wiki/home.rbs +52 -0
  18. data/sig/generated/spreen_wiki/sidebar.rbs +29 -0
  19. data/sig/generated/spreen_wiki/unknown_wiki_count_list_exporter.rbs +38 -0
  20. data/sig/generated/spreen_wiki/unknown_wiki_list_exporter_for_llm.rbs +35 -0
  21. data/sig/generated/spreen_wiki/version.rbs +5 -0
  22. data/sig/generated/spreen_wiki.rbs +8 -0
  23. metadata +23 -23
  24. data/lib/spreen/wiki/application.rb +0 -151
  25. data/lib/spreen/wiki/cli.rb +0 -159
  26. data/lib/spreen/wiki/configuration.rb +0 -217
  27. data/lib/spreen/wiki/home.rb +0 -102
  28. data/lib/spreen/wiki/sidebar.rb +0 -50
  29. data/lib/spreen/wiki/unknown_wiki_count_list_exporter.rb +0 -61
  30. data/lib/spreen/wiki/unknown_wiki_list_exporter_for_llm.rb +0 -53
  31. data/lib/spreen/wiki.rb +0 -20
  32. data/sig/generated/spreen/wiki/application.rbs +0 -93
  33. data/sig/generated/spreen/wiki/cli.rbs +0 -70
  34. data/sig/generated/spreen/wiki/configuration.rbs +0 -150
  35. data/sig/generated/spreen/wiki/home.rbs +0 -54
  36. data/sig/generated/spreen/wiki/sidebar.rbs +0 -31
  37. data/sig/generated/spreen/wiki/unknown_wiki_count_list_exporter.rbs +0 -40
  38. data/sig/generated/spreen/wiki/unknown_wiki_list_exporter_for_llm.rbs +0 -37
  39. data/sig/generated/spreen/wiki/version.rbs +0 -7
  40. data/sig/generated/spreen/wiki.rbs +0 -10
  41. /data/lib/{spreen/wiki → spreen_wiki}/templates/category/english.md +0 -0
  42. /data/lib/{spreen/wiki → spreen_wiki}/templates/category/japanese.md +0 -0
  43. /data/lib/{spreen/wiki → spreen_wiki}/templates/owner/english.md +0 -0
  44. /data/lib/{spreen/wiki → spreen_wiki}/templates/owner/japanese.md +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae1421288cd76d1d43326a72cdfd2c2c2dd9bb617becd061468e7a2789f80b31
4
- data.tar.gz: 3ab8bc5f1d779379fe2137cd0869071150c06fc74ce4566cda84c57a03a6d5af
3
+ metadata.gz: 4e9507fb9c2097bc8c1221f21934eebeef6eac80a6e85ec217afed7a031a2db9
4
+ data.tar.gz: 5ce2f2ca5db1a5c9244b475d3866ca32d7463b4deb5beb4cfca270c869b1051a
5
5
  SHA512:
6
- metadata.gz: d2658be24738c7ad2a5a9738f45dcb5505b8fa1c4054554e9c8384e5782df2c01027e5aff529ae59715123343f472ec4dab0e1cac36008a66be0128401311090
7
- data.tar.gz: 56454356e70d3d8d6a7c3013084d5c3babf65d9e4042ce1f8f55a306b90169ff9184753d65e20ff058a6f4bdf7e1f6f74b64d2c28f0d73f5a994082f43e65211
6
+ metadata.gz: f27d7c79f9e9e3fd7185471eca0b83c08ccbf441c2e03ec37ea578a944b5d1ac459bdde7af863e4332b072e090bda90698bf01b39de37b21ff2f301fce2f12c5
7
+ data.tar.gz: 2ce2882fb724ca7066026b5b2fbb6468f43c81d617c9cc9af4c8d56d8636b7850c39899018bc3a4ca3a1f0ea3f6d10bcf6595f3100f3d185de905f95ec9d6545
data/README.md CHANGED
@@ -35,12 +35,12 @@ Persistent settings, custom labels and additional languages go into a [`.spreen.
35
35
  ## 3. Library Usage
36
36
 
37
37
  ```ruby
38
- require 'spreen/wiki'
38
+ require 'spreen_wiki'
39
39
 
40
- Spreen::Wiki::Home.run(base_path: '.', organisation: 'your-org', repository: 'your-repo')
41
- Spreen::Wiki::Sidebar.run(base_path: '.', organisation: 'your-org', repository: 'your-repo')
42
- Spreen::Wiki::UnknownWikiCountListExporter.run(base_path: '.', output: 'report.txt')
43
- Spreen::Wiki::UnknownWikiListExporterForLLM.run(base_path: '.', group_by: 'Category', language: 'Japanese')
40
+ SpreenWiki::Home.run(base_path: '.', organisation: 'your-org', repository: 'your-repo')
41
+ SpreenWiki::Sidebar.run(base_path: '.', organisation: 'your-org', repository: 'your-repo')
42
+ SpreenWiki::UnknownWikiCountListExporter.run(base_path: '.', output: 'report.txt')
43
+ SpreenWiki::UnknownWikiListExporterForLLM.run(base_path: '.', group_by: 'Category', language: 'Japanese')
44
44
  ```
45
45
 
46
46
  All classes accept `base_path:`, `group_by:`, `language:`, `home_overflow:` plus the configuration keywords `organisation:`, `repository:`, `wiki_url:`, `owner_base_url:`, `template_dir:`, `excluded_dirs:`, `config_path:` (and `output:` on the exporters).
@@ -49,8 +49,9 @@ All classes accept `base_path:`, `group_by:`, `language:`, `home_overflow:` plus
49
49
 
50
50
  ### 4-1. Environment
51
51
 
52
- - Ruby 4.0.5
53
- - Bundler 4.0.x
52
+ - Ruby 4.0.6
53
+ - Gemfile 4.0.16
54
+ - Bundler 4.0.16
54
55
 
55
56
  ```command
56
57
  $ bundle install
data/exe/spreen CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'spreen/wiki'
4
+ require 'spreen_wiki'
5
5
 
6
- exit Spreen::Wiki::CLI.start
6
+ exit SpreenWiki::CLI.start
data/lib/spreen-wiki.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  # Convenience shim so that both `require 'spreen-wiki'` (the gem name) and
5
- # `require 'spreen/wiki'` (the conventional path) work.
6
- require_relative 'spreen/wiki'
5
+ # `require 'spreen_wiki'` (the conventional path) work.
6
+ require_relative 'spreen_wiki'
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require_relative 'configuration'
5
+
6
+ module SpreenWiki
7
+ # Base class for the wiki-organising commands. Loads the wiki tree under
8
+ # base_path, groups it by Owner or Category in either English or Japanese,
9
+ # and exposes the resulting maps to its subclasses. Grouping criteria,
10
+ # languages and labels are resolved through Configuration and can be
11
+ # extended via a `.spreen.yml` file.
12
+ class Application
13
+ class NotImplementedError < StandardError; end
14
+
15
+ # @rbs base_path: String
16
+ # @rbs group_by: String
17
+ # @rbs language: String
18
+ # @rbs home_overflow: (bool | String)
19
+ # @rbs **options: untyped
20
+ # @rbs return: untyped
21
+ def self.run(base_path: Dir.pwd, group_by: 'Owner', language: 'English', home_overflow: 'false', **)
22
+ instance = new(base_path:, group_by:, language:, home_overflow:, **)
23
+ instance.validate!
24
+ instance.run
25
+ end
26
+
27
+ # @rbs base_path: String
28
+ # @rbs group_by: String
29
+ # @rbs language: String
30
+ # @rbs home_overflow: (bool | String)
31
+ # @rbs config: Configuration?
32
+ # @rbs **options: untyped
33
+ # @rbs return: void
34
+ def initialize(base_path: Dir.pwd, group_by: 'Owner', language: 'English', home_overflow: 'false',
35
+ config: nil, **)
36
+ @base_path = base_path
37
+ @group_by = group_by
38
+ @language = language
39
+ @home_overflow = parse_home_overflow(home_overflow)
40
+ @config = config || Configuration.new(base_path:, **)
41
+ @path_to_home = File.join(base_path, 'Home.md')
42
+ @path_to_sidebar = File.join(base_path, '_Sidebar.md')
43
+ @paths_to_wikis = Dir[File.join(base_path, '**', '*.md')]
44
+ @excluded_paths = @config.excluded_dirs.flat_map { |dir| Dir[File.join(base_path, dir, '**', '*.md')] }
45
+ end
46
+
47
+ # @rbs return: void
48
+ def validate!
49
+ raise ArgumentError, "Invalid group_by: `#{group_by}`" unless config.group_bys.include?(group_by)
50
+ raise ArgumentError, "Invalid language: `#{language}`" unless config.languages(group_by).include?(language)
51
+
52
+ validate_home_overflow!
53
+ end
54
+
55
+ # @rbs return: untyped
56
+ def run
57
+ raise NotImplementedError, 'This method must be implemented in each subclass.'
58
+ end
59
+
60
+ private
61
+
62
+ attr_reader :base_path,
63
+ :group_by,
64
+ :language,
65
+ :home_overflow,
66
+ :config,
67
+ :path_to_home,
68
+ :path_to_sidebar,
69
+ :paths_to_wikis,
70
+ :excluded_paths
71
+
72
+ # @rbs return: void
73
+ def validate_home_overflow!
74
+ return if [true, false].include?(home_overflow)
75
+
76
+ raise ArgumentError, "Invalid home_overflow: `#{home_overflow}` must be boolean"
77
+ end
78
+
79
+ # @rbs raw: (bool | String)
80
+ # @rbs return: (bool | String)
81
+ def parse_home_overflow(raw)
82
+ case raw
83
+ when 'true' then true
84
+ when 'false' then false
85
+ else raw
86
+ end
87
+ end
88
+
89
+ # @rbs return: Array[String]
90
+ def target_paths
91
+ @target_paths ||= paths_to_wikis.reject do |path_to_wiki|
92
+ path_to_wiki == path_to_home ||
93
+ path_to_wiki == path_to_sidebar ||
94
+ excluded_paths.include?(path_to_wiki)
95
+ end
96
+ end
97
+
98
+ # @rbs return: Regexp
99
+ def target_regexp
100
+ @target_regexp ||= config.target_regexp(group_by)
101
+ end
102
+
103
+ # @rbs return: String
104
+ def no_declaration
105
+ @no_declaration ||= config.no_declaration(group_by, language)
106
+ end
107
+
108
+ # @rbs return: Hash[String, Array[String]]
109
+ def wiki_maps_with_namespace
110
+ @wiki_maps_with_namespace ||= begin
111
+ named = Hash.new { |hash, namespace| hash[namespace] = [] } #: Hash[String, Array[String]]
112
+ undeclared = Hash.new { |hash, namespace| hash[namespace] = [] } #: Hash[String, Array[String]]
113
+ target_paths.each { |target_path| populate_namespace(target_path, named, undeclared) }
114
+ named.sort.to_h.merge(undeclared)
115
+ end
116
+ end
117
+
118
+ # @rbs target_path: String
119
+ # @rbs named: Hash[String, Array[String]]
120
+ # @rbs undeclared: Hash[String, Array[String]]
121
+ # @rbs return: void
122
+ def populate_namespace(target_path, named, undeclared)
123
+ return unless File.exist?(target_path)
124
+
125
+ namespace = namespace_for(target_path)
126
+ bucket = namespace == no_declaration ? undeclared : named
127
+ bucket[namespace] << File.basename(target_path)
128
+ end
129
+
130
+ # @rbs target_path: String
131
+ # @rbs return: String
132
+ def namespace_for(target_path)
133
+ declaration = File.open(target_path, &:gets)
134
+ return no_declaration unless declaration&.start_with?(target_regexp)
135
+
136
+ declaration.chomp.gsub(target_regexp, '')
137
+ end
138
+
139
+ # @rbs return: Hash[String, Array[String]]
140
+ def owned_wiki_maps
141
+ @owned_wiki_maps ||= wiki_maps_with_namespace.select { |namespace, _| namespace.include?('@') }
142
+ end
143
+
144
+ # @rbs return: Hash[String, Array[String]]
145
+ def plain_wiki_maps
146
+ @plain_wiki_maps ||= wiki_maps_with_namespace.reject { |namespace, _| namespace.include?('@') }
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require 'optparse'
5
+ require_relative 'version'
6
+ require_relative 'home'
7
+ require_relative 'sidebar'
8
+ require_relative 'unknown_wiki_count_list_exporter'
9
+ require_relative 'unknown_wiki_list_exporter_for_llm'
10
+
11
+ module SpreenWiki
12
+ # Command line interface behind the `spreen` executable:
13
+ # `spreen <update|count-report|llm-export> [options]`.
14
+ class CLI
15
+ HELP = <<~HELP
16
+ Usage: spreen <command> [options]
17
+
18
+ Commands:
19
+ update Regenerate Home.md and _Sidebar.md from the wiki tree
20
+ count-report Export a count report of the unknown-namespace wikis
21
+ llm-export Export the unknown-namespace wiki list for an LLM
22
+ version Print the version
23
+
24
+ Run `spreen <command> --help` to list the command's options.
25
+ HELP
26
+
27
+ # Flags shared by every command, mapped to the keyword arguments of the
28
+ # underlying classes. --exclude and the per-command flags are wired up
29
+ # separately because they need a type or a fixed value.
30
+ COMMON_OPTIONS = {
31
+ '--path PATH' => [:base_path, 'Path to the wiki clone (default: current directory)'],
32
+ '--group-by GROUP_BY' => [:group_by, 'Grouping criteria (default: Owner)'],
33
+ '--language LANGUAGE' => [:language, 'Language of the labels (default: English)'],
34
+ '--org ORGANISATION' => [:organisation, 'GitHub organisation/user hosting the wiki'],
35
+ '--repo REPOSITORY' => [:repository, 'GitHub repository whose wiki is organised'],
36
+ '--wiki-url URL' => [:wiki_url, 'Wiki URL (overrides the one derived from --org/--repo)'],
37
+ '--template-dir DIR' => [:template_dir, 'Directory of <group_by>/<language>.md Home templates'],
38
+ '--config FILE' => [:config_path, 'Config file path (default: <path>/.spreen.yml)']
39
+ }.freeze
40
+
41
+ # @rbs argv: Array[String]
42
+ # @rbs return: Integer
43
+ def self.start(argv = ARGV)
44
+ new(argv).run
45
+ end
46
+
47
+ # @rbs argv: Array[String]
48
+ # @rbs return: void
49
+ def initialize(argv)
50
+ @argv = argv.dup
51
+ @options = { base_path: Dir.pwd, group_by: 'Owner', language: 'English', home_overflow: 'false' }
52
+ end
53
+
54
+ # @rbs return: Integer
55
+ def run
56
+ command = argv.shift
57
+ case command
58
+ when 'update', 'count-report', 'llm-export' then execute(command)
59
+ when 'version', '--version', '-v' then print_version
60
+ when nil, 'help', '--help', '-h' then print_help(command)
61
+ else unknown_command(command)
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :argv, :options
68
+
69
+ # @rbs command: String
70
+ # @rbs return: Integer
71
+ def execute(command)
72
+ parser(command).parse!(argv)
73
+ __send__(command.tr('-', '_'))
74
+ 0
75
+ rescue ArgumentError, OptionParser::ParseError => e
76
+ warn e.message
77
+ 1
78
+ end
79
+
80
+ # @rbs return: Integer
81
+ def print_version
82
+ puts VERSION
83
+ 0
84
+ end
85
+
86
+ # @rbs command: String?
87
+ # @rbs return: Integer
88
+ def print_help(command)
89
+ puts HELP
90
+ command.nil? ? 1 : 0
91
+ end
92
+
93
+ # @rbs command: String
94
+ # @rbs return: Integer
95
+ def unknown_command(command)
96
+ warn "Unknown command: `#{command}`"
97
+ warn HELP
98
+ 1
99
+ end
100
+
101
+ # @rbs return: void
102
+ def update
103
+ wiki_url = Home.run(**options)
104
+ Sidebar.run(**options)
105
+ puts 'Updated Home.md and _Sidebar.md.'
106
+ puts "Check them out at '#{wiki_url}' !!" if wiki_url
107
+ end
108
+
109
+ # @rbs return: void
110
+ def count_report
111
+ count_list_by_namespace, path_to_export = UnknownWikiCountListExporter.run(**options)
112
+ puts count_list_by_namespace
113
+ puts "Exported the unknown wiki count report to '#{path_to_export}'."
114
+ end
115
+
116
+ # @rbs return: void
117
+ def llm_export
118
+ path_to_export = UnknownWikiListExporterForLLM.run(**options)
119
+ puts "Exported the unknown wiki list for LLM to '#{path_to_export}'."
120
+ end
121
+
122
+ # @rbs command: String
123
+ # @rbs return: OptionParser
124
+ def parser(command)
125
+ OptionParser.new("Usage: spreen #{command} [options]") do |opt|
126
+ add_common_options(opt)
127
+ add_command_options(opt, command)
128
+ end
129
+ end
130
+
131
+ # @rbs opt: OptionParser
132
+ # @rbs return: void
133
+ def add_common_options(opt)
134
+ COMMON_OPTIONS.each do |flag, (key, description)|
135
+ opt.on(flag, description) { |value| options[key] = value }
136
+ end
137
+ opt.on('--exclude DIR1,DIR2', Array, 'Directories to skip while scanning') do |dirs|
138
+ options[:excluded_dirs] = dirs
139
+ end
140
+ end
141
+
142
+ # @rbs opt: OptionParser
143
+ # @rbs command: String
144
+ # @rbs return: void
145
+ def add_command_options(opt, command)
146
+ if command == 'update'
147
+ opt.on('--overflow', 'Split Home into per-namespace pages under wikis-by-owner/') do
148
+ options[:home_overflow] = 'true'
149
+ end
150
+ else
151
+ opt.on('--output FILENAME', 'Export filename (relative to --path unless absolute)') do |filename|
152
+ options[:output] = filename
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require 'yaml'
5
+
6
+ module SpreenWiki
7
+ # Resolves the runtime configuration for a wiki checkout. Every value is
8
+ # looked up with the same precedence: explicit keyword argument, then the
9
+ # `.spreen.yml` file under base_path (or config_path), then the
10
+ # ORGANISATION_NAME environment variable (organisation only), then the
11
+ # built-in defaults.
12
+ class Configuration
13
+ CONFIG_FILENAME = '.spreen.yml'
14
+ EMPTY_HASH = {}.freeze #: Hash[String, untyped]
15
+ DEFAULT_TEMPLATE_DIR = File.expand_path(File.join(__dir__.to_s, 'templates'))
16
+ DEFAULT_EXCLUDED_DIRS = %w[spreen-wiki wikis-by-owner].freeze
17
+
18
+ # Built-in labels: how the first line of a wiki page is parsed (regexp),
19
+ # which namespace collects pages without a declaration (no_declaration),
20
+ # which namespaces the count report covers (unknown_namespaces) and which
21
+ # namespace the LLM export targets (llm_target_namespace). A config file
22
+ # can override any of these, add languages or add group_by criteria.
23
+ DEFAULT_LABELS = {
24
+ 'Owner' => {
25
+ 'regexp' => '[Oo]wner:\s?',
26
+ 'languages' => {
27
+ 'English' => {
28
+ 'no_declaration' => 'Unowned',
29
+ 'unknown_namespaces' => ['Unknown Owner nor Necessity', 'Unowned but Necessary', 'Unowned'],
30
+ 'llm_target_namespace' => 'Unknown Owner nor Necessity'
31
+ },
32
+ 'Japanese' => {
33
+ 'no_declaration' => 'Owner記名なし',
34
+ 'unknown_namespaces' => [
35
+ 'Ownerチームが不明だが必要なページ群',
36
+ 'Ownerチーム・要or不要が不明なページ群',
37
+ 'Owner記名なし'
38
+ ],
39
+ 'llm_target_namespace' => 'Ownerチーム・要or不要が不明なページ群'
40
+ }
41
+ }
42
+ },
43
+ 'Category' => {
44
+ 'regexp' => '[Cc]ategory:\s?',
45
+ 'languages' => {
46
+ 'English' => {
47
+ 'no_declaration' => 'Uncategorised',
48
+ 'unknown_namespaces' => ['Uncategorised'],
49
+ 'llm_target_namespace' => 'Uncategorised'
50
+ },
51
+ 'Japanese' => {
52
+ 'no_declaration' => 'Category記載なし',
53
+ 'unknown_namespaces' => ['Category記載なし'],
54
+ 'llm_target_namespace' => 'Category記載なし'
55
+ }
56
+ }
57
+ }
58
+ }.freeze
59
+
60
+ attr_reader :organisation, :repository, :wiki_url, :owner_base_url, :excluded_dirs, :template_dir
61
+
62
+ # @rbs!
63
+ # @organisation: String?
64
+ # @repository: String?
65
+ # @wiki_url: String?
66
+ # @owner_base_url: String?
67
+ # @excluded_dirs: Array[String]
68
+ # @template_dir: String
69
+ # @file: Hash[String, untyped]
70
+ # @labels: Hash[String, untyped]
71
+
72
+ # @rbs base_path: String
73
+ # @rbs config_path: String?
74
+ # @rbs organisation: String?
75
+ # @rbs repository: String?
76
+ # @rbs wiki_url: String?
77
+ # @rbs owner_base_url: String?
78
+ # @rbs excluded_dirs: Array[String]?
79
+ # @rbs template_dir: String?
80
+ # @rbs return: void
81
+ def initialize(base_path:, config_path: nil, organisation: nil, repository: nil, wiki_url: nil,
82
+ owner_base_url: nil, excluded_dirs: nil, template_dir: nil)
83
+ @file = load_file(config_path || File.join(base_path, CONFIG_FILENAME))
84
+ @organisation = resolve_organisation(organisation)
85
+ @repository = resolve(repository, 'repository')
86
+ @wiki_url = resolve_wiki_url(wiki_url)
87
+ @owner_base_url = resolve_owner_base_url(owner_base_url)
88
+ @excluded_dirs = excluded_dirs || file.fetch('exclude', DEFAULT_EXCLUDED_DIRS)
89
+ @template_dir = template_dir || file.fetch('template_dir', DEFAULT_TEMPLATE_DIR)
90
+ @labels = deep_merge(DEFAULT_LABELS, file.fetch('labels', EMPTY_HASH))
91
+ end
92
+
93
+ # @rbs return: Array[String]
94
+ def group_bys
95
+ labels.keys
96
+ end
97
+
98
+ # @rbs group_by: String
99
+ # @rbs return: Array[String]
100
+ def languages(group_by)
101
+ label_for(group_by).fetch('languages', EMPTY_HASH).keys
102
+ end
103
+
104
+ # @rbs group_by: String
105
+ # @rbs return: Regexp
106
+ def target_regexp(group_by)
107
+ Regexp.new(label_for(group_by).fetch('regexp', ''))
108
+ end
109
+
110
+ # @rbs group_by: String
111
+ # @rbs language: String
112
+ # @rbs return: String
113
+ def no_declaration(group_by, language)
114
+ language_labels(group_by, language).fetch('no_declaration', '')
115
+ end
116
+
117
+ # @rbs group_by: String
118
+ # @rbs language: String
119
+ # @rbs return: Array[String]
120
+ def unknown_namespaces(group_by, language)
121
+ language_labels(group_by, language).fetch('unknown_namespaces', [])
122
+ end
123
+
124
+ # @rbs group_by: String
125
+ # @rbs language: String
126
+ # @rbs return: String
127
+ def llm_target_namespace(group_by, language)
128
+ language_labels(group_by, language).fetch('llm_target_namespace', '')
129
+ end
130
+
131
+ # @rbs group_by: String
132
+ # @rbs language: String
133
+ # @rbs return: String
134
+ def path_to_template(group_by, language)
135
+ File.join(template_dir, group_by.downcase, "#{language.downcase}.md")
136
+ end
137
+
138
+ private
139
+
140
+ attr_reader :file, :labels
141
+
142
+ # Treats empty strings the same as nil so that callers can pass values
143
+ # straight from optional environment variables or CLI flags.
144
+ # @rbs value: String?
145
+ # @rbs return: String?
146
+ def presence(value)
147
+ value unless value.nil? || value.empty?
148
+ end
149
+
150
+ # @rbs explicit: String?
151
+ # @rbs key: String
152
+ # @rbs return: String?
153
+ def resolve(explicit, key)
154
+ presence(explicit) || presence(file[key])
155
+ end
156
+
157
+ # @rbs explicit: String?
158
+ # @rbs return: String?
159
+ def resolve_organisation(explicit)
160
+ resolve(explicit, 'organisation') || presence(ENV.fetch('ORGANISATION_NAME', nil))
161
+ end
162
+
163
+ # @rbs explicit: String?
164
+ # @rbs return: String?
165
+ def resolve_wiki_url(explicit)
166
+ resolve(explicit, 'wiki_url') || default_wiki_url
167
+ end
168
+
169
+ # @rbs explicit: String?
170
+ # @rbs return: String?
171
+ def resolve_owner_base_url(explicit)
172
+ resolve(explicit, 'owner_base_url') || default_owner_base_url
173
+ end
174
+
175
+ # @rbs path: String
176
+ # @rbs return: Hash[String, untyped]
177
+ def load_file(path)
178
+ return {} unless File.exist?(path)
179
+
180
+ YAML.safe_load_file(path) || {}
181
+ end
182
+
183
+ # @rbs group_by: String
184
+ # @rbs return: Hash[String, untyped]
185
+ def label_for(group_by)
186
+ labels.fetch(group_by, EMPTY_HASH)
187
+ end
188
+
189
+ # @rbs group_by: String
190
+ # @rbs language: String
191
+ # @rbs return: Hash[String, untyped]
192
+ def language_labels(group_by, language)
193
+ label_for(group_by).fetch('languages', EMPTY_HASH).fetch(language, EMPTY_HASH)
194
+ end
195
+
196
+ # @rbs return: String?
197
+ def default_wiki_url
198
+ "https://github.com/#{organisation}/#{repository}/wiki" if organisation && repository
199
+ end
200
+
201
+ # @rbs return: String?
202
+ def default_owner_base_url
203
+ "https://github.com/orgs/#{organisation}/teams/" if organisation
204
+ end
205
+
206
+ # @rbs base: Hash[String, untyped]
207
+ # @rbs override: Hash[String, untyped]
208
+ # @rbs return: Hash[String, untyped]
209
+ def deep_merge(base, override)
210
+ base.merge(override) do |_key, old_value, new_value|
211
+ old_value.is_a?(Hash) && new_value.is_a?(Hash) ? deep_merge(old_value, new_value) : new_value
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require 'fileutils'
5
+ require_relative 'application'
6
+
7
+ module SpreenWiki
8
+ # Generates the wiki Home page, optionally splitting per-owner content out
9
+ # into `wikis-by-owner/<owner>.md` when home_overflow is enabled. Returns
10
+ # the configured wiki URL (if any) so callers can point at the result.
11
+ class Home < Application
12
+ # @rbs return: String?
13
+ def run
14
+ home_overflow ? write_concise_home_passage : write_home_passage
15
+ config.wiki_url
16
+ end
17
+
18
+ private
19
+
20
+ # @rbs return: String
21
+ def path_to_wikis_by_owner
22
+ @path_to_wikis_by_owner ||= File.join(base_path, 'wikis-by-owner')
23
+ end
24
+
25
+ # @rbs return: String
26
+ def path_to_home_template
27
+ path = config.path_to_template(group_by, language)
28
+ unless File.exist?(path)
29
+ raise ArgumentError, "Missing Home template: `#{path}`. Ship one there or configure template_dir."
30
+ end
31
+
32
+ path
33
+ end
34
+
35
+ # @rbs return: Array[String]
36
+ def home_passage
37
+ @home_passage ||= File.readlines(path_to_home_template).append("\n")
38
+ end
39
+
40
+ # @rbs return: void
41
+ def write_home_passage
42
+ FileUtils.rm_rf(path_to_wikis_by_owner)
43
+ owned_wiki_maps.each { |namespace, wikis| append_block(home_passage, namespace, wikis, owned: true) }
44
+ plain_wiki_maps.each { |namespace, wikis| append_block(home_passage, namespace, wikis, owned: false) }
45
+ File.write(path_to_home, home_passage.join.chomp)
46
+ end
47
+
48
+ # @rbs return: void
49
+ def write_concise_home_passage
50
+ FileUtils.mkdir_p(path_to_wikis_by_owner)
51
+ write_per_namespace_files
52
+ write_home_table_of_contents
53
+ end
54
+
55
+ # @rbs return: void
56
+ def write_per_namespace_files
57
+ owned_wiki_maps.each { |namespace, wikis| write_overflow_block(namespace, wikis, owned: true) }
58
+ plain_wiki_maps.each { |namespace, wikis| write_overflow_block(namespace, wikis, owned: false) }
59
+ end
60
+
61
+ # @rbs return: void
62
+ def write_home_table_of_contents
63
+ (owned_wiki_maps.keys + plain_wiki_maps.keys).each { |namespace| home_passage << "- [[#{namespace}]]\n" }
64
+ home_passage << "\n"
65
+ File.write(path_to_home, home_passage.join.chomp)
66
+ end
67
+
68
+ # @rbs passage: Array[String]
69
+ # @rbs namespace: String
70
+ # @rbs wikis: Array[String]
71
+ # @rbs owned: bool
72
+ # @rbs return: void
73
+ def append_block(passage, namespace, wikis, owned:)
74
+ passage << "#{heading_for(namespace, owned:)}\n"
75
+ passage << "\n"
76
+ wikis.each { |wiki| passage << "- [[#{wiki.gsub('.md', '')}]]\n" }
77
+ passage << "\n"
78
+ end
79
+
80
+ # @rbs namespace: String
81
+ # @rbs wikis: Array[String]
82
+ # @rbs owned: bool
83
+ # @rbs return: void
84
+ def write_overflow_block(namespace, wikis, owned:)
85
+ scratch = [] #: Array[String]
86
+ append_block(scratch, namespace, wikis, owned:)
87
+ File.write(File.join(path_to_wikis_by_owner, "#{namespace}.md"), scratch.join.chomp)
88
+ end
89
+
90
+ # @rbs namespace: String
91
+ # @rbs owned: bool
92
+ # @rbs return: String
93
+ def heading_for(namespace, owned:)
94
+ owner_base_url = config.owner_base_url
95
+ return "## #{namespace}" unless owned && owner_base_url
96
+
97
+ "## [#{namespace}](#{owner_base_url + namespace.delete('@')})"
98
+ end
99
+ end
100
+ end