kotoshu 0.3.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +18 -0
- data/CHANGELOG.md +182 -0
- data/CLAUDE.md +172 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +31 -0
- data/README.adoc +955 -0
- data/Rakefile +12 -0
- data/SECURITY.md +93 -0
- data/examples/01_basic_word_checking.rb +38 -0
- data/examples/02_text_document_checking.rb +77 -0
- data/examples/03_dictionary_backends.rb +137 -0
- data/examples/04_trie_data_structure.rb +146 -0
- data/examples/05_suggestion_algorithms.rb +239 -0
- data/examples/06_configuration_advanced.rb +287 -0
- data/examples/07_multi_language_dictionaries.rb +278 -0
- data/exe/kotoshu +6 -0
- data/lib/kotoshu/algorithms/capitalization.rb +276 -0
- data/lib/kotoshu/algorithms/lookup.rb +876 -0
- data/lib/kotoshu/algorithms/ngram_suggest.rb +270 -0
- data/lib/kotoshu/algorithms/permutations.rb +283 -0
- data/lib/kotoshu/algorithms/phonet_suggest.rb +167 -0
- data/lib/kotoshu/algorithms/suggest.rb +575 -0
- data/lib/kotoshu/algorithms.rb +14 -0
- data/lib/kotoshu/analyzers/semantic_analyzer.rb +295 -0
- data/lib/kotoshu/cache/base_cache.rb +596 -0
- data/lib/kotoshu/cache/cache.rb +91 -0
- data/lib/kotoshu/cache/frequency_cache.rb +224 -0
- data/lib/kotoshu/cache/language_cache.rb +454 -0
- data/lib/kotoshu/cache/lookup_cache.rb +166 -0
- data/lib/kotoshu/cache/model_cache.rb +513 -0
- data/lib/kotoshu/cache/suggestion_cache.rb +113 -0
- data/lib/kotoshu/cache.rb +40 -0
- data/lib/kotoshu/cli/auto_setup.rb +71 -0
- data/lib/kotoshu/cli/batch_reporter.rb +315 -0
- data/lib/kotoshu/cli/cache_command.rb +356 -0
- data/lib/kotoshu/cli/display_formatter.rb +431 -0
- data/lib/kotoshu/cli/errors.rb +36 -0
- data/lib/kotoshu/cli/interactive_reviewer.rb +319 -0
- data/lib/kotoshu/cli/language_resolver.rb +91 -0
- data/lib/kotoshu/cli/navigation_manager.rb +272 -0
- data/lib/kotoshu/cli/progress_reporter.rb +114 -0
- data/lib/kotoshu/cli/status_report.rb +130 -0
- data/lib/kotoshu/cli.rb +627 -0
- data/lib/kotoshu/commands/cache_command.rb +424 -0
- data/lib/kotoshu/commands/check_command.rb +312 -0
- data/lib/kotoshu/commands/model_command.rb +295 -0
- data/lib/kotoshu/components/passthrough_spell_checker.rb +72 -0
- data/lib/kotoshu/components/pos_tagger.rb +98 -0
- data/lib/kotoshu/components/spell_checker.rb +73 -0
- data/lib/kotoshu/components/synthesizer.rb +60 -0
- data/lib/kotoshu/components/tokenizer.rb +58 -0
- data/lib/kotoshu/components/whitespace_tokenizer.rb +96 -0
- data/lib/kotoshu/configuration/builder.rb +209 -0
- data/lib/kotoshu/configuration/resolver.rb +124 -0
- data/lib/kotoshu/configuration.rb +702 -0
- data/lib/kotoshu/core/exceptions.rb +165 -0
- data/lib/kotoshu/core/indexed_dictionary.rb +291 -0
- data/lib/kotoshu/core/models/affix_rule.rb +260 -0
- data/lib/kotoshu/core/models/result/document_result.rb +263 -0
- data/lib/kotoshu/core/models/result/word_result.rb +203 -0
- data/lib/kotoshu/core/models/word.rb +142 -0
- data/lib/kotoshu/core/trie/builder.rb +119 -0
- data/lib/kotoshu/core/trie/node.rb +94 -0
- data/lib/kotoshu/core/trie/trie.rb +249 -0
- data/lib/kotoshu/core.rb +28 -0
- data/lib/kotoshu/data/common_words/de.yml +1800 -0
- data/lib/kotoshu/data/common_words/en.yml +1215 -0
- data/lib/kotoshu/data/common_words/es.yml +750 -0
- data/lib/kotoshu/data/common_words/fr.yml +1015 -0
- data/lib/kotoshu/data/common_words/pt.yml +870 -0
- data/lib/kotoshu/data/common_words/ru.yml +484 -0
- data/lib/kotoshu/data/common_words_loader.rb +152 -0
- data/lib/kotoshu/data_structures/bloom_filter.rb +176 -0
- data/lib/kotoshu/debug_logger.rb +146 -0
- data/lib/kotoshu/debug_mode.rb +134 -0
- data/lib/kotoshu/defaults.rb +86 -0
- data/lib/kotoshu/dictionaries/catalog.rb +817 -0
- data/lib/kotoshu/dictionary/base.rb +237 -0
- data/lib/kotoshu/dictionary/cspell.rb +254 -0
- data/lib/kotoshu/dictionary/custom.rb +224 -0
- data/lib/kotoshu/dictionary/hunspell.rb +526 -0
- data/lib/kotoshu/dictionary/plain_text.rb +282 -0
- data/lib/kotoshu/dictionary/repository.rb +248 -0
- data/lib/kotoshu/dictionary/unified.rb +260 -0
- data/lib/kotoshu/dictionary/unix_words.rb +218 -0
- data/lib/kotoshu/documents/asciidoc_document.rb +441 -0
- data/lib/kotoshu/documents/document.rb +229 -0
- data/lib/kotoshu/documents/location.rb +139 -0
- data/lib/kotoshu/documents/markdown_document.rb +389 -0
- data/lib/kotoshu/documents/plain_text_document.rb +147 -0
- data/lib/kotoshu/embeddings/embedding_pipeline.rb +244 -0
- data/lib/kotoshu/embeddings/lru_cache.rb +233 -0
- data/lib/kotoshu/embeddings/onnx_runtime_model.rb +388 -0
- data/lib/kotoshu/embeddings/protocol.rb +83 -0
- data/lib/kotoshu/embeddings/protocols.rb +17 -0
- data/lib/kotoshu/embeddings/registry.rb +182 -0
- data/lib/kotoshu/embeddings/search.rb +192 -0
- data/lib/kotoshu/embeddings/similarity_engine.rb +248 -0
- data/lib/kotoshu/embeddings/similarity_search.rb +331 -0
- data/lib/kotoshu/embeddings/vocabulary.rb +257 -0
- data/lib/kotoshu/embeddings.rb +97 -0
- data/lib/kotoshu/fluent_checker.rb +91 -0
- data/lib/kotoshu/grammar/pattern_matchers/base_matcher.rb +48 -0
- data/lib/kotoshu/grammar/pattern_matchers/double_negative_matcher.rb +105 -0
- data/lib/kotoshu/grammar/pattern_matchers/possessive_context_matcher.rb +77 -0
- data/lib/kotoshu/grammar/pattern_matchers/vowel_sound_matcher.rb +83 -0
- data/lib/kotoshu/grammar/rule.rb +95 -0
- data/lib/kotoshu/grammar/rule_engine.rb +111 -0
- data/lib/kotoshu/grammar/rule_loader.rb +31 -0
- data/lib/kotoshu/grammar.rb +18 -0
- data/lib/kotoshu/integrity/audit_log.rb +88 -0
- data/lib/kotoshu/integrity/manifest.rb +117 -0
- data/lib/kotoshu/integrity/net_http.rb +46 -0
- data/lib/kotoshu/integrity.rb +25 -0
- data/lib/kotoshu/keyboard/layout.rb +115 -0
- data/lib/kotoshu/keyboard/layouts/azerty.rb +57 -0
- data/lib/kotoshu/keyboard/layouts/dvorak.rb +56 -0
- data/lib/kotoshu/keyboard/layouts/jcuken.rb +59 -0
- data/lib/kotoshu/keyboard/layouts/qwerty.rb +54 -0
- data/lib/kotoshu/keyboard/layouts/qwertz.rb +57 -0
- data/lib/kotoshu/keyboard/registry.rb +146 -0
- data/lib/kotoshu/keyboard.rb +60 -0
- data/lib/kotoshu/language/detector.rb +242 -0
- data/lib/kotoshu/language/identifier.rb +378 -0
- data/lib/kotoshu/language/languages/base.rb +256 -0
- data/lib/kotoshu/language/normalizer/base.rb +137 -0
- data/lib/kotoshu/language/registry.rb +147 -0
- data/lib/kotoshu/language/resources/ar/common_words.txt +6753 -0
- data/lib/kotoshu/language/resources/ar/confusion_sets.txt +11 -0
- data/lib/kotoshu/language/resources/de/common_words.txt +10003 -0
- data/lib/kotoshu/language/resources/de/confusion_sets.txt +246 -0
- data/lib/kotoshu/language/resources/en/common_words.txt +9979 -0
- data/lib/kotoshu/language/resources/en/confusion_sets.txt +871 -0
- data/lib/kotoshu/language/resources/es/common_words.txt +9992 -0
- data/lib/kotoshu/language/resources/es/confusion_sets.txt +17 -0
- data/lib/kotoshu/language/resources/fr/common_words.txt +9993 -0
- data/lib/kotoshu/language/resources/fr/confusion_sets.txt +76 -0
- data/lib/kotoshu/language/resources/pt/common_words.txt +9977 -0
- data/lib/kotoshu/language/resources/pt/confusion_sets.txt +18 -0
- data/lib/kotoshu/language/resources/ru/common_words.txt +9951 -0
- data/lib/kotoshu/language/resources/ru/confusion_sets.txt +5 -0
- data/lib/kotoshu/language/tokenizer/base.rb +170 -0
- data/lib/kotoshu/language/tokenizer/french_tokenizer.rb +170 -0
- data/lib/kotoshu/language/tokenizer/german_tokenizer.rb +41 -0
- data/lib/kotoshu/language/tokenizer/japanese_tokenizer.rb +60 -0
- data/lib/kotoshu/language/tokenizer/latin_tokenizer.rb +141 -0
- data/lib/kotoshu/language/tokenizer/portuguese_tokenizer.rb +160 -0
- data/lib/kotoshu/language/tokenizer/russian_tokenizer.rb +95 -0
- data/lib/kotoshu/language/tokenizer/spanish_tokenizer.rb +122 -0
- data/lib/kotoshu/language.rb +99 -0
- data/lib/kotoshu/languages/de/language.rb +546 -0
- data/lib/kotoshu/languages/en/language.rb +448 -0
- data/lib/kotoshu/languages/es/language.rb +459 -0
- data/lib/kotoshu/languages/fr/language.rb +493 -0
- data/lib/kotoshu/languages/ja/language.rb +477 -0
- data/lib/kotoshu/languages/pt/language.rb +423 -0
- data/lib/kotoshu/languages/ru/language.rb +404 -0
- data/lib/kotoshu/languages.rb +43 -0
- data/lib/kotoshu/metrics_collector.rb +222 -0
- data/lib/kotoshu/metrics_module.rb +110 -0
- data/lib/kotoshu/models/context.rb +119 -0
- data/lib/kotoshu/models/embedding_model.rb +182 -0
- data/lib/kotoshu/models/fasttext_model.rb +220 -0
- data/lib/kotoshu/models/nearest_neighbor.rb +87 -0
- data/lib/kotoshu/models/onnx_model.rb +333 -0
- data/lib/kotoshu/models/semantic_error.rb +165 -0
- data/lib/kotoshu/models/suggestion.rb +106 -0
- data/lib/kotoshu/models/word_embedding.rb +107 -0
- data/lib/kotoshu/paths.rb +53 -0
- data/lib/kotoshu/personal_dictionary.rb +94 -0
- data/lib/kotoshu/plugins/plugin.rb +61 -0
- data/lib/kotoshu/plugins/registry.rb +120 -0
- data/lib/kotoshu/project_config.rb +76 -0
- data/lib/kotoshu/readers/aff_data.rb +356 -0
- data/lib/kotoshu/readers/aff_reader.rb +375 -0
- data/lib/kotoshu/readers/condition_checker.rb +142 -0
- data/lib/kotoshu/readers/dic_reader.rb +118 -0
- data/lib/kotoshu/readers/file_reader.rb +347 -0
- data/lib/kotoshu/readers/lookup_builder.rb +299 -0
- data/lib/kotoshu/readers/readers.rb +6 -0
- data/lib/kotoshu/readers.rb +9 -0
- data/lib/kotoshu/resource_bundle.rb +30 -0
- data/lib/kotoshu/resource_manager.rb +295 -0
- data/lib/kotoshu/results/result.rb +165 -0
- data/lib/kotoshu/scripts/fasttext_to_onnx.py +275 -0
- data/lib/kotoshu/source_registry.rb +74 -0
- data/lib/kotoshu/spellchecker/parallel_checker.rb +90 -0
- data/lib/kotoshu/spellchecker.rb +298 -0
- data/lib/kotoshu/string_metrics.rb +153 -0
- data/lib/kotoshu/suggestions/context.rb +55 -0
- data/lib/kotoshu/suggestions/generator.rb +175 -0
- data/lib/kotoshu/suggestions/pipeline.rb +135 -0
- data/lib/kotoshu/suggestions/strategies/base_strategy.rb +296 -0
- data/lib/kotoshu/suggestions/strategies/composite_strategy.rb +140 -0
- data/lib/kotoshu/suggestions/strategies/edit_distance_strategy.rb +671 -0
- data/lib/kotoshu/suggestions/strategies/keyboard_proximity_strategy.rb +228 -0
- data/lib/kotoshu/suggestions/strategies/ngram_strategy.rb +130 -0
- data/lib/kotoshu/suggestions/strategies/phonetic_strategy.rb +329 -0
- data/lib/kotoshu/suggestions/strategies/semantic_strategy.rb +316 -0
- data/lib/kotoshu/suggestions/strategies/symspell_strategy.rb +275 -0
- data/lib/kotoshu/suggestions/suggestion.rb +174 -0
- data/lib/kotoshu/suggestions/suggestion_set.rb +238 -0
- data/lib/kotoshu/version.rb +5 -0
- data/lib/kotoshu.rb +493 -0
- data/script/validate_all_dictionaries.rb +444 -0
- data/sig/kotoshu.rbs +4 -0
- data/test_oop.rb +79 -0
- metadata +298 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kotoshu
|
|
4
|
+
module Models
|
|
5
|
+
# Immutable value object for word embeddings.
|
|
6
|
+
#
|
|
7
|
+
# Represents a word and its vector representation in a semantic space.
|
|
8
|
+
# Used for semantic similarity calculations and nearest neighbor searches.
|
|
9
|
+
#
|
|
10
|
+
# @example Creating an embedding
|
|
11
|
+
# embedding = WordEmbedding.new("hello", [0.1, -0.2, 0.3], "en")
|
|
12
|
+
# embedding.similarity(other_embedding) # => 0.85
|
|
13
|
+
#
|
|
14
|
+
# @see https://fasttext.cc/docs/en/crawl-vectors.html FastText crawl vectors
|
|
15
|
+
class WordEmbedding
|
|
16
|
+
attr_reader :word, :vector, :language_code, :dimension
|
|
17
|
+
|
|
18
|
+
# Create a new word embedding.
|
|
19
|
+
#
|
|
20
|
+
# @param word [String] The word
|
|
21
|
+
# @param vector [Array<Float>] The word's vector representation
|
|
22
|
+
# @param language_code [String] ISO 639-1 language code
|
|
23
|
+
# @param dimension [Integer] Vector dimension (default: 300 for FastText)
|
|
24
|
+
# @raise [ArgumentError] if vector doesn't match dimension
|
|
25
|
+
def initialize(word, vector, language_code, dimension: 300)
|
|
26
|
+
raise ArgumentError, "Vector dimension mismatch" unless vector.size == dimension
|
|
27
|
+
|
|
28
|
+
@word = word
|
|
29
|
+
@vector = vector.freeze
|
|
30
|
+
@language_code = language_code
|
|
31
|
+
@dimension = dimension
|
|
32
|
+
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Calculate cosine similarity with another embedding.
|
|
37
|
+
#
|
|
38
|
+
# Cosine similarity measures the cosine of the angle between two vectors.
|
|
39
|
+
# Returns 1.0 for identical vectors, 0.0 for orthogonal vectors.
|
|
40
|
+
#
|
|
41
|
+
# @param other [WordEmbedding] Another embedding
|
|
42
|
+
# @return [Float] Similarity score (0.0 to 1.0)
|
|
43
|
+
# @raise [TypeError] if other is not a WordEmbedding
|
|
44
|
+
def similarity(other)
|
|
45
|
+
raise TypeError, "Must be WordEmbedding" unless other.is_a?(WordEmbedding)
|
|
46
|
+
|
|
47
|
+
return 0.0 if @dimension != other.dimension
|
|
48
|
+
|
|
49
|
+
dot_product = @vector.zip(other.vector).map { |a, b| a * b }.sum
|
|
50
|
+
magnitude_a = vector_magnitude
|
|
51
|
+
magnitude_b = other.vector_magnitude
|
|
52
|
+
|
|
53
|
+
return 0.0 if magnitude_a.zero? || magnitude_b.zero?
|
|
54
|
+
|
|
55
|
+
dot_product / (magnitude_a * magnitude_b)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Calculate Euclidean distance from another embedding.
|
|
59
|
+
#
|
|
60
|
+
# @param other [WordEmbedding] Another embedding
|
|
61
|
+
# @return [Float] Euclidean distance
|
|
62
|
+
# @raise [TypeError] if other is not a WordEmbedding
|
|
63
|
+
def distance(other)
|
|
64
|
+
raise TypeError, "Must be WordEmbedding" unless other.is_a?(WordEmbedding)
|
|
65
|
+
|
|
66
|
+
return Float::INFINITY if @dimension != other.dimension
|
|
67
|
+
|
|
68
|
+
Math.sqrt(@vector.zip(other.vector).map { |a, b| (a - b)**2 }.sum)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Check if this embedding is equal to another.
|
|
72
|
+
#
|
|
73
|
+
# @param other [Object] Another object
|
|
74
|
+
# @return [Boolean] True if words and languages match
|
|
75
|
+
def ==(other)
|
|
76
|
+
return false unless other.is_a?(WordEmbedding)
|
|
77
|
+
|
|
78
|
+
@word == other.word && @language_code == other.language_code
|
|
79
|
+
end
|
|
80
|
+
alias_method :eql?, :==
|
|
81
|
+
|
|
82
|
+
# Hash code for hash table usage.
|
|
83
|
+
#
|
|
84
|
+
# @return [Integer] Hash code
|
|
85
|
+
def hash
|
|
86
|
+
[@word, @language_code].hash
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# String representation.
|
|
90
|
+
#
|
|
91
|
+
# @return [String] Human-readable representation
|
|
92
|
+
def to_s
|
|
93
|
+
"#{self.class.name}[#{@word}, #{@language_code}, #{@dimension}D]"
|
|
94
|
+
end
|
|
95
|
+
alias_method :inspect, :to_s
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
# Calculate vector magnitude (Euclidean norm).
|
|
100
|
+
#
|
|
101
|
+
# @return [Float] Magnitude
|
|
102
|
+
def vector_magnitude
|
|
103
|
+
@magnitude ||= Math.sqrt(@vector.map { |x| x * x }.sum)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Kotoshu
|
|
6
|
+
# Centralized XDG Base Directory paths.
|
|
7
|
+
#
|
|
8
|
+
# Resolves cache, config, and data paths per the XDG Base Directory
|
|
9
|
+
# Specification. Kotoshu-specific env vars (KOTOSHU_CACHE_PATH etc.)
|
|
10
|
+
# take precedence over the generic XDG_*_HOME vars, which take
|
|
11
|
+
# precedence over the hardcoded defaults.
|
|
12
|
+
#
|
|
13
|
+
# Default layout:
|
|
14
|
+
# ~/.cache/kotoshu/ downloaded dicts, models, frequency lists
|
|
15
|
+
# ~/.config/kotoshu/ user-edited config, personal dictionary
|
|
16
|
+
# ~/.local/share/kotoshu/ append-only data (audit log)
|
|
17
|
+
module Paths
|
|
18
|
+
class << self
|
|
19
|
+
def cache_path
|
|
20
|
+
ENV.fetch("KOTOSHU_CACHE_PATH", nil) || xdg("CACHE", "cache")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def config_path
|
|
24
|
+
ENV.fetch("KOTOSHU_CONFIG_PATH", nil) || xdg("CONFIG", "config")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def data_path
|
|
28
|
+
ENV.fetch("KOTOSHU_DATA_PATH", nil) || xdg("DATA", "local/share")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def audit_log_path
|
|
32
|
+
ENV.fetch("KOTOSHU_AUDIT_LOG", nil) || File.join(data_path, "audit.log")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def personal_dictionary_path
|
|
36
|
+
ENV.fetch("KOTOSHU_PERSONAL_DIC", nil) || File.join(config_path, "personal.dic")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def ensure_exist!
|
|
40
|
+
FileUtils.mkdir_p(cache_path)
|
|
41
|
+
FileUtils.mkdir_p(config_path)
|
|
42
|
+
FileUtils.mkdir_p(data_path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def xdg(suffix, default_subdir)
|
|
48
|
+
base = ENV.fetch("XDG_#{suffix}_HOME", nil) || File.join(Dir.home, ".#{default_subdir}")
|
|
49
|
+
File.join(base, "kotoshu")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Kotoshu
|
|
6
|
+
# Personal dictionary for user-specific words.
|
|
7
|
+
#
|
|
8
|
+
# Stored in ~/.config/kotoshu/personal.dic (Hunspell format) under the
|
|
9
|
+
# XDG config directory. Override via KOTOSHU_PERSONAL_DIC.
|
|
10
|
+
class PersonalDictionary
|
|
11
|
+
PERSONAL_FILE = Kotoshu::Paths.personal_dictionary_path
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Add a word to personal dictionary.
|
|
15
|
+
#
|
|
16
|
+
# @param word [String] Word to add
|
|
17
|
+
# @return [Boolean] True if added
|
|
18
|
+
def add_word(word)
|
|
19
|
+
return false if word.nil? || word.empty?
|
|
20
|
+
|
|
21
|
+
ensure_directory
|
|
22
|
+
words = load_words
|
|
23
|
+
|
|
24
|
+
unless words.include?(word.downcase)
|
|
25
|
+
words << word.downcase
|
|
26
|
+
save_words(words)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get all personal words.
|
|
33
|
+
#
|
|
34
|
+
# @return [Array<String>] All personal words
|
|
35
|
+
def words
|
|
36
|
+
load_words
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Remove a word from personal dictionary.
|
|
40
|
+
#
|
|
41
|
+
# @param word [String] Word to remove
|
|
42
|
+
# @return [Boolean] True if removed
|
|
43
|
+
def remove_word(word)
|
|
44
|
+
return false if word.nil? || word.empty?
|
|
45
|
+
|
|
46
|
+
words = load_words
|
|
47
|
+
if words.delete(word.downcase)
|
|
48
|
+
save_words(words)
|
|
49
|
+
true
|
|
50
|
+
else
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if word is in personal dictionary.
|
|
56
|
+
#
|
|
57
|
+
# @param word [String] Word to check
|
|
58
|
+
# @return [Boolean] True if present
|
|
59
|
+
def include?(word)
|
|
60
|
+
return false if word.nil? || word.empty?
|
|
61
|
+
|
|
62
|
+
load_words.include?(word.downcase)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Ensure personal dictionary's parent directory exists.
|
|
68
|
+
def ensure_directory
|
|
69
|
+
FileUtils.mkdir_p(File.dirname(PERSONAL_FILE))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Load words from personal dictionary file.
|
|
73
|
+
#
|
|
74
|
+
# @return [Array<String>] List of words
|
|
75
|
+
def load_words
|
|
76
|
+
return [] unless File.exist?(PERSONAL_FILE)
|
|
77
|
+
|
|
78
|
+
File.readlines(PERSONAL_FILE, chomp: true)
|
|
79
|
+
.reject { |line| line.empty? || line.start_with?("#") }
|
|
80
|
+
.map(&:strip)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Save words to personal dictionary file.
|
|
84
|
+
#
|
|
85
|
+
# @param words [Array<String>] Words to save
|
|
86
|
+
def save_words(words)
|
|
87
|
+
ensure_directory
|
|
88
|
+
File.open(PERSONAL_FILE, "w") do |f|
|
|
89
|
+
words.sort.uniq.each { |word| f.puts word }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kotoshu
|
|
4
|
+
module Plugins
|
|
5
|
+
# Base class for plugins.
|
|
6
|
+
#
|
|
7
|
+
# Plugins provide extensible functionality with dependency injection.
|
|
8
|
+
#
|
|
9
|
+
# @example Creating a plugin
|
|
10
|
+
# class MyPlugin < Kotoshu::Plugins::Plugin
|
|
11
|
+
# def self.plugin_name
|
|
12
|
+
# :my_plugin
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# def self.dependencies
|
|
16
|
+
# [:dictionary]
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# def self.provides
|
|
20
|
+
# [:suggestions]
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# def initialize(dictionary:)
|
|
24
|
+
# @dictionary = dictionary
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
class Plugin
|
|
28
|
+
# @return [Symbol] Plugin name
|
|
29
|
+
def self.plugin_name
|
|
30
|
+
raise NotImplementedError, "#{name} must define .plugin_name"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Array<Symbol>] Dependencies
|
|
34
|
+
def self.dependencies
|
|
35
|
+
[]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Array<Symbol>] Provided services
|
|
39
|
+
def self.provides
|
|
40
|
+
[]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Lifecycle hook called before plugin starts.
|
|
44
|
+
#
|
|
45
|
+
# Override in subclass to add startup logic.
|
|
46
|
+
def before_start
|
|
47
|
+
# Override in subclass
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Lifecycle hook called after plugin stops.
|
|
51
|
+
#
|
|
52
|
+
# Override in subclass to add cleanup logic.
|
|
53
|
+
def after_stop
|
|
54
|
+
# Override in subclass
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Error raised when a dependency cannot be resolved.
|
|
59
|
+
class DependencyError < StandardError; end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "plugin"
|
|
4
|
+
|
|
5
|
+
module Kotoshu
|
|
6
|
+
module Plugins
|
|
7
|
+
# Registry for managing plugins and their dependencies.
|
|
8
|
+
#
|
|
9
|
+
# @example Registering a plugin
|
|
10
|
+
# registry = Registry.new
|
|
11
|
+
# registry.register(:dictionary, MyDictionary)
|
|
12
|
+
# registry.register(:suggestions, MySuggestions)
|
|
13
|
+
#
|
|
14
|
+
# @example Creating an instance with DI
|
|
15
|
+
# suggestions = registry.create_instance(MySuggestions)
|
|
16
|
+
class Registry
|
|
17
|
+
# @return [Hash] Registered services
|
|
18
|
+
attr_reader :services
|
|
19
|
+
|
|
20
|
+
# @return [Hash] Registered plugins
|
|
21
|
+
attr_reader :plugins
|
|
22
|
+
|
|
23
|
+
# Create a new registry.
|
|
24
|
+
def initialize
|
|
25
|
+
@services = {}
|
|
26
|
+
@plugins = {}
|
|
27
|
+
@singletons = {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Register a service.
|
|
31
|
+
#
|
|
32
|
+
# @param name [Symbol] Service name
|
|
33
|
+
# @param klass [Class] Service class or instance
|
|
34
|
+
# @return [self] Self for chaining
|
|
35
|
+
def register(name, klass)
|
|
36
|
+
@services[name] = klass
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Register a plugin.
|
|
41
|
+
#
|
|
42
|
+
# @param plugin [Class] Plugin class
|
|
43
|
+
# @return [self] Self for chaining
|
|
44
|
+
def register_plugin(plugin)
|
|
45
|
+
@plugins[plugin.plugin_name] = plugin
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Get a service instance.
|
|
50
|
+
#
|
|
51
|
+
# @param name [Symbol] Service name
|
|
52
|
+
# @return [Object] Service instance
|
|
53
|
+
def get_service(name)
|
|
54
|
+
service = @services[name]
|
|
55
|
+
|
|
56
|
+
raise DependencyError, "Unknown service: #{name}" unless service
|
|
57
|
+
|
|
58
|
+
# Return singleton if already created
|
|
59
|
+
return @singletons[name] if @singletons.key?(name)
|
|
60
|
+
|
|
61
|
+
# Create new instance
|
|
62
|
+
instance = service.is_a?(Class) ? service.new : service
|
|
63
|
+
|
|
64
|
+
# Store singleton
|
|
65
|
+
@singletons[name] = instance if service.is_a?(Class)
|
|
66
|
+
instance
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Create an instance with dependency injection.
|
|
70
|
+
#
|
|
71
|
+
# @param klass [Class] Class to instantiate
|
|
72
|
+
# @return [Object] Created instance
|
|
73
|
+
def create_instance(klass)
|
|
74
|
+
return klass.new unless klass.is_a?(Class)
|
|
75
|
+
|
|
76
|
+
# Get dependencies if it's a Plugin
|
|
77
|
+
if klass < Plugin
|
|
78
|
+
dependencies = resolve_dependencies(klass)
|
|
79
|
+
klass.new(**dependencies)
|
|
80
|
+
else
|
|
81
|
+
klass.new
|
|
82
|
+
end
|
|
83
|
+
rescue ArgumentError => e
|
|
84
|
+
raise DependencyError, "Failed to create instance of #{klass}: #{e.message}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Check if a service is registered.
|
|
88
|
+
#
|
|
89
|
+
# @param name [Symbol] Service name
|
|
90
|
+
# @return [Boolean] True if registered
|
|
91
|
+
def registered?(name)
|
|
92
|
+
@services.key?(name)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Clear all singletons (force re-instantiation).
|
|
96
|
+
#
|
|
97
|
+
# @return [self] Self for chaining
|
|
98
|
+
def clear_singletons
|
|
99
|
+
@singletons.clear
|
|
100
|
+
self
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Resolve dependencies for a plugin.
|
|
106
|
+
#
|
|
107
|
+
# @param plugin [Class] Plugin class
|
|
108
|
+
# @return [Hash] Keyword arguments for initialization
|
|
109
|
+
def resolve_dependencies(plugin)
|
|
110
|
+
deps = {}
|
|
111
|
+
|
|
112
|
+
plugin.dependencies.each do |dep|
|
|
113
|
+
deps[dep] = get_service(dep)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
deps
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kotoshu
|
|
4
|
+
# Project configuration for .kotoshu file.
|
|
5
|
+
#
|
|
6
|
+
# Auto-discovers .kotoshu file by searching up directory tree.
|
|
7
|
+
class ProjectConfig
|
|
8
|
+
CONFIG_FILE = ".kotoshu"
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# Load project config for given path.
|
|
12
|
+
#
|
|
13
|
+
# @param start_path [String] Starting directory
|
|
14
|
+
# @return [Hash, nil] Configuration hash or nil
|
|
15
|
+
def load(start_path = Dir.pwd)
|
|
16
|
+
path = find_config_file(start_path)
|
|
17
|
+
return nil unless path
|
|
18
|
+
|
|
19
|
+
parse_config_file(path)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Check if project config exists.
|
|
23
|
+
#
|
|
24
|
+
# @param start_path [String] Starting directory
|
|
25
|
+
# @return [Boolean] True if config exists
|
|
26
|
+
def exists?(start_path = Dir.pwd)
|
|
27
|
+
!find_config_file(start_path).nil?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get ignore patterns from project config.
|
|
31
|
+
#
|
|
32
|
+
# @param start_path [String] Starting directory
|
|
33
|
+
# @return [Hash] Configuration with ignore patterns
|
|
34
|
+
def ignore_patterns(start_path = Dir.pwd)
|
|
35
|
+
config = load(start_path) || {}
|
|
36
|
+
{
|
|
37
|
+
words: config["ignore_words"] || [],
|
|
38
|
+
patterns: (config["ignore_patterns"] || []).map { |p| Regexp.new(p) }
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Find .kotoshu file by searching up directory tree.
|
|
45
|
+
#
|
|
46
|
+
# @param start_path [String] Starting directory
|
|
47
|
+
# @return [String, nil] Path to config file or nil
|
|
48
|
+
def find_config_file(start_path)
|
|
49
|
+
path = Pathname.new(start_path)
|
|
50
|
+
|
|
51
|
+
while path
|
|
52
|
+
config_file = path.join(CONFIG_FILE)
|
|
53
|
+
return config_file.to_s if config_file.file?
|
|
54
|
+
|
|
55
|
+
parent = path.parent
|
|
56
|
+
break if parent == path # Reached root
|
|
57
|
+
|
|
58
|
+
path = parent
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Parse .kotoshu YAML file.
|
|
65
|
+
#
|
|
66
|
+
# @param path [String] Path to config file
|
|
67
|
+
# @return [Hash] Parsed configuration
|
|
68
|
+
def parse_config_file(path)
|
|
69
|
+
require "yaml"
|
|
70
|
+
YAML.load_file(path) || {}
|
|
71
|
+
rescue ArgumentError, Psych::SyntaxError
|
|
72
|
+
{}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|