metanorma-standoc 1.6.4 → 1.7.3

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +17 -0
  3. data/lib/asciidoctor/standoc/base.rb +8 -16
  4. data/lib/asciidoctor/standoc/basicdoc.rng +50 -3
  5. data/lib/asciidoctor/standoc/cleanup.rb +1 -1
  6. data/lib/asciidoctor/standoc/cleanup_block.rb +0 -8
  7. data/lib/asciidoctor/standoc/cleanup_boilerplate.rb +33 -20
  8. data/lib/asciidoctor/standoc/cleanup_inline.rb +3 -1
  9. data/lib/asciidoctor/standoc/cleanup_ref.rb +17 -24
  10. data/lib/asciidoctor/standoc/cleanup_terms.rb +4 -6
  11. data/lib/asciidoctor/standoc/converter.rb +4 -49
  12. data/lib/asciidoctor/standoc/front_contributor.rb +8 -4
  13. data/lib/asciidoctor/standoc/inline.rb +7 -5
  14. data/lib/asciidoctor/standoc/isodoc.rng +48 -3
  15. data/lib/asciidoctor/standoc/macros.rb +30 -61
  16. data/lib/asciidoctor/standoc/macros_terms.rb +82 -0
  17. data/lib/asciidoctor/standoc/ref.rb +12 -21
  18. data/lib/asciidoctor/standoc/section.rb +22 -20
  19. data/lib/asciidoctor/standoc/table.rb +12 -0
  20. data/lib/asciidoctor/standoc/term_lookup_cleanup.rb +86 -0
  21. data/lib/asciidoctor/standoc/utils.rb +2 -0
  22. data/lib/metanorma/standoc/version.rb +1 -1
  23. data/metanorma-standoc.gemspec +1 -2
  24. data/spec/asciidoctor-standoc/base_spec.rb +14 -8
  25. data/spec/asciidoctor-standoc/blocks_spec.rb +14 -9
  26. data/spec/asciidoctor-standoc/cleanup_sections_spec.rb +18 -13
  27. data/spec/asciidoctor-standoc/cleanup_spec.rb +75 -11
  28. data/spec/asciidoctor-standoc/inline_spec.rb +4 -5
  29. data/spec/asciidoctor-standoc/macros_lutaml_spec.rb +1 -1
  30. data/spec/asciidoctor-standoc/macros_plantuml_spec.rb +307 -0
  31. data/spec/asciidoctor-standoc/macros_spec.rb +258 -277
  32. data/spec/asciidoctor-standoc/refs_dl_spec.rb +4 -4
  33. data/spec/asciidoctor-standoc/section_spec.rb +155 -12
  34. data/spec/asciidoctor-standoc/table_spec.rb +146 -0
  35. data/spec/fixtures/diagram_definitions.lutaml +4 -4
  36. data/spec/vcr_cassettes/dated_iso_ref_joint_iso_iec.yml +49 -49
  37. data/spec/vcr_cassettes/isobib_get_123.yml +13 -13
  38. data/spec/vcr_cassettes/isobib_get_123_1.yml +25 -25
  39. data/spec/vcr_cassettes/isobib_get_123_1_fr.yml +33 -33
  40. data/spec/vcr_cassettes/isobib_get_123_2001.yml +14 -14
  41. data/spec/vcr_cassettes/isobib_get_124.yml +14 -14
  42. data/spec/vcr_cassettes/rfcbib_get_rfc8341.yml +8 -8
  43. data/spec/vcr_cassettes/separates_iev_citations_by_top_level_clause.yml +71 -75
  44. metadata +7 -18
@@ -1,6 +1,4 @@
1
1
  require "asciidoctor"
2
- require "fontist"
3
- require "fontist/manifest/install"
4
2
  require "metanorma/util"
5
3
  require "metanorma/standoc/version"
6
4
  require "asciidoctor/standoc/base"
@@ -39,6 +37,9 @@ module Asciidoctor
39
37
  inline_macro Asciidoctor::Standoc::AutonumberInlineMacro
40
38
  inline_macro Asciidoctor::Standoc::VariantInlineMacro
41
39
  inline_macro Asciidoctor::Standoc::FootnoteBlockInlineMacro
40
+ inline_macro Asciidoctor::Standoc::TermRefInlineMacro
41
+ inline_macro Asciidoctor::Standoc::IndexXrefInlineMacro
42
+ inline_macro Asciidoctor::Standoc::IndexRangeInlineMacro
42
43
  block Asciidoctor::Standoc::ToDoAdmonitionBlock
43
44
  treeprocessor Asciidoctor::Standoc::ToDoInlineAdmonitionBlock
44
45
  block Asciidoctor::Standoc::PlantUMLBlockMacro
@@ -52,6 +53,7 @@ module Asciidoctor
52
53
  include ::Asciidoctor::Standoc::Base
53
54
  include ::Asciidoctor::Standoc::Front
54
55
  include ::Asciidoctor::Standoc::Lists
56
+ include ::Asciidoctor::Standoc::Refs
55
57
  include ::Asciidoctor::Standoc::Inline
56
58
  include ::Asciidoctor::Standoc::Blocks
57
59
  include ::Asciidoctor::Standoc::Section
@@ -69,8 +71,6 @@ module Asciidoctor
69
71
  basebackend "html"
70
72
  outfilesuffix ".xml"
71
73
  @libdir = File.dirname(self.class::_file || __FILE__)
72
-
73
- install_fonts(opts)
74
74
  end
75
75
 
76
76
  class << self
@@ -86,51 +86,6 @@ module Asciidoctor
86
86
  File.join(@libdir, "../../isodoc/html", file)
87
87
  end
88
88
 
89
- def flavor_name
90
- self.class.name.split("::")&.[](-2).downcase
91
- end
92
-
93
- def fonts_manifest
94
- File.expand_path(File.join(@libdir, "../../metanorma/", flavor_name, "fonts_manifest.yaml"))
95
- end
96
-
97
- def install_fonts(options={})
98
- if options[:no_install_fonts]
99
- Metanorma::Util.log("[fontist] Skip font installation because" \
100
- " --no-install-fonts argument passed", :debug)
101
- return
102
- end
103
-
104
- if fonts_manifest.nil? || !File.exist?(fonts_manifest)
105
- Metanorma::Util.log("[fontist] Skip font installation because" \
106
- " font manifest file doesn't exists/defined", :debug)
107
- return
108
- end
109
-
110
- begin
111
- Fontist::Manifest::Install.call(
112
- fonts_manifest,
113
- confirmation: options[:agree_to_terms] ? "yes" : "no"
114
- )
115
- rescue Fontist::Errors::LicensingError
116
- if !options[:agree_to_terms]
117
- Metanorma::Util.log("[fontist] --agree-to-terms option missing." \
118
- " You must accept font licenses to install fonts.", :debug)
119
- elsif options[:continue_without_fonts]
120
- Metanorma::Util.log("[fontist] Processing will continue without" \
121
- " fonts installed", :debug)
122
- else
123
- Metanorma::Util.log("[fontist] Aborting without proper fonts" \
124
- " installed", :fatal)
125
- end
126
- rescue Fontist::Errors::NonSupportedFontError
127
- flavor = flavor_name || "cli"
128
- Metanorma::Util.log("[fontist] '#{font}' font is not supported. " \
129
- "Please go to github.com/metanorma/metanorma-#{flavor}/issues" \
130
- " to report this issue.", :info)
131
- end
132
- end
133
-
134
89
  alias_method :embedded, :content
135
90
  alias_method :verse, :quote
136
91
  alias_method :audio, :skip
@@ -46,8 +46,9 @@ module Asciidoctor
46
46
 
47
47
  # , " => ," : CSV definition does not deal with space followed by quote
48
48
  # at start of field
49
- def csv_split(s, delim = ",")
50
- CSV.parse_line(s&.gsub(/, "(?!")/, ',"'),
49
+ def csv_split(s, delim = ";")
50
+ return if s.nil?
51
+ CSV.parse_line(s&.gsub(/#{delim} "(?!")/, "#{delim}\""),
51
52
  liberal_parsing: true,
52
53
  col_sep: delim)&.compact&.map { |x| x.strip }
53
54
  end
@@ -115,8 +116,11 @@ module Asciidoctor
115
116
  node.attr("affiliation#{suffix}") and p.affiliation do |a|
116
117
  a.organization do |o|
117
118
  o.name node.attr("affiliation#{suffix}")
118
- abbr = node.attr("affiliation_abbrev#{suffix}") and
119
- o.abbreviation abbr
119
+ a = node.attr("affiliation_subdiv#{suffix}")
120
+ abbr = node.attr("affiliation_abbrev#{suffix}") and o.abbreviation abbr
121
+ csv_split(node.attr("affiliation_subdiv#{suffix}"))&.each do |s|
122
+ o.subdivision s
123
+ end
120
124
  node.attr("address#{suffix}") and o.address do |ad|
121
125
  ad.formattedAddress do |f|
122
126
  f << node.attr("address#{suffix}").gsub(/ \+\n/, "<br/>")
@@ -165,6 +165,7 @@ module Asciidoctor
165
165
  when "domain" then xml.domain { |a| a << node.text }
166
166
 
167
167
  when "strike" then xml.strike { |s| s << node.text }
168
+ when "underline" then xml.underline { |s| s << node.text }
168
169
  when "smallcap" then xml.smallcap { |s| s << node.text }
169
170
  when "keyword" then xml.keyword { |s| s << node.text }
170
171
  else
@@ -211,11 +212,12 @@ module Asciidoctor
211
212
  def inline_indexterm(node)
212
213
  noko do |xml|
213
214
  node.type == :visible and xml << node.text
214
- terms = node.attr("terms") ||
215
- [Nokogiri::XML("<a>#{node.text}</a>").xpath("//text()").text]
216
- xml.index nil, **attr_code(primary: terms[0],
217
- secondary: terms.dig(1),
218
- tertiary: terms.dig(2))
215
+ terms = (node.attr("terms") || [node.text]).map { |x| xml_encode(x) }
216
+ xml.index do |i|
217
+ i.primary { |x| x << terms[0] }
218
+ a = terms.dig(1) and i.secondary { |x| x << a }
219
+ a = terms.dig(2) and i.tertiary { |x| x << a }
220
+ end
219
221
  end.join
220
222
  end
221
223
  end
@@ -55,6 +55,13 @@
55
55
  <param name="pattern">\i\c*|\c+#\c+</param>
56
56
  </data>
57
57
  </attribute>
58
+ <optional>
59
+ <attribute name="to">
60
+ <data type="string">
61
+ <param name="pattern">\i\c*|\c+#\c+</param>
62
+ </data>
63
+ </attribute>
64
+ </optional>
58
65
  <optional>
59
66
  <attribute name="type">
60
67
  <ref name="ReferenceFormat"/>
@@ -246,6 +253,12 @@
246
253
  <data type="boolean"/>
247
254
  </attribute>
248
255
  </optional>
256
+ <optional>
257
+ <attribute name="width"/>
258
+ </optional>
259
+ <optional>
260
+ <ref name="colgroup"/>
261
+ </optional>
249
262
  <optional>
250
263
  <ref name="tname"/>
251
264
  </optional>
@@ -764,6 +777,18 @@
764
777
  </define>
765
778
  </include>
766
779
  <!-- end overrides -->
780
+ <define name="colgroup">
781
+ <element name="colgroup">
782
+ <oneOrMore>
783
+ <ref name="col"/>
784
+ </oneOrMore>
785
+ </element>
786
+ </define>
787
+ <define name="col">
788
+ <element name="col">
789
+ <attribute name="width"/>
790
+ </element>
791
+ </define>
767
792
  <define name="TextElement" combine="choice">
768
793
  <ref name="concept"/>
769
794
  </define>
@@ -814,6 +839,9 @@
814
839
  <data type="boolean"/>
815
840
  </attribute>
816
841
  </optional>
842
+ <optional>
843
+ <attribute name="number"/>
844
+ </optional>
817
845
  <optional>
818
846
  <attribute name="obligation">
819
847
  <choice>
@@ -869,9 +897,11 @@
869
897
  <element name="code">
870
898
  <text/>
871
899
  </element>
872
- <element name="text">
873
- <text/>
874
- </element>
900
+ <optional>
901
+ <element name="text">
902
+ <text/>
903
+ </element>
904
+ </optional>
875
905
  </element>
876
906
  </define>
877
907
  <define name="standard-document">
@@ -1041,6 +1071,9 @@
1041
1071
  </choice>
1042
1072
  </attribute>
1043
1073
  </optional>
1074
+ <optional>
1075
+ <attribute name="number"/>
1076
+ </optional>
1044
1077
  <optional>
1045
1078
  <attribute name="type"/>
1046
1079
  </optional>
@@ -1094,6 +1127,9 @@
1094
1127
  <optional>
1095
1128
  <attribute name="type"/>
1096
1129
  </optional>
1130
+ <optional>
1131
+ <attribute name="number"/>
1132
+ </optional>
1097
1133
  <optional>
1098
1134
  <ref name="section-title"/>
1099
1135
  </optional>
@@ -1196,6 +1232,9 @@
1196
1232
  <optional>
1197
1233
  <attribute name="type"/>
1198
1234
  </optional>
1235
+ <optional>
1236
+ <attribute name="number"/>
1237
+ </optional>
1199
1238
  <optional>
1200
1239
  <attribute name="obligation">
1201
1240
  <choice>
@@ -1524,6 +1563,7 @@
1524
1563
  <value>add</value>
1525
1564
  <value>modify</value>
1526
1565
  <value>delete</value>
1566
+ <value>replace</value>
1527
1567
  </choice>
1528
1568
  </attribute>
1529
1569
  <optional>
@@ -1554,6 +1594,11 @@
1554
1594
  </optional>
1555
1595
  <optional>
1556
1596
  <element name="newcontent">
1597
+ <optional>
1598
+ <attribute name="id">
1599
+ <data type="ID"/>
1600
+ </attribute>
1601
+ </optional>
1557
1602
  <zeroOrMore>
1558
1603
  <ref name="BasicBlock"/>
1559
1604
  </zeroOrMore>
@@ -3,6 +3,7 @@ require "fileutils"
3
3
  require "uuidtools"
4
4
  require "yaml"
5
5
  require_relative "./macros_plantuml.rb"
6
+ require_relative "./macros_terms.rb"
6
7
  require_relative "./datamodel/attributes_table_preprocessor.rb"
7
8
  require_relative "./datamodel/diagram_preprocessor.rb"
8
9
  require "metanorma-plugin-datastruct"
@@ -10,81 +11,49 @@ require "metanorma-plugin-lutaml"
10
11
 
11
12
  module Asciidoctor
12
13
  module Standoc
13
- class AltTermInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
14
+ class InheritInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
14
15
  use_dsl
15
- named :alt
16
+ named :inherit
16
17
  parse_content_as :text
17
18
  using_format :short
18
19
 
19
20
  def process(parent, _target, attrs)
20
21
  out = Asciidoctor::Inline.new(parent, :quoted, attrs["text"]).convert
21
- %{<admitted>#{out}</admitted>}
22
+ %{<inherit>#{out}</inherit>}
22
23
  end
23
24
  end
24
25
 
25
- class DeprecatedTermInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
26
+ class IndexXrefInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
26
27
  use_dsl
27
- named :deprecated
28
- parse_content_as :text
29
- using_format :short
28
+ named :index
30
29
 
31
- def process(parent, _target, attrs)
32
- out = Asciidoctor::Inline.new(parent, :quoted, attrs["text"]).convert
33
- %{<deprecates>#{out}</deprecates>}
30
+ def preprocess_attrs(attrs)
31
+ return unless attrs.size > 1 && attrs.size < 5
32
+ ret = { primary: attrs[1], target: attrs[attrs.size] }
33
+ ret[:secondary] = attrs[2] if attrs.size > 2
34
+ ret[:tertiary] = attrs[3] if attrs.size > 3
35
+ ret
34
36
  end
35
- end
36
-
37
- class DomainTermInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
38
- use_dsl
39
- named :domain
40
- parse_content_as :text
41
- using_format :short
42
37
 
43
- def process(parent, _target, attrs)
44
- out = Asciidoctor::Inline.new(parent, :quoted, attrs["text"]).convert
45
- %{<domain>#{out}</domain>}
38
+ def process(_parent, target, attr)
39
+ args = preprocess_attrs(attr) or return
40
+ ret = "<index-xref also='#{target == 'also'}'><primary>#{args[:primary]}</primary>"
41
+ ret += "<secondary>#{args[:secondary]}</secondary>" if args[:secondary]
42
+ ret += "<tertiary>#{args[:tertiary]}</tertiary>" if args[:tertiary]
43
+ ret + "<target>#{args[:target]}</target></index-xref>"
46
44
  end
47
45
  end
48
46
 
49
- class InheritInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
47
+ class IndexRangeInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
50
48
  use_dsl
51
- named :inherit
49
+ named "index-range".to_sym
52
50
  parse_content_as :text
53
- using_format :short
54
-
55
- def process(parent, _target, attrs)
56
- out = Asciidoctor::Inline.new(parent, :quoted, attrs["text"]).convert
57
- %{<inherit>#{out}</inherit>}
58
- end
59
- end
60
-
61
- class ConceptInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
62
- use_dsl
63
- named :concept
64
- name_positional_attributes "id", "word", "term"
65
- # match %r{concept:(?<target>[^\[]*)\[(?<content>|.*?[^\\])\]$}
66
- match /\{\{(?<content>|.*?[^\\])\}\}/
67
- using_format :short
68
-
69
- # deal with locality attrs and their disruption of positional attrs
70
- def preprocess_attrs(attrs)
71
- attrs.delete("term") if attrs["term"] && !attrs["word"]
72
- attrs.delete(3) if attrs[3] == attrs["term"]
73
- a = attrs.keys.reject { |k| k.is_a?(String) || [1, 2].include?(k) }
74
- attrs["word"] ||= attrs[a[0]] if !a.empty?
75
- attrs["term"] ||= attrs[a[1]] if a.length > 1
76
- attrs
77
- end
78
51
 
79
- def process(parent, _target, attr)
80
- attr = preprocess_attrs(attr)
81
- localities = attr.keys.reject { |k| %w(id word term).include? k }.
82
- reject { |k| k.is_a? Numeric }.
83
- map { |k| "#{k}=#{attr[k]}" }.join(",")
84
- text = [localities, attr["word"]].reject{ |k| k.nil? || k.empty? }.
85
- join(",")
86
- out = Asciidoctor::Inline.new(parent, :quoted, text).convert
87
- %{<concept key="#{attr['id']}" term="#{attr['term']}">#{out}</concept>}
52
+ def process(parent, target, attr)
53
+ text = attr["text"]
54
+ text = "((#{text}))" unless /^\(\(.+\)\)$/.match(text)
55
+ out = parent.sub_macros(text)
56
+ out.sub(/<index>/, "<index to='#{target}'>")
88
57
  end
89
58
  end
90
59
 
@@ -135,7 +104,7 @@ module Asciidoctor
135
104
  if (attributes.size == 1) && attributes.key?("text")
136
105
  rt = attributes["text"]
137
106
  elsif (attributes.size == 2) && attributes.key?(1) &&
138
- attributes.key?("rpbegin")
107
+ attributes.key?("rpbegin")
139
108
  # for example, html5ruby:楽聖少女[がくせいしょうじょ]
140
109
  rt = attributes[1] || ""
141
110
  else
@@ -158,7 +127,7 @@ module Asciidoctor
158
127
  attrs["name"] = "todo"
159
128
  attrs["caption"] = "TODO"
160
129
  create_block parent, :admonition, reader.lines, attrs,
161
- content_model: :compound
130
+ content_model: :compound
162
131
  end
163
132
  end
164
133
 
@@ -171,7 +140,7 @@ module Asciidoctor
171
140
  para.set_attr("caption", "TODO")
172
141
  para.lines[0].sub!(/^TODO: /, "")
173
142
  todo = Block.new parent, :admonition, attributes: para.attributes,
174
- source: para.lines, content_model: :compound
143
+ source: para.lines, content_model: :compound
175
144
  parent.blocks[parent.blocks.index(para)] = todo
176
145
  end
177
146
  end
@@ -197,8 +166,8 @@ module Asciidoctor
197
166
  /^(?<lang>[^-]*)(-(?<script>.*))?$/ =~ target
198
167
  out = Asciidoctor::Inline.new(parent, :quoted, attrs["text"]).convert
199
168
  script ?
200
- %{<variant lang=#{lang} script=#{script}>#{out}</variant>} :
201
- %{<variant lang=#{lang}>#{out}</variant>}
169
+ %{<variant lang=#{lang} script=#{script}>#{out}</variant>} :
170
+ %{<variant lang=#{lang}>#{out}</variant>}
202
171
  end
203
172
  end
204
173
 
@@ -0,0 +1,82 @@
1
+ module Asciidoctor
2
+ module Standoc
3
+ class AltTermInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
4
+ use_dsl
5
+ named :alt
6
+ parse_content_as :text
7
+ using_format :short
8
+
9
+ def process(parent, _target, attrs)
10
+ out = Asciidoctor::Inline.new(parent, :quoted, attrs["text"]).convert
11
+ %{<admitted>#{out}</admitted>}
12
+ end
13
+ end
14
+
15
+ class DeprecatedTermInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
16
+ use_dsl
17
+ named :deprecated
18
+ parse_content_as :text
19
+ using_format :short
20
+
21
+ def process(parent, _target, attrs)
22
+ out = Asciidoctor::Inline.new(parent, :quoted, attrs["text"]).convert
23
+ %{<deprecates>#{out}</deprecates>}
24
+ end
25
+ end
26
+
27
+ class DomainTermInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
28
+ use_dsl
29
+ named :domain
30
+ parse_content_as :text
31
+ using_format :short
32
+
33
+ def process(parent, _target, attrs)
34
+ out = Asciidoctor::Inline.new(parent, :quoted, attrs["text"]).convert
35
+ %{<domain>#{out}</domain>}
36
+ end
37
+ end
38
+
39
+ # Macro to transform `term[X,Y]` into em, termxref xml
40
+ class TermRefInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
41
+ use_dsl
42
+ named :term
43
+ name_positional_attributes 'name', 'termxref'
44
+ using_format :short
45
+
46
+ def process(_parent, _target, attrs)
47
+ termref = attrs['termxref'] || attrs['name']
48
+ "<em>#{attrs['name']}</em> (<termxref>#{termref}</termxref>)"
49
+ end
50
+ end
51
+
52
+ class ConceptInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
53
+ use_dsl
54
+ named :concept
55
+ name_positional_attributes "id", "word", "term"
56
+ # match %r{concept:(?<target>[^\[]*)\[(?<content>|.*?[^\\])\]$}
57
+ match /\{\{(?<content>|.*?[^\\])\}\}/
58
+ using_format :short
59
+
60
+ # deal with locality attrs and their disruption of positional attrs
61
+ def preprocess_attrs(attrs)
62
+ attrs.delete("term") if attrs["term"] && !attrs["word"]
63
+ attrs.delete(3) if attrs[3] == attrs["term"]
64
+ a = attrs.keys.reject { |k| k.is_a?(String) || [1, 2].include?(k) }
65
+ attrs["word"] ||= attrs[a[0]] if !a.empty?
66
+ attrs["term"] ||= attrs[a[1]] if a.length > 1
67
+ attrs
68
+ end
69
+
70
+ def process(parent, _target, attr)
71
+ attr = preprocess_attrs(attr)
72
+ localities = attr.keys.reject { |k| %w(id word term).include? k }.
73
+ reject { |k| k.is_a? Numeric }.
74
+ map { |k| "#{k}=#{attr[k]}" }.join(",")
75
+ text = [localities, attr["word"]].reject{ |k| k.nil? || k.empty? }.
76
+ join(",")
77
+ out = Asciidoctor::Inline.new(parent, :quoted, text).convert
78
+ %{<concept key="#{attr['id']}" term="#{attr['term']}">#{out}</concept>}
79
+ end
80
+ end
81
+ end
82
+ end