metanorma-ietf 1.0.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 (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.hound.yml +3 -0
  4. data/.oss-guides.rubocop.yml +1077 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.ribose.yml +65 -0
  7. data/.rubocop.tb.yml +650 -0
  8. data/.rubocop.yml +15 -0
  9. data/.travis.yml +23 -0
  10. data/CODE_OF_CONDUCT.md +74 -0
  11. data/Gemfile +4 -0
  12. data/Guardfile +22 -0
  13. data/LICENSE +25 -0
  14. data/README.adoc +1660 -0
  15. data/Rakefile +6 -0
  16. data/bin/asciidoctor-rfc2 +14 -0
  17. data/bin/asciidoctor-rfc3 +14 -0
  18. data/bin/console +14 -0
  19. data/bin/rspec +17 -0
  20. data/bin/setup +8 -0
  21. data/docs/installation.md +21 -0
  22. data/docs/navigation.md +10 -0
  23. data/docs/overview.md +5 -0
  24. data/lib/asciidoctor/rfc.rb +8 -0
  25. data/lib/asciidoctor/rfc/common/base.rb +531 -0
  26. data/lib/asciidoctor/rfc/common/front.rb +120 -0
  27. data/lib/asciidoctor/rfc/v2/base.rb +379 -0
  28. data/lib/asciidoctor/rfc/v2/blocks.rb +261 -0
  29. data/lib/asciidoctor/rfc/v2/converter.rb +60 -0
  30. data/lib/asciidoctor/rfc/v2/front.rb +69 -0
  31. data/lib/asciidoctor/rfc/v2/inline_anchor.rb +111 -0
  32. data/lib/asciidoctor/rfc/v2/lists.rb +135 -0
  33. data/lib/asciidoctor/rfc/v2/table.rb +114 -0
  34. data/lib/asciidoctor/rfc/v2/validate.rb +32 -0
  35. data/lib/asciidoctor/rfc/v2/validate2.rng +716 -0
  36. data/lib/asciidoctor/rfc/v3/base.rb +329 -0
  37. data/lib/asciidoctor/rfc/v3/blocks.rb +246 -0
  38. data/lib/asciidoctor/rfc/v3/converter.rb +62 -0
  39. data/lib/asciidoctor/rfc/v3/front.rb +122 -0
  40. data/lib/asciidoctor/rfc/v3/inline_anchor.rb +89 -0
  41. data/lib/asciidoctor/rfc/v3/lists.rb +176 -0
  42. data/lib/asciidoctor/rfc/v3/svg.rng +9081 -0
  43. data/lib/asciidoctor/rfc/v3/table.rb +65 -0
  44. data/lib/asciidoctor/rfc/v3/validate.rb +34 -0
  45. data/lib/asciidoctor/rfc/v3/validate.rng +2143 -0
  46. data/lib/metanorma-ietf.rb +7 -0
  47. data/lib/metanorma/ietf.rb +8 -0
  48. data/lib/metanorma/ietf/processor.rb +89 -0
  49. data/lib/metanorma/ietf/version.rb +5 -0
  50. data/metanorma-ietf.gemspec +51 -0
  51. data/rfc2629-other.ent +61 -0
  52. data/rfc2629-xhtml.ent +165 -0
  53. data/rfc2629.dtd +312 -0
  54. metadata +289 -0
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "metanorma-ietf"
4
+ require "asciidoctor/cli"
5
+
6
+ options = Asciidoctor::Cli::Options.new backend: "rfc2", header_footer: true
7
+ # FIXME This is a really bizarre API. Please make me simpler.
8
+
9
+ exit 0 if options.parse!(ARGV) == 0
10
+
11
+ invoker = Asciidoctor::Cli::Invoker.new options
12
+ GC.start
13
+ invoker.invoke!
14
+ exit invoker.code
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "metanorma-ietf"
4
+ require "asciidoctor/cli"
5
+
6
+ options = Asciidoctor::Cli::Options.new backend: "rfc3", header_footer: true
7
+ # FIXME This is a really bizarre API. Please make me simpler.
8
+
9
+ exit 0 if options.parse!(ARGV) == 0
10
+
11
+ invoker = Asciidoctor::Cli::Invoker.new options
12
+ GC.start
13
+ invoker.invoke!
14
+ exit invoker.code
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "metanorma-ietf"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/rspec ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This file was generated by Bundler.
4
+ #
5
+ # The application 'rspec' is installed as part of a gem, and
6
+ # this file is here to facilitate running it.
7
+ #
8
+
9
+ require "pathname"
10
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path(
11
+ "../../Gemfile", Pathname.new(__FILE__).realpath
12
+ )
13
+
14
+ require "rubygems"
15
+ require "bundler/setup"
16
+
17
+ load Gem.bin_path("rspec-core", "rspec")
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,21 @@
1
+ ---
2
+ title: Installation
3
+ ---
4
+
5
+ Add this line to your application’s Gemfile:
6
+
7
+ ```
8
+ gem "metanorma-ietf"
9
+ ```
10
+
11
+ And then execute:
12
+
13
+ ```
14
+ $ bundle
15
+ ```
16
+
17
+ Or install it yourself as:
18
+
19
+ ```
20
+ $ gem install metanorma-cli
21
+ ```
@@ -0,0 +1,10 @@
1
+ ---
2
+ sections:
3
+ - name: Introduction
4
+ items:
5
+ - overview
6
+ - installation
7
+ - name: Usage
8
+ items:
9
+ - basic
10
+ ---
data/docs/overview.md ADDED
@@ -0,0 +1,5 @@
1
+ ---
2
+ title: Overview
3
+ ---
4
+
5
+ metanorma-ietf lets you write Internet-Drafts and RFCs using the Metanorma publishing toolchain.
@@ -0,0 +1,8 @@
1
+ module Asciidoctor
2
+ module Rfc
3
+
4
+ end
5
+ end
6
+
7
+ require_relative "rfc/v2/converter"
8
+ require_relative "rfc/v3/converter"
@@ -0,0 +1,531 @@
1
+ require "date"
2
+ require "nokogiri"
3
+ require "htmlentities"
4
+ require "json"
5
+ require "pathname"
6
+ require "open-uri"
7
+ require "pp"
8
+ require "set"
9
+
10
+ module Asciidoctor
11
+ module Rfc::Common
12
+ module Base
13
+ def convert(node, transform = nil, opts = {})
14
+ transform ||= node.node_name
15
+ opts.empty? ? (send transform, node) : (send transform, node, opts)
16
+ end
17
+
18
+ def document_ns_attributes(_doc)
19
+ # ' xmlns="http://projectmallard.org/1.0/" xmlns:its="http://www.w3.org/2005/11/its"'
20
+ nil
21
+ end
22
+
23
+ def content(node)
24
+ node.content
25
+ end
26
+
27
+ def skip(node, name = nil)
28
+ warn %(asciidoctor: WARNING (#{current_location(node)}): converter missing for #{name || node.node_name} node in RFC backend)
29
+ nil
30
+ end
31
+
32
+ # Syntax:
33
+ # = Title
34
+ # Author
35
+ # :HEADER
36
+ #
37
+ # ABSTRACT
38
+ #
39
+ # NOTE: note
40
+ #
41
+ # @note (boilerplate is ignored)
42
+ def preamble(node)
43
+ result = []
44
+
45
+ # NOTE: *list is V3, verse is V2, paragraph is both
46
+ abstractable_contexts = %i{paragraph dlist olist ulist verse open}
47
+
48
+ abstract_blocks = node.blocks.take_while do |block|
49
+ abstractable_contexts.include? block.context
50
+ end
51
+
52
+ remainder_blocks = node.blocks[abstract_blocks.length..-1]
53
+
54
+ result << noko do |xml|
55
+ if abstract_blocks.any?
56
+ xml.abstract do |xml_abstract|
57
+ xml_abstract << abstract_blocks.map(&:render).flatten.join("\n")
58
+ end
59
+ end
60
+ xml << remainder_blocks.map(&:render).flatten.join("\n")
61
+ end
62
+
63
+ result << "</front><middle>"
64
+ result
65
+ end
66
+
67
+ IETF_AREAS = ["art", "Applications and Real-Time",
68
+ "gen", "General",
69
+ "int", "Internet",
70
+ "ops", "Operations and Management",
71
+ "rtg", "Routing",
72
+ "sec", "Security",
73
+ "tsv", "Transport"].freeze
74
+
75
+ # Syntax:
76
+ # = Title
77
+ # Author
78
+ # :area x, y
79
+ def area(node, xml)
80
+ node.attr("area")&.split(/, ?/)&.each do |ar|
81
+ if ar =~ / Area$/i
82
+ warn %(asciidoctor: WARNING (#{current_location(node)}): stripping suffix "Area" from area #{ar})
83
+ ar = ar.gsub(/ Area$/i, "")
84
+ end
85
+ warn %(asciidoctor: WARNING (#{current_location(node)}): unrecognised area #{ar}) unless IETF_AREAS.include?(ar)
86
+ xml.area { |a| a << ar }
87
+ end
88
+ end
89
+
90
+ # Syntax:
91
+ # = Title
92
+ # Author
93
+ # :workgroup x, y
94
+ def workgroup(node, xml)
95
+ workgroups = cache_workgroup(node)
96
+ node.attr("workgroup")&.split(/, ?/)&.each do |wg|
97
+ if wg =~ / (Working Group)$/i
98
+ warn %(asciidoctor: WARNING (#{current_location(node)}): suffix "Working Group" will be stripped in published RFC from #{wg})
99
+ wg_norm = wg.gsub(/ Working Group$/i, "")
100
+ end
101
+ if wg =~ / (Research Group)$/i
102
+ warn %(asciidoctor: WARNING (#{current_location(node)}): suffix "Research Group" will be stripped from working group #{wg})
103
+ wg_norm = wg.gsub(/ Research Group$/i, "")
104
+ end
105
+ warn %(asciidoctor: WARNING (#{current_location(node)}): unrecognised working group #{wg}) unless workgroups.include?(wg_norm)
106
+ xml.workgroup { |w| w << wg }
107
+ end
108
+ end
109
+
110
+ # Syntax:
111
+ # = Title
112
+ # Author
113
+ # :keyword x, y
114
+ def keyword(node, xml)
115
+ node.attr("keyword")&.split(/, ?/)&.each do |kw|
116
+ xml.keyword { |k| k << kw }
117
+ end
118
+ end
119
+
120
+ def paragraph1(node)
121
+ result = []
122
+ result1 = node.content
123
+ if result1 =~ /^(<t>|<dl>|<ol>|<ul>)/
124
+ result = result1
125
+ else
126
+ t_attributes = {
127
+ anchor: node.id,
128
+ }
129
+ result << noko { |xml| xml.t result1, **attr_code(t_attributes) }
130
+ end
131
+ result
132
+ end
133
+
134
+ def inline_indexterm(node)
135
+ # supports only primary and secondary terms
136
+ # primary attribute (highlighted major entry) not supported
137
+ if node.type == :visible
138
+ iref_attributes = {
139
+ item: node.text,
140
+ }
141
+ node.text + noko { |xml| xml.iref **attr_code(iref_attributes) }.join
142
+ else
143
+ terms = node.attr "terms"
144
+ warn %(asciidoctor: WARNING (#{current_location(node)}): only primary and secondary index terms supported: #{terms.join(': ')}) if terms.size > 2
145
+ iref_attributes = {
146
+ item: terms[0],
147
+ subitem: (terms.size > 1 ? terms[1] : nil),
148
+ }
149
+ noko { |xml| xml.iref **attr_code(iref_attributes) }.join
150
+ end
151
+ end
152
+
153
+ # ulist repurposed as reference list
154
+ def reflist(node)
155
+ # ++++
156
+ # <xml>
157
+ # ++++
158
+ result = []
159
+ if node.context == :pass
160
+ node.lines.each do |item|
161
+ # undo XML substitution
162
+ ref = item.gsub(/\&lt;/, "<").gsub(/\&gt;/, ">")
163
+ result << ref
164
+ end
165
+ else
166
+ warn %(asciidoctor: WARNING (#{current_location(node)}): references are not raw XML: #{node.context})
167
+ end
168
+ result
169
+ end
170
+
171
+ def open(node)
172
+ # open block is a container of multiple blocks, treated as a single block.
173
+ # We append each contained block to its parent
174
+ result = []
175
+ if node.role == "comment"
176
+ return noko do |xml|
177
+ xml.comment " " + [flatten_rawtext(node).map { |x| [x, ""] } ].flatten.join("\n") + " "
178
+ end
179
+ end
180
+
181
+ if node.blocks?
182
+ node.blocks.each do |b|
183
+ result << send(b.context, b)
184
+ end
185
+ else
186
+ result = paragraph(node)
187
+ end
188
+ result
189
+ end
190
+
191
+ # def dash(camel_cased_word)
192
+ # camel_cased_word.gsub(/([a-z])([A-Z])/, '\1-\2').downcase
193
+ # end
194
+
195
+ def common_rfc_pis(node)
196
+ # Below are generally applicable Processing Instructions (PIs)
197
+ # that most I-Ds might want to use, common to v2 and v3.
198
+ # These are set only if explicitly specified, with the exception
199
+ # of compact and subcompact
200
+ rfc_pis = {
201
+ artworkdelimiter: node.attr("artworkdelimiter"),
202
+ artworklines: node.attr("artworklines"),
203
+ authorship: node.attr("authorship"),
204
+ autobreaks: node.attr("autobreaks"),
205
+ background: node.attr("background"),
206
+ colonspace: node.attr("colonspace"),
207
+ comments: node.attr("comments"),
208
+ docmapping: node.attr("docmapping"),
209
+ editing: node.attr("editing"),
210
+ emoticonic: node.attr("emoticonic"),
211
+ footer: node.attr("footer"),
212
+ header: node.attr("header"),
213
+ inline: node.attr("inline"),
214
+ iprnotified: node.attr("iprnotified"),
215
+ linkmailto: node.attr("linkmailto"),
216
+ linefile: node.attr("linefile"),
217
+ notedraftinprogress: node.attr("notedraftinprogress"),
218
+ private: node.attr("private"),
219
+ refparent: node.attr("refparent"),
220
+ rfcedstyle: node.attr("rfcedstyle"),
221
+ slides: node.attr("slides"),
222
+ "text-list-symbols": node.attr("text-list-symbols"),
223
+ tocappendix: node.attr("tocappendix"),
224
+ tocindent: node.attr("tocindent"),
225
+ tocnarrow: node.attr("tocnarrow"),
226
+ tocompact: node.attr("tocompact"),
227
+ topblock: node.attr("topblock"),
228
+ useobject: node.attr("useobject"),
229
+
230
+ # give errors regarding ID-nits and DTD validation
231
+ strict: node.attr("strict") || "yes",
232
+
233
+ # Vertical whitespace control
234
+ # (using these PIs as follows is recommended by the RFC Editor)
235
+
236
+ # do not start each main section on a new page
237
+ compact: node.attr("compact") || "yes",
238
+ # keep one blank line between list items
239
+ subcompact: node.attr("subcompact") || "no",
240
+
241
+ # TOC control
242
+ # generate a ToC
243
+ toc: node.attr("toc-include") == "false" ? "no" : "yes",
244
+
245
+ # the number of levels of subsections in ToC. default: 3
246
+ tocdepth: node.attr("toc-depth") || "4",
247
+
248
+ # use anchors rather than numbers for references
249
+ symrefs: node.attr("sym-refs") || "yes",
250
+ # sort references
251
+ sortrefs: node.attr("sort-refs") || "yes",
252
+ }
253
+
254
+ attr_code(rfc_pis)
255
+ end
256
+
257
+ def set_pis(node, doc)
258
+ # Below are generally applicable Processing Instructions (PIs)
259
+ # that most I-Ds might want to use. (Here they are set differently than
260
+ # their defaults in xml2rfc v1.32)
261
+
262
+ if node.attr("rfc2629xslt") != "false"
263
+ pi = Nokogiri::XML::ProcessingInstruction.new(doc, "xml-stylesheet",
264
+ 'type="text/xsl" href="rfc2629.xslt"')
265
+ doc.root.add_previous_sibling(pi)
266
+ end
267
+
268
+ doc.create_internal_subset("rfc", nil, "rfc2629.dtd")
269
+ rfc_pis = common_rfc_pis(node)
270
+ rfc_pis.each_pair do |k, v|
271
+ pi = Nokogiri::XML::ProcessingInstruction.new(doc,
272
+ "rfc",
273
+ "#{k}=\"#{v}\"")
274
+ doc.root.add_previous_sibling(pi)
275
+ end
276
+
277
+ doc
278
+ end
279
+
280
+ # extract references which can be expressed as externally defined entities
281
+ def extract_entities(node, xmldoc)
282
+ refs = xmldoc.xpath("//reference")
283
+ ret = []
284
+ biblio = cache_biblio(node)
285
+ refs.each do |ref|
286
+ next if ref.parent.name == "referencegroup"
287
+ id = ref.at('.//seriesInfo[@name="Internet-Draft"]')
288
+ anchor = ref["anchor"]
289
+ url = if id.nil?
290
+ biblio[anchor]
291
+ else
292
+ biblio["I-D.#{id['value']}"] # the specific version reference
293
+ end
294
+ if biblio.has_key? anchor
295
+ ret << { entity: anchor,
296
+ node: ref,
297
+ url: url }
298
+ end
299
+ end
300
+ ret
301
+ end
302
+
303
+ # if node contains blocks, flatten them into a single line
304
+ def flatten(node)
305
+ result = []
306
+ result << node.text if node.respond_to?(:text)
307
+ if node.blocks?
308
+ node.blocks.each { |b| result << flatten(b) }
309
+ else
310
+ result << node.content
311
+ end
312
+ result.reject(&:empty?)
313
+ end
314
+
315
+ # if node contains blocks, flatten them into a single line; and extract only raw text
316
+ def flatten_rawtext(node)
317
+ result = []
318
+ if node.respond_to?(:blocks) && node.blocks?
319
+ node.blocks.each { |b| result << flatten_rawtext(b) }
320
+ elsif node.respond_to?(:lines)
321
+ node.lines.each do |x|
322
+ result << if node.respond_to?(:context) && (node.context == :literal || node.context == :listing)
323
+ x.gsub(/</, "&lt;").gsub(/>/, "&gt;")
324
+ else
325
+ # strip not only HTML tags <tag>, but also Asciidoc crossreferences <<xref>>
326
+ x.gsub(/<[^>]*>+/, "")
327
+ end
328
+ end
329
+ elsif node.respond_to?(:text)
330
+ result << node.text.gsub(/<[^>]*>+/, "")
331
+ else
332
+ result << node.content.gsub(/<[^>]*>+/, "")
333
+ end
334
+ result.reject(&:empty?)
335
+ end
336
+
337
+ # block for processing XML document fragments as XHTML, to allow for HTMLentities
338
+ def noko(&block)
339
+ # fragment = ::Nokogiri::XML::DocumentFragment.parse("")
340
+ # fragment.doc.create_internal_subset("xml", nil, "xhtml.dtd")
341
+ head = <<HERE
342
+ <!DOCTYPE html SYSTEM
343
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
344
+ <html xmlns="http://www.w3.org/1999/xhtml">
345
+ <head>
346
+ <title></title>
347
+ <meta charset="UTF-8" />
348
+ </head>
349
+ <body>
350
+ </body>
351
+ </html>
352
+ HERE
353
+ doc = ::Nokogiri::XML.parse(head)
354
+ fragment = doc.fragment("")
355
+ ::Nokogiri::XML::Builder.with fragment, &block
356
+ fragment.to_xml(encoding: "US-ASCII").lines.map { |l| l.gsub(/\s*\n/, "") }
357
+ end
358
+
359
+ def attr_code(attributes)
360
+ attributes = attributes.reject { |_, val| val.nil? }.map
361
+ attributes.map do |k, v|
362
+ [k, (v.is_a? String) ? HTMLEntities.new.decode(v) : v]
363
+ end.to_h
364
+ end
365
+
366
+ def current_location(node)
367
+ return "Line #{node.lineno}" if node.respond_to?(:lineno) && !node.lineno.nil? && !node.lineno.empty?
368
+ return "ID #{node.id}" if node.respond_to?(:id) && !node.id.nil?
369
+ while !node.nil? && (!node.respond_to?(:level) || node.level > 0) && node.context != :section
370
+ node = node.parent
371
+ return "Section: #{node.title}" if !node.nil? && node.context == :section
372
+ end
373
+ "??"
374
+ end
375
+
376
+ def cache_workgroup(node)
377
+ wgcache_name = "#{Dir.home}/.asciidoc-rfc-workgroup-cache.json"
378
+ # If we are required to, clear the wg cache
379
+ if node.attr("flush-caches") == "true"
380
+ system("rm -f #{wgcache_name}")
381
+ end
382
+ # Is there already a wg cache? If not, create it.
383
+ wg = []
384
+ if Pathname.new(wgcache_name).file?
385
+ File.open(wgcache_name, "r") do |f|
386
+ wg = JSON.parse(f.read)
387
+ end
388
+ else
389
+ File.open(wgcache_name, "w") do |b|
390
+ STDERR.puts "Reading workgroups from https://tools.ietf.org/wg/..."
391
+ Kernel.open("https://tools.ietf.org/wg/") do |f|
392
+ f.each_line do |line|
393
+ line.scan(%r{<td width="50%" style='padding: 0 1ex'>([^<]+)</td>}) do |w|
394
+ wg << w[0].gsub(/\s+$/, "").gsub(/ Working Group$/, "")
395
+ end
396
+ end
397
+ end
398
+ STDERR.puts "Reading workgroups from https://irtf.org/groups..."
399
+ Kernel.open("https://irtf.org/groups", ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE) do |f|
400
+ f.each_line do |line|
401
+ line.scan(%r{<a title="([^"]+) Research Group"[^>]+>([^<]+)<}) do |w|
402
+ wg << w[0].gsub(/\s+$/, "")
403
+ wg << w[1].gsub(/\s+$/, "") # abbrev
404
+ end
405
+ end
406
+ end
407
+ b << wg.to_json
408
+ end
409
+ end
410
+ wg
411
+ end
412
+
413
+ def cache_biblio(node)
414
+ bibliocache_name = "#{Dir.home}/.asciidoc-rfc-biblio-cache.json"
415
+ # If we are required to, clear the biblio cache
416
+ if node.attr("flush-caches") == "true"
417
+ system("rm -f #{bibliocache_name}")
418
+ end
419
+ # Is there already a biblio cache? If not, create it.
420
+ biblio = {}
421
+ if Pathname.new(bibliocache_name).file?
422
+ File.open(bibliocache_name, "r") do |f|
423
+ biblio = JSON.parse(f.read)
424
+ end
425
+ else
426
+ File.open(bibliocache_name, "w") do |b|
427
+ STDERR.puts "Reading references from https://xml2rfc.tools.ietf.org/public/rfc/bibxml/..."
428
+ Kernel.open("https://xml2rfc.tools.ietf.org/public/rfc/bibxml/") do |f|
429
+ # I'm just working off the ls output
430
+ f.each_line do |line|
431
+ line.scan(/a href="reference.RFC.(\d+).xml">/) do |w|
432
+ biblio["RFC#{w[0]}"] = "https://xml2rfc.tools.ietf.org/public/rfc/bibxml/reference.RFC.#{w[0]}.xml"
433
+ end
434
+ end
435
+ ["https://xml2rfc.tools.ietf.org/public/rfc/bibxml2/",
436
+ "https://xml2rfc.tools.ietf.org/public/rfc/bibxml3/",
437
+ "https://xml2rfc.tools.ietf.org/public/rfc/bibxml4/",
438
+ "https://xml2rfc.tools.ietf.org/public/rfc/bibxml5/"].each do |url|
439
+ STDERR.puts "Reading references from #{url}..."
440
+ Kernel.open(url) do |f1|
441
+ f1.each_line do |line|
442
+ line.scan(/a href="reference.(\S+).xml">/) do |w|
443
+ biblio[w[0]] = "#{url}/reference.#{w[0]}.xml"
444
+ end
445
+ end
446
+ end
447
+ end
448
+ end
449
+ b << biblio.to_json
450
+ end
451
+ end
452
+ biblio
453
+ end
454
+
455
+ # insert bibliography based on anchors, references directory, and list of normatives in doc attribute
456
+ def insert_biblio(node, xmldoc)
457
+ # we want no references in this document, so we can ignore any anchors of references
458
+ xmldoc.xpath("//referencegroup | //reference").each(&:remove)
459
+ refs = Set.new
460
+ xmldoc.xpath("//xref | //relref").each { |r| refs << r["target"] }
461
+ anchors1 = Set.new
462
+ # we have no references in this document, so any remaining anchors are internal cross-refs only
463
+ xmldoc.xpath("//@anchor").each { |r| anchors1 << r.value }
464
+ refs = refs - anchors1
465
+ anchors = {}
466
+
467
+ norm_refs_spec = Set.new(node.attr("normative").split(/,[ ]?/))
468
+ anchors[:norm] = refs.intersection(norm_refs_spec)
469
+ anchors[:info] = refs - anchors[:norm]
470
+ seen_refs = { norm: Set.new, info: Set.new }
471
+ refxml_in = { norm: {}, info: {} }
472
+ refxml_out = { norm: [], info: [] }
473
+
474
+ bibliodir = node.attr("biblio-dir")
475
+ Dir.foreach bibliodir do |f|
476
+ next if [".", ".."].include? f
477
+ text = File.read("#{bibliodir}/#{f}", encoding: "utf-8")
478
+ next unless text =~ /<reference/
479
+ text =~ /<reference[^>]*anchor=['"]([^'"]*)/
480
+ anchor = Regexp.last_match(1)
481
+ next if anchor.nil? || anchor.empty?
482
+ if anchors[:norm].include?(anchor)
483
+ refxml_in[:norm][anchor] = text
484
+ seen_refs[:norm] << anchor
485
+ else
486
+ refxml_in[:info][anchor] = text
487
+ seen_refs[:info] << anchor
488
+ end
489
+ end
490
+
491
+ biblio = cache_biblio(node)
492
+ [:norm, :info].each do |reftype|
493
+ anchors[reftype].each do |r|
494
+ if refxml_in[reftype].has_key?(r)
495
+ # priority to on-disk references over skeleton references: they may contain draft information
496
+ refxml_out[reftype] << refxml_in[reftype][r]
497
+ elsif biblio.has_key?(r)
498
+ refxml_out[reftype] << %{<reference anchor="#{r}"/>}
499
+ else
500
+ warn "Reference #{r} has not been includes in references directory, and is not a recognised external RFC reference"
501
+ end
502
+ end
503
+ end
504
+
505
+ xml_location = xmldoc.at('//references[@title="Normative References" or name="Normative References"]')
506
+ xml_location&.children = Nokogiri::XML.fragment(refxml_out[:norm].join)
507
+ xml_location = xmldoc.at('//references[@title="Informative References" or name="Informative References"]')
508
+ xml_location&.children = Nokogiri::XML.fragment(refxml_out[:info].join)
509
+ xmldoc
510
+ end
511
+
512
+ def smart_quote_cleanup(xmldoc)
513
+ # smart quotes: handle smart apostrophe
514
+ xmldoc.traverse do |node|
515
+ if node.text?
516
+ node.content = node.content.tr("\u2019", "'")
517
+ node.content = node.content.gsub(/\&#8217;/, "'")
518
+ node.content = node.content.gsub(/\&#x2019;/, "'")
519
+ elsif node.element?
520
+ node.attributes.each do |k, v|
521
+ node.set_attribute(k, v.content.tr("\u2019", "'"))
522
+ node.set_attribute(k, v.content.gsub(/\&#8217;/, "'"))
523
+ node.set_attribute(k, v.content.gsub(/\&#x2019;/, "'"))
524
+ end
525
+ end
526
+ end
527
+ xmldoc
528
+ end
529
+ end
530
+ end
531
+ end