synclenote 0.0.1 → 0.1.1

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: 909401f6f0f32860825c396ef438f53fd53447260570a88c234ae330b38dbb26
4
- data.tar.gz: b49889059a4adbc1a370acbcb7b3943aeb6099ea724b4008146cad193bd1b344
3
+ metadata.gz: 6842ad72e5f7f899d1b5d962b8af0db7cae65c99e16693cedd43de396b51c529
4
+ data.tar.gz: 296010bb3a8b526e43a49986314076bb85be2cd9a7c2cade2fd4a50eb6cf361f
5
5
  SHA512:
6
- metadata.gz: 802161aaf2e5bee2769aa5da00352ee0029a829b428afa72f67aa71259458bc7594e21512ac838973aa103fe0c46c076ddf772f7eba78f6b6692b4bb9c5527c6
7
- data.tar.gz: 6c8b25dfbbbbf568ffbee06de751f4267620fb4ed309d8d391823dbf6f0cda951f3682eb59a927e58fedfa999cf7e988a2d073acf46f559d3dc2f6ea7e3b9672
6
+ metadata.gz: b64e8832799ca114404b838d28fb159d4730072041caf405601d372ada55a25816741486cdcc864e89b4ea8c500a7b4de640d7f5bf26d1c01c4abc5a3beddc92
7
+ data.tar.gz: 7f1a3179d818e0fcff4e3174ce66d517d05e87ed8f31447cbaaa6c80e6ef2888501286c315107ca7050e380fb8be94d1ccd8252a749c0897fda393a89d472821
@@ -0,0 +1,68 @@
1
+ name: test
2
+
3
+ on:
4
+ - pull_request
5
+ - push
6
+ - workflow_dispatch
7
+
8
+ jobs:
9
+ test:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os:
14
+ - macos
15
+ - ubuntu
16
+ - windows
17
+ ruby_version:
18
+ - 3.3
19
+ - 3.2
20
+ - 3.1
21
+ runs-on: ${{ matrix.os }}-latest
22
+ steps:
23
+ - uses: actions/checkout@v3
24
+ - uses: kenchan0130/actions-system-info@1.2.0
25
+ id: system-info
26
+ - name: Cache rubygems
27
+ uses: actions/cache@v3.2.2
28
+ with:
29
+ path: vendor/bundle
30
+ key: ${{ runner.os }}-${{ steps.system-info.outputs.release }}-rubygems-${{ matrix.ruby_version }}-${{ hashFiles('**/Gemfile') }}-${{ hashFiles('**/*.gemspec') }}
31
+ restore-keys: |
32
+ ${{ runner.os }}-${{ steps.system-info.outputs.release }}-rubygems-${{ matrix.ruby_version }}
33
+ - name: Set up Ruby
34
+ uses: ruby/setup-ruby@v1
35
+ with:
36
+ ruby-version: ${{ matrix.ruby_version }}
37
+ - name: Install dependencies
38
+ shell: bash
39
+ run: |
40
+ set -eux
41
+ gem update --system
42
+ bundle config path vendor/bundle
43
+ bundle install --jobs "${{ steps.system-info.outputs.cpu-core }}" --retry 3
44
+ - name: Run tests
45
+ run: bundle exec rake
46
+ - name: Output versions
47
+ if: always()
48
+ shell: bash
49
+ run: |
50
+ set -eux
51
+ run_if_exist() {
52
+ if test "x$(which $1)" != "x"
53
+ then
54
+ "$@"
55
+ fi
56
+ }
57
+ uname -a
58
+ run_if_exist sw_vers # macos
59
+ run_if_exist lsb_release --all # ubuntu
60
+ ruby --version
61
+ gem --version
62
+ bundle --version
63
+ - name: Output gem versions
64
+ if: always()
65
+ run: bundle list
66
+ - name: Output deb package versions
67
+ if: always() && matrix.os == 'ubuntu'
68
+ run: dpkg -l
data/.gitignore CHANGED
@@ -19,3 +19,4 @@ tmp
19
19
  *.o
20
20
  *.a
21
21
  mkmf.log
22
+ /Gemfile.lock
data/Rakefile CHANGED
@@ -1,3 +1,9 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rake/testtask"
2
3
 
3
- task default: %i(build)
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList["test/**/*_test.rb"]
7
+ end
8
+
9
+ task(default: %i[test build])
@@ -1,5 +1,3 @@
1
- require "synclenote"
2
-
3
1
  require "yaml"
4
2
  require "pathname"
5
3
  require "logger"
@@ -135,16 +133,6 @@ EOS
135
133
  # -*- mode: yaml -*-
136
134
  # vi: set ft=yaml :
137
135
 
138
- EOS
139
-
140
- HEADER = <<EOS.freeze
141
- <?xml version="1.0" encoding="UTF-8"?>
142
- <!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
143
- <en-note>
144
- EOS
145
-
146
- FOOTER = <<EOS.freeze
147
- </en-note>
148
136
  EOS
149
137
 
150
138
  def load_yaml_path(path)
@@ -181,45 +169,8 @@ EOS
181
169
  return 4 # 4 sec for sandbox
182
170
  end
183
171
 
184
- def build_html(markdown_text)
185
- @formatter ||= Redcarpet::Markdown.new(
186
- Redcarpet::Render::HTML.new(
187
- filter_html: true,
188
- hard_wrap: true,
189
- ),
190
- underline: true,
191
- lax_spacing: true,
192
- footnotes: true,
193
- no_intra_emphasis: true,
194
- superscript: true,
195
- strikethrough: true,
196
- tables: true,
197
- space_after_headers: true,
198
- fenced_code_blocks: true,
199
- # autolink: true,
200
- )
201
- html = @formatter.render(markdown_text)
202
- return html
203
- end
204
-
205
- def create_note(note_path, options = {})
206
- title = nil
207
- tags = ["syncle"]
208
- body = nil
209
- note_path.open do |f|
210
- title = f.gets.chomp.sub(/\A#\s*/, "")
211
- if md = /\A(?<tags>(?:\[.*?\])+)(?:\s|\z)/.match(title)
212
- tags += md[:tags].scan(/\[(.*?)\]/).flatten
213
- title = md.post_match
214
- end
215
- body = f.read
216
- end
217
- html = build_html(body)
218
- content = HEADER +
219
- html.gsub(/ class=\".*?\"/, "").gsub(/<(br|hr|img).*?>/, "\\&</\\1>") +
220
- FOOTER
221
- o = options.merge(title: title, content: content, tagNames: tags)
222
- return Evernote::EDAM::Type::Note.new(o)
172
+ def create_note(note_path, **options)
173
+ return Synclenote::NoteBuilder.(note_path, **options)
223
174
  end
224
175
 
225
176
  def target_note?(note)
@@ -245,7 +196,7 @@ EOS
245
196
  }.to_yaml)
246
197
  tmp_file.close
247
198
  path.parent.mkpath
248
- FileUtils.mv(tmp_file.path, path)
199
+ FileUtils.mv(tmp_file.path, path) if !Synclenote::DRY_RUN
249
200
  end
250
201
  end
251
202
 
@@ -257,7 +208,12 @@ EOS
257
208
  return
258
209
  end
259
210
  logger.debug("doing createNote: %s" % note_path)
260
- created_note = note_store.createNote(token, new_note)
211
+ created_note =
212
+ if Synclenote::DRY_RUN
213
+ Evernote::EDAM::Type::Note.new(guid: "created_note_dummy_guid")
214
+ else
215
+ note_store.createNote(token, new_note)
216
+ end
261
217
  logger.debug("done createNote.")
262
218
  create_note_sync_status_file(note_sync_status_path, created_note,
263
219
  Time.now)
@@ -273,7 +229,12 @@ EOS
273
229
  return
274
230
  end
275
231
  logger.debug("doing updateNote: %s %s" % [note_path, guid])
276
- updated_note = note_store.updateNote(token, new_note)
232
+ updated_note =
233
+ if Synclenote::DRY_RUN
234
+ Evernote::EDAM::Type::Note.new(guid: "updated_note_dummy_guid")
235
+ else
236
+ note_store.updateNote(token, new_note)
237
+ end
277
238
  logger.debug("done updateNote.")
278
239
  create_note_sync_status_file(note_sync_status_path, updated_note, Time.now)
279
240
  logger.debug("created: %s" % note_sync_status_path)
@@ -283,10 +244,10 @@ EOS
283
244
  note_sync_status = load_yaml_path(note_sync_status_path)
284
245
  guid = note_sync_status[:guid]
285
246
  logger.debug("doing deleteNote: %s" % guid)
286
- note_store.deleteNote(token, guid)
247
+ note_store.deleteNote(token, guid) if !Synclenote::DRY_RUN
287
248
  logger.debug("done deleteNote.")
288
249
 
289
- note_sync_status_path.delete
250
+ note_sync_status_path.delete if !Synclenote::DRY_RUN
290
251
  logger.debug("removed: %s" % note_sync_status_path)
291
252
  end
292
253
  end
@@ -1,5 +1,3 @@
1
- require "synclenote"
2
-
3
1
  require "ostruct"
4
2
 
5
3
  module Synclenote::Configuration
@@ -0,0 +1,52 @@
1
+ require "nokogiri"
2
+ require "redcarpet"
3
+
4
+ class Synclenote::MarkdownToEnmlBuilder
5
+ HEADER = <<~EOS
6
+ <?xml version="1.0" encoding="UTF-8"?>
7
+ <!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
8
+ <en-note>
9
+ EOS
10
+
11
+ FOOTER = <<~EOS
12
+ </en-note>
13
+ EOS
14
+
15
+ class << self
16
+ def call(markdown_text)
17
+ formatter = Redcarpet::Markdown.new(
18
+ Redcarpet::Render::HTML.new(
19
+ filter_html: true,
20
+ hard_wrap: true,
21
+ ),
22
+ underline: true,
23
+ lax_spacing: true,
24
+ footnotes: true,
25
+ no_intra_emphasis: true,
26
+ superscript: true,
27
+ strikethrough: true,
28
+ tables: true,
29
+ space_after_headers: true,
30
+ fenced_code_blocks: true,
31
+ # autolink: true,
32
+ )
33
+ html = formatter.render(markdown_text)
34
+ root = Nokogiri::HTML::DocumentFragment.parse(html)
35
+
36
+ root.css("*[class]").each do |node|
37
+ node.remove_attribute("class")
38
+ end
39
+
40
+ # '<a href="example.html?foo=bar&baz=quux">'
41
+ # =>
42
+ # '<a href="example.html?foo=bar&amp;baz=quux">'
43
+ treated_html = root.to_html
44
+ content = [
45
+ HEADER,
46
+ treated_html.gsub(/<(br|hr|img).*?>/, "\\&</\\1>"),
47
+ FOOTER,
48
+ ].join
49
+ return content
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,41 @@
1
+ module Synclenote::NoteBuilder
2
+ DEFAULT_TAGS = %w[syncle]
3
+
4
+ TITLE_IF_EMPTY = "no title"
5
+
6
+ class << self
7
+ def call(raw_note, guid: nil)
8
+ lines = raw_note.each_line.lazy
9
+ title, tags_in_raw_note, body_markdown = *parse_lines(lines)
10
+ title = TITLE_IF_EMPTY if /\A\s*\z/ === title
11
+ tags = (DEFAULT_TAGS + tags_in_raw_note).uniq
12
+ enml = Synclenote::MarkdownToEnmlBuilder.(body_markdown)
13
+
14
+ options = {
15
+ title:,
16
+ content: enml,
17
+ tagNames: tags,
18
+ }
19
+ options[:guid] = guid if guid
20
+ return Evernote::EDAM::Type::Note.new(options)
21
+ end
22
+
23
+ private
24
+
25
+ def parse_lines(lines)
26
+ tags = []
27
+ title = lines.first.chomp.sub(/\A#\s*/, "")
28
+ lines = lines.drop(1)
29
+ md = /\A(?<tags>(?:\[.*?\])+)(?:\s|\z)/.match(title)
30
+ if md
31
+ tags = md[:tags].scan(/\[(.*?)\]/).flatten
32
+ title = md.post_match
33
+ end
34
+
35
+ lines = lines.drop_while { |l| /\A\s*\z/ === l }
36
+ body_markdown = lines.to_a.join
37
+
38
+ return [title, tags, body_markdown]
39
+ end
40
+ end
41
+ end
@@ -1,3 +1,3 @@
1
1
  module Synclenote
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.1"
3
3
  end
data/lib/synclenote.rb CHANGED
@@ -1,11 +1,14 @@
1
1
  # This module is a namespace for this gem.
2
2
  module Synclenote
3
- autoload :VERSION, "synclenote/varsion"
4
-
5
- autoload :Command, "synclenote/command"
6
- autoload :Configuration, "synclenote/configuration"
3
+ DRY_RUN = false
7
4
 
8
5
  def self.configure(version, &block)
9
6
  Configuration.run(version, &block)
10
7
  end
11
8
  end
9
+
10
+ (Pathname(__dir__).glob("**/*.rb") - [Pathname(__FILE__)]).map { |path|
11
+ path.sub_ext("")
12
+ }.sort.each do |path|
13
+ require(path.relative_path_from(__dir__))
14
+ end
data/synclenote.gemspec CHANGED
@@ -24,10 +24,13 @@ Gem::Specification.new do |spec|
24
24
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
25
25
  spec.require_paths = ["lib"]
26
26
 
27
+ spec.add_runtime_dependency "base64" # suppress warnings
27
28
  spec.add_runtime_dependency "evernote_oauth"
29
+ spec.add_runtime_dependency "nokogiri"
28
30
  spec.add_runtime_dependency "redcarpet"
29
31
  spec.add_runtime_dependency "thor"
30
32
  spec.add_development_dependency "bundler"
31
33
  spec.add_development_dependency "debug"
32
34
  spec.add_development_dependency "rake"
35
+ spec.add_development_dependency "test-unit"
33
36
  end
@@ -0,0 +1,58 @@
1
+ require "test_helper"
2
+
3
+ class Synclenote::MarkdownToEnmlBuilderTest < Test::Unit::TestCase
4
+ sub_test_case(".call") do
5
+ data(
6
+ regular_text: {
7
+ markdown_text: <<~EOS,
8
+ body
9
+ EOS
10
+ expected_enml_text: <<~EOS,
11
+ <p>body</p>
12
+ EOS
13
+ },
14
+ fenced_code_blocks: {
15
+ markdown_text: <<~EOS,
16
+ ```ruby
17
+ 1 + 2 * 3
18
+ ```
19
+ EOS
20
+ # remove "class" attribute
21
+ expected_enml_text: <<~EOS,
22
+ <pre><code>1 + 2 * 3
23
+ </code></pre>
24
+ EOS
25
+ },
26
+ hard_wrap: {
27
+ markdown_text: <<~EOS,
28
+ abc
29
+ def
30
+ EOS
31
+ # convert "<br/>" to "<br></br>"
32
+ expected_enml_text: <<~EOS,
33
+ <p>abc<br></br>
34
+ def</p>
35
+ EOS
36
+ },
37
+ hyperlink: {
38
+ markdown_text: <<~EOS,
39
+ [link text](https://example.org/?foo=bar&baz=quux;hoge=fuga#anchor)
40
+ EOS
41
+ # convert "&" to "&amp;"
42
+ expected_enml_text: <<~EOS,
43
+ <p><a href="https://example.org/?foo=bar&amp;baz=quux;hoge=fuga#anchor">link text</a></p>
44
+ EOS
45
+ },
46
+ )
47
+ test("returns ENML String") do |h|
48
+ assert_equal(
49
+ [
50
+ Synclenote::MarkdownToEnmlBuilder::HEADER,
51
+ h[:expected_enml_text],
52
+ Synclenote::MarkdownToEnmlBuilder::FOOTER,
53
+ ].join,
54
+ Synclenote::MarkdownToEnmlBuilder.(h[:markdown_text]),
55
+ )
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,68 @@
1
+ require "test_helper"
2
+
3
+ class Synclenote::NoteBuilderTest < Test::Unit::TestCase
4
+ sub_test_case(".call") do
5
+ data(
6
+ regular: {
7
+ raw_note_text: <<~EOS,
8
+ # title
9
+
10
+ body
11
+ EOS
12
+ expected_title: "title",
13
+ expected_additional_tag_names: [],
14
+ expected_content_body: <<~EOS,
15
+ <p>body</p>
16
+ EOS
17
+ },
18
+ no_title: {
19
+ raw_note_text: <<~EOS,
20
+ # \t
21
+
22
+ body
23
+ EOS
24
+ expected_title: Synclenote::NoteBuilder::TITLE_IF_EMPTY,
25
+ expected_additional_tag_names: [],
26
+ expected_content_body: <<~EOS,
27
+ <p>body</p>
28
+ EOS
29
+ },
30
+ have_tags: {
31
+ raw_note_text: <<~EOS,
32
+ # [foo][bar-baz] title
33
+
34
+ body
35
+ EOS
36
+ expected_title: "title",
37
+ expected_additional_tag_names: %w[foo bar-baz],
38
+ expected_content_body: <<~EOS,
39
+ <p>body</p>
40
+ EOS
41
+ },
42
+ )
43
+ test("returns Note object") do |h|
44
+ Tempfile.create do |f|
45
+ f.write(h[:raw_note_text])
46
+ f.close
47
+ raw_note_path = Pathname(f.path)
48
+ note = Synclenote::NoteBuilder.(raw_note_path)
49
+ assert_equal(h[:expected_title], note.title)
50
+ assert_equal(
51
+ [
52
+ *Synclenote::NoteBuilder::DEFAULT_TAGS,
53
+ *h[:expected_additional_tag_names],
54
+ ].sort,
55
+ note.tagNames.sort,
56
+ )
57
+ assert_equal(
58
+ [
59
+ Synclenote::MarkdownToEnmlBuilder::HEADER,
60
+ h[:expected_content_body],
61
+ Synclenote::MarkdownToEnmlBuilder::FOOTER,
62
+ ].join,
63
+ note.content,
64
+ )
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ require "test/unit"
2
+
3
+ require "synclenote"
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: synclenote
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuya.Nishida.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-01-13 00:00:00.000000000 Z
11
+ date: 2024-01-18 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: base64
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: evernote_oauth
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -24,6 +38,20 @@ dependencies:
24
38
  - - ">="
25
39
  - !ruby/object:Gem::Version
26
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: nokogiri
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: redcarpet
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +122,20 @@ dependencies:
94
122
  - - ">="
95
123
  - !ruby/object:Gem::Version
96
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: test-unit
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
97
139
  description: A synchronization tool for GFM (GitHub Flavored Markdown) files and Evernote.
98
140
  email:
99
141
  - yuya@j96.org
@@ -103,10 +145,10 @@ extensions: []
103
145
  extra_rdoc_files: []
104
146
  files:
105
147
  - ".github/dependabot.yml"
148
+ - ".github/workflows/test.yml"
106
149
  - ".gitignore"
107
150
  - ".travis.yml"
108
151
  - Gemfile
109
- - Gemfile.lock
110
152
  - Guardfile
111
153
  - LICENSE.txt
112
154
  - README.md
@@ -115,8 +157,13 @@ files:
115
157
  - lib/synclenote.rb
116
158
  - lib/synclenote/command.rb
117
159
  - lib/synclenote/configuration.rb
160
+ - lib/synclenote/markdown_to_enml_builder.rb
161
+ - lib/synclenote/note_builder.rb
118
162
  - lib/synclenote/version.rb
119
163
  - synclenote.gemspec
164
+ - test/lib/synclenote/markdown_to_enml_builder_test.rb
165
+ - test/lib/synclenote/note_builder_test.rb
166
+ - test/test_helper.rb
120
167
  homepage: https://github.com/nishidayuya/synclenote
121
168
  licenses:
122
169
  - X11
@@ -140,4 +187,7 @@ rubygems_version: 3.5.3
140
187
  signing_key:
141
188
  specification_version: 4
142
189
  summary: A synchronization tool for GFM (GitHub Flavored Markdown) files and Evernote.
143
- test_files: []
190
+ test_files:
191
+ - test/lib/synclenote/markdown_to_enml_builder_test.rb
192
+ - test/lib/synclenote/note_builder_test.rb
193
+ - test/test_helper.rb
data/Gemfile.lock DELETED
@@ -1,49 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- synclenote (0.0.1)
5
- evernote_oauth
6
- redcarpet
7
- thor
8
-
9
- GEM
10
- remote: https://rubygems.org/
11
- specs:
12
- debug (1.6.2)
13
- irb (>= 1.3.6)
14
- reline (>= 0.3.1)
15
- evernote-thrift (1.25.2)
16
- evernote_oauth (0.2.3)
17
- evernote-thrift
18
- oauth (>= 0.4.1)
19
- hashie (5.0.0)
20
- io-console (0.5.11)
21
- irb (1.4.1)
22
- reline (>= 0.3.0)
23
- oauth (1.1.0)
24
- oauth-tty (~> 1.0, >= 1.0.1)
25
- snaky_hash (~> 2.0)
26
- version_gem (~> 1.1)
27
- oauth-tty (1.0.5)
28
- version_gem (~> 1.1, >= 1.1.1)
29
- rake (13.0.6)
30
- redcarpet (3.6.0)
31
- reline (0.3.1)
32
- io-console (~> 0.5)
33
- snaky_hash (2.0.1)
34
- hashie
35
- version_gem (~> 1.1, >= 1.1.1)
36
- thor (1.3.0)
37
- version_gem (1.1.3)
38
-
39
- PLATFORMS
40
- x86_64-linux
41
-
42
- DEPENDENCIES
43
- bundler
44
- debug
45
- rake
46
- synclenote!
47
-
48
- BUNDLED WITH
49
- 2.3.10