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,219 @@
1
+ require "net/http"
2
+ require_relative "../core"
3
+ require_relative "../json"
4
+ require_relative "../sae"
5
+ require_relative "../doi"
6
+
7
+ module Relaton
8
+ module Sae
9
+ #
10
+ # Crawl every SAE standard from Crossref into a data repo.
11
+ #
12
+ # sae.org answers a script with a CAPTCHA, and Crossref's text search does
13
+ # not find a standard by its identifier, but SAE deposits each edition
14
+ # with a DOI under prefix `10.4271` and type `standard`. This walks that
15
+ # set with Crossref's deep-paging cursor (1000 records a request, about 48
16
+ # requests), maps each record with `Relaton::Doi::Parser`, and writes one
17
+ # document per edition and the pubid `index-v2`.
18
+ #
19
+ class DataFetcher < Core::DataFetcher
20
+ SOURCES = %w[sae-crossref].freeze
21
+ URL = "https://api.crossref.org/prefixes/10.4271/works".freeze
22
+ ROWS = 1000
23
+ # Crossref's public pool allows far more; one request a second is polite
24
+ # and still finishes the crawl in about a minute of waits.
25
+ GAP = 1
26
+ # Attempts for one page before the crawl gives up.
27
+ ATTEMPTS = 5
28
+ RETRY_CODES = %w[429 500 502 503 504].freeze
29
+
30
+ def fetch(source = nil)
31
+ unless SOURCES.include? source
32
+ raise ArgumentError,
33
+ "Unknown source `#{source}`, expected one of #{SOURCES}"
34
+ end
35
+
36
+ @newest = {}
37
+ each_page { |items| items.each { |work| parse_work work } }
38
+ index.save
39
+ report_errors
40
+ end
41
+
42
+ def index
43
+ # `pubid_class:` on the producer too: FileIO#save calls `to_hash` only
44
+ # for instances of it, so without it the crawl writes v1-shaped rows
45
+ # under a v2 name.
46
+ @index ||= Relaton::Index.find_or_create(
47
+ :sae, file: "#{INDEXFILE}.yaml", pubid_class: ::Pubid::Sae::Identifier
48
+ )
49
+ end
50
+
51
+ def log_error(msg)
52
+ Util.error msg
53
+ end
54
+
55
+ private
56
+
57
+ def each_page
58
+ cursor = "*"
59
+ loop do
60
+ message = get_page(cursor)
61
+ items = message["items"]
62
+ break if items.nil? || items.empty?
63
+
64
+ yield items
65
+ cursor = message["next-cursor"]
66
+ break unless cursor
67
+ end
68
+ end
69
+
70
+ #
71
+ # One page of the cursor walk. A throttle or a server error is retried
72
+ # with a growing wait; anything else, or a page that keeps failing,
73
+ # stops the crawl, because a partial crawl would publish a partial index.
74
+ #
75
+ # @param cursor [String]
76
+ # @return [Hash] the `message` of the response
77
+ #
78
+ def get_page(cursor) # rubocop:disable Metrics/MethodLength
79
+ uri = page_uri cursor
80
+ ATTEMPTS.times do |n|
81
+ pacer.wait
82
+ resp, failure = attempt uri
83
+ return Relaton::Json.parse_external(resp.body)["message"] if resp&.code == "200"
84
+ if resp && !RETRY_CODES.include?(resp.code)
85
+ raise Relaton::RequestError, "Crossref HTTP #{resp.code} for #{uri}"
86
+ end
87
+
88
+ break if n == ATTEMPTS - 1
89
+
90
+ Util.warn "Crossref #{failure || "HTTP #{resp.code}"}, " \
91
+ "retry #{n + 1} of #{ATTEMPTS - 1}"
92
+ sleep backoff(resp, n + 1)
93
+ end
94
+ raise Relaton::RequestError,
95
+ "Crossref keeps failing for #{uri} after #{ATTEMPTS} attempts"
96
+ end
97
+
98
+ # A transport error is retried like a 5xx: one timeout in ~48 requests
99
+ # must not fail the crawl.
100
+ #
101
+ # @return [Array(Net::HTTPResponse|nil, String|nil)] the response, or nil
102
+ # and the error message
103
+ def attempt(uri)
104
+ [http_get(uri), nil]
105
+ rescue *Bibliography::NET_ERRORS => e
106
+ [nil, "#{e.class}: #{e.message}"]
107
+ end
108
+
109
+ def page_uri(cursor)
110
+ uri = URI URL
111
+ uri.query = URI.encode_www_form(filter: "type:standard", rows: ROWS,
112
+ cursor: cursor)
113
+ uri
114
+ end
115
+
116
+ def http_get(uri)
117
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
118
+ open_timeout: 30, read_timeout: 120) do |http|
119
+ http.get uri.request_uri, "User-Agent" => Relaton::Doi::Crossref::USER_AGENT
120
+ end
121
+ end
122
+
123
+ # Crossref sends `X-Rate-Limit-Interval: 1s`; a throttled response can
124
+ # carry no such header, so the wait is never less than a second.
125
+ def backoff(resp, num)
126
+ interval = resp&.[]("x-rate-limit-interval").to_s[/\d+/].to_i
127
+ [interval, 1].max * num * 2
128
+ end
129
+
130
+ def pacer
131
+ @pacer ||= Core::Pacer.new(gap: GAP)
132
+ end
133
+
134
+ #
135
+ # Write one Crossref record and index it.
136
+ #
137
+ # @param work [Hash] a Crossref work
138
+ #
139
+ def parse_work(work)
140
+ code = DoiCode.parse work["DOI"], approved_year: approved_year(work)
141
+ return Util.warn("Not an SAE DOI: `#{work['DOI']}`") unless code
142
+
143
+ item = to_item work, code
144
+ file = write_unique code.native, serialize(item)
145
+ add_to_index code, work, file
146
+ end
147
+
148
+ def approved_year(work)
149
+ (work["approved"] || work["issued"])&.dig("date-parts", 0, 0)
150
+ end
151
+
152
+ #
153
+ # The `Relaton::Doi::Parser` item, with SAE's edition code (as SAE
154
+ # prints it) as its primary identifier and the DOI kept beside it.
155
+ #
156
+ # @return [Relaton::Sae::ItemData]
157
+ #
158
+ def to_item(work, code)
159
+ bib = Relaton::Doi::Parser.parse work
160
+ bib.id = code.native.delete(" /").gsub(/\W/, "")
161
+ bib.docidentifier = [
162
+ Bib::Docidentifier.new(type: "SAE", content: code.printed,
163
+ primary: true),
164
+ *bib.docidentifier.map do |d|
165
+ Bib::Docidentifier.new(type: d.type, content: d.content)
166
+ end,
167
+ ]
168
+ bib.ext = Bib::Ext.new(doctype: Bib::Doctype.new(content: "standard"),
169
+ flavor: "sae")
170
+ Item.from_yaml Bib::Item.to_yaml(bib)
171
+ end
172
+
173
+ #
174
+ # Index the edition under its pubid, or record why it cannot be.
175
+ #
176
+ # An id pubid cannot read is recorded in `@errors` (the inherited
177
+ # `report_errors` logs it and opens the crawl's GitHub issue) and the row
178
+ # is skipped: `Relaton::Index` rejects the WHOLE index when one row fails
179
+ # to deserialize. The data file is already written, so the document is
180
+ # unindexed, never lost.
181
+ #
182
+ # The key carries the month, so two editions of one year are two rows.
183
+ # Two DOIs that spell one code (`ams-4121j`, `ams4121j`) share a key;
184
+ # the one with the later DOI date, then approval date, keeps the row.
185
+ #
186
+ def add_to_index(code, work, file)
187
+ pubid = parse_pubid code
188
+ unless pubid
189
+ @errors[code.native] =
190
+ "Unparseable SAE id `#{code.native}` was not indexed (#{file})"
191
+ return
192
+ end
193
+
194
+ key = pubid.to_s
195
+ stamp = [code.date.to_s, approved_stamp(work)]
196
+ return if @newest[key] && (@newest[key] <=> stamp) >= 0
197
+
198
+ @newest[key] = stamp
199
+ index.add_or_update pubid, file
200
+ end
201
+
202
+ def parse_pubid(code)
203
+ ::Pubid::Sae::Identifier.parse code.reference
204
+ rescue ::Pubid::Errors::Error, ArgumentError
205
+ nil
206
+ end
207
+
208
+ def approved_stamp(work)
209
+ Array(work.dig("approved", "date-parts", 0)).map do |d|
210
+ d.to_i.to_s.rjust(2, "0")
211
+ end.join("-")
212
+ end
213
+
214
+ def to_yaml(bib) = bib.to_yaml
215
+ def to_xml(bib) = Bibdata.to_xml(bib)
216
+ def to_bibxml(bib) = bib.to_rfcxml
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,83 @@
1
+ module Relaton
2
+ module Sae
3
+ #
4
+ # The SAE codes a Crossref DOI names.
5
+ #
6
+ # SAE deposits one DOI per edition, and its suffix is SAE's own edition
7
+ # code in lowercase: `10.4271/j3016_202104` is `SAE J3016_202104`, the
8
+ # April 2021 edition of J3016. Aerospace standards are told apart by a
9
+ # revision letter instead and carry no date (`10.4271/ams4121j`).
10
+ #
11
+ # Three strings come out of it:
12
+ #
13
+ # * `#native` — the code in the DOI's spelling (`SAE AMSQQA250/4B`). The
14
+ # output file is named after it, so a file name never depends on pubid.
15
+ # * `#printed` — the code as SAE prints it (`SAE AMS-QQ-A-250/4B`), the
16
+ # document's primary identifier.
17
+ # * `#reference` — the form the index row is parsed from. It is the
18
+ # native code when the DOI has a date; an undated code gets the year of
19
+ # the record's approval date, so a dated reference also reaches an
20
+ # aerospace revision (`SAE AMS4121J:2017`).
21
+ #
22
+ class DoiCode
23
+ PREFIX = "10.4271/".freeze
24
+ # A code, then an optional `_YYYYMM` edition date. The code takes letters,
25
+ # digits, the `/` of a part (`j2337/2`) and any `_` that is not that date
26
+ # (`as5259/7a_a1`, `j1939da_4q2012`: 1,163 of the 2026-11 deposits); what
27
+ # it means is pubid's business, so nothing else is decided here.
28
+ SUFFIX = %r{\A(?<code>[a-z0-9][a-z0-9/._-]*?)(?:_(?<date>\d{6}))?\z}i
29
+
30
+ attr_reader :code, :date, :year
31
+
32
+ #
33
+ # @param doi [String, nil] e.g. `10.4271/j3016_202104`
34
+ # @param approved_year [String, Integer, nil] the record's approval year
35
+ # @return [Relaton::Sae::DoiCode, nil] nil for a DOI that is not SAE's
36
+ #
37
+ def self.parse(doi, approved_year: nil)
38
+ suffix = doi.to_s.strip
39
+ return unless suffix.downcase.start_with? PREFIX
40
+
41
+ match = SUFFIX.match suffix[PREFIX.size..]
42
+ return unless match
43
+
44
+ new(match[:code].upcase, match[:date], approved_year)
45
+ end
46
+
47
+ def initialize(code, date, approved_year)
48
+ @code = code
49
+ @date = date
50
+ @year = date ? date[0, 4] : approved_year&.to_s
51
+ end
52
+
53
+ # @return [String] SAE's edition code, e.g. `SAE J3016_202104`
54
+ def native
55
+ date ? "SAE #{code}_#{date}" : "SAE #{code}"
56
+ end
57
+
58
+ # @return [String] the code SAE prints, or the native code when pubid
59
+ # cannot read it
60
+ def printed
61
+ pubid&.to_s || native
62
+ end
63
+
64
+ # @return [String] the reference the index row is parsed from
65
+ def reference
66
+ return native if date || !year
67
+
68
+ "SAE #{code}:#{year}"
69
+ end
70
+
71
+ # @return [Pubid::Sae::Identifier, nil] the native code, parsed
72
+ def pubid
73
+ return @pubid if defined? @pubid
74
+
75
+ @pubid = begin
76
+ ::Pubid::Sae::Identifier.parse native
77
+ rescue ::Pubid::Errors::Error, ArgumentError
78
+ nil
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,10 @@
1
+ module Relaton
2
+ module Sae
3
+ # An SAE bibliographic item. SAE adds no element to the shared model: the
4
+ # Crossref record maps onto `Bib` as it is, so the only flavor-specific
5
+ # value is `ext/flavor`.
6
+ class Item < Bib::Item
7
+ model ItemData
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,6 @@
1
+ module Relaton
2
+ module Sae
3
+ class ItemData < Bib::ItemData
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,62 @@
1
+ require_relative "../core/processor"
2
+
3
+ module Relaton
4
+ module Sae
5
+ class Processor < Relaton::Core::Processor
6
+ attr_reader :idtype
7
+
8
+ def initialize # rubocop:disable Lint/MissingSuper
9
+ @short = :relaton_sae
10
+ @prefix = "SAE"
11
+ @defaultprefix = /^SAE\s/
12
+ @idtype = "SAE"
13
+ @datasets = %w[sae-crossref]
14
+ # Db cache key: `Pubid::Sae` reads the reference and SAE's native
15
+ # edition code (`SAE J3016_202104`) alike.
16
+ @pubid_flavor = :Sae
17
+ end
18
+
19
+ # @param code [String, Pubid::Sae::Identifier]
20
+ # @param date [String, nil] year
21
+ # @param opts [Hash]
22
+ # @return [Relaton::Sae::ItemData, nil]
23
+ def get(code, date, opts)
24
+ require_relative "../sae"
25
+ Bibliography.get(code, date, opts)
26
+ end
27
+
28
+ # @param source [String] `sae-crossref`
29
+ # @param opts [Hash] `output:`, `format:`
30
+ def fetch_data(source, opts)
31
+ require_relative "data_fetcher"
32
+ DataFetcher.fetch(source, **opts)
33
+ end
34
+
35
+ # @param xml [String]
36
+ # @return [Relaton::Sae::ItemData]
37
+ def from_xml(xml)
38
+ require_relative "../sae"
39
+ Item.from_xml xml
40
+ end
41
+
42
+ # @param yaml [String]
43
+ # @return [Relaton::Sae::ItemData]
44
+ def from_yaml(yaml)
45
+ require_relative "../sae"
46
+ Item.from_yaml yaml
47
+ end
48
+
49
+ # @return [String]
50
+ def grammar_hash
51
+ require_relative "../sae"
52
+ @grammar_hash ||= Sae.grammar_hash
53
+ end
54
+
55
+ def remove_index_file
56
+ require_relative "../sae"
57
+ Relaton::Index.find_or_create(:sae, url: true,
58
+ file: "#{INDEXFILE}.yaml").remove_file
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,9 @@
1
+ module Relaton
2
+ module Sae
3
+ module Util
4
+ extend Relaton::Bib::Util
5
+
6
+ PROGNAME = "relaton-sae".freeze
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,37 @@
1
+ require "digest/md5"
2
+ require "pubid"
3
+ require_relative "bib"
4
+ require_relative "index"
5
+ require_relative "version"
6
+
7
+ # Relaton::Sae is the SAE International flavor (relaton/relaton-sae#1).
8
+ #
9
+ # sae.org answers a script with a CAPTCHA, so the flavor reads SAE's Crossref
10
+ # deposits instead: every SAE standard edition has its own DOI
11
+ # (`10.4271/j3016_202104`). `DataFetcher` crawls them into the
12
+ # `relaton-data-sae` data repo with a pubid `index-v2`, and `Bibliography`
13
+ # reads that index. See lib/relaton/sae/CLAUDE.md.
14
+ module Relaton
15
+ module Sae
16
+ INDEXFILE = "index-v2".freeze
17
+ # The machine index on the data repo's Pages site (relaton#206).
18
+ PAGES_URL = "https://relaton.github.io/relaton-data-sae/".freeze
19
+ # Where the documents the index names are read from.
20
+ ENDPOINT = "https://raw.githubusercontent.com/relaton/relaton-data-sae/main/".freeze
21
+
22
+ autoload :Util, "relaton/sae/util"
23
+ autoload :DoiCode, "relaton/sae/doi_code"
24
+ autoload :ItemData, "relaton/sae/item_data"
25
+ autoload :Item, "relaton/sae/item"
26
+ autoload :Bibitem, "relaton/sae/bibitem"
27
+ autoload :Bibdata, "relaton/sae/bibdata"
28
+ autoload :Bibliography, "relaton/sae/bibliography"
29
+ autoload :Processor, "relaton/sae/processor"
30
+
31
+ # Returns hash of XML grammar
32
+ # @return [String]
33
+ def self.grammar_hash
34
+ Digest::MD5.hexdigest Relaton::VERSION
35
+ end
36
+ end
37
+ end
@@ -88,4 +88,4 @@ Tests are round-trip serialization tests: load a fixture, serialize to XML/YAML,
88
88
  - **relaton-bib** (~> 2.0.0-alpha.1) — base bibliographic model with LutaML-Model integration
89
89
  - **faraday** — HTTP client
90
90
  - **ruby-jing** (dev) — RelaxNG schema validation in tests
91
- - **equivalent-xml** (dev) — XML comparison in tests
91
+ - **canon** (dev) — XML comparison in tests
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "relaton/core/processor"
3
+ require_relative "../core/processor"
4
4
 
5
5
  module Relaton
6
6
  module Un
data/lib/relaton/un.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require "addressable"
2
- require "relaton/bib"
2
+ require_relative "bib"
3
3
  require_relative "version"
4
4
  require_relative "un/util"
5
5
  require_relative "un/item_data"
@@ -1,3 +1,3 @@
1
1
  module Relaton
2
- VERSION = "3.0.7".freeze
2
+ VERSION = "3.1.0".freeze
3
3
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "net/http"
4
- require "relaton/bib/hash_parser_v1"
4
+ require_relative "../bib/hash_parser_v1"
5
5
 
6
6
  module Relaton
7
7
  module W3c
@@ -1,4 +1,4 @@
1
- require "relaton/core"
1
+ require_relative "../core"
2
2
  require "w3c_api"
3
3
  require_relative "../w3c"
4
4
  require_relative "safe_realize"
@@ -1,4 +1,4 @@
1
- require "relaton/core/processor"
1
+ require_relative "../core/processor"
2
2
 
3
3
  module Relaton
4
4
  module W3c
data/lib/relaton/w3c.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require "pubid"
2
2
  require "pubid/w3c"
3
- require "relaton/bib"
4
- require "relaton/index"
3
+ require_relative "bib"
4
+ require_relative "index"
5
5
  require_relative "version"
6
6
  require_relative "w3c/util"
7
7
  require_relative "w3c/item"
@@ -1,4 +1,4 @@
1
- require "relaton/core"
1
+ require_relative "../core"
2
2
 
3
3
  module Relaton
4
4
  module Xsf
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- require "relaton/core/processor"
2
+ require_relative "../core/processor"
3
3
 
4
4
  module Relaton
5
5
  module Xsf
data/lib/relaton/xsf.rb CHANGED
@@ -5,8 +5,8 @@ require "mechanize"
5
5
  # index code names. Processor#remove_index_file names no pubid class: the
6
6
  # delete never reads the index (see lib/relaton/index/CLAUDE.md).
7
7
  require "pubid"
8
- require "relaton/index"
9
- require "relaton/bib"
8
+ require_relative "index"
9
+ require_relative "bib"
10
10
  require_relative "version"
11
11
  require_relative "xsf/util"
12
12
  require_relative "xsf/item_data"
@@ -0,0 +1,17 @@
1
+ # The per-flavor `autoload` lines live in `relaton.rb`, not `relaton/db`.
2
+ # `index_name_for`/`pubid_class_for` resolve `Relaton::<Flavor>::INDEXFILE`, so
3
+ # without the umbrella every `--pubid-flavor` fails with "no relaton flavor".
4
+ # Declaring autoloads is cheap: no flavor is loaded until it is referenced.
5
+ require_relative "relaton"
6
+ require_relative "relaton/db"
7
+
8
+ module Relaton
9
+ module Cli
10
+ end
11
+ end
12
+
13
+ require_relative "relaton/bibcollection"
14
+ require_relative "relaton/bibdata"
15
+ require_relative "relaton/element_finder"
16
+ require_relative "relaton/cli/yaml_convertor"
17
+ require_relative "relaton/cli"
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "relaton/render/iso690"
4
+
5
+ module Relaton
6
+ module Render
7
+ # A bibliographic item whose style template resolves to nothing: the
8
+ # single-item API raises, the batch API skips
9
+ class Unrenderable < StandardError; end
10
+
11
+ end
12
+ end
data/lib/relaton.rb CHANGED
@@ -4,7 +4,7 @@
4
4
  # autoloaded: referencing it (or any nested constant) the first time triggers
5
5
  # `require "relaton/<flavor>"`, loading that flavor's code on demand rather than
6
6
  # at `require "relaton"`. When adding a flavor, add an autoload line here.
7
- require "relaton/version"
7
+ require_relative "relaton/version"
8
8
  require "moxml"
9
9
 
10
10
  # YAML through stdlib Psych. The former yeptris drop-in rebind
@@ -50,6 +50,7 @@ module Relaton
50
50
  autoload :Oasis, "relaton/oasis"
51
51
  autoload :Oiml, "relaton/oiml"
52
52
  autoload :Omg, "relaton/omg"
53
+ autoload :Sae, "relaton/sae"
53
54
  autoload :Un, "relaton/un"
54
55
  autoload :W3c, "relaton/w3c"
55
56
  autoload :Xsf, "relaton/xsf"
@@ -100,4 +101,4 @@ module Relaton
100
101
  end
101
102
  end
102
103
 
103
- require "relaton/db"
104
+ require_relative "relaton/db"