forspell 0.0.2
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/README.md +88 -0
- data/exe/clone_repos.sh +72 -0
- data/exe/create_dictionary +47 -0
- data/exe/forspell +4 -0
- data/exe/generate_logs +8 -0
- data/lib/forspell/cli.rb +91 -0
- data/lib/forspell/dictionaries/en_US.aff +205 -0
- data/lib/forspell/dictionaries/en_US.dic +49271 -0
- data/lib/forspell/file_list.rb +42 -0
- data/lib/forspell/loaders/base.rb +37 -0
- data/lib/forspell/loaders/c.rb +27 -0
- data/lib/forspell/loaders/markdown.rb +75 -0
- data/lib/forspell/loaders/ruby.rb +25 -0
- data/lib/forspell/loaders/source.rb +24 -0
- data/lib/forspell/loaders.rb +23 -0
- data/lib/forspell/reporter.rb +99 -0
- data/lib/forspell/ruby.dict +77 -0
- data/lib/forspell/runner.rb +35 -0
- data/lib/forspell/sanitizer.rb +17 -0
- data/lib/forspell/speller.rb +38 -0
- data/lib/forspell/word_matcher.rb +18 -0
- data/lib/forspell.rb +3 -0
- metadata +68 -0
| @@ -0,0 +1,42 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Forspell
         | 
| 4 | 
            +
              class FileList
         | 
| 5 | 
            +
                include Enumerable
         | 
| 6 | 
            +
                class PathLoadError < StandardError; end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                EXTENSION_GLOBS = %w[
         | 
| 9 | 
            +
                  rb
         | 
| 10 | 
            +
                  c
         | 
| 11 | 
            +
                  cpp
         | 
| 12 | 
            +
                  cxx
         | 
| 13 | 
            +
                  md
         | 
| 14 | 
            +
                ].freeze
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def initialize(paths:, exclude_paths:)
         | 
| 17 | 
            +
                  @paths = paths
         | 
| 18 | 
            +
                  @exclude_paths = exclude_paths
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def each(&block)
         | 
| 22 | 
            +
                  to_process = @paths.flat_map(&method(:expand_paths))
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  to_exclude = @exclude_paths.flat_map(&method(:expand_paths))
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  (to_process - to_exclude).map{ |path| path.gsub('//', '/')}
         | 
| 27 | 
            +
                    .each(&block)
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                private
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def expand_paths(path)
         | 
| 33 | 
            +
                  if File.directory?(path)
         | 
| 34 | 
            +
                    Dir.glob(File.join(path, '**', "*.{#{EXTENSION_GLOBS.join(',')}}"))
         | 
| 35 | 
            +
                  elsif File.exists? path
         | 
| 36 | 
            +
                    path
         | 
| 37 | 
            +
                  else
         | 
| 38 | 
            +
                    raise PathLoadError, path
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
            end
         | 
| @@ -0,0 +1,37 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'backports/2.4.0/hash'
         | 
| 4 | 
            +
            require_relative '../sanitizer'
         | 
| 5 | 
            +
            require_relative '../word_matcher'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Forspell::Loaders
         | 
| 8 | 
            +
              Word = Struct.new(:file, :line, :text)
         | 
| 9 | 
            +
              
         | 
| 10 | 
            +
              class Base
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def initialize(file: nil, text: nil)
         | 
| 13 | 
            +
                  @file = file
         | 
| 14 | 
            +
                  @input = text || input
         | 
| 15 | 
            +
                  @words = []
         | 
| 16 | 
            +
                  @errors = []
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def read
         | 
| 20 | 
            +
                  extract_words.each { |word| word.text = Forspell::Sanitizer.sanitize(word.text) }
         | 
| 21 | 
            +
                               .select{ |word| Forspell::WordMatcher.word?(word.text) }
         | 
| 22 | 
            +
                               .reject { |w| w.text.nil? || w.text.empty? }
         | 
| 23 | 
            +
                rescue YARD::Parser::ParserSyntaxError, RuntimeError => e
         | 
| 24 | 
            +
                  raise Forspell::Loaders::ParsingError, e.message
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                private
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def input
         | 
| 30 | 
            +
                  File.read(@file)
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def extract_words
         | 
| 34 | 
            +
                  raise NotImplementedError
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
            end
         | 
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require_relative 'source'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Forspell::Loaders
         | 
| 5 | 
            +
              class C < Source
         | 
| 6 | 
            +
                def input
         | 
| 7 | 
            +
                  res = super
         | 
| 8 | 
            +
                  res.encode('UTF-8', invalid: :replace, replace: '?') unless res.valid_encoding?
         | 
| 9 | 
            +
                  res
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                private
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def comments
         | 
| 15 | 
            +
                  YARD::Parser::C::CParser.new(@input, @file).parse
         | 
| 16 | 
            +
                    .grep(YARD::Parser::C::Comment)
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def text(comment)
         | 
| 20 | 
            +
                  comment.source
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def line(comment)
         | 
| 24 | 
            +
                  comment.line
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
| @@ -0,0 +1,75 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'kramdown'
         | 
| 4 | 
            +
            require 'kramdown-parser-gfm'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            require_relative './base'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module Forspell::Loaders
         | 
| 9 | 
            +
              class Markdown < Base
         | 
| 10 | 
            +
                class FilteredHash
         | 
| 11 | 
            +
                  PERMITTED_TYPES = %i[
         | 
| 12 | 
            +
                    text
         | 
| 13 | 
            +
                    smart_quote
         | 
| 14 | 
            +
                  ].freeze
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def convert(el, options)
         | 
| 17 | 
            +
                    return if !PERMITTED_TYPES.include?(el.type) && el.children.empty?
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    hash = { type: el.type }
         | 
| 20 | 
            +
                    hash[:attr] = el.attr unless el.attr.empty?
         | 
| 21 | 
            +
                    hash[:value] = el.value unless el.value.nil?
         | 
| 22 | 
            +
                    hash[:location] = el.options[:location]
         | 
| 23 | 
            +
                    unless el.children.empty?
         | 
| 24 | 
            +
                      hash[:children] = []
         | 
| 25 | 
            +
                      el.children.each { |child| hash[:children] << convert(child, options) }
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                    hash
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                PARSER = 'GFM'
         | 
| 32 | 
            +
                SPECIAL_CHARS_MAP = {
         | 
| 33 | 
            +
                  lsquo: "'",
         | 
| 34 | 
            +
                  rsquo: "'",
         | 
| 35 | 
            +
                  ldquo: '"',
         | 
| 36 | 
            +
                  rdquo: '"'
         | 
| 37 | 
            +
                }.freeze
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def extract_words
         | 
| 40 | 
            +
                  document = Kramdown::Document.new(@input, input: PARSER)
         | 
| 41 | 
            +
                  tree = FilteredHash.new.convert(document.root, document.options)
         | 
| 42 | 
            +
                  chunks = extract_chunks(tree)
         | 
| 43 | 
            +
                  result = []
         | 
| 44 | 
            +
                  return result if chunks.empty?
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  group_by_location = chunks.group_by { |res| res[:location] }
         | 
| 47 | 
            +
                                            .transform_values do |lines|
         | 
| 48 | 
            +
                    lines.map { |v| SPECIAL_CHARS_MAP[v[:value]] || v[:value] }.join.split(/\s|,|;|—/)
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                  group_by_location.each_pair do |location, words|
         | 
| 51 | 
            +
                    words.reject(&:empty?)
         | 
| 52 | 
            +
                         .each { |word| result << Word.new(@file, location || 0, word) }
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  result
         | 
| 56 | 
            +
                rescue RuntimeError => e
         | 
| 57 | 
            +
                  raise Forspell::Loaders::ParsingError, e.message
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                private
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def extract_chunks(tree)
         | 
| 63 | 
            +
                  tree[:children].grep(Hash).flat_map do |child|
         | 
| 64 | 
            +
                    if child[:children]
         | 
| 65 | 
            +
                      extract_chunks(child)
         | 
| 66 | 
            +
                    else
         | 
| 67 | 
            +
                      {
         | 
| 68 | 
            +
                        location: child[:location],
         | 
| 69 | 
            +
                        value: child[:value]
         | 
| 70 | 
            +
                      }
         | 
| 71 | 
            +
                    end
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
              end
         | 
| 75 | 
            +
            end
         | 
| @@ -0,0 +1,25 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'yard'
         | 
| 4 | 
            +
            require 'yard/parser/ruby/ruby_parser'
         | 
| 5 | 
            +
            require_relative 'source'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Forspell::Loaders
         | 
| 8 | 
            +
              class Ruby < Source
         | 
| 9 | 
            +
                private
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def comments
         | 
| 12 | 
            +
                  YARD::Parser::Ruby::RubyParser.new(@input, @file).parse
         | 
| 13 | 
            +
                    .tokens.select{ |token| token.first == :comment }
         | 
| 14 | 
            +
                  # example: [:comment, "# def loader_class path\n", [85, 2356]]
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def text(comment)
         | 
| 18 | 
            +
                  comment[1]
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def line(comment)
         | 
| 22 | 
            +
                  comment.last.first
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'yard'
         | 
| 4 | 
            +
            require_relative 'base'
         | 
| 5 | 
            +
            require_relative 'markdown'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Forspell
         | 
| 8 | 
            +
              module Loaders
         | 
| 9 | 
            +
                class Source < Base
         | 
| 10 | 
            +
                  private
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def extract_words
         | 
| 13 | 
            +
                    comments.flat_map do |comment|
         | 
| 14 | 
            +
                      Markdown.new(text: text(comment)).read
         | 
| 15 | 
            +
                              .map do |word|
         | 
| 16 | 
            +
                                word.file = @file
         | 
| 17 | 
            +
                                word.line += line(comment) - 1
         | 
| 18 | 
            +
                                word
         | 
| 19 | 
            +
                              end
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative 'loaders/markdown'
         | 
| 4 | 
            +
            require_relative 'loaders/ruby'
         | 
| 5 | 
            +
            require_relative 'loaders/c'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Forspell
         | 
| 8 | 
            +
              module Loaders
         | 
| 9 | 
            +
                class ParsingError < StandardError; end
         | 
| 10 | 
            +
                
         | 
| 11 | 
            +
                EXT_TO_PARSER_CLASS = {
         | 
| 12 | 
            +
                  '.rb' => Loaders::Ruby,
         | 
| 13 | 
            +
                  '.c' => Loaders::C,
         | 
| 14 | 
            +
                  '.cpp' => Loaders::C,
         | 
| 15 | 
            +
                  '.cxx' => Loaders::C,
         | 
| 16 | 
            +
                  '.md' => Loaders::Markdown
         | 
| 17 | 
            +
                }.freeze
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def self.for(path)
         | 
| 20 | 
            +
                  EXT_TO_PARSER_CLASS[File.extname(path)].new(file: path)
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
            end
         | 
| @@ -0,0 +1,99 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'fileutils'
         | 
| 4 | 
            +
            require 'pastel'
         | 
| 5 | 
            +
            require 'logger'
         | 
| 6 | 
            +
            require 'json'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module Forspell
         | 
| 9 | 
            +
              class Reporter
         | 
| 10 | 
            +
                SUCCESS_CODE = 0
         | 
| 11 | 
            +
                ERROR_CODE = 1
         | 
| 12 | 
            +
                ERROR_FORMAT = '%<file>s:%<line>i: %<text>s (suggestions: %<suggestions>s)'
         | 
| 13 | 
            +
                SUMMARY = "Forspell inspects *.rb, *.c, *.cpp, *.md files\n"\
         | 
| 14 | 
            +
                          '%<files>i inspected, %<errors>s detected'
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def initialize(logfile:,
         | 
| 17 | 
            +
                               verbose:,
         | 
| 18 | 
            +
                               format:)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  FileUtils.touch(logfile) if logfile.is_a?(String)
         | 
| 21 | 
            +
                  @logger = Logger.new(logfile || STDERR)
         | 
| 22 | 
            +
                  @logger.level = verbose ? Logger::INFO : Logger::WARN
         | 
| 23 | 
            +
                  @logger.formatter = proc { |*, msg| "#{msg}\n" }
         | 
| 24 | 
            +
                  @format = format
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  @pastel = Pastel.new(enabled: $stdout.tty?)
         | 
| 27 | 
            +
                  @errors = []
         | 
| 28 | 
            +
                  @files = []
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def file(path)
         | 
| 32 | 
            +
                  @logger.info "Processing #{path}"
         | 
| 33 | 
            +
                  @files << path
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def error(word, suggestions)
         | 
| 37 | 
            +
                  @errors << [word, suggestions]
         | 
| 38 | 
            +
                  puts readable(word, suggestions) if @format == 'readable'
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def parsing_error(error)
         | 
| 42 | 
            +
                  @logger.error "Parsing error in #{@files.last}: #{error}"
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def path_load_error path
         | 
| 46 | 
            +
                  @logger.error "Path not found: #{path}"
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def report
         | 
| 50 | 
            +
                  case @format
         | 
| 51 | 
            +
                  when 'readable'
         | 
| 52 | 
            +
                    print_summary
         | 
| 53 | 
            +
                  when 'dictionary'
         | 
| 54 | 
            +
                    print_dictionary
         | 
| 55 | 
            +
                  when 'json', 'yaml'
         | 
| 56 | 
            +
                    print_formatted
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                def finalize
         | 
| 61 | 
            +
                  @errors.empty? ? SUCCESS_CODE : ERROR_CODE
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                private
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def readable(word, suggestions)
         | 
| 67 | 
            +
                  format(ERROR_FORMAT,
         | 
| 68 | 
            +
                         file: word[:file],
         | 
| 69 | 
            +
                         line: word[:line],
         | 
| 70 | 
            +
                         text: @pastel.red(word[:text]),
         | 
| 71 | 
            +
                         suggestions: suggestions.join(', '))
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                def print_formatted
         | 
| 75 | 
            +
                  @errors.map { |word, suggestions| word.to_h.merge(suggestions: suggestions) }
         | 
| 76 | 
            +
                         .public_send("to_#{@format}")
         | 
| 77 | 
            +
                         .tap { |res| puts res }
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                def print_summary
         | 
| 81 | 
            +
                  err_count = @errors.size
         | 
| 82 | 
            +
                  color = err_count.positive? ? :red : :green
         | 
| 83 | 
            +
                  total_errors_colorized = @pastel.decorate(err_count.to_s, color)
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  puts format(SUMMARY, files: @files.size, errors: total_errors_colorized)
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                def print_dictionary
         | 
| 89 | 
            +
                  @errors.map(&:first)
         | 
| 90 | 
            +
                         .group_by(&:text)
         | 
| 91 | 
            +
                         .transform_values { |v| v.map(&:file).uniq }
         | 
| 92 | 
            +
                         .sort_by { |word, *| word.downcase }
         | 
| 93 | 
            +
                         .each do |text, files|
         | 
| 94 | 
            +
                    files.each { |file| puts "\# #{file}" }
         | 
| 95 | 
            +
                    puts @pastel.decorate(text, :red)
         | 
| 96 | 
            +
                  end
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
              end
         | 
| 99 | 
            +
            end
         | 
| @@ -0,0 +1,77 @@ | |
| 1 | 
            +
            Gemfile: example
         | 
| 2 | 
            +
            Rakefile
         | 
| 3 | 
            +
            accessor: example
         | 
| 4 | 
            +
            admin: example
         | 
| 5 | 
            +
            args
         | 
| 6 | 
            +
            async
         | 
| 7 | 
            +
            attr: example
         | 
| 8 | 
            +
            backend: example
         | 
| 9 | 
            +
            backport: port
         | 
| 10 | 
            +
            backtrace: example
         | 
| 11 | 
            +
            bitwise
         | 
| 12 | 
            +
            boolean: example
         | 
| 13 | 
            +
            builtin
         | 
| 14 | 
            +
            bundler
         | 
| 15 | 
            +
            charset: example
         | 
| 16 | 
            +
            codepoint: example
         | 
| 17 | 
            +
            composable
         | 
| 18 | 
            +
            config
         | 
| 19 | 
            +
            dataset: set
         | 
| 20 | 
            +
            deserialization
         | 
| 21 | 
            +
            deserialize: rise
         | 
| 22 | 
            +
            dir
         | 
| 23 | 
            +
            encoding: example
         | 
| 24 | 
            +
            enum
         | 
| 25 | 
            +
            fallback: example
         | 
| 26 | 
            +
            filesystem: system
         | 
| 27 | 
            +
            formatter: example
         | 
| 28 | 
            +
            geospatial
         | 
| 29 | 
            +
            i18n
         | 
| 30 | 
            +
            initializer: example
         | 
| 31 | 
            +
            inline
         | 
| 32 | 
            +
            io
         | 
| 33 | 
            +
            lexer: example
         | 
| 34 | 
            +
            lib: rib
         | 
| 35 | 
            +
            lifecycle: example
         | 
| 36 | 
            +
            memoization
         | 
| 37 | 
            +
            memoized
         | 
| 38 | 
            +
            metadata
         | 
| 39 | 
            +
            middleware: example
         | 
| 40 | 
            +
            mixin: toxin
         | 
| 41 | 
            +
            monkeypatch: patch
         | 
| 42 | 
            +
            monkeypatching
         | 
| 43 | 
            +
            multithreaded
         | 
| 44 | 
            +
            multithreading
         | 
| 45 | 
            +
            mutex: class
         | 
| 46 | 
            +
            namespace: example
         | 
| 47 | 
            +
            override: ride
         | 
| 48 | 
            +
            param: example
         | 
| 49 | 
            +
            parens
         | 
| 50 | 
            +
            parser: example
         | 
| 51 | 
            +
            plugin: penguin
         | 
| 52 | 
            +
            pre
         | 
| 53 | 
            +
            prepend: append
         | 
| 54 | 
            +
            proc
         | 
| 55 | 
            +
            refactor
         | 
| 56 | 
            +
            refactoring
         | 
| 57 | 
            +
            regex
         | 
| 58 | 
            +
            regexp
         | 
| 59 | 
            +
            repo
         | 
| 60 | 
            +
            rubygems
         | 
| 61 | 
            +
            runtime: example
         | 
| 62 | 
            +
            stderr
         | 
| 63 | 
            +
            stdin
         | 
| 64 | 
            +
            stdlib
         | 
| 65 | 
            +
            stdout
         | 
| 66 | 
            +
            stylesheet: sheet
         | 
| 67 | 
            +
            subclass: class
         | 
| 68 | 
            +
            subclassing
         | 
| 69 | 
            +
            subtype: example
         | 
| 70 | 
            +
            superclass: class
         | 
| 71 | 
            +
            timestamp: stamp
         | 
| 72 | 
            +
            tokenizer: example
         | 
| 73 | 
            +
            truthy
         | 
| 74 | 
            +
            unescape
         | 
| 75 | 
            +
            unicode
         | 
| 76 | 
            +
            username: example
         | 
| 77 | 
            +
            whitespace: example
         | 
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require_relative 'loaders'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Forspell
         | 
| 5 | 
            +
              class Runner
         | 
| 6 | 
            +
                def initialize(files:, speller:, reporter:)
         | 
| 7 | 
            +
                  @files = files
         | 
| 8 | 
            +
                  @speller = speller
         | 
| 9 | 
            +
                  @reporter = reporter
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def call
         | 
| 13 | 
            +
                  @files.each do |path|
         | 
| 14 | 
            +
                    process_file path
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  @reporter.report
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  self
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                private
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def process_file path
         | 
| 25 | 
            +
                  @reporter.file(path) 
         | 
| 26 | 
            +
                  
         | 
| 27 | 
            +
                  words = Loaders.for(path).read
         | 
| 28 | 
            +
                  words.reject { |word| @speller.correct?(word.text) }
         | 
| 29 | 
            +
                       .each { |word| @reporter.error(word, @speller.suggest(word.text)) }
         | 
| 30 | 
            +
              
         | 
| 31 | 
            +
                rescue Forspell::Loaders::ParsingError => e
         | 
| 32 | 
            +
                  @reporter.parsing_error(e) and return
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'sanitize'
         | 
| 4 | 
            +
            require 'cgi'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Forspell
         | 
| 7 | 
            +
              module Sanitizer    
         | 
| 8 | 
            +
                REMOVE_PUNCT = /[[:punct:]&&[^\-\'\_\.]]$/.freeze
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def self.sanitize(input)
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  CGI.unescapeHTML(Sanitize.fragment(input,
         | 
| 13 | 
            +
                                                     elements: [], remove_contents: true))
         | 
| 14 | 
            +
                     .gsub(REMOVE_PUNCT, '').gsub(/[\!\.\?]{1}$/, '')
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,38 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'ffi/hunspell'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Forspell
         | 
| 6 | 
            +
              class Speller
         | 
| 7 | 
            +
                attr_reader :dictionary
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                SUGGESTIONS_SIZE = 3
         | 
| 10 | 
            +
                HUNSPELL_DIRS = [File.join(__dir__, 'dictionaries')]
         | 
| 11 | 
            +
                RUBY_DICT = File.join(__dir__, 'ruby.dict')
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def initialize(main_dictionary, *custom_dictionaries)
         | 
| 14 | 
            +
                  FFI::Hunspell.directories = HUNSPELL_DIRS << File.dirname(main_dictionary)
         | 
| 15 | 
            +
                  @dictionary = FFI::Hunspell.dict(File.basename(main_dictionary))
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  [RUBY_DICT, *custom_dictionaries].flat_map { |path| File.read(path).split("\n") }
         | 
| 18 | 
            +
                                                   .compact
         | 
| 19 | 
            +
                                                   .map { |line| line.gsub(/\s*\#.*$/, '') }
         | 
| 20 | 
            +
                                                   .reject(&:empty?)
         | 
| 21 | 
            +
                                                   .map { |line| line.split(/\s*:\s*/, 2) }
         | 
| 22 | 
            +
                                                   .each do |word, example|
         | 
| 23 | 
            +
                    example ? @dictionary.add_with_affix(word, example) : @dictionary.add(word)
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                rescue ArgumentError
         | 
| 26 | 
            +
                  puts "Unable to find dictionary #{main_dictionary}"
         | 
| 27 | 
            +
                  exit(2)
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def correct?(word)
         | 
| 31 | 
            +
                  dictionary.check?(word)
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def suggest(word)
         | 
| 35 | 
            +
                  dictionary.suggest(word).first(SUGGESTIONS_SIZE)
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
            end
         | 
| @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            require 'backports/2.4.0/regexp/match'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Forspell
         | 
| 4 | 
            +
              module WordMatcher
         | 
| 5 | 
            +
                WORD = %r{^
         | 
| 6 | 
            +
                  \'?                # could start with apostrophe
         | 
| 7 | 
            +
                  ([a-z]|[A-Z])?     # at least one letter,
         | 
| 8 | 
            +
                  ([[:lower:]])+     # then any number of letters,
         | 
| 9 | 
            +
                  ([\'\-])?          # optional dash/apostrophe,
         | 
| 10 | 
            +
                  ([[:lower:]])*     # another bunch of letters
         | 
| 11 | 
            +
                  \'?                # could end with apostrophe
         | 
| 12 | 
            +
                $}x
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def self.word? text
         | 
| 15 | 
            +
                  WORD.match?(text)
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         | 
    
        data/lib/forspell.rb
    ADDED
    
    
    
        metadata
    ADDED
    
    | @@ -0,0 +1,68 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: forspell
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.0.2
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Kirill Kuprikov
         | 
| 8 | 
            +
            autorequire: 
         | 
| 9 | 
            +
            bindir: exe
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2019-04-01 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies: []
         | 
| 13 | 
            +
            description: Forspell is spellchecker for code and documentation.It uses well-known
         | 
| 14 | 
            +
              hunspell tool and dictionary, provides customizable output, and could be easily
         | 
| 15 | 
            +
              integrated into CI pipeline.
         | 
| 16 | 
            +
            email: kkuprikov@gmail.com
         | 
| 17 | 
            +
            executables:
         | 
| 18 | 
            +
            - forspell
         | 
| 19 | 
            +
            extensions: []
         | 
| 20 | 
            +
            extra_rdoc_files: []
         | 
| 21 | 
            +
            files:
         | 
| 22 | 
            +
            - README.md
         | 
| 23 | 
            +
            - exe/clone_repos.sh
         | 
| 24 | 
            +
            - exe/create_dictionary
         | 
| 25 | 
            +
            - exe/forspell
         | 
| 26 | 
            +
            - exe/generate_logs
         | 
| 27 | 
            +
            - lib/forspell.rb
         | 
| 28 | 
            +
            - lib/forspell/cli.rb
         | 
| 29 | 
            +
            - lib/forspell/dictionaries/en_US.aff
         | 
| 30 | 
            +
            - lib/forspell/dictionaries/en_US.dic
         | 
| 31 | 
            +
            - lib/forspell/file_list.rb
         | 
| 32 | 
            +
            - lib/forspell/loaders.rb
         | 
| 33 | 
            +
            - lib/forspell/loaders/base.rb
         | 
| 34 | 
            +
            - lib/forspell/loaders/c.rb
         | 
| 35 | 
            +
            - lib/forspell/loaders/markdown.rb
         | 
| 36 | 
            +
            - lib/forspell/loaders/ruby.rb
         | 
| 37 | 
            +
            - lib/forspell/loaders/source.rb
         | 
| 38 | 
            +
            - lib/forspell/reporter.rb
         | 
| 39 | 
            +
            - lib/forspell/ruby.dict
         | 
| 40 | 
            +
            - lib/forspell/runner.rb
         | 
| 41 | 
            +
            - lib/forspell/sanitizer.rb
         | 
| 42 | 
            +
            - lib/forspell/speller.rb
         | 
| 43 | 
            +
            - lib/forspell/word_matcher.rb
         | 
| 44 | 
            +
            homepage: http://github.com/kkuprikov/forspell
         | 
| 45 | 
            +
            licenses:
         | 
| 46 | 
            +
            - MIT
         | 
| 47 | 
            +
            metadata: {}
         | 
| 48 | 
            +
            post_install_message: 
         | 
| 49 | 
            +
            rdoc_options: []
         | 
| 50 | 
            +
            require_paths:
         | 
| 51 | 
            +
            - lib
         | 
| 52 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 53 | 
            +
              requirements:
         | 
| 54 | 
            +
              - - ">="
         | 
| 55 | 
            +
                - !ruby/object:Gem::Version
         | 
| 56 | 
            +
                  version: '0'
         | 
| 57 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 58 | 
            +
              requirements:
         | 
| 59 | 
            +
              - - ">="
         | 
| 60 | 
            +
                - !ruby/object:Gem::Version
         | 
| 61 | 
            +
                  version: '0'
         | 
| 62 | 
            +
            requirements: []
         | 
| 63 | 
            +
            rubyforge_project: 
         | 
| 64 | 
            +
            rubygems_version: 2.7.8
         | 
| 65 | 
            +
            signing_key: 
         | 
| 66 | 
            +
            specification_version: 4
         | 
| 67 | 
            +
            summary: For spelling check
         | 
| 68 | 
            +
            test_files: []
         |