asciidoctor-confluence_publisher 0.1.0

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