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