asciidoctor-confluence_publisher 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +24 -0
  3. data/.gitignore +14 -0
  4. data/Gemfile +7 -0
  5. data/README.md +54 -0
  6. data/Rakefile +10 -0
  7. data/asciidoctor-confluence_publisher.gemspec +31 -0
  8. data/bin/confluence-publisher +7 -0
  9. data/lib/asciidoctor/confluence_publisher.rb +12 -0
  10. data/lib/asciidoctor/confluence_publisher/asciidoc.rb +39 -0
  11. data/lib/asciidoctor/confluence_publisher/command.rb +59 -0
  12. data/lib/asciidoctor/confluence_publisher/confluence_api.rb +236 -0
  13. data/lib/asciidoctor/confluence_publisher/invoker.rb +154 -0
  14. data/lib/asciidoctor/confluence_publisher/model/ancestor.rb +9 -0
  15. data/lib/asciidoctor/confluence_publisher/model/attachment.rb +15 -0
  16. data/lib/asciidoctor/confluence_publisher/model/base.rb +21 -0
  17. data/lib/asciidoctor/confluence_publisher/model/page.rb +26 -0
  18. data/lib/asciidoctor/confluence_publisher/model/property.rb +14 -0
  19. data/lib/asciidoctor/confluence_publisher/model/space.rb +9 -0
  20. data/lib/asciidoctor/confluence_publisher/model/version.rb +9 -0
  21. data/lib/asciidoctor/confluence_publisher/version.rb +5 -0
  22. data/lib/asciidoctor_confluence_publisher.rb +1 -0
  23. data/template/block_admonition.html.haml +6 -0
  24. data/template/block_example.haml.haml +4 -0
  25. data/template/block_image.html.haml +10 -0
  26. data/template/block_listing.html.haml +18 -0
  27. data/template/block_olist.html.haml +8 -0
  28. data/template/block_paragraph.html.haml +4 -0
  29. data/template/block_preamble.html.haml +1 -0
  30. data/template/block_quote.html.haml +9 -0
  31. data/template/block_stem.html.haml +3 -0
  32. data/template/block_table.html.haml +24 -0
  33. data/template/block_toc.html.haml +7 -0
  34. data/template/block_ulist.html.haml +15 -0
  35. data/template/block_verse.html.haml +9 -0
  36. data/template/block_video.html.haml +11 -0
  37. data/template/document.html.haml +1 -0
  38. data/template/embedded.html.haml +4 -0
  39. data/template/helpers.rb +171 -0
  40. data/template/inline_anchor.html.haml +20 -0
  41. data/template/inline_image.html.haml +7 -0
  42. data/template/section.html.haml +6 -0
  43. metadata +143 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c02a16d80d2c6942088552370ab3f10033ec630caaaf6972830e48c1a11ceee3
4
+ data.tar.gz: 8a14b5a3ad568ce9fd8cc1151b5ad36a76ef3533bc24b4462ab78e351d6e2482
5
+ SHA512:
6
+ metadata.gz: e7bb82e7e9159b8a9c9a319fdb82ff6684d61c9af1fce1e85227fd9645e606cd547d549dc971d4c388c17c367a57af6fde3aa5f0154c979a0bcebf050da707f0
7
+ data.tar.gz: 2ec63862b46679825befa2c4aa878bead193426cf81c471791b275ac8c8e931486fc3de0401c3dc5ea478f97cb3040b13e91b3fc9b1ab40af589beca770de676
@@ -0,0 +1,24 @@
1
+ name: Ruby Gem
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ test:
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ os: [ubuntu]
15
+ ruby: [2.0, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7]
16
+ runs-on: ${{ matrix.os }}-latest
17
+ continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }}
18
+ steps:
19
+ - uses: actions/checkout@v2
20
+ - uses: ruby/setup-ruby@v1
21
+ with:
22
+ ruby-version: ${{ matrix.ruby }}
23
+ - run: bundle install
24
+ - run: bundle exec rake
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .idea/
10
+ .rakeTasks
11
+ Gemfile.lock
12
+ .DS_Store
13
+ *.env
14
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in asciidoctor-asciidoctor_confluence.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "minitest", "~> 5.0"
@@ -0,0 +1,54 @@
1
+ # Asciidoctor-ConfluencePublisher
2
+
3
+ Asciidoctor-ConfluencePublisher is a command line tool that parse asciidoc files,
4
+ generate confluence compatible html and upload the content and attachment to confluence.
5
+
6
+ This repo inspired by [confluence-publisher](https://github.com/confluence-publisher/confluence-publisher)
7
+
8
+ ## Installation
9
+
10
+ ```ruby
11
+ gem install asciidoctor-confluence_publisher
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ### configuration
17
+ `asciidoctor-confluence_publisher` is built on asciidoctor gem, so the gem is compatible with
18
+ all the arguments of asciidoctor.
19
+
20
+ The configuration of confluence can be set via attribute(`-a attr=attr_value`), or set via system environment.
21
+ The attribute or environment are:
22
+
23
+
24
+ attribute name | environment variable | note | required
25
+ --- | --- | --- | ---
26
+ confluence_host | CONFLUENCE_HOST | confluence host with protocol. | Y |
27
+ space | SPACE | confluence page space. | Y |
28
+ username | CONFLUENCE_USERNAME | confluence username. | Y |
29
+ password | CONFLUENCE_PASSWORD | confluence password. | Y |
30
+ ancestor_id | ANCESTOR_ID | page ancestor id. | Y |
31
+ proxy | CONFLUENCE_PROXY | confluence http proxy. | N |
32
+ skip_verify_ssl | - | whether skip verify ssl. | N |
33
+
34
+ It is recomanded that use environment for confluence related host, username and password, for example [autoenv](https://github.com/inishchith/autoenv)
35
+
36
+ ### Run
37
+
38
+ ```bash
39
+ confluence-publisher [file or directory]
40
+ ```
41
+ The title of source file will be the title in confluence.
42
+
43
+ If the final argument is a file, it will only processed the single file. It will recursively process all
44
+ the source file except file starting with `_`, for directory parameter.
45
+
46
+
47
+ ## Development
48
+
49
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
50
+
51
+ ## Contributing
52
+
53
+ Bug reports and pull requests are welcome on GitHub at https://github.com/polarlights/asciidoctor-confluence_publisher.
54
+
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,31 @@
1
+ require_relative 'lib/asciidoctor/confluence_publisher/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "asciidoctor-confluence_publisher"
5
+ spec.version = Asciidoctor::ConfluencePublisher::VERSION
6
+ spec.authors = ["polarlights"]
7
+ spec.email = ["godhuyang@hotmail.com"]
8
+
9
+ spec.summary = %q{Parse asciidoc and publish the document to confluence.}
10
+ spec.description = %q{Asciidoctor-Confluence parse asciidoc and publish the document to confluence.}
11
+ spec.homepage = "https://github.com/polarlights/asciidoctor-confluence_publisher"
12
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.0.0")
13
+
14
+ spec.metadata["homepage_uri"] = spec.homepage
15
+ spec.metadata["source_code_uri"] = spec.homepage
16
+ spec.metadata["changelog_uri"] = spec.homepage + '/releases'
17
+
18
+ # Specify which files should be added to the gem when it is released.
19
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
+ end
23
+ spec.executables = ['confluence-publisher']
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_runtime_dependency 'asciidoctor', '~> 2.0.0'
27
+ spec.add_runtime_dependency 'haml', '~> 5.1.0'
28
+ spec.add_runtime_dependency 'rest-client', '~> 2.1.0'
29
+
30
+ spec.add_development_dependency 'webmock', '~> 3.8.0'
31
+ end
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ $LOAD_PATH.unshift File.expand_path '../../lib', __FILE__
4
+ require 'asciidoctor/confluence_publisher/command'
5
+ require 'pry-byebug'
6
+
7
+ Asciidoctor::ConfluencePublisher::Command.execute ARGV
@@ -0,0 +1,12 @@
1
+ require 'asciidoctor'
2
+ require_relative "confluence_publisher/version"
3
+ require_relative "confluence_publisher/invoker"
4
+ require_relative 'confluence_publisher/model/base'
5
+ require_relative 'confluence_publisher/model/ancestor'
6
+ require_relative 'confluence_publisher/model/attachment'
7
+ require_relative 'confluence_publisher/model/property'
8
+ require_relative 'confluence_publisher/model/space'
9
+ require_relative 'confluence_publisher/model/version'
10
+ require_relative 'confluence_publisher/model/page'
11
+ require_relative 'confluence_publisher/confluence_api'
12
+ require_relative 'confluence_publisher/asciidoc'
@@ -0,0 +1,39 @@
1
+ module Asciidoctor
2
+ module ConfluencePublisher
3
+ class Asciidoc
4
+ attr_reader :path, :children
5
+
6
+ def initialize(path)
7
+ @path = path
8
+ @children = []
9
+ end
10
+
11
+ def is_leaves?
12
+ !is_directory?
13
+ end
14
+
15
+ def is_directory?
16
+ File.directory?(path)
17
+ end
18
+
19
+ def add_child(child)
20
+ return if child.nil?
21
+ @children << child
22
+ end
23
+
24
+ def to_s
25
+ inspect
26
+ end
27
+
28
+ def has_any_leaves?
29
+ traverse_file_tree(self)
30
+ end
31
+
32
+ private
33
+ def traverse_file_tree(root)
34
+ return true if root.is_leaves?
35
+ return root.children.any? { |node| traverse_file_tree(node) }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,59 @@
1
+ require 'asciidoctor/confluence_publisher'
2
+ require 'asciidoctor/cli'
3
+
4
+ module Asciidoctor
5
+ module ConfluencePublisher
6
+ class Command
7
+ def self.execute(args)
8
+ options = Asciidoctor::Cli::Options.new
9
+
10
+ unless args != ['-v'] && (args & ['-V', '--version']).empty?
11
+ $stdout.write %(Asciidoctor Confluence #{Asciidoctor::ConfluencePublisher::VERSION} using )
12
+ options.print_version
13
+ exit 0
14
+ end
15
+
16
+ orig_args = args.dup
17
+ # if the parameter is a directory, it will set to the root of source_file
18
+ source_dir = nil
19
+ 2.times do
20
+ result = options.parse! args
21
+ if result.is_a? Integer
22
+ if args.size == 1
23
+ file = args.first
24
+ fstat = ::File.stat file
25
+ if fstat.ftype == 'directory' && (input_files = parse_directory_files(file)).size > 0
26
+ source_dir = file
27
+ orig_args.reject! { |_arg| file == _arg }
28
+ orig_args.concat input_files
29
+ args = orig_args
30
+ else
31
+ exit result
32
+ end
33
+ else
34
+ exit result
35
+ end
36
+ end
37
+ end
38
+
39
+ options[:asciidoc_source_dir] = source_dir
40
+ invoker = Asciidoctor::ConfluencePublisher::Invoker.new options
41
+ GC.start
42
+ invoker.invoke!
43
+ end
44
+
45
+ private
46
+ # hack asciidoctor to support folder
47
+ def self.parse_directory_files(directory)
48
+ infiles = []
49
+ file = File.join(directory, "**/*.{asc,adoc,asciidoc}")
50
+ if (matches = ::Dir.glob file).size > 0
51
+ infiles = matches
52
+ end
53
+ # reject file start with "_", in conversion it is a included file
54
+ infiles.reject! { |file| File.basename(file).start_with?("_")}
55
+ infiles
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,236 @@
1
+ require 'rest-client'
2
+ require 'json'
3
+
4
+ module Asciidoctor
5
+ module ConfluencePublisher
6
+ class ConfluenceApi
7
+ attr_reader :host, :space, :username
8
+
9
+ def initialize(host, space, username, password, skip_verify_ssl: false, proxy: nil)
10
+ @host = host
11
+ @space = space
12
+ @username = username
13
+ @password = password
14
+ @skip_verify_ssl = skip_verify_ssl
15
+ RestClient.proxy = proxy if proxy
16
+ end
17
+
18
+ # create a confluence page
19
+ #
20
+ def create_page(title, content, ancestor_id)
21
+ url = host + '/rest/api/content?expand=version,ancestors,space'
22
+ payload = {
23
+ title: title,
24
+ type: 'page',
25
+ space: {key: space},
26
+ ancestors: Array(ancestor_id).map { |ans_id| { id: ans_id } },
27
+ body: {
28
+ storage: {
29
+ value: content,
30
+ representation: 'storage'
31
+ }
32
+ }
33
+ }
34
+
35
+ req_result = send_request(:post, url, payload, default_headers)
36
+ Model::Page.new req_result[:body] if req_result[:success]
37
+ end
38
+
39
+ def update_page(page_id, title, content)
40
+ url = host + "/rest/api/content/#{page_id}"
41
+ current_page = get_page_by_id(page_id)
42
+ payload = {
43
+ title: title,
44
+ type: 'page',
45
+ body: {
46
+ storage: {
47
+ value: content,
48
+ representation: 'storage'
49
+ }
50
+ },
51
+ version: {
52
+ number: current_page.version.number + 1
53
+ }
54
+ }
55
+
56
+ req_result = send_request(:put, url, payload, default_headers)
57
+ Model::Page.new req_result[:body] if req_result[:success]
58
+ end
59
+
60
+ def get_page_by_id(page_id)
61
+ url = host + "/rest/api/content/#{page_id}"
62
+ payload = {
63
+ expand: 'version,space,ancestors'
64
+ }
65
+ begin
66
+ req_result = send_request(:get, url, payload, default_headers)
67
+ Model::Page.new(req_result[:body]) if req_result[:success]
68
+ rescue => e
69
+ $stderr.puts "not found page with id #{page_id}. message: #{e.message}"
70
+ end
71
+ end
72
+
73
+ def get_pages_by_title(title)
74
+ url = host + '/rest/api/content'
75
+ payload = {
76
+ type: 'page',
77
+ spaceKey: space,
78
+ title: title,
79
+ expand: 'ancestors,space,version'
80
+ }
81
+
82
+ start =0
83
+ limit = 30
84
+ result = []
85
+ loop do
86
+ pageable = { start: start, limit: limit}
87
+ req_result = send_request(:get, url, payload.merge(pageable), default_headers)
88
+ no_data = true
89
+ if req_result[:success] && req_result[:body]['size'] > 0
90
+ result.concat req_result[:body]['results'].map { |page| Model::Page.new(page) }
91
+ no_data = req_result[:body]['size'] < req_result[:body]['limit']
92
+ end
93
+ break if no_data
94
+ start += 1
95
+ end
96
+ result
97
+ end
98
+
99
+ def get_attachments(page_id)
100
+ url = host + "/rest/api/content/#{page_id}/child/attachment"
101
+ start = 0
102
+ limit = 50
103
+
104
+ result = []
105
+ loop do
106
+ payload = { start: start, limit: limit}
107
+ req_result = send_request(:get, url, payload, default_headers)
108
+ no_data = true
109
+ if req_result[:success] && req_result[:body]['size'] > 0
110
+ result.concat req_result[:body]['results'].map { |attachment| Model::Attachment.new attachment }
111
+ end
112
+
113
+ break if no_data
114
+ start += 1
115
+ end
116
+ result
117
+ end
118
+
119
+ def create_attachment(page_id, file_path)
120
+ url = host + "/rest/api/content/#{page_id}/child/attachment"
121
+ payload = {
122
+ file: File.new(file_path, 'rb')
123
+ }
124
+ header = {
125
+ x_atlassian_token: 'nocheck'
126
+ }
127
+
128
+ req_result = send_request(:post, url, payload, default_headers.merge(header), multipart: true)
129
+ Model::Attachment.new(req_result[:body]) if req_result[:success]
130
+ end
131
+
132
+ def update_attachment(page_id, attachment_id, file_path)
133
+ url = host + "/rest/api/content/#{page_id}/child/attachment/#{attachment_id}/data"
134
+ payload = {
135
+ file: File.new(file_path, 'rb')
136
+ }
137
+ header = {
138
+ x_atlassian_token: 'nocheck',
139
+ content_type: 'multipart/form-data'
140
+ }
141
+
142
+ req_result = send_request(:post, url, payload, default_headers.merge(header))
143
+ Model::Attachment.new(req_result[:body]) if req_result[:success]
144
+ end
145
+
146
+ def set_page_property(owner_id, key, value)
147
+ url = host + "/rest/api/content/#{owner_id}/property/#{key}?expand=version"
148
+ current_property = get_page_property(owner_id, key)
149
+ payload = {
150
+ value: value,
151
+ version: {
152
+ number: (current_property && current_property.version.number).to_i + 1
153
+ }
154
+ }
155
+
156
+ req_result = send_request(:put, url, payload, default_headers)
157
+ Model::Property.new req_result[:body] if req_result[:success]
158
+ end
159
+
160
+ def get_page_property(owner_id, key)
161
+ url = host + "/rest/api/content/#{owner_id}/property/#{key}"
162
+ payload = {
163
+ expand: 'version'
164
+ }
165
+
166
+ begin
167
+ req_result = send_request(:get, url, payload, default_headers)
168
+ Model::Property.new req_result[:body] if req_result[:success]
169
+ rescue => e
170
+ end
171
+ end
172
+
173
+ def remove_page_property(owner_id, key)
174
+ url = host + "/rest/api/content/#{owner_id}/property/#{key}"
175
+ payload = {}
176
+
177
+ send_request(:delete, url, payload, default_headers)
178
+ end
179
+
180
+ private
181
+ def default_headers
182
+ {
183
+ authorization: "Basic #{basic_auth_val}",
184
+ accept: 'application/json',
185
+ content_type: 'application/json'
186
+ }
187
+ end
188
+
189
+ def send_request(mthd, url, req_payload = {}, req_headers = {}, req_options = {})
190
+ headers = req_headers.dup
191
+ payload = req_payload.dup
192
+ options = req_options.dup
193
+ options[:timeout] = 30
194
+ if %w(get delete).include? mthd.to_s
195
+ headers.merge!({ params: payload })
196
+ payload = {}
197
+ elsif req_headers.empty?
198
+ headers = { content_type: 'application/json' }
199
+ end
200
+
201
+ if req_options[:multipart]
202
+ headers.delete(:content_type)
203
+ else
204
+ payload = payload.to_json
205
+ end
206
+
207
+ request_params = {
208
+ method: mthd,
209
+ url: url,
210
+ payload: payload,
211
+ headers: headers,
212
+ timeout: 30
213
+ }
214
+ request_params[:verify_ssl] = false if @skip_verify_ssl
215
+
216
+ RestClient::Request.execute(request_params) do |resp, _, _|
217
+ begin
218
+ if resp.code.between?(200, 399)
219
+ return { success: true, code: resp.code, body: resp.body.length > 1 && JSON.parse(resp.body) }
220
+ elsif resp.code == 401
221
+ raise RuntimeError, 'invalid username or password, please confirm it.'
222
+ else
223
+ raise RuntimeError, "send request to confluence failed, code: #{resp.code}, error: #{resp.body}"
224
+ end
225
+ rescue => error
226
+ raise RuntimeError, "send request to confluence failed, code: #{resp.code}, error: #{error.message}"
227
+ end
228
+ end
229
+ end
230
+
231
+ def basic_auth_val
232
+ Base64.strict_encode64 "#{@username}:#{@password}"
233
+ end
234
+ end
235
+ end
236
+ end