simple_english 0.2.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.
- checksums.yaml +4 -4
- data/README.md +61 -39
- data/docs/RULES.md +1 -1
- data/lib/simple_english/cli.rb +53 -15
- data/lib/simple_english/client/daemon.rb +225 -0
- data/lib/simple_english/client/language_tool.rb +95 -0
- data/lib/simple_english/daemon/engine.rb +40 -0
- data/lib/simple_english/{http.rb → daemon/http.rb} +10 -4
- data/lib/simple_english/{server.rb → daemon/server.rb} +113 -7
- data/lib/simple_english/{extractor.rb → lint/extractor.rb} +6 -1
- data/lib/simple_english/lint/finding.rb +9 -0
- data/lib/simple_english/lint/lint_plan.rb +22 -0
- data/lib/simple_english/{markdown.rb → lint/markdown.rb} +28 -10
- data/lib/simple_english/lint/plain_text.rb +30 -0
- data/lib/simple_english/{result.rb → lint/result.rb} +1 -1
- data/lib/simple_english/{config.rb → setup/config.rb} +3 -2
- data/lib/simple_english/setup/fingerprint.rb +38 -0
- data/lib/simple_english/{install.rb → setup/install.rb} +19 -9
- data/lib/simple_english/setup/languagetool.rb +226 -0
- data/lib/simple_english/version.rb +1 -1
- data/lib/simple_english.rb +25 -43
- data/rules/simple-english.xml +5 -1
- metadata +42 -36
- data/lib/simple_english/client.rb +0 -165
- data/lib/simple_english/engine.rb +0 -31
- data/lib/simple_english/finding.rb +0 -8
- data/lib/simple_english/languagetool.rb +0 -143
- data/lib/simple_english/plain_text.rb +0 -18
- /data/lib/simple_english/{annotated_text.rb → lint/annotated_text.rb} +0 -0
- /data/lib/simple_english/{counts.rb → lint/counts.rb} +0 -0
- /data/lib/simple_english/{paragraph.rb → lint/paragraph.rb} +0 -0
- /data/lib/simple_english/{segment.rb → lint/segment.rb} +0 -0
- /data/lib/simple_english/{span.rb → lint/span.rb} +0 -0
- /data/lib/simple_english/{suppressions.rb → lint/suppressions.rb} +0 -0
|
@@ -0,0 +1,226 @@
|
|
|
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/daemon/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 {"dir" => install dir, "downloaded" => bool} so `se setup`
|
|
26
|
+
# can say what it found and what it fetched, or nil after warning
|
|
27
|
+
# why. Pure Ruby throughout: the download uses Net::HTTP and the
|
|
28
|
+
# unpack parses the zip with stdlib Zlib, so setup needs no curl,
|
|
29
|
+
# unzip, or unpack gem on the machine.
|
|
30
|
+
def install(dir)
|
|
31
|
+
require "fileutils"
|
|
32
|
+
dest = File.join(dir, "LanguageTool-#{LT_VERSION}")
|
|
33
|
+
if File.exist?(File.join(dest, "languagetool-commandline.jar"))
|
|
34
|
+
remove_stale_versions(dir, dest)
|
|
35
|
+
return {"dir" => dest, "downloaded" => false}
|
|
36
|
+
end
|
|
37
|
+
zip = File.join(dir, "LanguageTool-#{LT_VERSION}.zip")
|
|
38
|
+
FileUtils.mkdir_p(dir)
|
|
39
|
+
return nil unless download(download_url, zip)
|
|
40
|
+
return nil unless extract(zip, dir)
|
|
41
|
+
File.delete(zip)
|
|
42
|
+
remove_stale_versions(dir, dest)
|
|
43
|
+
{"dir" => dest, "downloaded" => true}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def download_url
|
|
47
|
+
"https://languagetool.org/download/LanguageTool-#{LT_VERSION}.zip"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Streams URL to TO and follows redirects. Warns and returns false
|
|
51
|
+
# on any transport or HTTP failure, so callers never see a stack
|
|
52
|
+
# trace.
|
|
53
|
+
def download(url, to, redirects: 3)
|
|
54
|
+
require "net/http"
|
|
55
|
+
uri = URI(url)
|
|
56
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
57
|
+
open_timeout: 10, read_timeout: 60) do |http|
|
|
58
|
+
http.request(Net::HTTP::Get.new(uri)) do |response|
|
|
59
|
+
case response
|
|
60
|
+
when Net::HTTPRedirection
|
|
61
|
+
return false if redirects.zero?
|
|
62
|
+
return download(response["location"], to, redirects: redirects - 1)
|
|
63
|
+
when Net::HTTPSuccess
|
|
64
|
+
File.binwrite(to, "")
|
|
65
|
+
File.open(to, "wb") do |file|
|
|
66
|
+
response.read_body { |chunk| file.write(chunk) }
|
|
67
|
+
end
|
|
68
|
+
return true
|
|
69
|
+
else
|
|
70
|
+
warn "error: download failed: HTTP #{response.code} from #{url}"
|
|
71
|
+
return false
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
rescue SystemCallError, SocketError, Timeout::Error,
|
|
76
|
+
OpenSSL::SSL::SSLError, Net::ProtocolError => e
|
|
77
|
+
warn "error: download failed: #{e.class}: #{e.message}"
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Unpacks ZIP into DIR on stdlib Zlib: the End of Central Directory
|
|
82
|
+
# record locates the central directory, each entry names a local
|
|
83
|
+
# header, and each stream inflates raw. The input is the one pinned
|
|
84
|
+
# LanguageTool distro, not arbitrary archives, so zip64 (4 GB+ or
|
|
85
|
+
# 65,535+ entries) is refused, not parsed. Warns and returns false
|
|
86
|
+
# on any failure.
|
|
87
|
+
def extract(zip, dir)
|
|
88
|
+
require "fileutils"
|
|
89
|
+
require "zlib"
|
|
90
|
+
bytes = File.binread(zip)
|
|
91
|
+
tail = eocd(bytes)
|
|
92
|
+
raise Zlib::DataError, "not a zip archive" if tail.nil?
|
|
93
|
+
count, cd_offset = tail
|
|
94
|
+
pos = cd_offset
|
|
95
|
+
count.times do
|
|
96
|
+
name, method, csize, crc, local, pos = cd_entry(bytes, pos)
|
|
97
|
+
raise Zlib::DataError, "corrupt central directory" if name.nil?
|
|
98
|
+
next if name.end_with?("/")
|
|
99
|
+
target = safe_entry_target(dir, name)
|
|
100
|
+
if target.nil?
|
|
101
|
+
warn "error: zip entry escapes the install dir: #{name}"
|
|
102
|
+
return false
|
|
103
|
+
end
|
|
104
|
+
data_at = local_data_offset(bytes, local)
|
|
105
|
+
raise Zlib::DataError, "corrupt local header" if data_at.nil?
|
|
106
|
+
data = inflate_entry(bytes.byteslice(data_at, csize), method)
|
|
107
|
+
raise Zlib::DataError, "CRC mismatch: #{name}" unless Zlib.crc32(data) == crc
|
|
108
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
109
|
+
File.binwrite(target, data)
|
|
110
|
+
end
|
|
111
|
+
true
|
|
112
|
+
rescue Zlib::Error, SystemCallError => e
|
|
113
|
+
warn "error: unpack failed: #{e.class}: #{e.message}"
|
|
114
|
+
false
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The End of Central Directory record: the entry count and the
|
|
118
|
+
# central directory offset, or nil. The record ends the archive,
|
|
119
|
+
# possibly behind a comment of at most 65,535 bytes, so the scan
|
|
120
|
+
# starts at the tail. Zip64 sentinels are refused: the pinned
|
|
121
|
+
# distro is far under the limits.
|
|
122
|
+
def eocd(bytes)
|
|
123
|
+
limit = [bytes.bytesize, 65_557].min
|
|
124
|
+
window = bytes.byteslice(bytes.bytesize - limit, limit)
|
|
125
|
+
at = window.rindex("PK\x05\x06")
|
|
126
|
+
return nil if at.nil?
|
|
127
|
+
rec = window.byteslice(at, 22)
|
|
128
|
+
return nil if rec.bytesize < 22
|
|
129
|
+
count = rec.byteslice(10, 2).unpack1("v")
|
|
130
|
+
cd = rec.byteslice(16, 4).unpack1("V")
|
|
131
|
+
return nil if count == 0xFFFF || cd == 0xFFFFFFFF
|
|
132
|
+
[count, cd]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# One central-directory record at POS: the entry name,
|
|
136
|
+
# compression method, compressed size, CRC-32, local header
|
|
137
|
+
# offset, and the offset of the next record. nil when the record
|
|
138
|
+
# is truncated or unsigned.
|
|
139
|
+
def cd_entry(bytes, pos)
|
|
140
|
+
head = bytes.byteslice(pos, 46)
|
|
141
|
+
return nil unless head.byteslice(0, 4) == "PK\x01\x02" && head.bytesize == 46
|
|
142
|
+
name_len = head.byteslice(28, 2).unpack1("v")
|
|
143
|
+
name = bytes.byteslice(pos + 46, name_len)
|
|
144
|
+
[name,
|
|
145
|
+
head.byteslice(10, 2).unpack1("v"),
|
|
146
|
+
head.byteslice(20, 4).unpack1("V"),
|
|
147
|
+
head.byteslice(16, 4).unpack1("V"),
|
|
148
|
+
head.byteslice(42, 4).unpack1("V"),
|
|
149
|
+
pos + 46 + name_len + head.byteslice(30, 2).unpack1("v") +
|
|
150
|
+
head.byteslice(32, 2).unpack1("v")]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Where an entry's data sits: the local header at OFFSET names
|
|
154
|
+
# its own name and extra lengths, and the data follows them. nil
|
|
155
|
+
# when the header is truncated or unsigned.
|
|
156
|
+
def local_data_offset(bytes, offset)
|
|
157
|
+
head = bytes.byteslice(offset, 30)
|
|
158
|
+
return nil unless head.byteslice(0, 4) == "PK\x03\x04" && head.bytesize == 30
|
|
159
|
+
offset + 30 + head.byteslice(26, 2).unpack1("v") +
|
|
160
|
+
head.byteslice(28, 2).unpack1("v")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# One entry's bytes: STORED is copied, DEFLATE is inflated raw.
|
|
164
|
+
def inflate_entry(blob, method)
|
|
165
|
+
return blob if method.zero?
|
|
166
|
+
return Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(blob) if method == 8
|
|
167
|
+
raise Zlib::DataError, "unsupported compression method #{method}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# The path to write zip entry NAME into, or nil when NAME escapes
|
|
171
|
+
# DIR. A download is untrusted input: this is the zip-slip
|
|
172
|
+
# guard.
|
|
173
|
+
def safe_entry_target(dir, name)
|
|
174
|
+
target = File.expand_path(File.join(dir, name))
|
|
175
|
+
base = File.expand_path(dir) + File::SEPARATOR
|
|
176
|
+
target.start_with?(base) ? target : nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# One gem version pins one LanguageTool version. Older dirs are
|
|
180
|
+
# hundreds of MB each, so delete them on every setup run and keep
|
|
181
|
+
# only the current version (and any zips: they are re-deleted or
|
|
182
|
+
# re-extracted by their own flow).
|
|
183
|
+
def remove_stale_versions(dir, keep)
|
|
184
|
+
require "fileutils"
|
|
185
|
+
Dir.glob(File.join(dir, "LanguageTool-*")).each do |entry|
|
|
186
|
+
FileUtils.rm_rf(entry) if File.directory?(entry) && entry != keep
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Proves the installation works end to end: java runs the
|
|
191
|
+
# commandline jar and the custom rules load. The command exits
|
|
192
|
+
# nonzero on a broken java, a corrupt jar, or invalid rules XML,
|
|
193
|
+
# so setup can report a real verdict instead of "files exist".
|
|
194
|
+
# Returns a boolean, never raises or warns: the caller owns the
|
|
195
|
+
# message.
|
|
196
|
+
def smoke(install)
|
|
197
|
+
rule = rule_ids.first
|
|
198
|
+
return false if rule.nil?
|
|
199
|
+
require "tmpdir"
|
|
200
|
+
Dir.mktmpdir do |dir|
|
|
201
|
+
text = File.join(dir, "smoke.txt")
|
|
202
|
+
File.write(text, "Check this sentence.\n")
|
|
203
|
+
system(install.java!, "-jar", install.commandline_jar,
|
|
204
|
+
"--language", "en", "--rulefile", RULES_FILE,
|
|
205
|
+
"--enable", rule, "--enabledonly", text,
|
|
206
|
+
out: File::NULL, err: File::NULL) ? true : false
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# All rule IDs in RULES_FILE, in document order. The tag scan pairs each opening rule or rulegroup
|
|
211
|
+
# tag with the id attribute inside it, so attribute order never
|
|
212
|
+
# matters. It reads the repo's own file, which the test suite
|
|
213
|
+
# round-trips through LanguageTool, so malformed XML fails the
|
|
214
|
+
# build, not the boot. The daemon's enabledRules parameter on
|
|
215
|
+
# every lint request needs this list. A rule LT loads but the
|
|
216
|
+
# list omits is dead, because enabledOnly is set.
|
|
217
|
+
# examples_check asserts this list against a real XML parser,
|
|
218
|
+
# and LT itself rejects duplicate ids at load, so no dedup is
|
|
219
|
+
# needed here.
|
|
220
|
+
def rule_ids(path = RULES_FILE)
|
|
221
|
+
File.read(path).scan(/<(?:rule|rulegroup)\b[^>]*>/).filter_map do |tag|
|
|
222
|
+
tag[/\bid="([^"]+)"/, 1]
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
data/lib/simple_english.rb
CHANGED
|
@@ -5,19 +5,23 @@
|
|
|
5
5
|
# This file is the composition root. The pieces live in lib/simple_english/.
|
|
6
6
|
|
|
7
7
|
require_relative "simple_english/version"
|
|
8
|
-
require_relative "simple_english/finding"
|
|
9
|
-
require_relative "simple_english/
|
|
10
|
-
require_relative "simple_english/
|
|
11
|
-
require_relative "simple_english/
|
|
12
|
-
require_relative "simple_english/
|
|
13
|
-
require_relative "simple_english/
|
|
14
|
-
require_relative "simple_english/
|
|
15
|
-
require_relative "simple_english/
|
|
16
|
-
require_relative "simple_english/
|
|
17
|
-
require_relative "simple_english/
|
|
18
|
-
require_relative "simple_english/
|
|
19
|
-
require_relative "simple_english/
|
|
20
|
-
require_relative "simple_english/
|
|
8
|
+
require_relative "simple_english/lint/finding"
|
|
9
|
+
require_relative "simple_english/lint/paragraph"
|
|
10
|
+
require_relative "simple_english/lint/markdown"
|
|
11
|
+
require_relative "simple_english/lint/counts"
|
|
12
|
+
require_relative "simple_english/setup/languagetool"
|
|
13
|
+
require_relative "simple_english/setup/install"
|
|
14
|
+
require_relative "simple_english/lint/extractor"
|
|
15
|
+
require_relative "simple_english/lint/lint_plan"
|
|
16
|
+
require_relative "simple_english/lint/annotated_text"
|
|
17
|
+
require_relative "simple_english/lint/suppressions"
|
|
18
|
+
require_relative "simple_english/setup/config"
|
|
19
|
+
require_relative "simple_english/setup/fingerprint"
|
|
20
|
+
require_relative "simple_english/client/language_tool"
|
|
21
|
+
require_relative "simple_english/client/daemon"
|
|
22
|
+
require_relative "simple_english/daemon/engine"
|
|
23
|
+
require_relative "simple_english/daemon/http"
|
|
24
|
+
require_relative "simple_english/daemon/server"
|
|
21
25
|
|
|
22
26
|
module SimpleEnglish
|
|
23
27
|
module_function
|
|
@@ -30,48 +34,26 @@ module SimpleEnglish
|
|
|
30
34
|
Client.lint(text)
|
|
31
35
|
end
|
|
32
36
|
|
|
33
|
-
# One entry point for files.
|
|
34
|
-
#
|
|
37
|
+
# One entry point for files. LintPlan decides what kind of lint
|
|
38
|
+
# this is. Both tiers share it, so they cannot drift. Code files
|
|
39
|
+
# lint comments through the daemon. nil means unreachable:
|
|
35
40
|
# the caller decides how fatal that is.
|
|
36
41
|
def lint_file(path, text = File.read(path))
|
|
37
42
|
language = Extractor.language_for(path)
|
|
43
|
+
plan = LintPlan.call(text, language)
|
|
44
|
+
return [] if plan.nil?
|
|
38
45
|
findings =
|
|
39
|
-
if
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
if plan == :prose
|
|
47
|
+
lint_text(text)
|
|
48
|
+
else
|
|
42
49
|
return nil unless Client.ensure_up(install: Install.from_env)
|
|
43
50
|
result = Client.lint(text, language: language)
|
|
44
51
|
if result.nil?
|
|
45
52
|
warn "error: se daemon did not answer. Run `se serve` and read its output."
|
|
46
53
|
end
|
|
47
54
|
result
|
|
48
|
-
else
|
|
49
|
-
lint_text(text)
|
|
50
55
|
end
|
|
51
56
|
return nil unless findings
|
|
52
57
|
Suppressions.filter(text, findings)
|
|
53
58
|
end
|
|
54
|
-
|
|
55
|
-
def corpus_test
|
|
56
|
-
pairs = Dir.glob(File.expand_path("../test/corpus/*-before.md", __dir__))
|
|
57
|
-
.sort
|
|
58
|
-
failed = pairs.flat_map do |before|
|
|
59
|
-
after = before.sub(/-before\.md\z/, "-after.md")
|
|
60
|
-
before_findings = lint_text(File.read(before))
|
|
61
|
-
after_findings = lint_text(File.read(after))
|
|
62
|
-
messages = []
|
|
63
|
-
if before_findings.nil? || after_findings.nil?
|
|
64
|
-
messages << "#{before}: daemon unreachable"
|
|
65
|
-
else
|
|
66
|
-
messages << "#{before}: no findings, the rule set misses this case" if before_findings.empty?
|
|
67
|
-
unless after_findings.empty?
|
|
68
|
-
messages << "#{after}: still flagged: #{after_findings.map(&:rule).join(", ")}"
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
messages
|
|
72
|
-
end
|
|
73
|
-
failed.each { |failure| puts "FAIL #{failure}" }
|
|
74
|
-
puts "corpus: #{pairs.size} pairs, #{failed.size} failures"
|
|
75
|
-
failed.empty?
|
|
76
|
-
end
|
|
77
59
|
end
|
data/rules/simple-english.xml
CHANGED
|
@@ -56,10 +56,14 @@
|
|
|
56
56
|
</rule>
|
|
57
57
|
|
|
58
58
|
<rule id="SE_ING_AFTER_COMMA" name="No -ing after a comma">
|
|
59
|
-
<pattern><token>,</token><token postag="VBG"
|
|
59
|
+
<pattern><token>,</token><token postag="VBG"><exception scope="next" regexp="yes">[,)]|and|or</exception></token></pattern>
|
|
60
60
|
<message>Start a new sentence instead of the -ing phrase.</message>
|
|
61
61
|
<example type="incorrect">The tool runs<marker>, making</marker> it easy.</example>
|
|
62
62
|
<example type="correct">The tool runs. It is easy.</example>
|
|
63
|
+
<example type="correct">The walkthrough (accounts, VPC, secrets, troubleshooting) is in the guide.</example>
|
|
64
|
+
<example type="correct">The guide covers billing, troubleshooting, and support for each environment.</example>
|
|
65
|
+
<example type="correct">The guide covers billing, troubleshooting and support for each environment.</example>
|
|
66
|
+
<example type="correct">The guide covers billing, troubleshooting or support for each environment.</example>
|
|
63
67
|
</rule>
|
|
64
68
|
|
|
65
69
|
<rule id="SE_KEEP_THAT" name="make sure that">
|
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.
|
|
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-
|
|
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:
|
|
42
|
+
name: rake
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
45
|
- - "~>"
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '
|
|
48
|
-
type: :
|
|
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: '
|
|
54
|
+
version: '13.0'
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name:
|
|
56
|
+
name: minitest
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
59
|
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
82
|
+
version: '5.27'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
|
-
name:
|
|
84
|
+
name: rubyzip
|
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
|
86
86
|
requirements:
|
|
87
87
|
- - "~>"
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '
|
|
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: '
|
|
96
|
+
version: '3.0'
|
|
97
97
|
- !ruby/object:Gem::Dependency
|
|
98
|
-
name:
|
|
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:
|
|
112
|
+
name: json
|
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
|
114
114
|
requirements:
|
|
115
115
|
- - ">="
|
|
@@ -135,31 +135,37 @@ 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/
|
|
142
|
-
- lib/simple_english/
|
|
143
|
-
- lib/simple_english/
|
|
144
|
-
- lib/simple_english/
|
|
145
|
-
- lib/simple_english/
|
|
146
|
-
- lib/simple_english/
|
|
147
|
-
- lib/simple_english/
|
|
148
|
-
- lib/simple_english/
|
|
149
|
-
- lib/simple_english/
|
|
150
|
-
- lib/simple_english/
|
|
151
|
-
- lib/simple_english/
|
|
152
|
-
- lib/simple_english/
|
|
153
|
-
- lib/simple_english/
|
|
154
|
-
- lib/simple_english/
|
|
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
|
|
160
163
|
licenses:
|
|
161
164
|
- MIT
|
|
162
|
-
metadata:
|
|
165
|
+
metadata:
|
|
166
|
+
homepage_uri: https://github.com/TonyCTHsu/simple-english
|
|
167
|
+
source_code_uri: https://github.com/TonyCTHsu/simple-english
|
|
168
|
+
changelog_uri: https://github.com/TonyCTHsu/simple-english/blob/v0.4.0/CHANGELOG.md
|
|
163
169
|
post_install_message:
|
|
164
170
|
rdoc_options: []
|
|
165
171
|
require_paths:
|
|
@@ -1,165 +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 in Java UTF-16 code units.
|
|
18
|
-
# Astral characters (emoji) count as two. Count them so a
|
|
19
|
-
# newline comparison cannot drift past a boundary. A match
|
|
20
|
-
# starting at the newline itself belongs to the next line.
|
|
21
|
-
def offset_to_line(text, offset)
|
|
22
|
-
line = 1
|
|
23
|
-
units = 0
|
|
24
|
-
text.each_char do |char|
|
|
25
|
-
line += 1 if char == "\n" && units <= offset
|
|
26
|
-
return line if units >= offset
|
|
27
|
-
units += (char.ord > 0xFFFF) ? 2 : 1
|
|
28
|
-
end
|
|
29
|
-
line
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# The 1-based column of a UTF-16 offset within its line. Astral
|
|
33
|
-
# characters count as two units, like the match offset. A match
|
|
34
|
-
# starting at the newline itself is column one of the next line.
|
|
35
|
-
def offset_to_column(text, offset)
|
|
36
|
-
column = 0
|
|
37
|
-
units = 0
|
|
38
|
-
text.each_char do |char|
|
|
39
|
-
if char == "\n"
|
|
40
|
-
return 1 if units >= offset
|
|
41
|
-
column = 0
|
|
42
|
-
units += 1
|
|
43
|
-
next
|
|
44
|
-
end
|
|
45
|
-
return column + 1 if units >= offset
|
|
46
|
-
units += (char.ord > 0xFFFF) ? 2 : 1
|
|
47
|
-
column += 1
|
|
48
|
-
end
|
|
49
|
-
column + 1
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
DEFAULT_PORT = 8181
|
|
53
|
-
REQUEST_TIMEOUT = 30
|
|
54
|
-
|
|
55
|
-
def url
|
|
56
|
-
ENV.fetch("SE_SERVER_URL") { "http://localhost:#{DEFAULT_PORT}" }
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def post(uri, params, read_timeout: REQUEST_TIMEOUT)
|
|
60
|
-
Net::HTTP.start(uri.host, uri.port, open_timeout: 5,
|
|
61
|
-
read_timeout: read_timeout) do |http|
|
|
62
|
-
request = Net::HTTP::Post.new(uri.request_uri)
|
|
63
|
-
request.set_form_data(params)
|
|
64
|
-
http.request(request)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
private_class_method :post
|
|
69
|
-
|
|
70
|
-
# Pattern rules. `payload` is Markdown-stripped text (String) or an
|
|
71
|
-
# AnnotatedText::Result (code comments): one payload interface,
|
|
72
|
-
# #lt_params and #locate, either side of the daemon's LT request.
|
|
73
|
-
def check(payload, base_url: url)
|
|
74
|
-
payload = to_payload(payload)
|
|
75
|
-
params = {"language" => "en",
|
|
76
|
-
"enabledRules" => SimpleEnglish::LanguageTool.rule_ids.join(","),
|
|
77
|
-
"enabledOnly" => "true"}.merge(payload.lt_params)
|
|
78
|
-
response = post(URI("#{base_url}/v2/check"), params)
|
|
79
|
-
parse_matches(JSON.parse(response.body).fetch("matches"), payload)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def parse_matches(matches, payload)
|
|
83
|
-
payload = to_payload(payload)
|
|
84
|
-
matches.map do |match|
|
|
85
|
-
line, column = payload.locate(match.fetch("offset"))
|
|
86
|
-
Finding.new(line: line, column: column,
|
|
87
|
-
rule: match.fetch("rule").fetch("id"),
|
|
88
|
-
message: with_context(match.fetch("message"), match))
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Prefix the offending text, so a finding says what to change,
|
|
93
|
-
# not only how. LanguageTool returns it in the match context.
|
|
94
|
-
# Context offsets are Java UTF-16 code units, like the match
|
|
95
|
-
# offset.
|
|
96
|
-
def with_context(message, match)
|
|
97
|
-
context = match["context"] or return message
|
|
98
|
-
matched = context.fetch("text", "")[
|
|
99
|
-
context.fetch("offset", 0).to_i, context.fetch("length", 0).to_i]
|
|
100
|
-
matched.empty? ? message : "\"#{matched}\" - #{message}"
|
|
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
|
|
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
|
-
rule: hash.fetch("rule"), message: hash.fetch("message"))
|
|
123
|
-
end
|
|
124
|
-
rescue SystemCallError, SocketError, Timeout::Error,
|
|
125
|
-
JSON::ParserError, TypeError
|
|
126
|
-
nil
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def up?(base_url: url)
|
|
130
|
-
uri = URI(base_url)
|
|
131
|
-
Net::HTTP.start(uri.host, uri.port, open_timeout: 1,
|
|
132
|
-
read_timeout: 2) { |http| http.head("/") }
|
|
133
|
-
true
|
|
134
|
-
rescue Errno::ECONNREFUSED, SocketError, Timeout::Error
|
|
135
|
-
false
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# True when the daemon answers. Starts it when we own the default
|
|
139
|
-
# URL. A custom SE_SERVER_URL belongs to someone else, so
|
|
140
|
-
# never spawn against it. It returns false when unusable. The
|
|
141
|
-
# caller owns the exit status. Diagnostics go to stderr here, where the cause
|
|
142
|
-
# is known.
|
|
143
|
-
def ensure_up(install: SimpleEnglish::Install.from_env)
|
|
144
|
-
return true if up?
|
|
145
|
-
if ENV["SE_SERVER_URL"]
|
|
146
|
-
warn "error: SE_SERVER_URL is set but #{url} does not answer."
|
|
147
|
-
return false
|
|
148
|
-
end
|
|
149
|
-
unless File.exist?(install.server_jar)
|
|
150
|
-
warn install.setup_error
|
|
151
|
-
return false
|
|
152
|
-
end
|
|
153
|
-
warn "se: daemon not running; starting it (first lint takes ~15s)..."
|
|
154
|
-
bin = File.expand_path("../../bin/se", __dir__)
|
|
155
|
-
Process.spawn(RbConfig.ruby, bin, "serve", out: File::NULL, err: File::NULL)
|
|
156
|
-
deadline = Time.now + 90
|
|
157
|
-
until Time.now > deadline
|
|
158
|
-
return true if up?
|
|
159
|
-
sleep 0.5
|
|
160
|
-
end
|
|
161
|
-
warn "error: se daemon did not come up. Run `se serve` and read its output."
|
|
162
|
-
false
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
end
|