simple_english 0.3.0 → 0.4.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +47 -31
  3. data/docs/RULES.md +1 -1
  4. data/lib/simple_english/cli.rb +36 -13
  5. data/lib/simple_english/client/daemon.rb +225 -0
  6. data/lib/simple_english/client/language_tool.rb +95 -0
  7. data/lib/simple_english/daemon/engine.rb +40 -0
  8. data/lib/simple_english/{http.rb → daemon/http.rb} +10 -4
  9. data/lib/simple_english/{server.rb → daemon/server.rb} +113 -7
  10. data/lib/simple_english/lint/lint_plan.rb +22 -0
  11. data/lib/simple_english/lint/plain_text.rb +30 -0
  12. data/lib/simple_english/{result.rb → lint/result.rb} +1 -1
  13. data/lib/simple_english/{config.rb → setup/config.rb} +3 -2
  14. data/lib/simple_english/setup/fingerprint.rb +38 -0
  15. data/lib/simple_english/{install.rb → setup/install.rb} +19 -9
  16. data/lib/simple_english/setup/languagetool.rb +226 -0
  17. data/lib/simple_english/version.rb +1 -1
  18. data/lib/simple_english.rb +25 -43
  19. metadata +39 -36
  20. data/lib/simple_english/client.rb +0 -166
  21. data/lib/simple_english/engine.rb +0 -31
  22. data/lib/simple_english/languagetool.rb +0 -143
  23. data/lib/simple_english/plain_text.rb +0 -17
  24. /data/lib/simple_english/{annotated_text.rb → lint/annotated_text.rb} +0 -0
  25. /data/lib/simple_english/{counts.rb → lint/counts.rb} +0 -0
  26. /data/lib/simple_english/{extractor.rb → lint/extractor.rb} +0 -0
  27. /data/lib/simple_english/{finding.rb → lint/finding.rb} +0 -0
  28. /data/lib/simple_english/{markdown.rb → lint/markdown.rb} +0 -0
  29. /data/lib/simple_english/{paragraph.rb → lint/paragraph.rb} +0 -0
  30. /data/lib/simple_english/{segment.rb → lint/segment.rb} +0 -0
  31. /data/lib/simple_english/{span.rb → lint/span.rb} +0 -0
  32. /data/lib/simple_english/{suppressions.rb → lint/suppressions.rb} +0 -0
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_english
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - TonyCTHsu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-29 00:00:00.000000000 Z
11
+ date: 2026-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tree_sitter_language_pack
@@ -39,63 +39,63 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rubyzip
42
+ name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '3.0'
48
- type: :runtime
47
+ version: '13.0'
48
+ type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '3.0'
54
+ version: '13.0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rake
56
+ name: minitest
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '13.0'
61
+ version: '6.0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '13.0'
68
+ version: '6.0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: minitest
70
+ name: minitest-mock
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '6.0'
75
+ version: '5.27'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '6.0'
82
+ version: '5.27'
83
83
  - !ruby/object:Gem::Dependency
84
- name: minitest-mock
84
+ name: rubyzip
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '5.27'
89
+ version: '3.0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '5.27'
96
+ version: '3.0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: json
98
+ name: rexml
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - ">="
@@ -109,7 +109,7 @@ dependencies:
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
- name: rexml
112
+ name: json
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - ">="
@@ -135,25 +135,28 @@ files:
135
135
  - bin/se
136
136
  - docs/RULES.md
137
137
  - lib/simple_english.rb
138
- - lib/simple_english/annotated_text.rb
139
138
  - lib/simple_english/cli.rb
140
- - lib/simple_english/client.rb
141
- - lib/simple_english/config.rb
142
- - lib/simple_english/counts.rb
143
- - lib/simple_english/engine.rb
144
- - lib/simple_english/extractor.rb
145
- - lib/simple_english/finding.rb
146
- - lib/simple_english/http.rb
147
- - lib/simple_english/install.rb
148
- - lib/simple_english/languagetool.rb
149
- - lib/simple_english/markdown.rb
150
- - lib/simple_english/paragraph.rb
151
- - lib/simple_english/plain_text.rb
152
- - lib/simple_english/result.rb
153
- - lib/simple_english/segment.rb
154
- - lib/simple_english/server.rb
155
- - lib/simple_english/span.rb
156
- - lib/simple_english/suppressions.rb
139
+ - lib/simple_english/client/daemon.rb
140
+ - lib/simple_english/client/language_tool.rb
141
+ - lib/simple_english/daemon/engine.rb
142
+ - lib/simple_english/daemon/http.rb
143
+ - lib/simple_english/daemon/server.rb
144
+ - lib/simple_english/lint/annotated_text.rb
145
+ - lib/simple_english/lint/counts.rb
146
+ - lib/simple_english/lint/extractor.rb
147
+ - lib/simple_english/lint/finding.rb
148
+ - lib/simple_english/lint/lint_plan.rb
149
+ - lib/simple_english/lint/markdown.rb
150
+ - lib/simple_english/lint/paragraph.rb
151
+ - lib/simple_english/lint/plain_text.rb
152
+ - lib/simple_english/lint/result.rb
153
+ - lib/simple_english/lint/segment.rb
154
+ - lib/simple_english/lint/span.rb
155
+ - lib/simple_english/lint/suppressions.rb
156
+ - lib/simple_english/setup/config.rb
157
+ - lib/simple_english/setup/fingerprint.rb
158
+ - lib/simple_english/setup/install.rb
159
+ - lib/simple_english/setup/languagetool.rb
157
160
  - lib/simple_english/version.rb
158
161
  - rules/simple-english.xml
159
162
  homepage: https://github.com/TonyCTHsu/simple-english
@@ -162,7 +165,7 @@ licenses:
162
165
  metadata:
163
166
  homepage_uri: https://github.com/TonyCTHsu/simple-english
164
167
  source_code_uri: https://github.com/TonyCTHsu/simple-english
165
- changelog_uri: https://github.com/TonyCTHsu/simple-english/blob/v0.3.0/CHANGELOG.md
168
+ changelog_uri: https://github.com/TonyCTHsu/simple-english/blob/v0.4.0/CHANGELOG.md
166
169
  post_install_message:
167
170
  rdoc_options: []
168
171
  require_paths:
@@ -1,166 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # HTTP client for the LanguageTool server and the se daemon.
4
-
5
- require "json"
6
- require "net/http"
7
- require "rbconfig"
8
- require "uri"
9
-
10
- require_relative "plain_text"
11
- require_relative "languagetool"
12
-
13
- module SimpleEnglish
14
- module Client
15
- module_function
16
-
17
- # LanguageTool reports offsets and lengths in Java UTF-16 code units.
18
- # Return a 1-based line and UTF-16 column for its 0-based offset.
19
- def offset_to_position(text, offset)
20
- line = 1
21
- column = 1
22
- units = 0
23
- text.each_char do |char|
24
- return [line, column] if units >= offset
25
- units += (char.ord > 0xFFFF) ? 2 : 1
26
- if char == "\n"
27
- line += 1
28
- column = 1
29
- else
30
- column += (char.ord > 0xFFFF) ? 2 : 1
31
- end
32
- end
33
- [line, column]
34
- end
35
-
36
- DEFAULT_PORT = 8181
37
- REQUEST_TIMEOUT = 30
38
-
39
- def url
40
- ENV.fetch("SE_SERVER_URL") { "http://localhost:#{DEFAULT_PORT}" }
41
- end
42
-
43
- def post(uri, params, read_timeout: REQUEST_TIMEOUT)
44
- Net::HTTP.start(uri.host, uri.port, open_timeout: 5,
45
- read_timeout: read_timeout) do |http|
46
- request = Net::HTTP::Post.new(uri.request_uri)
47
- request.set_form_data(params)
48
- http.request(request)
49
- end
50
- end
51
-
52
- private_class_method :post
53
-
54
- # Pattern rules. `payload` is Markdown-stripped text (String) or an
55
- # AnnotatedText::Result (code comments): one payload interface,
56
- # #lt_params and #locate, either side of the daemon's LT request.
57
- def check(payload, base_url: url)
58
- payload = to_payload(payload)
59
- params = {"language" => "en",
60
- "enabledRules" => SimpleEnglish::LanguageTool.rule_ids.join(","),
61
- "enabledOnly" => "true"}.merge(payload.lt_params)
62
- response = post(URI("#{base_url}/v2/check"), params)
63
- parse_matches(JSON.parse(response.body).fetch("matches"), payload)
64
- end
65
-
66
- def parse_matches(matches, payload)
67
- payload = to_payload(payload)
68
- matches.map do |match|
69
- offset = match.fetch("offset")
70
- line, column = payload.locate(offset)
71
- end_line, end_column = payload.locate(offset + match.fetch("length"))
72
- Finding.new(line: line, column: column,
73
- end_line: end_line, end_column: end_column,
74
- rule: match.fetch("rule").fetch("id"),
75
- message: with_context(match.fetch("message"), match))
76
- end
77
- end
78
-
79
- # Prefix the offending text, so a finding says what to change,
80
- # not only how. Context positions use Java UTF-16 code units.
81
- def with_context(message, match)
82
- context = match["context"] or return message
83
- matched = utf16_slice(context.fetch("text", ""),
84
- context.fetch("offset", 0).to_i, context.fetch("length", 0).to_i)
85
- matched.empty? ? message : "\"#{matched}\" - #{message}"
86
- end
87
-
88
- def utf16_slice(text, offset, length)
89
- first = utf16_index(text, offset)
90
- last = utf16_index(text, offset + length)
91
- text[first...last]
92
- end
93
-
94
- def utf16_index(text, offset)
95
- units = 0
96
- text.each_char.with_index do |char, index|
97
- return index if units >= offset
98
- units += (char.ord > 0xFFFF) ? 2 : 1
99
- end
100
- text.length
101
- end
102
-
103
- def to_payload(payload)
104
- payload.is_a?(String) ? PlainText.new(payload) : payload
105
- end
106
-
107
- private_class_method :to_payload, :with_context, :utf16_slice, :utf16_index
108
-
109
- # Full lint via the se daemon. Raw Markdown in, or code
110
- # source with a language for the comment pipeline. nil when
111
- # unreachable or the response is unusable, so callers can fall
112
- # back without seeing a stack trace.
113
- def lint(text, base_url: url, language: nil)
114
- params = {"text" => text}
115
- params["language"] = language if language
116
- response = post(URI("#{base_url}/lint"), params)
117
- return nil unless response.is_a?(Net::HTTPSuccess)
118
- body = JSON.parse(response.body)
119
- return nil unless body.is_a?(Array)
120
- body.map do |hash|
121
- Finding.new(line: hash.fetch("line"), column: hash["column"],
122
- end_line: hash["end_line"], end_column: hash["end_column"],
123
- rule: hash.fetch("rule"), message: hash.fetch("message"))
124
- end
125
- rescue SystemCallError, SocketError, Timeout::Error,
126
- JSON::ParserError, TypeError
127
- nil
128
- end
129
-
130
- def up?(base_url: url)
131
- uri = URI(base_url)
132
- Net::HTTP.start(uri.host, uri.port, open_timeout: 1,
133
- read_timeout: 2) { |http| http.head("/") }
134
- true
135
- rescue Errno::ECONNREFUSED, SocketError, Timeout::Error
136
- false
137
- end
138
-
139
- # True when the daemon answers. Starts it when we own the default
140
- # URL. A custom SE_SERVER_URL belongs to someone else, so
141
- # never spawn against it. It returns false when unusable. The
142
- # caller owns the exit status. Diagnostics go to stderr here, where the cause
143
- # is known.
144
- def ensure_up(install: SimpleEnglish::Install.from_env)
145
- return true if up?
146
- if ENV["SE_SERVER_URL"]
147
- warn "error: SE_SERVER_URL is set but #{url} does not answer."
148
- return false
149
- end
150
- unless File.exist?(install.server_jar)
151
- warn install.setup_error
152
- return false
153
- end
154
- warn "se: daemon not running; starting it (first lint takes ~15s)..."
155
- bin = File.expand_path("../../bin/se", __dir__)
156
- Process.spawn(RbConfig.ruby, bin, "serve", out: File::NULL, err: File::NULL)
157
- deadline = Time.now + 90
158
- until Time.now > deadline
159
- return true if up?
160
- sleep 0.5
161
- end
162
- warn "error: se daemon did not come up. Run `se serve` and read its output."
163
- false
164
- end
165
- end
166
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # The complete lint engine, daemon-side. Never call SimpleEnglish.lint_text
4
- # here: that call probes the daemon and recurses into the server.
5
-
6
- require "json"
7
-
8
- module SimpleEnglish
9
- module Engine
10
- module_function
11
-
12
- def lint(text, base_url:, language: nil)
13
- if language
14
- spans = SimpleEnglish::Extractor.comment_spans(text, language)
15
- spans.empty? ? [] :
16
- SimpleEnglish::Client.check(
17
- SimpleEnglish::AnnotatedText.build(text, spans), base_url: base_url
18
- )
19
- else
20
- stripped = SimpleEnglish::Markdown.strip(text)
21
- (SimpleEnglish::Counts.check(stripped) +
22
- SimpleEnglish::Client.check(stripped, base_url: base_url))
23
- .sort_by { |finding| [finding.line, finding.rule] }
24
- end
25
- end
26
-
27
- def lint_json(text, base_url:, language: nil)
28
- JSON.generate(lint(text, base_url: base_url, language: language).map(&:to_h))
29
- end
30
- end
31
- end
@@ -1,143 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # The pinned LanguageTool distro: its version, its rules file, and the
4
- # downloader. Everything that resolves "where it is on this machine and
5
- # which java runs it" lives in Install. The engine runs as the daemon's
6
- # inner HTTP server (lib/simple_english/server.rb).
7
-
8
- module SimpleEnglish
9
- module LanguageTool
10
- RULES_FILE = File.expand_path("../../rules/simple-english.xml", __dir__)
11
- # The only place a LanguageTool version number appears. `se
12
- # setup` downloads this version, and every jar path derives from it. No
13
- # env var can point at another one.
14
- LT_VERSION = "6.6"
15
- CACHE_DIR_ENV = "SE_CACHE_DIR"
16
- JAVA_ENV = "SE_JAVA"
17
- # macOS ships a /usr/bin/java stub that reports no runtime. Homebrew
18
- # installs the real one here, outside PATH.
19
- HOMEBREW_JAVA = "/opt/homebrew/opt/openjdk/bin/java"
20
- TIMEOUT_SECONDS = 300
21
-
22
- module_function
23
-
24
- # Download and unpack the pinned LanguageTool into DIR. Idempotent:
25
- # returns the install directory, or nil after warning why. Pure Ruby
26
- # throughout: the download uses Net::HTTP and the unpack uses rubyzip,
27
- # so setup needs no curl or unzip on the machine.
28
- def install(dir)
29
- require "fileutils"
30
- dest = File.join(dir, "LanguageTool-#{LT_VERSION}")
31
- if File.exist?(File.join(dest, "languagetool-commandline.jar"))
32
- remove_stale_versions(dir, dest)
33
- return dest
34
- end
35
- zip = File.join(dir, "LanguageTool-#{LT_VERSION}.zip")
36
- FileUtils.mkdir_p(dir)
37
- return nil unless download(download_url, zip)
38
- return nil unless extract(zip, dir)
39
- File.delete(zip)
40
- remove_stale_versions(dir, dest)
41
- dest
42
- end
43
-
44
- def download_url
45
- "https://languagetool.org/download/LanguageTool-#{LT_VERSION}.zip"
46
- end
47
-
48
- # Streams URL to TO and follows redirects. Warns and returns false
49
- # on any transport or HTTP failure, so callers never see a stack
50
- # trace.
51
- def download(url, to, redirects: 3)
52
- require "net/http"
53
- uri = URI(url)
54
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
55
- open_timeout: 10, read_timeout: 60) do |http|
56
- http.request(Net::HTTP::Get.new(uri)) do |response|
57
- case response
58
- when Net::HTTPRedirection
59
- return false if redirects.zero?
60
- return download(response["location"], to, redirects: redirects - 1)
61
- when Net::HTTPSuccess
62
- File.binwrite(to, "")
63
- File.open(to, "wb") do |file|
64
- response.read_body { |chunk| file.write(chunk) }
65
- end
66
- return true
67
- else
68
- warn "error: download failed: HTTP #{response.code} from #{url}"
69
- return false
70
- end
71
- end
72
- end
73
- rescue SystemCallError, SocketError, Timeout::Error,
74
- OpenSSL::SSL::SSLError, Net::ProtocolError => e
75
- warn "error: download failed: #{e.class}: #{e.message}"
76
- false
77
- end
78
-
79
- # Unpacks ZIP into DIR and refuses entries that try to escape it.
80
- # Warns and returns false on any failure.
81
- def extract(zip, dir)
82
- require "zip"
83
- Zip::File.open(zip) do |archive|
84
- archive.each do |entry|
85
- unless safe_entry_target(dir, entry.name)
86
- warn "error: zip entry escapes the install dir: #{entry.name}"
87
- return false
88
- end
89
- entry.extract(entry.name, destination_directory: dir,
90
- create_parent_directories: true)
91
- end
92
- end
93
- true
94
- rescue SystemCallError, Zip::Error => e
95
- warn "error: unpack failed: #{e.class}: #{e.message}"
96
- false
97
- end
98
-
99
- # The path to write zip entry NAME into, or nil when NAME escapes
100
- # DIR. A download is untrusted input: this is the zip-slip
101
- # guard.
102
- def safe_entry_target(dir, name)
103
- target = File.expand_path(File.join(dir, name))
104
- base = File.expand_path(dir) + File::SEPARATOR
105
- target.start_with?(base) ? target : nil
106
- end
107
-
108
- # One gem version pins one LanguageTool version. Older dirs are
109
- # hundreds of MB each, so delete them on every setup run and keep
110
- # only the current version (and any zips: they are re-deleted or
111
- # re-extracted by their own flow).
112
- def remove_stale_versions(dir, keep)
113
- require "fileutils"
114
- Dir.glob(File.join(dir, "LanguageTool-*")).each do |entry|
115
- FileUtils.rm_rf(entry) if File.directory?(entry) && entry != keep
116
- end
117
- end
118
-
119
- # Proves the installation works end to end: java runs the
120
- # commandline jar and the custom rules load. The command exits
121
- # nonzero on a broken java, a corrupt jar, or invalid rules XML,
122
- # so setup can report a real verdict instead of "files exist".
123
- # Returns a boolean, never raises or warns: the caller owns the
124
- # message.
125
- def smoke(install)
126
- rule = rule_ids.first
127
- return false if rule.nil?
128
- require "tmpdir"
129
- Dir.mktmpdir do |dir|
130
- text = File.join(dir, "smoke.txt")
131
- File.write(text, "Check this sentence.\n")
132
- system(install.java!, "-jar", install.commandline_jar,
133
- "--language", "en", "--rulefile", RULES_FILE,
134
- "--enable", rule, "--enabledonly", text,
135
- out: File::NULL, err: File::NULL) ? true : false
136
- end
137
- end
138
-
139
- def rule_ids
140
- File.read(RULES_FILE).scan(/<rule(?:group)? id="(\w+)"/).flatten
141
- end
142
- end
143
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SimpleEnglish
4
- module Client
5
- # The plain-text payload for /v2/check, duck-compatible with
6
- # AnnotatedText::Result: #lt_params and #locate.
7
- PlainText = Struct.new(:text) do
8
- def lt_params
9
- {"text" => text}
10
- end
11
-
12
- def locate(utf16_offset)
13
- Client.offset_to_position(text, utf16_offset)
14
- end
15
- end
16
- end
17
- end
File without changes
File without changes
File without changes
File without changes