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.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +24 -0
- data/.gitignore +14 -0
- data/Gemfile +7 -0
- data/README.md +54 -0
- data/Rakefile +10 -0
- data/asciidoctor-confluence_publisher.gemspec +31 -0
- data/bin/confluence-publisher +7 -0
- data/lib/asciidoctor/confluence_publisher.rb +12 -0
- data/lib/asciidoctor/confluence_publisher/asciidoc.rb +39 -0
- data/lib/asciidoctor/confluence_publisher/command.rb +59 -0
- data/lib/asciidoctor/confluence_publisher/confluence_api.rb +236 -0
- data/lib/asciidoctor/confluence_publisher/invoker.rb +154 -0
- data/lib/asciidoctor/confluence_publisher/model/ancestor.rb +9 -0
- data/lib/asciidoctor/confluence_publisher/model/attachment.rb +15 -0
- data/lib/asciidoctor/confluence_publisher/model/base.rb +21 -0
- data/lib/asciidoctor/confluence_publisher/model/page.rb +26 -0
- data/lib/asciidoctor/confluence_publisher/model/property.rb +14 -0
- data/lib/asciidoctor/confluence_publisher/model/space.rb +9 -0
- data/lib/asciidoctor/confluence_publisher/model/version.rb +9 -0
- data/lib/asciidoctor/confluence_publisher/version.rb +5 -0
- data/lib/asciidoctor_confluence_publisher.rb +1 -0
- data/template/block_admonition.html.haml +6 -0
- data/template/block_example.haml.haml +4 -0
- data/template/block_image.html.haml +10 -0
- data/template/block_listing.html.haml +18 -0
- data/template/block_olist.html.haml +8 -0
- data/template/block_paragraph.html.haml +4 -0
- data/template/block_preamble.html.haml +1 -0
- data/template/block_quote.html.haml +9 -0
- data/template/block_stem.html.haml +3 -0
- data/template/block_table.html.haml +24 -0
- data/template/block_toc.html.haml +7 -0
- data/template/block_ulist.html.haml +15 -0
- data/template/block_verse.html.haml +9 -0
- data/template/block_video.html.haml +11 -0
- data/template/document.html.haml +1 -0
- data/template/embedded.html.haml +4 -0
- data/template/helpers.rb +171 -0
- data/template/inline_anchor.html.haml +20 -0
- data/template/inline_image.html.haml +7 -0
- data/template/section.html.haml +6 -0
- metadata +143 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
@@ -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,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
|