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,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,9 @@
1
+ module Asciidoctor
2
+ module ConfluencePublisher
3
+ module Model
4
+ class Ancestor < Base
5
+ attr_accessor :id
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ module Asciidoctor
2
+ module ConfluencePublisher
3
+ module Model
4
+ class Attachment < Base
5
+ attr_accessor :id, :title
6
+ attr_reader :version
7
+
8
+ def version=(ver)
9
+ @version = Version.new(ver)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -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,14 @@
1
+ module Asciidoctor
2
+ module ConfluencePublisher
3
+ module Model
4
+ class Property < Base
5
+ attr_accessor :key, :value
6
+ attr_reader :version
7
+
8
+ def version=(ver)
9
+ @version = Version.new(ver)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ module Asciidoctor
2
+ module ConfluencePublisher
3
+ module Model
4
+ class Space < Base
5
+ attr_accessor :key
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Asciidoctor
2
+ module ConfluencePublisher
3
+ module Model
4
+ class Version < Base
5
+ attr_accessor :number
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module Asciidoctor
2
+ module ConfluencePublisher
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ require_relative "asciidoctor/confluence_publisher"
@@ -0,0 +1,6 @@
1
+ %ac:structured-macro{"ac:name" => admonition_name}
2
+ - if title?
3
+ %ac:parameter{"ac:name" => "title"}= title
4
+ %ac:rich-text-body
5
+ %p= content
6
+
@@ -0,0 +1,4 @@
1
+ %ac:structured-macro{ "ac:name" => "expand" }
2
+ - if title?
3
+ %ac:parameter{"ac:name" => "title"}= title
4
+ %ac:rich-text-body= content
@@ -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,8 @@
1
+ - if title?
2
+ .cp-olist-title= title
3
+ %ol
4
+ - items.each do |item|
5
+ %li
6
+ %p= item.text
7
+ - if item.blocks?
8
+ = item.content
@@ -0,0 +1,4 @@
1
+ - if title?
2
+ .cp-paragraph-title
3
+ %em= title
4
+ %p= content
@@ -0,0 +1 @@
1
+ =content
@@ -0,0 +1,9 @@
1
+ %blockquote=content
2
+ - if (attr? :attribution) or (attr? :citetitle)
3
+ .attribution{ style: 'text-align: right;'}
4
+ - if attr? :attribution
5
+ &#8212; #{attr :attribution}
6
+ - if attr? :citetitle
7
+ - if attr? :attribution
8
+ %br
9
+ %cite=attr :citetitle
@@ -0,0 +1,3 @@
1
+ %ac:structured-macro{ "ac:name" => "mathjax-block-macro" }
2
+ %ac:plain-text-body
3
+ = "<![CDATA[#{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,9 @@
1
+ %pre.content=content
2
+ - if (attr? :attribution) or (attr? :citetitle)
3
+ .attribution
4
+ - if attr? :attribution
5
+ &#8212; #{attr :attribution}
6
+ - if attr? :citetitle
7
+ - if attr? :attribution
8
+ %br
9
+ %cite=attr :citetitle
@@ -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
@@ -0,0 +1,4 @@
1
+ - if (attr? :toc) && (attr? 'toc-placement', 'auto')
2
+ - current_dir = Pathname.new(File.dirname(__FILE__)).realpath
3
+ %p =Haml::Engine.new(File.read("#{current_dir}/block_toc.html.haml")).render
4
+ = content
@@ -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('&amp;')
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