textlint-ruby 0.1.1 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34fcdaa79b17b3ea92addafa5125e0d28f4cd114fb338d0f6ec97d243070bdd1
4
- data.tar.gz: 28cd88e7d364424b8c75889f6c57fa92e03d02c2756f70974ac2d395a59e301b
3
+ metadata.gz: 5bcefd07ee04b3840f45267770a1faa99fe9ed9f7f5161b11df94fa0839bcb62
4
+ data.tar.gz: d2dd891a4a2ce1866d13dd6963b3944494f2ca19ad15aec67859743edb6c41b7
5
5
  SHA512:
6
- metadata.gz: e4b43a0e423cef7b375202b4b20f144563bd16434dd279bd991b1d29ab696f1852bb71f13dad18adb78fe407537fe9c620482ebcec692cd2c67f4224704203e6
7
- data.tar.gz: b2bc9ca563fe12d94d161508c24fdd261585db92e35088fad0ceeb83a5ae54c614454bb314a2db2b5facc62e28498508e22a10ecb4dcff4e44a78cbdb3abc2f3
6
+ metadata.gz: 9658421db72d38e73718eae4325d64b15d9b871976e8db31dda7ae2e3630348f66fbf30c30cdd18744f980e3cbdc5c34e89f02d15f455ed687a1176a377756fc
7
+ data.tar.gz: 979b738b6230d5d717a0d6e534128867274d641a003603f6ea8050b6e9462c5cd18dc2294e348d25f31078f3ea7dd42f4d3187ca8bef2ec6bcf78db0509e86ea
data/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
1
  ## [Unreleased]
2
2
 
3
+ - Add stdio server mode. textlint-plugin-ruby(>= v2.0.0) can connect with textlint-ruby via stdio.
4
+ - Remove textlint-ruby-optimized command
5
+
3
6
  ## [0.1.1]
4
7
 
5
8
  - Support ruby 2.5
data/README.md CHANGED
@@ -4,8 +4,6 @@ Ruby AST parser for [textlint-ruby-plugin](https://github.com/alpaca-tc/textlint
4
4
 
5
5
  ## Installation
6
6
 
7
- **TODO: release gems**
8
-
9
7
  Add this line to your application's Gemfile:
10
8
 
11
9
  ```ruby
@@ -25,11 +23,12 @@ Or install it yourself as:
25
23
  Parse ruby to textlint AST
26
24
 
27
25
  ```sh
26
+ # Parse specific file
28
27
  $ textlint-ruby ./path/to/file.rb
29
28
  {"type":"Document","raw":"...","range":[0,465],"loc":{"start":{"line":1,"column":0},"end":{"line":26,"column":0}},"children":[...]}
30
29
 
31
- # textlint-ruby-optimized is 10x faster but many features of ruby are disabled.
32
- $ textlint-ruby-optimized ./path/to/file.rb
30
+ # Boot textlint-ruby server for textlint-plugin-ruby
31
+ $ textlint-ruby --stdio
33
32
  ```
34
33
 
35
34
  ### Supported nodes
@@ -69,7 +68,7 @@ Supported node types are only `Document` and `Str` because this plugin is used t
69
68
  | ASTNodeTypes.ImageExit | TxtNode | |
70
69
  | ASTNodeTypes.HorizontalRule | TxtNode | |
71
70
  | ASTNodeTypes.HorizontalRuleExit | TxtNode | |
72
- | ASTNodeTypes.Comment | TxtTextNode | |
71
+ | ASTNodeTypes.Comment | TxtTextNode | yes |
73
72
  | ASTNodeTypes.CommentExit | TxtTextNode | |
74
73
  | ASTNodeTypes.Str | TxtTextNode | yes |
75
74
  | ASTNodeTypes.StrExit | TxtTextNode | |
data/bin/textlint-ruby CHANGED
@@ -5,21 +5,4 @@ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
5
 
6
6
  require 'textlint'
7
7
 
8
- path = ARGV[0]
9
-
10
- unless File.exist?(path.to_s)
11
- warn("Error: No such file or directory: #{path}")
12
- exit(1)
13
- end
14
-
15
- content = File.read(path)
16
-
17
- begin
18
- ast = Textlint::Parser.parse(content)
19
- puts(ast.as_textlint_json.to_json)
20
- rescue Textlint::SyntaxError => error
21
- warn("Failed to compile: #{path}")
22
- warn('')
23
- warn(error)
24
- exit(1)
25
- end
8
+ Textlint::Cli.run(ARGV.clone)
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module Textlint
6
+ class Cli
7
+ attr_reader :options
8
+
9
+ DEFAULT_OPTIONS = {
10
+ stdio: false,
11
+ paths: []
12
+ }.freeze
13
+
14
+ # @param argv [Array] ARGV
15
+ def self.run(argv)
16
+ new(argv).run
17
+ end
18
+
19
+ # @param argv [Array] ARGV
20
+ def initialize(argv)
21
+ @options = DEFAULT_OPTIONS.dup
22
+ parse_options(argv)
23
+ end
24
+
25
+ # Run textlint-ruby
26
+ #
27
+ # @return [void]
28
+ def run
29
+ if @options[:stdio]
30
+ run_stdio_server
31
+ else
32
+ run_file_parser
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def run_stdio_server
39
+ Textlint::Server.new.start
40
+ end
41
+
42
+ def run_file_parser
43
+ path = @options[:path]
44
+
45
+ unless File.exist?(path.to_s)
46
+ warn("Error: No such file or directory: #{path}")
47
+ exit(1)
48
+ end
49
+
50
+ content = File.read(path)
51
+
52
+ begin
53
+ ast = Textlint::Parser.parse(content)
54
+ puts(ast.as_textlint_json.to_json)
55
+ rescue Textlint::SyntaxError => error
56
+ warn("Failed to compile: #{path}")
57
+ warn('')
58
+ warn(error)
59
+ exit(1)
60
+ end
61
+ end
62
+
63
+ def parse_options(argv)
64
+ option_parser = ::OptionParser.new do |parser|
65
+ parser.banner = 'Usage: textlint-ruby [rubyfile_path] [options]'
66
+ parser.program_name = 'textlint-ruby'
67
+
68
+ parser.version = [
69
+ Textlint::VERSION::MAJOR,
70
+ Textlint::VERSION::MINOR,
71
+ Textlint::VERSION::TINY
72
+ ]
73
+
74
+ parser.on('--stdio', 'use stdio') do
75
+ @options[:stdio] = true
76
+ end
77
+
78
+ parser.on_tail('-h', '--help') do
79
+ puts option_parser.help
80
+ exit
81
+ end
82
+ end
83
+
84
+ @options[:path] = option_parser.parse(argv).first
85
+
86
+ if !@options[:stdio] && !@options[:path]
87
+ puts option_parser.help
88
+ exit(1)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -5,6 +5,8 @@ require 'ripper'
5
5
  module Textlint
6
6
  class Parser
7
7
  class RubyToTextlintAST < ::Ripper::Filter
8
+ EVENT_RE = /\Aon_(?<name>\w*)_(?:beg|end)\z/.freeze
9
+
8
10
  # @param src [String]
9
11
  # @param lines [Array<String>]
10
12
  def initialize(src)
@@ -12,20 +14,23 @@ module Textlint
12
14
  @src = src
13
15
  @pos = 0
14
16
  @lines = @src.lines
17
+ @events = []
15
18
  end
16
19
 
17
20
  private
18
21
 
22
+ # All events call this method
19
23
  # NOTE: Instance variables are allowed to assign only here to readable code.
20
24
  def on_default(event, token, node)
21
25
  @token = token
26
+ @event = event
22
27
 
23
28
  method_name = :"custom_#{event}"
24
29
 
25
30
  if respond_to?(method_name, true)
26
31
  @range = @pos...(@pos + @token.size)
27
32
  @raw = @src[@range]
28
- node = send(method_name, node)
33
+ send(method_name, node)
29
34
  end
30
35
 
31
36
  @pos += @token.size
@@ -34,6 +39,14 @@ module Textlint
34
39
  end
35
40
 
36
41
  def default_node_attributes(type:, **attributes)
42
+ break_count = @token.scan(Textlint::BREAK_RE).size
43
+
44
+ last_column = if break_count == 0
45
+ column + @token.size
46
+ else
47
+ @token.match(LAST_LINE_RE).to_s.size
48
+ end
49
+
37
50
  {
38
51
  type: type,
39
52
  raw: @raw,
@@ -43,12 +56,21 @@ module Textlint
43
56
  line: lineno,
44
57
  column: column
45
58
  ),
46
- end: end_txt_node_position
59
+ end: Textlint::Nodes::TxtNodePosition.new(
60
+ line: lineno + break_count,
61
+ column: last_column
62
+ )
47
63
  )
48
64
  }.merge(attributes)
49
65
  end
50
66
 
67
+ # "hello world"
68
+ # 'hello world'
69
+ # %q{hello world}
51
70
  def custom_on_tstring_content(parentNode)
71
+ begin_event_name, _begin_node = @events.last
72
+ return unless %w[tstring qwords].include?(begin_event_name)
73
+
52
74
  node = Textlint::Nodes::TxtTextNode.new(
53
75
  **default_node_attributes(
54
76
  type: Textlint::Nodes::STR,
@@ -61,20 +83,68 @@ module Textlint
61
83
  parentNode
62
84
  end
63
85
 
64
- def end_txt_node_position
65
- break_count = @token.scan(Textlint::BREAK_RE).size
86
+ # # hello world
87
+ def custom_on_comment(parentNode)
88
+ node = Textlint::Nodes::TxtTextNode.new(
89
+ **default_node_attributes(
90
+ type: Textlint::Nodes::COMMENT,
91
+ value: @token.gsub(/\A#/, '')
92
+ )
93
+ )
66
94
 
67
- last_column = if break_count == 0
68
- column + @token.size
69
- else
70
- @token.match(LAST_LINE_RE).to_s.size
71
- end
95
+ parentNode.children.push(node)
96
+ end
97
+
98
+ # =begin
99
+ # hello world
100
+ # =end
101
+ def custom_on_embdoc(parentNode)
102
+ _begin_event_name, begin_node = @events.last
103
+
104
+ node = Textlint::Nodes::TxtTextNode.new(
105
+ type: Textlint::Nodes::COMMENT,
106
+ loc: begin_node.loc,
107
+ range: begin_node.range,
108
+ raw: begin_node.raw,
109
+ value: @token
110
+ )
111
+
112
+ parentNode.children.push(node)
113
+ end
72
114
 
73
- Textlint::Nodes::TxtNodePosition.new(
74
- line: lineno + break_count,
75
- column: last_column
115
+ def event_name
116
+ matched = EVENT_RE.match(@event)
117
+ matched[:name] if matched
118
+ end
119
+
120
+ def on_beg_event(*)
121
+ @events.push(
122
+ [
123
+ event_name,
124
+ Textlint::Nodes::TxtTextNode.new(
125
+ **default_node_attributes(
126
+ type: nil, # NOTE: beg event has no type
127
+ value: @token
128
+ )
129
+ )
130
+ ]
76
131
  )
77
132
  end
133
+
134
+ def on_end_event(*)
135
+ @events.pop
136
+ end
137
+
138
+ alias custom_on_tstring_beg on_beg_event
139
+ alias custom_on_regexp_beg on_beg_event
140
+ alias custom_on_embexpr_beg on_beg_event
141
+ alias custom_on_qwords_beg on_beg_event
142
+ alias custom_on_embdoc_beg on_beg_event
143
+
144
+ alias custom_on_tstring_end on_end_event
145
+ alias custom_on_regexp_end on_end_event
146
+ alias custom_on_embexpr_end on_end_event
147
+ alias custom_on_embdoc_end on_end_event
78
148
  end
79
149
 
80
150
  # Parse ruby code to AST for textlint
@@ -86,33 +156,41 @@ module Textlint
86
156
  new(src).call
87
157
  end
88
158
 
89
- # @param src [String] ruby source code
90
- def initialize(src)
91
- @src = src
92
- end
93
-
94
- # Parse ruby code to AST for textlint
159
+ # Parse ruby code to TxtParentNode
160
+ #
161
+ # @param src [String]
95
162
  #
96
163
  # @return [Textlint::Nodes::TxtParentNode]
97
- def call
98
- check_syntax!
99
-
100
- document = Textlint::Nodes::TxtParentNode.new(
164
+ def self.build_document(src)
165
+ Textlint::Nodes::TxtParentNode.new(
101
166
  type: Textlint::Nodes::DOCUMENT,
102
- raw: @src,
103
- range: 0...@src.size,
167
+ raw: src,
168
+ range: 0...src.size,
104
169
  loc: Textlint::Nodes::TxtNodeLineLocation.new(
105
170
  start: Textlint::Nodes::TxtNodePosition.new(
106
171
  line: 1,
107
172
  column: 0
108
173
  ),
109
174
  end: Textlint::Nodes::TxtNodePosition.new(
110
- line: @src.split(Textlint::BREAK_RE).size + 1,
111
- column: @src.match(LAST_LINE_RE).to_s.size # extract last line
175
+ line: src.split(Textlint::BREAK_RE).size + 1,
176
+ column: src.match(LAST_LINE_RE).to_s.size # extract last line
112
177
  )
113
178
  )
114
179
  )
180
+ end
181
+
182
+ # @param src [String] ruby source code
183
+ def initialize(src)
184
+ @src = src
185
+ end
186
+
187
+ # Parse ruby code to AST for textlint
188
+ #
189
+ # @return [Textlint::Nodes::TxtParentNode]
190
+ def call
191
+ check_syntax!
115
192
 
193
+ document = self.class.build_document(@src)
116
194
  RubyToTextlintAST.new(@src).parse(document)
117
195
  end
118
196
 
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Textlint
6
+ class Server
7
+ REQUIRED_VERSION = '2.0.0'
8
+
9
+ AVAILABLE_ACTIONS = %w[
10
+ parse
11
+ info
12
+ ].freeze
13
+
14
+ # @param stdin [IO]
15
+ # @param stdout [IO]
16
+ # @param stderr [IO]
17
+ def initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr)
18
+ @stdin = stdin
19
+ @stdout = stdout
20
+ @stderr = stderr
21
+ end
22
+
23
+ # Start stdio server
24
+ #
25
+ # @return [void]
26
+ def start
27
+ @stdout.sync = true
28
+ trap_signals
29
+ start_server
30
+ end
31
+
32
+ private
33
+
34
+ def start_server
35
+ receive_stdin do |json|
36
+ action = json['action']
37
+ result = send(:"do_#{action}", json)
38
+ request_seq = json['seq']
39
+ response = { request_seq: request_seq, result: result }
40
+
41
+ @stdout.puts(JSON.dump(response))
42
+ end
43
+ end
44
+
45
+ def validate_request(json)
46
+ if Gem::Version.create(json['version'].to_s) < Gem::Version.create(REQUIRED_VERSION)
47
+ raise(Textlint::RequestError, "textlint-ruby requires textlin-plugin-ruby version >= #{REQUIRED_VERSION}")
48
+ elsif !AVAILABLE_ACTIONS.include?(json['action'])
49
+ raise(Textlint::RequestError, "Unknown action(#{json['action']}) given. Available actions are #{AVAILABLE_ACTIONS}")
50
+ end
51
+ end
52
+
53
+ # request spec
54
+ # { "seq": number, "action": string, "version": string, [key: string]: any }
55
+ #
56
+ # response spec
57
+ # action "info"
58
+ # { "request_seq": number, "version": string, result: string(version) }
59
+ #
60
+ # action "parse"
61
+ # { "request_seq": number, "version": string, result: string(AST for textlint) }
62
+ def receive_stdin
63
+ loop do
64
+ return if @stdin.eof?
65
+
66
+ line = @stdin.readline
67
+
68
+ begin
69
+ json = JSON.parse(line)
70
+ validate_request(json)
71
+ yield(json)
72
+ rescue JSON::JSONError
73
+ warn("Can't parse request to JSON")
74
+ rescue StandardError => error
75
+ warn(error.message)
76
+ end
77
+ end
78
+ end
79
+
80
+ def warn(message)
81
+ @stderr.puts(message)
82
+ end
83
+
84
+ def trap_signals
85
+ ::Signal.trap(:SIGINT) do
86
+ exit
87
+ end
88
+ end
89
+
90
+ def do_info(_json)
91
+ Textlint::VERSION::STRING
92
+ end
93
+
94
+ def do_parse(json)
95
+ path = json['path'].to_s
96
+
97
+ unless File.exist?(path)
98
+ raise(Textlint::RequestError, "Error: No such file or directory: #{path}")
99
+ end
100
+
101
+ content = File.read(path)
102
+
103
+ ast = begin
104
+ Textlint::Parser.parse(content)
105
+ rescue Textlint::SyntaxError
106
+ error = Textlint::RequestError.new("Failed to parse: #{path}. syntax error or the file is incompatible with the ruby(#{RUBY_VERSION}) running textlint-ruby")
107
+ warn(error)
108
+
109
+ Textlint::Parser.build_document(content)
110
+ end
111
+
112
+ ast.as_textlint_json
113
+ end
114
+ end
115
+ end
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Textlint
4
- VERSION = '0.1.1'
4
+ module VERSION
5
+ MAJOR = 2
6
+ MINOR = 0
7
+ TINY = 0
8
+
9
+ STRING = [MAJOR, MINOR, TINY].compact.join('.')
10
+ end
5
11
  end
data/lib/textlint.rb CHANGED
@@ -4,10 +4,14 @@ require 'json'
4
4
  require 'textlint/version'
5
5
  require 'textlint/nodes'
6
6
  require 'textlint/parser'
7
+ require 'textlint/cli'
8
+ require 'textlint/server'
7
9
 
8
10
  module Textlint
9
11
  BREAK_RE = /\r?\n/.freeze
10
12
  LAST_LINE_RE = /(?!\r?\n).*\z/.freeze
11
13
 
12
- class SyntaxError < StandardError; end
14
+ class Error < StandardError; end
15
+ class SyntaxError < Error; end
16
+ class RequestError < Error; end
13
17
  end
data/textlint.gemspec CHANGED
@@ -4,7 +4,7 @@ require_relative 'lib/textlint/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'textlint-ruby'
7
- spec.version = Textlint::VERSION
7
+ spec.version = Textlint::VERSION::STRING
8
8
  spec.authors = ['alpaca-tc']
9
9
  spec.email = ['alpaca-tc@alpaca.tc']
10
10
 
metadata CHANGED
@@ -1,21 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: textlint-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - alpaca-tc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-11-29 00:00:00.000000000 Z
11
+ date: 2021-12-02 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: ''
14
14
  email:
15
15
  - alpaca-tc@alpaca.tc
16
16
  executables:
17
17
  - textlint-ruby
18
- - textlint-ruby-optimized
19
18
  extensions: []
20
19
  extra_rdoc_files: []
21
20
  files:
@@ -31,11 +30,12 @@ files:
31
30
  - README.md
32
31
  - Rakefile
33
32
  - bin/textlint-ruby
34
- - bin/textlint-ruby-optimized
35
33
  - lib/textlint-ruby.rb
36
34
  - lib/textlint.rb
35
+ - lib/textlint/cli.rb
37
36
  - lib/textlint/nodes.rb
38
37
  - lib/textlint/parser.rb
38
+ - lib/textlint/server.rb
39
39
  - lib/textlint/version.rb
40
40
  - textlint.gemspec
41
41
  homepage: https://github.com/alpaca-tc/textlint-ruby
@@ -60,7 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  requirements: []
63
- rubygems_version: 3.2.32
63
+ rubygems_version: 3.2.22
64
64
  signing_key:
65
65
  specification_version: 4
66
66
  summary: ruby source code parser for textlint
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env ruby --disable-all
2
- # frozen_string_literal: true
3
-
4
- load "#{__dir__}/textlint-ruby"