textlint-ruby 0.1.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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"