asciidoctor-csl 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ce78146d55d20de4621859574c6bbdc2cefa673f8712c7149c0370bc121cf12
4
- data.tar.gz: be41bf38def6db2dc0608d6e5d0dbedf50f2abeaa439e78429dc7bf4db11aff0
3
+ metadata.gz: 91d11025bf31c30262f9edf298ebf53b5039c458d3d8415e782ee69778a0b2e5
4
+ data.tar.gz: 3f06bc987a676662545763a41483833e86a2eaade05cb4eb8aa7dff773171208
5
5
  SHA512:
6
- metadata.gz: 9449c142d9f2b667a8cd73e569eb61b889957ce49ceb9ad9221720f40e0e6870eb4154f9b2bffcd07b8b4db94ce203e0dccdd135781a1418f73358ea57d191cd
7
- data.tar.gz: af718af924f6876712b042a24e40112327fddf1ceac50c51cea26c0f3e1e7e3b09139059fca5592189cd24d8a5dde028503def04bd49dbbd83b99409b35eb1eb
6
+ metadata.gz: 75b1473dd6047b43cab042abc3d5cc1ea1cb3f0ec515342afa4db5f17a3d922b79a6a14b8abb22413b4d6ca0680c9bf580bb3ccd4819cddf6a02a9602010d1a3
7
+ data.tar.gz: ae2b790ba2d632736f6b1f9d61335b69ea86610f431623f5896f0e2b308d9fdc0da8023579cc0ca5ae716cd48bf9045bca8546c43f4924f2707da1938e5e03ce
data/README.adoc CHANGED
@@ -46,6 +46,11 @@ Defaults to `ieee`.
46
46
  The language used for terms such as "p." or "and".
47
47
  Defaults to the `lang` attribute of the document, or `en` if that is not set.
48
48
 
49
+ `csl-link-title`::
50
+ If set, the title of each entry links to the source's DOI or URL.
51
+ Citations are not affected.
52
+ Override it per macro with the `link-title` attribute of `bibliography::[]` and `bibitem:key[]`.
53
+
49
54
  === Citations
50
55
 
51
56
  `cite:key[]` cites a source.
@@ -87,12 +92,15 @@ Citations in section titles are not supported.
87
92
  By default, it only renders the cited sources.
88
93
  Use `bibliography::[select=all]` to include everything in the bibliography.
89
94
 
95
+ Use `bibliography::[link-title=true]` or `bibliography::[link-title=false]` to override the `csl-link-title` setting for this bibliography.
96
+
90
97
  Each entry gets an anchor with the source's key as ID, like `<<smith2020>>`.
91
98
 
92
99
  === Single entries
93
100
 
94
101
  `bibitem:key[]` renders the entry of a single source inline.
95
102
  It does not count as a citation: the source is not added to the bibliography and does not affect the numbering.
103
+ Like the bibliography, it accepts `link-title=true` or `link-title=false` to override the `csl-link-title` setting.
96
104
 
97
105
  == Citation Style Language support
98
106
 
@@ -7,7 +7,7 @@ require 'citeproc'
7
7
  module AsciidoctorCsl
8
8
  # BibitemMacro
9
9
  #
10
- # The `bibitem:key1[]` macro.
10
+ # The `bibitem:key1[link-title=false]` macro.
11
11
  class BibitemMacro < ::Asciidoctor::Extensions::InlineMacroProcessor
12
12
  include ::Asciidoctor::Logging
13
13
 
@@ -17,14 +17,17 @@ module AsciidoctorCsl
17
17
 
18
18
  attr_accessor :processor
19
19
 
20
- def process(parent, target, _attributes)
21
- entry = processor&.render_entry(target)
20
+ def process(parent, target, attributes)
21
+ options = attributes.key?('link-title') ? { link_titles: attributes['link-title'] != 'false' } : {}
22
+ entry = processor&.render_entry(target, **options)
22
23
  if entry.nil?
23
24
  logger.warn "bibitem: unknown reference: #{target}"
24
25
  return create_inline parent, :quoted, "[#{target}]"
25
26
  end
26
27
 
27
- create_inline parent, :quoted, entry, attributes: { 'subs' => :normal }
28
+ # Asciidoctor scans the output of inline macros for URLs again, linking those already linked.
29
+ # Encoding the colon of `://` hides them from that scan; it renders the same.
30
+ create_inline parent, :quoted, parent.apply_subs(entry).gsub('://', '&#58;//')
28
31
  end
29
32
  end
30
33
  end
@@ -6,7 +6,7 @@ require 'asciidoctor/extensions'
6
6
  module AsciidoctorCsl
7
7
  # BibliographyBlockMacro
8
8
  #
9
- # Provides the bibliography block macro. (bibliography::[select=cited])
9
+ # Provides the bibliography block macro. (bibliography::[select=cited, link-title=false])
10
10
  class BibliographyBlockMacro < ::Asciidoctor::Extensions::BlockMacroProcessor
11
11
  include ::Asciidoctor::Logging
12
12
 
@@ -30,6 +30,7 @@ module AsciidoctorCsl
30
30
  end
31
31
 
32
32
  block = create_block parent, :open, nil, { 'role' => 'bibliography' }
33
+ block.set_attr 'link-title', attrs['link-title'] if attrs.key?('link-title')
33
34
  blocks << block
34
35
  block
35
36
  end
@@ -50,10 +50,20 @@ module AsciidoctorCsl
50
50
 
51
51
  csl.engine.format.link_citations = @bibliography && attributes['link'] != 'false'
52
52
  text = csl.render(:citation, items)
53
- # Support footnote styles
54
- text = "footnote:[#{text.gsub(']', '\\]')}]" if csl.engine.style.info.citation_format == :note
53
+ return create_footnote parent, text if csl.engine.style.info.citation_format == :note
55
54
 
56
55
  create_inline parent, :quoted, text, attributes: { 'subs' => :normal }
57
56
  end
57
+
58
+ private
59
+
60
+ # Create the footnote like the footnote macro does, so the citation is not escaped to fit in `footnote:[]`.
61
+ def create_footnote(parent, text)
62
+ document = parent.document
63
+ content = parent.apply_subs text
64
+ index = document.counter 'footnote-number'
65
+ document.register :footnotes, ::Asciidoctor::Document::Footnote.new(index, nil, content)
66
+ create_inline parent, :footnote, content, attributes: { 'index' => index }
67
+ end
58
68
  end
59
69
  end
@@ -11,6 +11,7 @@ module AsciidoctorCsl
11
11
  csl_file = (document.attr 'csl-file').to_s
12
12
  csl_style = ((document.attr 'csl-style') || 'ieee').to_s
13
13
  csl_lang = (document.attr('csl-lang') || document.attr('lang') || 'en').to_s
14
+ csl_link_title = document.attr? 'csl-link-title'
14
15
 
15
16
  return if csl_file.empty?
16
17
 
@@ -19,7 +20,7 @@ module AsciidoctorCsl
19
20
  bibitem = document.extensions.inline_macros.map(&:instance).find { |m| m.name == :bibitem }
20
21
 
21
22
  path_to_csl = document.normalize_system_path csl_file, document.base_dir
22
- processor = Processor.new path_to_csl, csl_style, csl_lang
23
+ processor = Processor.new path_to_csl, csl_style, csl_lang, link_titles: csl_link_title
23
24
 
24
25
  # Collect all cited keys, in appearance order.
25
26
  document.find_by(traverse_documents: true) { |b| prose?(b) }.each do |block|
@@ -39,7 +40,10 @@ module AsciidoctorCsl
39
40
  cite.bibliography = !bibliography.blocks.empty?
40
41
  bibitem.processor = processor
41
42
 
42
- bibliography.blocks.each { |block| parse_content block, processor.build_bibliography_list }
43
+ bibliography.blocks.each do |block|
44
+ link_titles = block.attr?('link-title') ? block.attr('link-title') != 'false' : csl_link_title
45
+ parse_content block, processor.build_bibliography_list(link_titles:)
46
+ end
43
47
 
44
48
  nil
45
49
  end
@@ -21,13 +21,66 @@ module AsciidoctorCsl
21
21
 
22
22
  item.suppress! 'author' if item.suppress_author?
23
23
 
24
- text = render_layout(item, node)
24
+ text = in_xref { render_layout(item, node) }
25
25
  text = "<<#{item.id},#{text}>>" unless text.empty?
26
26
  join [item.prefix, text, item.suffix].compact
27
27
  end
28
+
29
+ private
30
+
31
+ def in_xref
32
+ format.in_xref = true
33
+ yield
34
+ ensure
35
+ format.in_xref = false
36
+ end
37
+ end
38
+
39
+ # Support for linking URLs in the title.
40
+ module LinkTitles
41
+ def render_bibliography(item, node)
42
+ return super unless format.respond_to?(:link_titles) && format.link_titles
43
+
44
+ target = link_target(item.data)
45
+ return super unless target
46
+
47
+ doi = item.data[:DOI]
48
+ url = item.data[:URL]
49
+ suppressed = item.data.suppressed.dup
50
+
51
+ format.title_link = target
52
+ # Blank DOI/URL so the style renders the entry as if it had neither:
53
+ # groups around them collapse as for any missing variable.
54
+ item.data[:DOI] = nil
55
+ item.data[:URL] = nil
56
+
57
+ result = super
58
+ return result unless format.title_link
59
+
60
+ # The style rendered no title: render the entry as usual.
61
+ format.title_link = nil
62
+ item.data[:DOI] = doi
63
+ item.data[:URL] = url
64
+ # Rendering suppresses variables used as substitutes; undo that too.
65
+ item.data.suppressed.replace suppressed
66
+ super
67
+ ensure
68
+ format.title_link = nil if format.respond_to?(:title_link=)
69
+ end
70
+
71
+ private
72
+
73
+ def link_target(data)
74
+ doi = data[:DOI].to_s.strip
75
+ return doi.match?(%r{\Ahttps?://}i) ? doi : "https://doi.org/#{doi}" unless doi.empty?
76
+
77
+ url = data[:URL].to_s.strip
78
+ url unless url.empty?
79
+ end
28
80
  end
29
81
  end
30
82
  end
31
83
 
32
84
  CiteProc::Item.prepend AsciidoctorCsl::CiteprocPatches::SeparateSuppression
33
85
  CiteProc::Ruby::Renderer.prepend AsciidoctorCsl::CiteprocPatches::LinkCitations
86
+ CiteProc::Ruby::Renderer.prepend AsciidoctorCsl::CiteprocPatches::LinkTitles
@@ -7,10 +7,20 @@ module AsciidoctorCsl
7
7
 
8
8
  PROTECTED_RX = /<<[^,>]+,|\{[\w-]+}|\b[a-z][\w-]*:\S*?\[[^\]]*\]|\[[^\]]*\](?=#)|\+\+\+.*?\+\+\+|\+[^+]+\+/
9
9
  PLACEHOLDER_RX = /\u0000(\d+)\u0000/
10
+ URL_VARIABLES = %w[URL DOI].freeze
10
11
 
11
12
  # Whether rendered citations link to their bibliography entry.
12
13
  attr_accessor :link_citations
13
14
 
15
+ # Whether bibliography titles link.
16
+ attr_accessor :link_titles
17
+
18
+ # The target for the next rendered title, set per entry by the renderer.
19
+ attr_accessor :title_link
20
+
21
+ # Whether we are rendering inside a citation's cross-reference, where links cannot nest.
22
+ attr_accessor :in_xref
23
+
14
24
  def bibliography(bibliography, _locale = nil)
15
25
  bibliography.connector = "\n\n"
16
26
  bibliography
@@ -77,5 +87,55 @@ module AsciidoctorCsl
77
87
  ensure
78
88
  output.gsub!(PLACEHOLDER_RX) { protected[Regexp.last_match(1).to_i] } if protected
79
89
  end
90
+
91
+ # An affix that became part of a link is not added again.
92
+ def prefix
93
+ @prefix_in_link ? '' : super
94
+ end
95
+
96
+ protected
97
+
98
+ # Escape URLs and DOIs before any formatting, so AsciiDoc does not parse them.
99
+ def setup!
100
+ super
101
+ escape_url if node.is_a?(CSL::Style::Text) && URL_VARIABLES.include?(node.variable)
102
+ end
103
+
104
+ def cleanup!
105
+ super
106
+ @prefix_in_link = nil
107
+ end
108
+
109
+ def finalize_content!
110
+ super
111
+
112
+ return unless title_link && node.is_a?(CSL::Style::Text) && node.variable == 'title'
113
+
114
+ output.replace "link:#{passthrough title_link}[#{output.gsub(']', '\]')}]"
115
+ self.title_link = nil # link only the first rendered title
116
+ end
117
+
118
+ private
119
+
120
+ # Link the value if it is a URL, or becomes one with its prefix (as in prefix="https://doi.org/");
121
+ # a value that already is a URL ignores such a prefix. Anything else is passed through as text.
122
+ def escape_url
123
+ url = if url?(output) then output
124
+ elsif url?(prefix + output) then prefix + output
125
+ end
126
+ @prefix_in_link = url && url?(prefix)
127
+
128
+ text = passthrough(url || output)
129
+ output.replace(url && !in_xref ? "link:#{text}[]" : text)
130
+ end
131
+
132
+ # Not `++text++`, which cannot contain `++`.
133
+ def passthrough(text)
134
+ "pass:c[#{text.gsub(']', '\]')}]"
135
+ end
136
+
137
+ def url?(text)
138
+ text.match?(%r{\Ahttps?://}i)
139
+ end
80
140
  end
81
141
  end
@@ -35,12 +35,14 @@ module AsciidoctorCsl
35
35
  def initialize(
36
36
  bibliography_file,
37
37
  style = 'ieee',
38
- locale = 'en'
38
+ locale = 'en',
39
+ link_titles: false
39
40
  )
40
41
  raise "File '#{bibliography_file}' is not found" unless FileTest.file? bibliography_file
41
42
 
42
43
  @style = style
43
44
  @locale = locale
45
+ @link_titles = link_titles
44
46
 
45
47
  # Citations in the order they appear in the document
46
48
  @citations = []
@@ -54,10 +56,11 @@ module AsciidoctorCsl
54
56
  @citations.concat keys.map(&:strip)
55
57
  end
56
58
 
57
- def render_entry(key)
59
+ # Render a single entry; link_titles overrides the document default.
60
+ def render_entry(key, link_titles: @link_titles)
58
61
  return nil unless @bibliography.key?(key)
59
62
 
60
- entries.render(:bibliography, id: key).first
63
+ render_bibliography_with(entries, link_titles) { entries.render(:bibliography, id: key).first }
61
64
  end
62
65
 
63
66
  # Finalize citation macro processing and build internal citation list.
@@ -70,16 +73,16 @@ module AsciidoctorCsl
70
73
  keys = render_all ? @citations | @bibliography.keys : @citations
71
74
  @citeproc.import(keys.filter_map { |key| @bibliography[key] })
72
75
 
73
- @rendered = @citeproc.bibliography
74
-
75
- @rendered.ids.each_with_index do |id, i|
76
+ @citeproc.bibliography.ids.each_with_index do |id, i|
76
77
  @citeproc[id][:'citation-number'] = i + 1
77
78
  end
78
79
  nil
79
80
  end
80
81
 
81
- def build_bibliography_list
82
- @rendered.ids.zip(@rendered.references).flat_map do |id, reference|
82
+ # Render the bibliography; link_titles overrides the document default.
83
+ def build_bibliography_list(link_titles: @link_titles)
84
+ rendered = render_bibliography_with(@citeproc, link_titles) { @citeproc.bibliography }
85
+ rendered.ids.zip(rendered.references).flat_map do |id, reference|
83
86
  ["[[#{id}]]#{reference}", '']
84
87
  end
85
88
  end
@@ -101,10 +104,25 @@ module AsciidoctorCsl
101
104
 
102
105
  def new_citeproc
103
106
  citeproc = CiteProc::Processor.new style: @style, format: :asciidoc, locale: @locale
107
+ citeproc.engine.format.link_titles = @link_titles
104
108
  use_asciidoc_quotes citeproc
105
109
  citeproc
106
110
  end
107
111
 
112
+ # Render bibliography entries with the given title link setting.
113
+ #
114
+ # Also forget earlier bibliography renders: subsequent-author-substitute
115
+ # compares against the last rendered entry, even from a previous render.
116
+ def render_bibliography_with(citeproc, link_titles)
117
+ format = citeproc.engine.format
118
+ previous = format.link_titles
119
+ format.link_titles = link_titles
120
+ citeproc.engine.renderer.state.history.memory.delete('bibliography')
121
+ yield
122
+ ensure
123
+ format.link_titles = previous
124
+ end
125
+
108
126
  # Override the quote handling by inserting dynamic AsciiDoc quotes.
109
127
  # That way, AsciiDoc handles the quotes, and it is consistent with the rest of the document.
110
128
  def use_asciidoc_quotes(processor)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AsciidoctorCsl
4
- VERSION = '0.1.0'
4
+ VERSION = '0.3.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asciidoctor-csl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Niko Strijbol