asciidoctor-csl 0.1.0 → 0.4.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: 3b880f935d5f43d4ccfb97b46ff5622cd61aeea3bc71f6b97a18cb010ca68d32
4
+ data.tar.gz: 64a474f6dd9e279f965687eb3296aed7a4a27c0f30b1396020c4a6ced437bfbb
5
5
  SHA512:
6
- metadata.gz: 9449c142d9f2b667a8cd73e569eb61b889957ce49ceb9ad9221720f40e0e6870eb4154f9b2bffcd07b8b4db94ce203e0dccdd135781a1418f73358ea57d191cd
7
- data.tar.gz: af718af924f6876712b042a24e40112327fddf1ceac50c51cea26c0f3e1e7e3b09139059fca5592189cd24d8a5dde028503def04bd49dbbd83b99409b35eb1eb
6
+ metadata.gz: a5d3e789eb43329656c173d70668e30d1aa456066a61debb33c1da10c163b2f888d3548afcc14b9e31bcd6a9330bc0e89197ce41e0370e5480fe9b7184770e9e
7
+ data.tar.gz: 0e323c23f6adea8c4c701626971ee37a5a9af23475ef345b26802440e2605c6c663513d6c94f0062ebbd62a430dc89f349a34a6e80bc99e2b4021e0c972612d1
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.
@@ -75,6 +80,10 @@ Other values are `book`, `column`, `figure`, `folio`, `issue`, `line`, `note`, `
75
80
  |`link`
76
81
  |If the document has a bibliography, each cited item links to its entry.
77
82
  Use `link=false` to disable this for a single citation.
83
+
84
+ |`parens`
85
+ |Use `parens=false` to leave out the parentheses around the citation, e.g. to write "(see Smith, 2020, or the appendix)" yourself.
86
+ This removes the prefix and suffix the style puts around the whole citation; it does not affect brackets around each item, as in IEEE.
78
87
  |===
79
88
 
80
89
  With multiple keys, the locator and suffix apply to the last source, and the prefix to the first.
@@ -87,12 +96,15 @@ Citations in section titles are not supported.
87
96
  By default, it only renders the cited sources.
88
97
  Use `bibliography::[select=all]` to include everything in the bibliography.
89
98
 
99
+ Use `bibliography::[link-title=true]` or `bibliography::[link-title=false]` to override the `csl-link-title` setting for this bibliography.
100
+
90
101
  Each entry gets an anchor with the source's key as ID, like `<<smith2020>>`.
91
102
 
92
103
  === Single entries
93
104
 
94
105
  `bibitem:key[]` renders the entry of a single source inline.
95
106
  It does not count as a citation: the source is not added to the bibliography and does not affect the numbering.
107
+ Like the bibliography, it accepts `link-title=true` or `link-title=false` to override the `csl-link-title` setting.
96
108
 
97
109
  == Citation Style Language support
98
110
 
@@ -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
@@ -7,7 +7,7 @@ require 'citeproc'
7
7
  module AsciidoctorCsl
8
8
  # CitationMacro
9
9
  #
10
- # The `cite:key1,key2[locator, label, prefix=..., suffix=..., link=false]` macro.
10
+ # The `cite:key1,key2[locator, label, prefix=..., suffix=..., link=false, parens=false]` macro.
11
11
  class CitationMacro < ::Asciidoctor::Extensions::InlineMacroProcessor
12
12
  include ::Asciidoctor::Logging
13
13
 
@@ -43,17 +43,23 @@ module AsciidoctorCsl
43
43
 
44
44
  items.last[:suffix] = ", #{attributes['suffix']}" if attributes['suffix']
45
45
 
46
- if csl.nil?
47
- logger.warn 'cite: CSL style not found'
48
- return nil
49
- end
50
-
51
46
  csl.engine.format.link_citations = @bibliography && attributes['link'] != 'false'
47
+ csl.engine.format.bare_citations = attributes['parens'] == 'false'
52
48
  text = csl.render(:citation, items)
53
- # Support footnote styles
54
- text = "footnote:[#{text.gsub(']', '\\]')}]" if csl.engine.style.info.citation_format == :note
49
+ return create_footnote parent, text if csl.engine.style.info.citation_format == :note
55
50
 
56
51
  create_inline parent, :quoted, text, attributes: { 'subs' => :normal }
57
52
  end
53
+
54
+ private
55
+
56
+ # Create the footnote like the footnote macro does, so the citation is not escaped to fit in `footnote:[]`.
57
+ def create_footnote(parent, text)
58
+ document = parent.document
59
+ content = parent.apply_subs text
60
+ index = document.counter 'footnote-number'
61
+ document.register :footnotes, ::Asciidoctor::Document::Footnote.new(index, nil, content)
62
+ create_inline parent, :footnote, content, attributes: { 'index' => index }
63
+ end
58
64
  end
59
65
  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,25 @@ 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
11
+ BLOCK_EDGE = "\u0001"
12
+ LINE_BREAK = " +\n{empty}" # {empty} keeps the next line from starting a list, comment, etc.
10
13
 
11
14
  # Whether rendered citations link to their bibliography entry.
12
15
  attr_accessor :link_citations
13
16
 
17
+ # Whether bibliography titles link.
18
+ attr_accessor :link_titles
19
+
20
+ # The target for the next rendered title, set per entry by the renderer.
21
+ attr_accessor :title_link
22
+
23
+ # Whether we are rendering inside a citation's cross-reference, where links cannot nest.
24
+ attr_accessor :in_xref
25
+
26
+ # Whether citations leave out the affixes of their layout.
27
+ attr_accessor :bare_citations
28
+
14
29
  def bibliography(bibliography, _locale = nil)
15
30
  bibliography.connector = "\n\n"
16
31
  bibliography
@@ -66,6 +81,18 @@ module AsciidoctorCsl
66
81
  end
67
82
  end
68
83
 
84
+ # Blocks get their own line.
85
+ # AsciiDoc has no margins, so the margin column stays inline.
86
+ def apply_display
87
+ case options[:display]
88
+ when 'block', 'indent' then output.replace "#{BLOCK_EDGE}#{output}#{BLOCK_EDGE}"
89
+ when 'left-margin' then output << ' '
90
+ when 'right-inline' then nil
91
+ else
92
+ logger.warn "csl: unsupported display '#{options[:display]}'"
93
+ end
94
+ end
95
+
69
96
  # Replace the AsciiDoc syntax by placeholders without letters, so we only transform actual text.
70
97
  def apply_text_case
71
98
  protected = []
@@ -77,5 +104,67 @@ module AsciidoctorCsl
77
104
  ensure
78
105
  output.gsub!(PLACEHOLDER_RX) { protected[Regexp.last_match(1).to_i] } if protected
79
106
  end
107
+
108
+ # An affix that became part of a link is not added again, and bare citations have no layout affixes.
109
+ def prefix
110
+ @prefix_in_link || bare_citation_layout? ? '' : super
111
+ end
112
+
113
+ def suffix
114
+ bare_citation_layout? ? '' : super
115
+ end
116
+
117
+ protected
118
+
119
+ # Before formatting, escape URLs and DOIs, so AsciiDoc does not parse them,
120
+ # and turn block edges into line breaks, so the layout's affixes stay on the first and last line.
121
+ def setup!
122
+ super
123
+ if node.is_a?(CSL::Style::Layout)
124
+ output.replace output.split(BLOCK_EDGE).map(&:strip).reject(&:empty?).join(LINE_BREAK)
125
+ end
126
+ escape_url if node.is_a?(CSL::Style::Text) && URL_VARIABLES.include?(node.variable)
127
+ end
128
+
129
+ def cleanup!
130
+ super
131
+ @prefix_in_link = nil
132
+ end
133
+
134
+ def finalize_content!
135
+ super
136
+
137
+ return unless title_link && node.is_a?(CSL::Style::Text) && node.variable == 'title'
138
+
139
+ output.replace "link:#{passthrough title_link}[#{output.gsub(']', '\]')}]"
140
+ self.title_link = nil # link only the first rendered title
141
+ end
142
+
143
+ private
144
+
145
+ # Link the value if it is a URL, or becomes one with its prefix (as in prefix="https://doi.org/");
146
+ # a value that already is a URL ignores such a prefix. Anything else is passed through as text.
147
+ def escape_url
148
+ url = if url?(output) then output
149
+ elsif url?(prefix + output) then prefix + output
150
+ end
151
+ @prefix_in_link = url && url?(prefix)
152
+
153
+ text = passthrough(url || output)
154
+ output.replace(url && !in_xref ? "link:#{text}[]" : text)
155
+ end
156
+
157
+ # Not `++text++`, which cannot contain `++`.
158
+ def passthrough(text)
159
+ "pass:c[#{text.gsub(']', '\]')}]"
160
+ end
161
+
162
+ def bare_citation_layout?
163
+ bare_citations && node.is_a?(CSL::Style::Layout) && node.parent.is_a?(CSL::Style::Citation)
164
+ end
165
+
166
+ def url?(text)
167
+ text.match?(%r{\Ahttps?://}i)
168
+ end
80
169
  end
81
170
  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.4.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.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Niko Strijbol