relaton 3.0.7 → 3.1.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 (240) hide show
  1. checksums.yaml +4 -4
  2. data/bin/relaton +20 -0
  3. data/lib/relaton/3gpp/processor.rb +1 -1
  4. data/lib/relaton/3gpp.rb +3 -3
  5. data/lib/relaton/adobe/processor.rb +1 -1
  6. data/lib/relaton/adobe.rb +3 -3
  7. data/lib/relaton/bib/item_data.rb +2 -2
  8. data/lib/relaton/bib/model/docidentifier.rb +17 -2
  9. data/lib/relaton/bib.rb +2 -2
  10. data/lib/relaton/bibcollection.rb +146 -0
  11. data/lib/relaton/bibdata.rb +92 -0
  12. data/lib/relaton/bipm/data_outcomes_parser.rb +6 -1
  13. data/lib/relaton/bipm/processor.rb +1 -1
  14. data/lib/relaton/bipm.rb +3 -3
  15. data/lib/relaton/bsi/processor.rb +1 -1
  16. data/lib/relaton/bsi.rb +1 -1
  17. data/lib/relaton/calconnect/data_fetcher.rb +2 -2
  18. data/lib/relaton/calconnect/model/item.rb +1 -1
  19. data/lib/relaton/calconnect/processor.rb +1 -1
  20. data/lib/relaton/calconnect.rb +2 -2
  21. data/lib/relaton/ccsds/data/fetcher.rb +1 -1
  22. data/lib/relaton/ccsds/model/item.rb +1 -1
  23. data/lib/relaton/ccsds/processor.rb +1 -1
  24. data/lib/relaton/ccsds/util.rb +1 -1
  25. data/lib/relaton/ccsds.rb +1 -1
  26. data/lib/relaton/cen/processor.rb +1 -1
  27. data/lib/relaton/cen.rb +2 -2
  28. data/lib/relaton/cie/data_fetcher.rb +3 -3
  29. data/lib/relaton/cie/processor.rb +1 -1
  30. data/lib/relaton/cie.rb +2 -2
  31. data/lib/relaton/cli/base_convertor.rb +134 -0
  32. data/lib/relaton/cli/command.rb +396 -0
  33. data/lib/relaton/cli/data_fetcher.rb +20 -0
  34. data/lib/relaton/cli/frontend/dist/404.html +18 -0
  35. data/lib/relaton/cli/frontend/dist/app.iife.js +1 -0
  36. data/lib/relaton/cli/frontend/dist/style.css +3 -0
  37. data/lib/relaton/cli/frontend/index.html +14 -0
  38. data/lib/relaton/cli/frontend/package-lock.json +2945 -0
  39. data/lib/relaton/cli/frontend/package.json +29 -0
  40. data/lib/relaton/cli/frontend/public/404.html +18 -0
  41. data/lib/relaton/cli/frontend/src/App.test.ts +501 -0
  42. data/lib/relaton/cli/frontend/src/App.vue +506 -0
  43. data/lib/relaton/cli/frontend/src/app.ts +90 -0
  44. data/lib/relaton/cli/frontend/src/components/DocumentDetail.test.ts +142 -0
  45. data/lib/relaton/cli/frontend/src/components/DocumentDetail.vue +228 -0
  46. data/lib/relaton/cli/frontend/src/components/DocumentRow.vue +88 -0
  47. data/lib/relaton/cli/frontend/src/components/Icon.test.ts +44 -0
  48. data/lib/relaton/cli/frontend/src/components/Icon.vue +55 -0
  49. data/lib/relaton/cli/frontend/src/components/Pagination.vue +26 -0
  50. data/lib/relaton/cli/frontend/src/dev/main.ts +100 -0
  51. data/lib/relaton/cli/frontend/src/dev/sample-data.json +71 -0
  52. data/lib/relaton/cli/frontend/src/lib/filter.test.ts +88 -0
  53. data/lib/relaton/cli/frontend/src/lib/filter.ts +95 -0
  54. data/lib/relaton/cli/frontend/src/lib/hydrate.test.ts +102 -0
  55. data/lib/relaton/cli/frontend/src/lib/hydrate.ts +55 -0
  56. data/lib/relaton/cli/frontend/src/lib/shards.test.ts +319 -0
  57. data/lib/relaton/cli/frontend/src/lib/shards.ts +183 -0
  58. data/lib/relaton/cli/frontend/src/lib/types.ts +148 -0
  59. data/lib/relaton/cli/frontend/src/lib/url.test.ts +151 -0
  60. data/lib/relaton/cli/frontend/src/lib/url.ts +111 -0
  61. data/lib/relaton/cli/frontend/src/shims-vue.d.ts +5 -0
  62. data/lib/relaton/cli/frontend/src/styles/index.css +27 -0
  63. data/lib/relaton/cli/frontend/src/test/fetch.ts +58 -0
  64. data/lib/relaton/cli/frontend/tsconfig.json +20 -0
  65. data/lib/relaton/cli/frontend/vite.config.ts +42 -0
  66. data/lib/relaton/cli/frontend/vitest.config.ts +11 -0
  67. data/lib/relaton/cli/frontend_assets.rb +58 -0
  68. data/lib/relaton/cli/full_text_search.rb +98 -0
  69. data/lib/relaton/cli/i18n.yaml +1 -0
  70. data/lib/relaton/cli/index_item_normalizer.rb +380 -0
  71. data/lib/relaton/cli/index_site_generator.rb +1043 -0
  72. data/lib/relaton/cli/relaton_file.rb +290 -0
  73. data/lib/relaton/cli/subcommand_collection.rb +263 -0
  74. data/lib/relaton/cli/subcommand_db.rb +81 -0
  75. data/lib/relaton/cli/templates/_document.liquid +111 -0
  76. data/lib/relaton/cli/templates/_index.liquid +61 -0
  77. data/lib/relaton/cli/templates/index/page.liquid +20 -0
  78. data/lib/relaton/cli/templates/index-style.css +1017 -0
  79. data/lib/relaton/cli/util.rb +8 -0
  80. data/lib/relaton/cli/xml_convertor.rb +47 -0
  81. data/lib/relaton/cli/xml_to_html_renderer.rb +117 -0
  82. data/lib/relaton/cli/yaml_convertor.rb +102 -0
  83. data/lib/relaton/cli.rb +103 -0
  84. data/lib/relaton/core/processor.rb +1 -1
  85. data/lib/relaton/core.rb +1 -1
  86. data/lib/relaton/db/registry.rb +1 -1
  87. data/lib/relaton/db.rb +1 -1
  88. data/lib/relaton/doi/CLAUDE.md +1 -1
  89. data/lib/relaton/doi/parser.rb +2 -1
  90. data/lib/relaton/doi/processor.rb +1 -1
  91. data/lib/relaton/doi.rb +4 -4
  92. data/lib/relaton/easc/processor.rb +1 -1
  93. data/lib/relaton/easc.rb +3 -3
  94. data/lib/relaton/ecma/data_fetcher.rb +1 -1
  95. data/lib/relaton/ecma/processor.rb +1 -1
  96. data/lib/relaton/ecma.rb +2 -2
  97. data/lib/relaton/element_finder.rb +29 -0
  98. data/lib/relaton/etsi/data_fetcher.rb +2 -2
  99. data/lib/relaton/etsi/item.rb +1 -1
  100. data/lib/relaton/etsi/processor.rb +2 -2
  101. data/lib/relaton/etsi.rb +1 -1
  102. data/lib/relaton/evs/processor.rb +1 -1
  103. data/lib/relaton/evs.rb +2 -2
  104. data/lib/relaton/gb/CLAUDE.md +1 -1
  105. data/lib/relaton/gb/bibliography.rb +1 -1
  106. data/lib/relaton/gb/processor.rb +1 -1
  107. data/lib/relaton/gb.rb +2 -2
  108. data/lib/relaton/gost/processor.rb +1 -1
  109. data/lib/relaton/gost.rb +3 -3
  110. data/lib/relaton/iala/processor.rb +1 -1
  111. data/lib/relaton/iala.rb +3 -3
  112. data/lib/relaton/iana/data_fetcher.rb +1 -1
  113. data/lib/relaton/iana/processor.rb +1 -1
  114. data/lib/relaton/iana.rb +2 -2
  115. data/lib/relaton/iec/data_fetcher.rb +1 -1
  116. data/lib/relaton/iec/processor.rb +1 -1
  117. data/lib/relaton/iec.rb +2 -2
  118. data/lib/relaton/ieee/processor.rb +1 -1
  119. data/lib/relaton/ieee.rb +2 -2
  120. data/lib/relaton/ietf/CLAUDE.md +1 -1
  121. data/lib/relaton/ietf/data_fetcher.rb +1 -1
  122. data/lib/relaton/ietf/processor.rb +1 -1
  123. data/lib/relaton/ietf.rb +2 -2
  124. data/lib/relaton/iho/CLAUDE.md +3 -3
  125. data/lib/relaton/iho/bibliography.rb +41 -27
  126. data/lib/relaton/iho/hash_parser_v1.rb +1 -1
  127. data/lib/relaton/iho/processor.rb +4 -3
  128. data/lib/relaton/iho.rb +2 -2
  129. data/lib/relaton/index.rb +1 -1
  130. data/lib/relaton/isbn/processor.rb +1 -1
  131. data/lib/relaton/isbn.rb +1 -1
  132. data/lib/relaton/iso/hash_parser_v1.rb +1 -1
  133. data/lib/relaton/iso/processor.rb +1 -1
  134. data/lib/relaton/iso.rb +3 -3
  135. data/lib/relaton/itu/processor.rb +1 -1
  136. data/lib/relaton/itu.rb +3 -3
  137. data/lib/relaton/jcgm/processor.rb +1 -1
  138. data/lib/relaton/jcgm.rb +2 -2
  139. data/lib/relaton/jis/processor.rb +1 -1
  140. data/lib/relaton/jis.rb +2 -2
  141. data/lib/relaton/nist/CLAUDE.md +11 -1
  142. data/lib/relaton/nist/README.adoc +7 -1
  143. data/lib/relaton/nist/hit_collection.rb +25 -1
  144. data/lib/relaton/nist/mods_parser.rb +32 -8
  145. data/lib/relaton/nist/processor.rb +1 -1
  146. data/lib/relaton/nist/pubs_export.rb +69 -11
  147. data/lib/relaton/nist.rb +2 -2
  148. data/lib/relaton/oasis/processor.rb +1 -1
  149. data/lib/relaton/oasis.rb +2 -2
  150. data/lib/relaton/ogc/CLAUDE.md +1 -1
  151. data/lib/relaton/ogc/processor.rb +1 -1
  152. data/lib/relaton/ogc.rb +2 -2
  153. data/lib/relaton/oiml/processor.rb +1 -1
  154. data/lib/relaton/oiml.rb +2 -2
  155. data/lib/relaton/omg/CLAUDE.md +1 -1
  156. data/lib/relaton/omg/processor.rb +1 -1
  157. data/lib/relaton/omg.rb +1 -1
  158. data/lib/relaton/plateau/CLAUDE.md +1 -1
  159. data/lib/relaton/plateau/data_fetcher.rb +1 -1
  160. data/lib/relaton/plateau/processor.rb +1 -1
  161. data/lib/relaton/plateau.rb +2 -2
  162. data/lib/relaton/render/general.rb +319 -0
  163. data/lib/relaton/render/iso690/csl_import.rb +129 -0
  164. data/lib/relaton/render/iso690/element.rb +153 -0
  165. data/lib/relaton/render/iso690/elements/access.rb +44 -0
  166. data/lib/relaton/render/iso690/elements/authorizer.rb +45 -0
  167. data/lib/relaton/render/iso690/elements/citeid.rb +39 -0
  168. data/lib/relaton/render/iso690/elements/component_part.rb +97 -0
  169. data/lib/relaton/render/iso690/elements/creator.rb +267 -0
  170. data/lib/relaton/render/iso690/elements/edition.rb +97 -0
  171. data/lib/relaton/render/iso690/elements/extent.rb +71 -0
  172. data/lib/relaton/render/iso690/elements/identifier.rb +46 -0
  173. data/lib/relaton/render/iso690/elements/location.rb +73 -0
  174. data/lib/relaton/render/iso690/elements/medium.rb +32 -0
  175. data/lib/relaton/render/iso690/elements/numeration.rb +26 -0
  176. data/lib/relaton/render/iso690/elements/production.rb +111 -0
  177. data/lib/relaton/render/iso690/elements/pub_date.rb +63 -0
  178. data/lib/relaton/render/iso690/elements/series.rb +66 -0
  179. data/lib/relaton/render/iso690/elements/size.rb +45 -0
  180. data/lib/relaton/render/iso690/elements/status.rb +30 -0
  181. data/lib/relaton/render/iso690/elements/stddoc.rb +40 -0
  182. data/lib/relaton/render/iso690/elements/title.rb +56 -0
  183. data/lib/relaton/render/iso690/elements/updated.rb +38 -0
  184. data/lib/relaton/render/iso690/elements.rb +67 -0
  185. data/lib/relaton/render/iso690/fields.rb +142 -0
  186. data/lib/relaton/render/iso690/i18n/en.yml +48 -0
  187. data/lib/relaton/render/iso690/i18n/fr.yml +14 -0
  188. data/lib/relaton/render/iso690/i18n/ja.yml +38 -0
  189. data/lib/relaton/render/iso690/i18n.rb +83 -0
  190. data/lib/relaton/render/iso690/identifier_kinds.rb +29 -0
  191. data/lib/relaton/render/iso690/index_renderer.rb +57 -0
  192. data/lib/relaton/render/iso690/kinds.rb +53 -0
  193. data/lib/relaton/render/iso690/renderer.rb +150 -0
  194. data/lib/relaton/render/iso690/rules/bipm.rb +186 -0
  195. data/lib/relaton/render/iso690/rules/jis.rb +103 -0
  196. data/lib/relaton/render/iso690/rules/nist.rb +384 -0
  197. data/lib/relaton/render/iso690/rules.rb +227 -0
  198. data/lib/relaton/render/iso690/style/locale.rb +59 -0
  199. data/lib/relaton/render/iso690/style/name_form.rb +87 -0
  200. data/lib/relaton/render/iso690/style/scheme.rb +33 -0
  201. data/lib/relaton/render/iso690/style/sort_rule.rb +25 -0
  202. data/lib/relaton/render/iso690/style/template_map.rb +40 -0
  203. data/lib/relaton/render/iso690/style/type_template.rb +45 -0
  204. data/lib/relaton/render/iso690/style.rb +227 -0
  205. data/lib/relaton/render/iso690/styles/apa.yml +45 -0
  206. data/lib/relaton/render/iso690/styles/author-date.yml +31 -0
  207. data/lib/relaton/render/iso690/styles/chicago.yml +49 -0
  208. data/lib/relaton/render/iso690/styles/ieee-sa.yml +45 -0
  209. data/lib/relaton/render/iso690/styles/jis-en.yml +68 -0
  210. data/lib/relaton/render/iso690/styles/jis-ja.yml +67 -0
  211. data/lib/relaton/render/iso690/styles/lncs.yml +54 -0
  212. data/lib/relaton/render/iso690/styles/mla.yml +56 -0
  213. data/lib/relaton/render/iso690/template.rb +248 -0
  214. data/lib/relaton/render/iso690.rb +28 -0
  215. data/lib/relaton/sae/CLAUDE.md +89 -0
  216. data/lib/relaton/sae/bibdata.rb +8 -0
  217. data/lib/relaton/sae/bibitem.rb +8 -0
  218. data/lib/relaton/sae/bibliography.rb +128 -0
  219. data/lib/relaton/sae/data_fetcher.rb +219 -0
  220. data/lib/relaton/sae/doi_code.rb +83 -0
  221. data/lib/relaton/sae/item.rb +10 -0
  222. data/lib/relaton/sae/item_data.rb +6 -0
  223. data/lib/relaton/sae/processor.rb +62 -0
  224. data/lib/relaton/sae/util.rb +9 -0
  225. data/lib/relaton/sae.rb +37 -0
  226. data/lib/relaton/un/CLAUDE.md +1 -1
  227. data/lib/relaton/un/processor.rb +1 -1
  228. data/lib/relaton/un.rb +1 -1
  229. data/lib/relaton/version.rb +1 -1
  230. data/lib/relaton/w3c/bibliography.rb +1 -1
  231. data/lib/relaton/w3c/data_fetcher.rb +1 -1
  232. data/lib/relaton/w3c/processor.rb +1 -1
  233. data/lib/relaton/w3c.rb +2 -2
  234. data/lib/relaton/xsf/data_fetcher.rb +1 -1
  235. data/lib/relaton/xsf/processor.rb +1 -1
  236. data/lib/relaton/xsf.rb +2 -2
  237. data/lib/relaton-cli.rb +17 -0
  238. data/lib/relaton-render.rb +12 -0
  239. data/lib/relaton.rb +3 -2
  240. metadata +203 -23
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ module Iso690
6
+ # A citation template as declared in a style instance: literal text
7
+ # interleaved with {{slot}} placeholders resolved against the ISO 690
8
+ # data element inventory. A leading literal attaches to the first
9
+ # slot, and a literal between two slots to the preceding one, so an
10
+ # absent field never orphans punctuation around it. The last rendered
11
+ # slot gives up its trailing literal in favour of the template
12
+ # terminator, appended once when any field rendered and the output
13
+ # does not already end with it.
14
+ class Template
15
+ SLOT = /\{\{\s*([\w-]+)\s*\}\}/.freeze
16
+
17
+ Field = Struct.new(:present?, :text)
18
+
19
+ def initialize(source)
20
+ @chunks = []
21
+ @terminator = ""
22
+ scan(source.to_s)
23
+ end
24
+
25
+ # The short cite renders the reference with the first-biblio
26
+ # marker appended to its first component: isodoc's styled
27
+ # references (eref2linkshort) split there (the 1.x
28
+ # fmt-first-biblio-delim)
29
+ def evaluate_short(fields, delim)
30
+ present = present_indices(fields)
31
+ return "" if present.empty?
32
+
33
+ # The split marker follows the leading run of present slots
34
+ # (the 1.x first SEGMENT: "{{creator}}{{role}}" splits after
35
+ # the role), before the first literal that follows it
36
+ lead_slots = []
37
+ @chunks.each_with_index do |(kind, _value), i|
38
+ break if kind != :slot || !present.include?(i)
39
+
40
+ lead_slots << i
41
+ end
42
+ split_at = lead_slots.last ? lead_slots.last + 1 : nil
43
+
44
+ out = +""
45
+ @chunks.each_with_index do |(kind, value), i|
46
+ text =
47
+ case kind
48
+ when :slot
49
+ field = fields[normalise(value)]
50
+ next unless field&.present?
51
+
52
+ field.text
53
+ else
54
+ next unless bridged?(i, present)
55
+
56
+ value
57
+ end
58
+ if split_at == i
59
+ out << fieldless(lead_slots, fields) if false
60
+ out << delim.to_s
61
+ split_at = nil
62
+ end
63
+ out << text unless text.nil?
64
+ end
65
+ out << delim.to_s if split_at == @chunks.length
66
+ terminate(out).rstrip
67
+ end
68
+
69
+ # 1.x segment-join semantics: present elements joined with the
70
+ # template's separators — a literal renders only when it
71
+ # separates (a run of absent elements between) two present
72
+ # elements; leading and trailing literals drop with the
73
+ # elements they would have attached to
74
+ def evaluate(fields)
75
+ present = present_indices(fields)
76
+ return "" if present.empty?
77
+
78
+ out = +"".dup
79
+ # Parenthetical groups balance on the template's literal
80
+ # parens alone: data text (a series run "(N.S.)", a host
81
+ # creator's closing paren) carries parens of its own that
82
+ # are no part of the template's group structure
83
+ paren_balance = 0
84
+ @chunks.each_with_index do |(kind, value), i|
85
+ if kind == :slot
86
+ field = fields[normalise(value)]
87
+ out << field.text if field&.present?
88
+ elsif value.to_s.match?(/\A[))]/)
89
+ # Symmetric close: with no pending open the close-paren
90
+ # drops, and any trailing punctuation bridges normally
91
+ closes = value.to_s[/\A[))]+/]
92
+ emitted = [paren_balance, closes.size].min
93
+ if emitted.positive?
94
+ out << closes[0, emitted] +
95
+ value.to_s[closes.size..].to_s
96
+ paren_balance -= emitted
97
+ else
98
+ rest = value.to_s.sub(/\A[))]+/, "")
99
+ # the trailing separator of a dropped group bridges as
100
+ # though the group were never there
101
+ if !rest.empty? && present.any? { |j| j > i } &&
102
+ group_gap?(i, present)
103
+ out << rest
104
+ end
105
+ end
106
+ elsif value.to_s.match?(/[((]\z/)
107
+ # A literal opening a parenthetical group ("({{series}})",
108
+ # "。({{series}})") attaches its paren to the slot it
109
+ # opens — the pair drops with an absent element, while
110
+ # any leading punctuation bridges normally
111
+ lead, paren = value.to_s.match(/\A(.*)([((])\z/).captures
112
+ out << lead if bridged_text?(i, lead, present)
113
+ if present.include?(i + 1)
114
+ out << paren
115
+ paren_balance += 1
116
+ end
117
+ elsif bridged?(i, present) ||
118
+ (value.include?("<") && present.any? { |j| j < i })
119
+ # markup attaches to the element it closes
120
+ out << value
121
+ elsif close_pending_paren?(out, value)
122
+ out << value
123
+ end
124
+ end
125
+ terminate(out).rstrip
126
+ end
127
+
128
+ private
129
+
130
+ def scan(source)
131
+ rest = source
132
+ while (slot = SLOT.match(rest))
133
+ @chunks << [:text, rest[0...slot.begin(0)]] if
134
+ slot.begin(0).positive?
135
+ @chunks << [:slot, slot[1]]
136
+ rest = rest[slot.end(0)..]
137
+ end
138
+ @terminator = rest
139
+ end
140
+
141
+ def present_indices(fields)
142
+ @chunks.each_index.select do |i|
143
+ kind, value = @chunks[i]
144
+ next false unless kind == :slot
145
+
146
+ field = fields[normalise(value)]
147
+ !field.nil? && field.present?
148
+ end
149
+ end
150
+
151
+ # The texts between this close and the last present slot are
152
+ # all parenthetical-group parts (each dropped or partial)
153
+ def group_gap?(index, present)
154
+ left = present.select { |i| i < index }.max
155
+ @chunks[(left + 1)...index].all? do |(_, text)|
156
+ text.to_s.match?(/\A[))]|[((]\z/)
157
+ end
158
+ end
159
+
160
+ # The leading punctuation of a paren-opening literal bridges
161
+ # per the segment-join rule (a bare paren has no lead)
162
+ def bridged_text?(index, lead, present)
163
+ return true if lead.empty?
164
+
165
+ first_slot = @chunks.index { |(kind, _)| kind == :slot }
166
+ return present.include?(first_slot) if index < first_slot
167
+
168
+ left = present.select { |i| i < index }.max
169
+ left && present.any? { |i| i > index } &&
170
+ !@chunks[(left + 1)...index].any? do |(kind, _)|
171
+ kind == :text
172
+ end
173
+ end
174
+
175
+ # A literal is the trailing separator of the element that
176
+ # precedes it: it renders when that element renders and a
177
+ # later element renders, and it is the FIRST literal of its
178
+ # gap (the 1.x segment join — absent elements between two
179
+ # present ones leave the single separator that follows the
180
+ # earlier present element; an absent slot directly before a
181
+ # present one, as "{{date}}{{disambiguator}}, {{access}}",
182
+ # does not swallow it). A leading literal (no slot precedes
183
+ # it) attaches to the first slot, emitting only when that
184
+ # slot is present.
185
+ def bridged?(index, present)
186
+ first_slot = @chunks.index { |(kind, _)| kind == :slot }
187
+ return present.include?(first_slot) if index < first_slot
188
+
189
+ left = present.select { |i| i < index }.max
190
+ left && present.any? { |i| i > index } &&
191
+ !@chunks[(left + 1)...index].any? do |(kind, _)|
192
+ kind == :text
193
+ end
194
+ end
195
+
196
+ # A template's paired literal ("({{production}})") closes even
197
+ # when nothing follows the present element it wraps. Fullwidth
198
+ # parens (CJK) pair the same way
199
+ def close_pending_paren?(out, value)
200
+ return false unless value.start_with?(")", "\uFF09")
201
+
202
+ opens = out.count("(") + out.count("\uFF08")
203
+ closes = out.count(")") + out.count("\uFF09")
204
+ opens > closes
205
+ end
206
+
207
+ def terminate(body)
208
+ # 1.x punctuation cleanup: a sentence period before a spaced
209
+ # comma yields to the comma (", 2013" over ". , 2013"; an
210
+ # initials period before a comma ("P.,") stands, being
211
+ # unspaced), and stacked separators around absent elements
212
+ # collapse into one (". : ." into ". "); an empty pair left
213
+ # by an absent element drops
214
+ body = body.gsub(/:\s*\.\s*/, ": ")
215
+ body = body.gsub(/\.\s+,/, ",")
216
+ body = body.gsub(/(?:[.,:;]\s+)+[.,:;]/, ". ")
217
+ body = body.gsub(/\.\s*\./, ". ")
218
+ body = body.gsub(/:\s*\.\s*/, ": ")
219
+ body = body.gsub(/\(\s*\)/, "")
220
+ body = body.gsub(/ +/, " ").gsub(" ,", ",")
221
+ terminator = unbalanced_close(body, @terminator)
222
+ return body if terminator.strip.empty?
223
+ if terminator !~ /[<\w]/ && body.rstrip.end_with?(terminator.strip)
224
+ return body
225
+ end
226
+
227
+ "#{body}#{terminator}"
228
+ end
229
+
230
+ # The terminator's closing parens stand only when the body
231
+ # opened them (their "(" died with an absent element)
232
+ def unbalanced_close(body, terminator)
233
+ return terminator unless terminator.start_with?(")")
234
+
235
+ open = body.count("(") - body.count(")")
236
+ return terminator.sub(/\A\)+/) { |closes| closes[0, open] } if
237
+ open.positive?
238
+
239
+ terminator.sub(/\A\)+/, "")
240
+ end
241
+
242
+ def normalise(slot)
243
+ slot.to_s.delete("_").downcase
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Render
5
+ # Citation rendering per the ISO 690 architecture: a style instance
6
+ # places the inventory's data elements (clause 7) in each resource
7
+ # kind's order (clause 8); the engine is a template evaluator over a
8
+ # field resolver and holds no styles, no vocabularies, no i18n.
9
+ # The v1 compatibility facade (relaton-render#90): isodoc's
10
+ # render-isodoc subclasses Relaton::Render::General.
11
+ autoload :General, "relaton/render/general"
12
+
13
+ module Iso690
14
+ autoload :Element, "relaton/render/iso690/element"
15
+ autoload :Elements, "relaton/render/iso690/elements"
16
+ autoload :Fields, "relaton/render/iso690/fields"
17
+ autoload :I18n, "relaton/render/iso690/i18n"
18
+ autoload :IndexRenderer, "relaton/render/iso690/index_renderer"
19
+ autoload :IdentifierKinds, "relaton/render/iso690/identifier_kinds"
20
+ autoload :Kinds, "relaton/render/iso690/kinds"
21
+ autoload :Renderer, "relaton/render/iso690/renderer"
22
+ autoload :Rules, "relaton/render/iso690/rules"
23
+ autoload :Style, "relaton/render/iso690/style"
24
+ autoload :CslImport, "relaton/render/iso690/csl_import"
25
+ autoload :Template, "relaton/render/iso690/template"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,89 @@
1
+ # CLAUDE.md — SAE flavor
2
+
3
+ `Relaton::Sae` fetches SAE International standards (relaton/relaton-sae#1).
4
+
5
+ ## Development
6
+
7
+ - `bundle exec rake spec:sae` — this flavor's suite (from the repo root).
8
+ - No VCR: the specs stub HTTP with WebMock. `spec/sae/fixtures/crossref/`
9
+ holds three Crossref cursor pages built from real records (11 works:
10
+ six J3016/J3194 editions, three AMS4121 revisions, a part and a
11
+ letter-pair number). `spec/sae/fixtures/index-v2.yaml` and `fixtures/data/` are the
12
+ **output of `DataFetcher` over those pages**: regenerate both together by
13
+ running the crawler over the page fixtures, never by hand.
14
+
15
+ ## Where the data comes from
16
+
17
+ **sae.org answers every script request with a CAPTCHA** (product pages and
18
+ search alike), so no scraper is possible. SAE deposits every standard edition
19
+ with Crossref instead: prefix `10.4271`, type `standard`, ~47,700 records. The
20
+ DOI suffix is SAE's own edition code in lowercase:
21
+
22
+ | DOI | SAE code | scheme |
23
+ |---|---|---|
24
+ | `10.4271/j3016_202104` | `SAE J3016_202104` | J-series: `_YYYYMM` edition date |
25
+ | `10.4271/ams4121j` | `SAE AMS4121J` | aerospace: revision letter, no date |
26
+ | `10.4271/j2337/2_201702` | `SAE J2337/2_201702` | a part |
27
+
28
+ Crossref's text search does **not** find a standard by its id (it matches
29
+ titles), so an undated reference cannot be answered live. The flavor is
30
+ therefore index-based, the ECMA/IALA shape:
31
+
32
+ - **Producer** — `DataFetcher` (`relaton fetch-data sae-crossref`) walks
33
+ `api.crossref.org/prefixes/10.4271/works?filter=type:standard` with the
34
+ deep-paging cursor (1000 a request, one request a second through
35
+ `Core::Pacer`; 429/5xx and transport errors retried with back-off, five
36
+ attempts, then `RequestError`, because
37
+ a partial crawl would publish a partial index). Each work goes through
38
+ `Relaton::Doi::Parser`, then gets SAE's code as its **primary**
39
+ docidentifier (type `SAE`), the DOI beside it, and `ext` doctype
40
+ `standard`, flavor `sae`.
41
+ - **Consumer** — `Bibliography.get` reads the `relaton-data-sae` Pages index
42
+ (`PAGES_URL`) and the document from `ENDPOINT` (raw GitHub).
43
+
44
+ ## Three identifier forms
45
+
46
+ `DoiCode` gives each record three strings:
47
+
48
+ - `#native` — the code in the DOI's spelling, `SAE J3016_202104`,
49
+ `SAE AMSQQA250/4B`. The output file is named after it
50
+ (`data/sae-amsqqa250-4b.yaml`), so a file name never depends on pubid.
51
+ - `#printed` — the code as SAE prints it (`SAE AMS-QQ-A-250/4B`): pubid's
52
+ render of the native code, or the native code when pubid cannot read it.
53
+ It is the primary docidentifier.
54
+ - `#reference` — what the index row is parsed from: the native code when the
55
+ DOI has a date, else the code with the year of the record's `approved` date
56
+ (`SAE AMS4121J:2017`), so a dated reference also reaches an aerospace
57
+ revision.
58
+
59
+ `Pubid::Sae` (pubid >= 2.0.9, pubid#665) reads all three, the month
60
+ included, so the `Db` keys the query and the fetched document with the
61
+ default `cache_pubid`. A reference pubid cannot read (`SAE J3016_2021`,
62
+ `SAE CPGM2016`) answers nil with a log line (relaton#235); it does not raise.
63
+
64
+ ## Selection
65
+
66
+ `index.search(pubid)` with no block, so the subset match `query === row`:
67
+ a revision, a year or a month the query omits matches any value; the type
68
+ (`J`, `AMS`, …) is compared, and a part and an amendment are `subset_strict`
69
+ (`SAE J2337` does not answer with `SAE J2337/2`). Of the matches the highest
70
+ revision wins (`[length, letters]`, so `AA` follows `Z`), then the latest
71
+ year and month, then the file name (the index sort is not stable). A year
72
+ argument goes on a **copy** of a pubid handed in by `Db`.
73
+
74
+ The index key carries the month, so two editions of one year are two rows.
75
+ Two DOIs that spell one code (`ams-4121j`, `ams4121j`) share a key, and
76
+ `DataFetcher#add_to_index` keeps the later one (DOI date, then approval
77
+ date).
78
+
79
+ ## What pubid still cannot read
80
+
81
+ About 960 of the ~47,700 deposits: 791 `CP*` collected-paper products (not
82
+ standards) and ~170 one-off shapes (`SUP1` supplements, language tails such
83
+ as `_PTBR`, `_4Q2012`, `_MMYYYY` dates). See pubid's `lib/pubid/sae/CLAUDE.md`.
84
+ Such a record is written, reported in `@errors` (the crawl's GitHub issue),
85
+ and not indexed: one row that fails to deserialize makes `Relaton::Index`
86
+ reject the whole index.
87
+
88
+ A multi-letter tail stays in `number` (`J1939DA`, and also `AMS4121AA`), so
89
+ a two-letter revision is its own bucket: `SAE AMS4121` does not reach it.
@@ -0,0 +1,8 @@
1
+ module Relaton
2
+ module Sae
3
+ class Bibdata < Item
4
+ model ItemData
5
+ include Bib::BibdataShared
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module Relaton
2
+ module Sae
3
+ class Bibitem < Item
4
+ model ItemData
5
+ include Bib::BibitemShared
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,128 @@
1
+ require "net/http"
2
+
3
+ module Relaton
4
+ module Sae
5
+ module Bibliography
6
+ # Transport failures of the document fetch. They become
7
+ # `Relaton::RequestError`, the one class `Relaton::Db#net_retry` retries.
8
+ NET_ERRORS = [
9
+ SocketError, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED,
10
+ Errno::ETIMEDOUT, EOFError, Net::HTTPBadResponse,
11
+ Net::HTTPHeaderSyntaxError, Net::ProtocolError, Net::OpenTimeout,
12
+ Net::ReadTimeout, OpenSSL::SSL::SSLError
13
+ ].freeze
14
+
15
+ class << self
16
+ #
17
+ # Fetch an SAE standard.
18
+ #
19
+ # @param ref [String, Pubid::Sae::Identifier] `SAE J3016`,
20
+ # `SAE J3016:2021`, or SAE's native edition code `SAE J3016_202104`
21
+ # @param year [String, nil]
22
+ # @param _opts [Hash]
23
+ # @return [Relaton::Sae::ItemData, nil]
24
+ #
25
+ def get(ref, year = nil, _opts = {})
26
+ Util.info "Fetching from Relaton repository ...", key: ref.to_s
27
+ pubid = parse_ref ref, year
28
+ return not_found(ref) unless pubid
29
+
30
+ row = best_match pubid
31
+ item = row && fetch_doc(row[:file])
32
+ return not_found(ref) unless item
33
+
34
+ Util.info "Found: `#{primary_id item}`", key: ref.to_s
35
+ item
36
+ end
37
+
38
+ private
39
+
40
+ def not_found(ref)
41
+ Util.info "Not found.", key: ref.to_s
42
+ nil
43
+ end
44
+
45
+ #
46
+ # The query as a `Pubid::Sae::Identifier`.
47
+ #
48
+ # A parsed pubid comes from `Relaton::Db` (relaton#205): it is not
49
+ # mutated, so a year argument goes on a copy. A reference pubid cannot
50
+ # read answers nil, so `get` answers not-found with a log line instead
51
+ # of raising (relaton#235): `Db` passes such a reference as a String.
52
+ #
53
+ # @return [Pubid::Sae::Identifier, nil]
54
+ #
55
+ def parse_ref(ref, year)
56
+ ref = parse_text(ref.to_s.strip) unless ref.is_a?(::Pubid::Sae::Identifier)
57
+ return ref unless ref && year
58
+
59
+ copy = ::Pubid::Sae::Identifier.from_hash ref.to_hash
60
+ copy.date = ::Pubid::Sae::Components::Date.new(year: year.to_i)
61
+ copy
62
+ end
63
+
64
+ def parse_text(text)
65
+ ::Pubid::Sae::Identifier.parse text
66
+ rescue ::Pubid::Errors::Error, ArgumentError => e
67
+ Util.info "Not an SAE identifier pubid reads: #{e.message}", key: text
68
+ nil
69
+ end
70
+
71
+ #
72
+ # The index row for a query: the newest edition among its matches.
73
+ #
74
+ # Passing the pubid (not a String) is what makes `Index::Type#search`
75
+ # narrow to the `number` bucket, and with no block it selects with
76
+ # pubid's subset match `query === row`: a revision, a year or a month
77
+ # the query omits matches any value, and the type (`J`, `AMS`, …), a
78
+ # part and an amendment must agree.
79
+ #
80
+ # Of the matches, the highest revision wins (aerospace standards), then
81
+ # the latest date (J-series editions). `r[:file]` breaks a tie, because
82
+ # the index sort is not stable.
83
+ #
84
+ # @param pubid [Pubid::Sae::Identifier]
85
+ # @return [Hash, nil]
86
+ #
87
+ def best_match(pubid)
88
+ index.search(pubid).max_by do |r|
89
+ id = r[:id]
90
+ [revision_rank(id.revision), id.year.to_i, id.date&.month.to_i,
91
+ r[:file]]
92
+ end
93
+ end
94
+
95
+ # `B` follows `A`, and a two-letter revision (`AA`) follows `Z`.
96
+ def revision_rank(revision)
97
+ [revision.to_s.size, revision.to_s]
98
+ end
99
+
100
+ # @param file [String] the row's path in the data repo
101
+ # @return [Relaton::Sae::ItemData, nil] nil when the file is missing
102
+ def fetch_doc(file)
103
+ uri = URI("#{ENDPOINT}#{file}")
104
+ resp = Net::HTTP.get_response uri
105
+ return if resp.code == "404"
106
+ unless resp.code == "200"
107
+ raise Relaton::RequestError,
108
+ "Could not access #{uri}: HTTP #{resp.code}"
109
+ end
110
+
111
+ Item.from_yaml(resp.body).tap { |i| i.fetched = Date.today.to_s }
112
+ rescue *NET_ERRORS => e
113
+ raise Relaton::RequestError, "Could not access #{uri}: #{e.message}"
114
+ end
115
+
116
+ def primary_id(item)
117
+ (item.docidentifier.detect(&:primary) || item.docidentifier.first)&.content
118
+ end
119
+
120
+ def index
121
+ Relaton::Index.find_or_create(
122
+ :sae, pages_url: PAGES_URL, pubid_class: ::Pubid::Sae::Identifier
123
+ )
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end