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
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module Asciidoctor
|
4
|
+
module ConfluencePublisher
|
5
|
+
class Invoker
|
6
|
+
# default template directory for asciidoctor_confluence marcos
|
7
|
+
DEFAULT_TEMPLATE_DIR = File.expand_path("../../../../template", __FILE__)
|
8
|
+
|
9
|
+
attr_reader :options
|
10
|
+
def initialize(options)
|
11
|
+
@options = options.dup
|
12
|
+
@options[:to_file] = nil
|
13
|
+
@options[:catalog_assets] = true
|
14
|
+
|
15
|
+
@options[:backend] = 'xhtml5'
|
16
|
+
@options[:template_dirs] = Array(options[:template_dirs]) << DEFAULT_TEMPLATE_DIR
|
17
|
+
@options[:to_file] = false
|
18
|
+
@options[:header_footer] = false
|
19
|
+
# confluence related configuration
|
20
|
+
attributes = options[:attributes] || {}
|
21
|
+
attributes['confluence_host'] ||= ENV['CONFLUENCE_HOST']
|
22
|
+
attributes['space'] ||= ENV['SPACE']
|
23
|
+
attributes['username'] ||= ENV['CONFLUENCE_USERNAME']
|
24
|
+
attributes['password'] ||= ENV['CONFLUENCE_PASSWORD']
|
25
|
+
@ancestor_id = (attributes['ancestor_id'] ||= ENV['ANCESTOR_ID'])
|
26
|
+
check_confluence_config(attributes)
|
27
|
+
|
28
|
+
proxy = attributes[:proxy] || ENV['CONFLUENCE_PROXY']
|
29
|
+
skip_verify_ssl = attributes['skip_verify_ssl'].to_s == 'true'
|
30
|
+
@confluence_client = ConfluenceApi.new(attributes['confluence_host'],
|
31
|
+
attributes['space'],
|
32
|
+
attributes['username'],
|
33
|
+
attributes['password'],
|
34
|
+
skip_verify_ssl: skip_verify_ssl,
|
35
|
+
proxy: proxy)
|
36
|
+
end
|
37
|
+
|
38
|
+
def invoke!
|
39
|
+
input_files = options[:asciidoc_source_dir] ? Array(options[:asciidoc_source_dir]) : options[:input_files]
|
40
|
+
|
41
|
+
input_files.map(&method(:build_file_tree)).each do |node|
|
42
|
+
traverse_file_tree(@ancestor_id, node, &method(:convert_and_publish))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def convert_and_publish(ancestor_id, input_file)
|
48
|
+
if File.file?(input_file)
|
49
|
+
document = Asciidoctor.load_file input_file, options
|
50
|
+
confluence_page = find_or_create_page(document.title, ancestor_id)
|
51
|
+
process_page_content(confluence_page.id, document.title, document.content)
|
52
|
+
process_page_attachments(confluence_page.id, [document.references[:links], document.references[:images]].flatten, File.dirname(input_file))
|
53
|
+
else
|
54
|
+
title = File.basename(input_file)
|
55
|
+
confluence_page = find_or_create_page(title, ancestor_id)
|
56
|
+
end
|
57
|
+
confluence_page.id
|
58
|
+
end
|
59
|
+
|
60
|
+
def process_page_attachments(page_id, assets, source_dir)
|
61
|
+
resource_property = 'resource_hash'
|
62
|
+
page_resource_prop = @confluence_client.get_page_property(page_id, resource_property)
|
63
|
+
page_attachment_hash = page_resource_prop && page_resource_prop.value || {}
|
64
|
+
attachment_hash = @confluence_client.get_attachments(page_id).map { |attachment| [attachment.title, attachment.id] }.to_h
|
65
|
+
|
66
|
+
has_asset_updated = false
|
67
|
+
assets.each do |link|
|
68
|
+
next if Asciidoctor::Helpers.uriish?(link)
|
69
|
+
source_file_dir = source_dir
|
70
|
+
if link.is_a? ::Struct
|
71
|
+
source_file_dir += "/#{link.imagesdir}"
|
72
|
+
end
|
73
|
+
file_path = File.join(source_file_dir, link.to_s)
|
74
|
+
next if !File.exist?(file_path)
|
75
|
+
file_md5 = Digest::MD5.hexdigest File.read(file_path)
|
76
|
+
filename = File.basename(file_path)
|
77
|
+
|
78
|
+
attachment_update_success = if attachment_hash.has_key?(filename)
|
79
|
+
if file_md5 != page_attachment_hash[filename]
|
80
|
+
@confluence_client.update_attachment(page_id, attachment_hash[filename], file_path)
|
81
|
+
has_asset_updated = true
|
82
|
+
end
|
83
|
+
else
|
84
|
+
@confluence_client.create_attachment(page_id, file_path)
|
85
|
+
has_asset_updated = true
|
86
|
+
end
|
87
|
+
page_attachment_hash[filename] = file_md5 if attachment_update_success
|
88
|
+
end
|
89
|
+
|
90
|
+
@confluence_client.set_page_property(page_id, resource_property, page_attachment_hash) if has_asset_updated
|
91
|
+
end
|
92
|
+
|
93
|
+
def process_page_content(page_id, title, document_content)
|
94
|
+
content_property = 'content_hash'
|
95
|
+
page_hash_prop = @confluence_client.get_page_property(page_id, content_property)
|
96
|
+
page_content_hash = page_hash_prop && page_hash_prop.value
|
97
|
+
|
98
|
+
if (new_content_hash = Digest::MD5.hexdigest(document_content)) != page_content_hash
|
99
|
+
@confluence_client.update_page(page_id, title, document_content)
|
100
|
+
@confluence_client.set_page_property(page_id, content_property, new_content_hash)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def check_confluence_config(attributes)
|
105
|
+
empty_attrs = %w(confluence_host space username password ancestor_id).select do |prop|
|
106
|
+
attr = attributes[prop]
|
107
|
+
attr.nil? || attr.to_s.strip.length < 1
|
108
|
+
end
|
109
|
+
raise empty_attrs.join(', ') if empty_attrs.size > 0
|
110
|
+
end
|
111
|
+
|
112
|
+
def build_file_tree(root)
|
113
|
+
node = nil
|
114
|
+
if File.directory?(root) || File.file?(root) && File.extname(root) =~ /.(adoc|asc|asciidoc)$/
|
115
|
+
node = Asciidoctor::ConfluencePublisher::Asciidoc.new(root)
|
116
|
+
end
|
117
|
+
return node if File.file?(root)
|
118
|
+
|
119
|
+
Dir.children(root).each do |dir|
|
120
|
+
next if dir.start_with?('.')
|
121
|
+
child = build_file_tree(File.join(root, dir))
|
122
|
+
node.add_child(child)
|
123
|
+
end
|
124
|
+
|
125
|
+
node
|
126
|
+
end
|
127
|
+
|
128
|
+
def traverse_file_tree(ancestor_id, root, &block)
|
129
|
+
new_ancestor_id = ancestor_id
|
130
|
+
if options[:asciidoc_source_dir] != root.path
|
131
|
+
new_ancestor_id = block.call(ancestor_id, root.path)
|
132
|
+
end
|
133
|
+
children = root.children
|
134
|
+
return if children.size.zero?
|
135
|
+
children.each do |child|
|
136
|
+
next if !child.has_any_leaves?
|
137
|
+
traverse_file_tree(new_ancestor_id, child, &block)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def find_or_create_page(title, ancestor_id)
|
142
|
+
confluence_pages = @confluence_client.get_pages_by_title(title).select { |page| page.contain_ancestor?(ancestor_id) }
|
143
|
+
if confluence_pages.size > 1
|
144
|
+
raise 'Too many duplicate page title'
|
145
|
+
elsif confluence_pages.size == 1
|
146
|
+
confluence_page = confluence_pages[0]
|
147
|
+
else
|
148
|
+
confluence_page = @confluence_client.create_page(title, '', ancestor_id)
|
149
|
+
end
|
150
|
+
confluence_page
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Asciidoctor
|
2
|
+
module ConfluencePublisher
|
3
|
+
module Model
|
4
|
+
class Base
|
5
|
+
def initialize(hash = {})
|
6
|
+
accessor_methods = public_methods(false).select { |mth| mth.to_s =~ /\w+=$/ }
|
7
|
+
hash.each do |key, val|
|
8
|
+
setter_method = "#{key}="
|
9
|
+
if accessor_methods.include?(setter_method.to_sym)
|
10
|
+
public_send(setter_method, val)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
inspect
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Asciidoctor
|
2
|
+
module ConfluencePublisher
|
3
|
+
module Model
|
4
|
+
class Page < Base
|
5
|
+
attr_accessor :id, :title
|
6
|
+
attr_reader :version, :ancestors, :space
|
7
|
+
|
8
|
+
def version=(ver)
|
9
|
+
@version = Version.new(ver)
|
10
|
+
end
|
11
|
+
|
12
|
+
def ancestors=(ancestors)
|
13
|
+
@ancestors = ancestors.map { |ans| Ancestor.new(ans) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def space=(space)
|
17
|
+
@space = Space.new(space)
|
18
|
+
end
|
19
|
+
|
20
|
+
def contain_ancestor?(ancestor_id)
|
21
|
+
@ancestors.any? { |ancestor| ancestor.id == ancestor_id.to_s }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "asciidoctor/confluence_publisher"
|
@@ -0,0 +1,10 @@
|
|
1
|
+
- haml_tag_if((attr? :link), :a, {href: (attr :link)}) do
|
2
|
+
- href_target = attr :target
|
3
|
+
%ac:image{ "ac:title" => title? && title, "ac:alt" => (attr :alt), "ac:height" => (attr :height), "ac:width" => (attr :width) }
|
4
|
+
- if uri_link? href_target
|
5
|
+
%ri:url{ "ri:value" => href_target }/
|
6
|
+
- else
|
7
|
+
%ri:attachment{ "ri:filename" => File.basename(href_target) }/
|
8
|
+
- if title?
|
9
|
+
.cp-image-title
|
10
|
+
%em= captioned_title
|
@@ -0,0 +1,18 @@
|
|
1
|
+
- if @style == 'source'
|
2
|
+
%ac:structured-macro{"ac:name" => "code"}
|
3
|
+
- if confluence_supported_lang(source_lang)
|
4
|
+
%ac:parameter{"ac:name" => "language"}= source_lang
|
5
|
+
- if title?
|
6
|
+
%ac:parameter{"ac:name" => "title"}= title
|
7
|
+
- if attr? :linenums
|
8
|
+
%ac:parameter{"ac:name" => "linenumbers"} true
|
9
|
+
- if attr? :start
|
10
|
+
%ac:parameter{"ac:name" => "firstline"}= attr :start
|
11
|
+
- wrap_code = (@document.attr? :prewrap) || (has_option?(:nowrap) && !(option? :nowrap))
|
12
|
+
%ac:parameter{"ac:name" => "collapse"}= wrap_code
|
13
|
+
%ac:plain-text-body= "<![CDATA[#{content}]]>"
|
14
|
+
- else
|
15
|
+
%ac:structured-macro{"ac:name" => "noformat"}
|
16
|
+
- if title?
|
17
|
+
%ac:parameter{"ac:name" => "title"}= title
|
18
|
+
%ac:plain-text-body= "<![CDATA[#{content}]]>"
|
@@ -0,0 +1 @@
|
|
1
|
+
=content
|
@@ -0,0 +1,24 @@
|
|
1
|
+
%table
|
2
|
+
- if title?
|
3
|
+
%caption.title= captioned_title
|
4
|
+
- unless (attr :rowcount).zero?
|
5
|
+
- [:head, :foot, :body].select {|tblsec| !@rows[tblsec].empty? }.each do |tblsec|
|
6
|
+
- haml_tag "t#{tblsec}" do
|
7
|
+
- @rows[tblsec].each do |row|
|
8
|
+
%tr
|
9
|
+
- row.each do |cell|
|
10
|
+
- haml_tag "#{(tblsec == :head || cell.style == :header) ? 'th' : 'td'}", colspan: cell.colspan, rowspan: cell.rowspan do
|
11
|
+
- if tblsec == :head
|
12
|
+
= cell.text
|
13
|
+
- else
|
14
|
+
- case cell.style
|
15
|
+
- when :asciidoc
|
16
|
+
%div= cell.content
|
17
|
+
- when :verse
|
18
|
+
.verse= cell.text
|
19
|
+
- when :literal
|
20
|
+
.literal
|
21
|
+
%pre= cell.text
|
22
|
+
- else
|
23
|
+
- cell.content.each do |text|
|
24
|
+
= text
|
@@ -0,0 +1,7 @@
|
|
1
|
+
%ac:structured-macro{"ac:name" => "toc"}
|
2
|
+
%ac:parameter{"ac:name" => "maxLevel"} 4
|
3
|
+
%ac:parameter{"ac:name" => "minLevel"} 2
|
4
|
+
%ac:parameter{"ac:name" => "outline"} true
|
5
|
+
%ac:parameter{"ac:name" => "indent"} 0px
|
6
|
+
%ac:parameter{"ac:name" => "style"} none
|
7
|
+
%ac:parameter{"ac:name" => "separator"} pipe
|
@@ -0,0 +1,15 @@
|
|
1
|
+
- if title?
|
2
|
+
.cp-ulist-title =title
|
3
|
+
%ul
|
4
|
+
- items.each_with_index do |item, index|
|
5
|
+
%li
|
6
|
+
- if item.attr? :checkbox
|
7
|
+
%ac:task-list
|
8
|
+
%ac:task
|
9
|
+
%ac:task-id= index
|
10
|
+
%ac:task-status= (item.attr? :checked) ? 'complete' : 'imcomplete'
|
11
|
+
%ac:task-body= item.text
|
12
|
+
- else
|
13
|
+
=item.text
|
14
|
+
- if item.blocks?
|
15
|
+
= item.content
|
@@ -0,0 +1,11 @@
|
|
1
|
+
%div{:id=>@id, :class=>['videoblock', @style, role]}
|
2
|
+
- if title?
|
3
|
+
.title=captioned_title
|
4
|
+
.content
|
5
|
+
- if video_iframe?
|
6
|
+
%iframe{:width=>(attr :width), :height=>(attr :height), :src=>video_uri, :frameborder=>0, :allowfullscreen=>!(option? :nofullscreen)}
|
7
|
+
- else
|
8
|
+
%video{:src=>video_uri, :width=>(attr :width), :height=>(attr :height),
|
9
|
+
:poster=>((attr :poster) ? media_uri(attr :poster) : nil), :autoplay=>(option? :autoplay),
|
10
|
+
:controls=>!(option? :nocontrols), :loop=>(option? :loop)}
|
11
|
+
Your browser does not support the video tag.
|
@@ -0,0 +1 @@
|
|
1
|
+
= content
|
data/template/helpers.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
# Add custom functions to this module that you want to use in your Haml
|
2
|
+
# templates. Within the template you can invoke them as top-level functions
|
3
|
+
# just like the built-in helper functions that Haml provides.
|
4
|
+
module Haml::Helpers
|
5
|
+
|
6
|
+
CG_ALPHA = '[a-zA-Z]'
|
7
|
+
CC_ALNUM = 'a-zA-Z0-9'
|
8
|
+
|
9
|
+
# Detects strings that resemble URIs.
|
10
|
+
#
|
11
|
+
# Examples
|
12
|
+
# http://domain
|
13
|
+
# https://domain
|
14
|
+
# file:///path
|
15
|
+
# data:info
|
16
|
+
#
|
17
|
+
# not c:/sample.adoc or c:\sample.adoc
|
18
|
+
#
|
19
|
+
UriRegexp = %r{^#{CG_ALPHA}[#{CC_ALNUM}.+-]+:/{0,2}}
|
20
|
+
|
21
|
+
def has_option? name
|
22
|
+
@attributes.has_key? %(#{name}-option)
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Returns corrected section level.
|
27
|
+
#
|
28
|
+
# @param sec [Asciidoctor::Section] the section node (default: self).
|
29
|
+
# @return [Integer]
|
30
|
+
#
|
31
|
+
def section_level(sec = self)
|
32
|
+
@_section_level ||= (sec.level == 0 && sec.special) ? 1 : sec.level
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Returns the captioned section's title, optionally numbered.
|
37
|
+
#
|
38
|
+
# @param sec [Asciidoctor::Section] the section node (default: self).
|
39
|
+
# @return [String]
|
40
|
+
#
|
41
|
+
def section_title(sec = self)
|
42
|
+
sectnumlevels = document.attr(:sectnumlevels, 3).to_i
|
43
|
+
|
44
|
+
if sec.numbered && !sec.caption && sec.level <= sectnumlevels
|
45
|
+
[sec.sectnum, sec.captioned_title].join(' ')
|
46
|
+
else
|
47
|
+
sec.captioned_title
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
#--------------------------------------------------------
|
52
|
+
# block_listing
|
53
|
+
#
|
54
|
+
|
55
|
+
def source_lang
|
56
|
+
attr :language, nil, false
|
57
|
+
end
|
58
|
+
|
59
|
+
def confluence_supported_lang(lang)
|
60
|
+
supported = ['actionscript3','applescript','bash','c#','cpp','css',
|
61
|
+
'coldfusion','delphi','diff','erl','groovy',
|
62
|
+
'xml','java','jfx','js','php','perl',
|
63
|
+
'text','powershell','py','ruby','sql','sass',
|
64
|
+
'scala','vb','yml']
|
65
|
+
supported.include? lang
|
66
|
+
end
|
67
|
+
|
68
|
+
#--------------------------------------------------------
|
69
|
+
# block_table
|
70
|
+
#
|
71
|
+
|
72
|
+
def autowidth?
|
73
|
+
option? :autowidth
|
74
|
+
end
|
75
|
+
|
76
|
+
def spread?
|
77
|
+
'spread' if !(option? 'autowidth') && (attr :tablepcwidth) == 100
|
78
|
+
end
|
79
|
+
|
80
|
+
#--------------------------------------------------------
|
81
|
+
# block_video
|
82
|
+
#
|
83
|
+
|
84
|
+
# @return [Boolean] +true+ if the video should be embedded in an iframe.
|
85
|
+
def video_iframe?
|
86
|
+
['vimeo', 'youtube'].include?(attr :poster)
|
87
|
+
end
|
88
|
+
|
89
|
+
def admonition_name
|
90
|
+
case (attr :name)
|
91
|
+
when 'note'
|
92
|
+
'info'
|
93
|
+
when 'tip'
|
94
|
+
'tip'
|
95
|
+
when 'caution', 'warning'
|
96
|
+
'note'
|
97
|
+
when 'important'
|
98
|
+
'warning'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Public: Efficiently checks whether the specified String resembles a URI
|
103
|
+
#
|
104
|
+
# Uses the Asciidoctor::UriSniffRx regex to check whether the String begins
|
105
|
+
# with a URI prefix (e.g., http://). No validation of the URI is performed.
|
106
|
+
#
|
107
|
+
# str - the String to check
|
108
|
+
#
|
109
|
+
# @return true if the String is a URI, false if it is not
|
110
|
+
def uri_link?(str)
|
111
|
+
str && (str.include? ':') && str =~ UriRegexp
|
112
|
+
end
|
113
|
+
|
114
|
+
def video_uri
|
115
|
+
case (attr :poster, '').to_sym
|
116
|
+
when :vimeo
|
117
|
+
params = {
|
118
|
+
:autoplay => (1 if option? 'autoplay'),
|
119
|
+
:loop => (1 if option? 'loop')
|
120
|
+
}
|
121
|
+
start_anchor = "#at=#{attr :start}" if attr? :start
|
122
|
+
"//player.vimeo.com/video/#{attr :target}#{start_anchor}#{url_query params}"
|
123
|
+
|
124
|
+
when :youtube
|
125
|
+
video_id, list_id = (attr :target).split('/', 2)
|
126
|
+
params = {
|
127
|
+
:rel => 0,
|
128
|
+
:start => (attr :start),
|
129
|
+
:end => (attr :end),
|
130
|
+
:list => (attr :list, list_id),
|
131
|
+
:autoplay => (1 if option? 'autoplay'),
|
132
|
+
:loop => (1 if option? 'loop'),
|
133
|
+
:controls => (0 if option? 'nocontrols')
|
134
|
+
}
|
135
|
+
"//www.youtube.com/embed/#{video_id}#{url_query params}"
|
136
|
+
else
|
137
|
+
anchor = [(attr :start), (attr :end)].join(',').chomp(',')
|
138
|
+
anchor.prepend '#t=' unless anchor.empty?
|
139
|
+
media_uri "#{attr :target}#{anchor}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Formats URL query parameters.
|
144
|
+
def url_query(params)
|
145
|
+
str = params.map { |k, v|
|
146
|
+
next if v.nil? || v.to_s.empty?
|
147
|
+
[k, v] * '='
|
148
|
+
}.compact.join('&')
|
149
|
+
|
150
|
+
str.prepend('?') unless str.empty?
|
151
|
+
end
|
152
|
+
|
153
|
+
#--------------------------------------------------------
|
154
|
+
# inline_anchor
|
155
|
+
#
|
156
|
+
# @return [String, nil] text of the xref anchor, or +nil+ if not found.
|
157
|
+
def xref_text
|
158
|
+
str = text || document.references[:ids][attr :refid || target]
|
159
|
+
str.tr_s("\n", ' ') if str
|
160
|
+
end
|
161
|
+
|
162
|
+
# removes leading hash from anchor targets
|
163
|
+
def anchor_name str
|
164
|
+
if str.start_with? "#"
|
165
|
+
str[1..str.length]
|
166
|
+
else
|
167
|
+
str
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|