asciidoctor-confluence_publisher 0.1.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.
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