relaton 3.0.0.pre.alpha.1 → 3.0.0.pre.alpha.2

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 (270) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +57 -1
  3. data/bin/console +0 -1
  4. data/lib/relaton/3gpp/bibliography.rb +82 -7
  5. data/lib/relaton/3gpp/data_fetcher.rb +51 -3
  6. data/lib/relaton/3gpp/docidentifier.rb +114 -0
  7. data/lib/relaton/3gpp/item.rb +6 -0
  8. data/lib/relaton/3gpp/parser.rb +1 -1
  9. data/lib/relaton/3gpp/processor.rb +4 -1
  10. data/lib/relaton/3gpp.rb +5 -1
  11. data/lib/relaton/adobe/bibdata.rb +8 -0
  12. data/lib/relaton/adobe/bibitem.rb +8 -0
  13. data/lib/relaton/adobe/bibliography.rb +92 -0
  14. data/lib/relaton/adobe/docidentifier.rb +49 -0
  15. data/lib/relaton/adobe/doctype.rb +14 -0
  16. data/lib/relaton/adobe/ext.rb +32 -0
  17. data/lib/relaton/adobe/item.rb +15 -0
  18. data/lib/relaton/adobe/item_base.rb +18 -0
  19. data/lib/relaton/adobe/item_data.rb +6 -0
  20. data/lib/relaton/adobe/processor.rb +45 -0
  21. data/lib/relaton/adobe/util.rb +8 -0
  22. data/lib/relaton/adobe.rb +37 -0
  23. data/lib/relaton/bib/model/address.rb +2 -2
  24. data/lib/relaton/bib/model/docidentifier.rb +24 -9
  25. data/lib/relaton/bib/model/localized_string.rb +1 -1
  26. data/lib/relaton/bib/model/structured_identifier.rb +10 -9
  27. data/lib/relaton/bib/sanitizer.rb +202 -6
  28. data/lib/relaton/bib.rb +0 -2
  29. data/lib/relaton/bipm/bibliography.rb +159 -10
  30. data/lib/relaton/bipm/data_fetcher.rb +26 -2
  31. data/lib/relaton/bipm/data_outcomes_parser.rb +3 -4
  32. data/lib/relaton/bipm/id_parser.rb +5 -4
  33. data/lib/relaton/bipm/model/structured_identifier.rb +21 -0
  34. data/lib/relaton/bipm/processor.rb +2 -2
  35. data/lib/relaton/bipm/rawdata_bipm_metrologia/fetcher.rb +2 -4
  36. data/lib/relaton/bipm/si_brochure_parser.rb +5 -3
  37. data/lib/relaton/bipm.rb +7 -1
  38. data/lib/relaton/bsi/bibliography.rb +115 -43
  39. data/lib/relaton/bsi/hit.rb +14 -0
  40. data/lib/relaton/bsi/hit_collection.rb +15 -16
  41. data/lib/relaton/bsi/model/docidentifier.rb +99 -1
  42. data/lib/relaton/bsi/processor.rb +1 -0
  43. data/lib/relaton/calconnect/bibliography.rb +12 -14
  44. data/lib/relaton/calconnect/data_fetcher.rb +77 -9
  45. data/lib/relaton/calconnect/docidentifier.rb +80 -0
  46. data/lib/relaton/calconnect/hit_collection.rb +65 -57
  47. data/lib/relaton/calconnect/model/item.rb +7 -0
  48. data/lib/relaton/calconnect/processor.rb +7 -1
  49. data/lib/relaton/calconnect.rb +11 -1
  50. data/lib/relaton/ccsds/data/fetcher.rb +17 -12
  51. data/lib/relaton/ccsds/data/parser.rb +1 -1
  52. data/lib/relaton/ccsds/hit_collection.rb +6 -1
  53. data/lib/relaton/ccsds/model/docidentifier.rb +121 -0
  54. data/lib/relaton/ccsds/model/item.rb +2 -0
  55. data/lib/relaton/cen/bibliography.rb +75 -47
  56. data/lib/relaton/cen/hit.rb +16 -1
  57. data/lib/relaton/cen/hit_collection.rb +59 -11
  58. data/lib/relaton/cen/model/docidentifier.rb +92 -1
  59. data/lib/relaton/cen/processor.rb +13 -8
  60. data/lib/relaton/cen/scraper.rb +13 -5
  61. data/lib/relaton/cen.rb +1 -0
  62. data/lib/relaton/cie/data_fetcher.rb +215 -30
  63. data/lib/relaton/cie/processor.rb +3 -1
  64. data/lib/relaton/cie/scrapper.rb +15 -2
  65. data/lib/relaton/cie.rb +2 -1
  66. data/lib/relaton/core/data_fetcher.rb +150 -3
  67. data/lib/relaton/core/governor.rb +320 -0
  68. data/lib/relaton/core/pacer.rb +134 -0
  69. data/lib/relaton/core/processor.rb +19 -0
  70. data/lib/relaton/core/request_error.rb +14 -0
  71. data/lib/relaton/core.rb +3 -0
  72. data/lib/relaton/db/registry.rb +41 -1
  73. data/lib/relaton/doi/crossref.rb +19 -2
  74. data/lib/relaton/doi/parser.rb +109 -15
  75. data/lib/relaton/easc/bibdata.rb +8 -0
  76. data/lib/relaton/easc/bibitem.rb +8 -0
  77. data/lib/relaton/easc/bibliography.rb +95 -0
  78. data/lib/relaton/easc/docidentifier.rb +100 -0
  79. data/lib/relaton/easc/doctype.rb +14 -0
  80. data/lib/relaton/easc/ext.rb +44 -0
  81. data/lib/relaton/easc/item.rb +13 -0
  82. data/lib/relaton/easc/item_base.rb +18 -0
  83. data/lib/relaton/easc/item_data.rb +6 -0
  84. data/lib/relaton/easc/processor.rb +46 -0
  85. data/lib/relaton/easc/util.rb +8 -0
  86. data/lib/relaton/easc.rb +35 -0
  87. data/lib/relaton/ecma/bibliography.rb +93 -25
  88. data/lib/relaton/ecma/data_fetcher.rb +71 -12
  89. data/lib/relaton/ecma/docidentifier.rb +124 -0
  90. data/lib/relaton/ecma/item.rb +2 -0
  91. data/lib/relaton/ecma/memento_parser.rb +1 -1
  92. data/lib/relaton/ecma/page_fetcher.rb +15 -3
  93. data/lib/relaton/ecma/parser_common.rb +2 -2
  94. data/lib/relaton/ecma/processor.rb +4 -1
  95. data/lib/relaton/ecma/standard_parser.rb +2 -2
  96. data/lib/relaton/ecma.rb +10 -1
  97. data/lib/relaton/etsi/bibliography.rb +67 -2
  98. data/lib/relaton/etsi/data_fetcher.rb +43 -4
  99. data/lib/relaton/etsi/processor.rb +3 -1
  100. data/lib/relaton/etsi.rb +2 -1
  101. data/lib/relaton/gb/bibliography.rb +55 -29
  102. data/lib/relaton/gb/docidentifier.rb +58 -9
  103. data/lib/relaton/gb/processor.rb +3 -0
  104. data/lib/relaton/gb/scraper.rb +27 -10
  105. data/lib/relaton/gost/bibdata.rb +8 -0
  106. data/lib/relaton/gost/bibitem.rb +8 -0
  107. data/lib/relaton/gost/bibliography.rb +107 -0
  108. data/lib/relaton/gost/docidentifier.rb +80 -0
  109. data/lib/relaton/gost/doctype.rb +16 -0
  110. data/lib/relaton/gost/ext.rb +46 -0
  111. data/lib/relaton/gost/item.rb +15 -0
  112. data/lib/relaton/gost/item_base.rb +18 -0
  113. data/lib/relaton/gost/item_data.rb +6 -0
  114. data/lib/relaton/gost/processor.rb +49 -0
  115. data/lib/relaton/gost/util.rb +8 -0
  116. data/lib/relaton/gost.rb +36 -0
  117. data/lib/relaton/iala/bibdata.rb +8 -0
  118. data/lib/relaton/iala/bibitem.rb +8 -0
  119. data/lib/relaton/iala/bibliography.rb +146 -0
  120. data/lib/relaton/iala/docidentifier.rb +89 -0
  121. data/lib/relaton/iala/doctype.rb +18 -0
  122. data/lib/relaton/iala/ext.rb +32 -0
  123. data/lib/relaton/iala/item.rb +21 -0
  124. data/lib/relaton/iala/item_base.rb +18 -0
  125. data/lib/relaton/iala/item_data.rb +6 -0
  126. data/lib/relaton/iala/processor.rb +43 -0
  127. data/lib/relaton/iala/relation.rb +7 -0
  128. data/lib/relaton/iala/util.rb +8 -0
  129. data/lib/relaton/iala.rb +35 -0
  130. data/lib/relaton/iana/bibliography.rb +67 -14
  131. data/lib/relaton/iana/data_fetcher.rb +35 -5
  132. data/lib/relaton/iana/processor.rb +3 -1
  133. data/lib/relaton/iana.rb +12 -1
  134. data/lib/relaton/iec/data_fetcher.rb +7 -1
  135. data/lib/relaton/iec/hit_collection.rb +1 -1
  136. data/lib/relaton/iec/model/docidentifier.rb +9 -5
  137. data/lib/relaton/iec/model/ext.rb +2 -2
  138. data/lib/relaton/iec/processor.rb +1 -0
  139. data/lib/relaton/ieee/bibliography.rb +25 -3
  140. data/lib/relaton/ieee/data_fetcher.rb +158 -17
  141. data/lib/relaton/ieee/idams_parser.rb +18 -11
  142. data/lib/relaton/ieee/processor.rb +4 -1
  143. data/lib/relaton/ieee/rawbib_id_parser.rb +291 -86
  144. data/lib/relaton/ieee.rb +2 -1
  145. data/lib/relaton/ietf/data_fetcher.rb +295 -12
  146. data/lib/relaton/ietf/processor.rb +7 -3
  147. data/lib/relaton/ietf/rfc/entry.rb +39 -3
  148. data/lib/relaton/ietf/scraper.rb +69 -36
  149. data/lib/relaton/ietf.rb +4 -1
  150. data/lib/relaton/iho/bibliography.rb +1 -1
  151. data/lib/relaton/iho/docidentifier.rb +1 -1
  152. data/lib/relaton/index/file_io.rb +11 -11
  153. data/lib/relaton/index/file_storage.rb +6 -1
  154. data/lib/relaton/index/pool.rb +6 -1
  155. data/lib/relaton/index/shard_source.rb +201 -0
  156. data/lib/relaton/index/type.rb +63 -12
  157. data/lib/relaton/index.rb +2 -1
  158. data/lib/relaton/iso/bibliography.rb +20 -15
  159. data/lib/relaton/iso/data_fetcher.rb +3 -3
  160. data/lib/relaton/iso/data_parser.rb +17 -3
  161. data/lib/relaton/iso/hit_collection.rb +27 -15
  162. data/lib/relaton/iso/item_data.rb +22 -0
  163. data/lib/relaton/iso/model/docidentifier.rb +24 -12
  164. data/lib/relaton/iso/processor.rb +1 -0
  165. data/lib/relaton/iso/scraper.rb +19 -3
  166. data/lib/relaton/itu/bibliography.rb +9 -4
  167. data/lib/relaton/itu/data_crawler_r.rb +664 -0
  168. data/lib/relaton/itu/data_fetcher.rb +496 -50
  169. data/lib/relaton/itu/data_merge_r.rb +149 -0
  170. data/lib/relaton/itu/data_parser_r.rb +163 -89
  171. data/lib/relaton/itu/data_parser_t.rb +228 -0
  172. data/lib/relaton/itu/family_cache.rb +177 -0
  173. data/lib/relaton/itu/governor.rb +56 -0
  174. data/lib/relaton/itu/hit.rb +9 -3
  175. data/lib/relaton/itu/hit_collection.rb +258 -86
  176. data/lib/relaton/itu/model/docidentifier.rb +67 -1
  177. data/lib/relaton/itu/model/structured_identifier.rb +19 -0
  178. data/lib/relaton/itu/processor.rb +10 -4
  179. data/lib/relaton/itu/pubid.rb +27 -5
  180. data/lib/relaton/itu/recommendation_fields.rb +334 -0
  181. data/lib/relaton/itu/recommendation_parser.rb +18 -149
  182. data/lib/relaton/itu/scraper.rb +13 -3
  183. data/lib/relaton/itu.rb +2 -1
  184. data/lib/relaton/jcgm/bibdata.rb +8 -0
  185. data/lib/relaton/jcgm/bibitem.rb +8 -0
  186. data/lib/relaton/jcgm/bibliography.rb +97 -0
  187. data/lib/relaton/jcgm/data_fetcher.rb +81 -0
  188. data/lib/relaton/jcgm/docidentifier.rb +102 -0
  189. data/lib/relaton/jcgm/doctype.rb +12 -0
  190. data/lib/relaton/jcgm/ext.rb +23 -0
  191. data/lib/relaton/jcgm/item.rb +20 -0
  192. data/lib/relaton/jcgm/item_base.rb +18 -0
  193. data/lib/relaton/jcgm/item_data.rb +6 -0
  194. data/lib/relaton/jcgm/meetings_parser.rb +175 -0
  195. data/lib/relaton/jcgm/processor.rb +71 -0
  196. data/lib/relaton/jcgm/relation.rb +9 -0
  197. data/lib/relaton/jcgm/structured_identifier.rb +40 -0
  198. data/lib/relaton/jcgm/util.rb +8 -0
  199. data/lib/relaton/jcgm.rb +24 -0
  200. data/lib/relaton/jis/bibliography.rb +8 -10
  201. data/lib/relaton/jis/data_fetcher.rb +21 -19
  202. data/lib/relaton/jis/docidentifier.rb +104 -5
  203. data/lib/relaton/jis/hit.rb +18 -23
  204. data/lib/relaton/jis/hit_collection.rb +19 -18
  205. data/lib/relaton/jis/processor.rb +1 -1
  206. data/lib/relaton/jis.rb +2 -3
  207. data/lib/relaton/logger/channels/gh_issue.rb +78 -13
  208. data/lib/relaton/nist/data_fetcher.rb +63 -13
  209. data/lib/relaton/nist/docidentifier.rb +165 -0
  210. data/lib/relaton/nist/item.rb +2 -0
  211. data/lib/relaton/nist/item_base.rb +16 -0
  212. data/lib/relaton/nist/mods_parser.rb +38 -12
  213. data/lib/relaton/nist/processor.rb +2 -1
  214. data/lib/relaton/nist/relation.rb +3 -0
  215. data/lib/relaton/nist/scraper.rb +6 -3
  216. data/lib/relaton/oasis/bibliography.rb +147 -6
  217. data/lib/relaton/oasis/data_fetcher.rb +41 -5
  218. data/lib/relaton/oasis/data_parser_utils.rb +37 -3
  219. data/lib/relaton/oasis/docidentifier.rb +54 -0
  220. data/lib/relaton/oasis/item.rb +3 -0
  221. data/lib/relaton/oasis/processor.rb +7 -1
  222. data/lib/relaton/oasis.rb +14 -1
  223. data/lib/relaton/ogc/data_fetcher.rb +23 -2
  224. data/lib/relaton/ogc/docidentifier.rb +105 -0
  225. data/lib/relaton/ogc/hit_collection.rb +78 -3
  226. data/lib/relaton/ogc/processor.rb +2 -1
  227. data/lib/relaton/ogc.rb +5 -1
  228. data/lib/relaton/oiml/bibliography.rb +90 -15
  229. data/lib/relaton/oiml/docidentifier.rb +18 -3
  230. data/lib/relaton/omg/docidentifier.rb +67 -0
  231. data/lib/relaton/omg/item.rb +1 -0
  232. data/lib/relaton/omg/processor.rb +1 -0
  233. data/lib/relaton/omg/scraper.rb +61 -16
  234. data/lib/relaton/omg.rb +1 -0
  235. data/lib/relaton/plateau/bibliography.rb +10 -3
  236. data/lib/relaton/plateau/data_fetcher.rb +25 -2
  237. data/lib/relaton/plateau/handbook_parser.rb +8 -1
  238. data/lib/relaton/plateau/hit.rb +10 -2
  239. data/lib/relaton/plateau/hit_collection.rb +31 -11
  240. data/lib/relaton/plateau/processor.rb +3 -1
  241. data/lib/relaton/plateau/technical_report_parser.rb +8 -1
  242. data/lib/relaton/plateau.rb +2 -1
  243. data/lib/relaton/sdo/config.rb +34 -0
  244. data/lib/relaton/sdo/fetcher.rb +52 -0
  245. data/lib/relaton/sdo/logo.rb +95 -0
  246. data/lib/relaton/sdo/name.rb +26 -0
  247. data/lib/relaton/sdo/organization.rb +71 -0
  248. data/lib/relaton/sdo/store.rb +49 -0
  249. data/lib/relaton/sdo.rb +29 -0
  250. data/lib/relaton/version.rb +1 -1
  251. data/lib/relaton/w3c/bibliography.rb +132 -12
  252. data/lib/relaton/w3c/data_fetcher.rb +194 -16
  253. data/lib/relaton/w3c/data_parser.rb +3 -3
  254. data/lib/relaton/w3c/docidentifier.rb +48 -0
  255. data/lib/relaton/w3c/governor.rb +32 -0
  256. data/lib/relaton/w3c/item.rb +3 -0
  257. data/lib/relaton/w3c/pubid.rb +12 -0
  258. data/lib/relaton/w3c/safe_realize.rb +110 -21
  259. data/lib/relaton/w3c.rb +12 -1
  260. data/lib/relaton/xsf/bibliography.rb +61 -1
  261. data/lib/relaton/xsf/data_fetcher.rb +55 -5
  262. data/lib/relaton/xsf/docidentifier.rb +46 -0
  263. data/lib/relaton/xsf/hit_collection.rb +31 -3
  264. data/lib/relaton/xsf/item.rb +6 -0
  265. data/lib/relaton/xsf/processor.rb +1 -0
  266. data/lib/relaton/xsf.rb +5 -1
  267. data/lib/relaton.rb +42 -0
  268. metadata +135 -24
  269. data/lib/relaton/ieee/pub_id.rb +0 -161
  270. data/lib/relaton/index/id_number.rb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc5caa9d5213f1beb15ba9b972f40a3b6acbf7440d58ca1a958d6c5537f172a2
4
- data.tar.gz: d3091a1f1228ffa5fd3113c41fc0781c4150a3705586fb070acfdb24057bd9f2
3
+ metadata.gz: 37fab215eeedb21350f3e195a6a803d47680d35f75767d782600312cef05fa8e
4
+ data.tar.gz: c4542c9253678a25c5656f213ca39e12689bef778f816b70a383333c0db422d8
5
5
  SHA512:
6
- metadata.gz: 6274eda8271b12eb7fc122d6125cd9c70c42351b00298b28777731f1b8fbb7d6fad98b837f5b7f686dede0b6aab5c2da36166fb44c83d25dceb394aa5314a1f5
7
- data.tar.gz: 3fc1db36905ae3ae93b8de640370b428688d804c8a86c5f5e4510e292fe6cf30ae69a74caf2125a636d9ae7f67dc27859aa79975b647bb911b1373789aa62f36
6
+ metadata.gz: bd5901f08f8b7c4359598dc38eaa65411679884c4c860350b8b129943fa366a13dcb3f0bf91048a9922d199b364caeaf4f320cdc059898f78a5f8088364b30cd
7
+ data.tar.gz: ccc9c3ce79b02416dd0a0c31a8dab36fe13da7556e9812ed5d403d66bcdc1c4ecb938d766f2376148315e4c0c3590e243c1ef5c5e97488df8a526df934df3af3
data/README.adoc CHANGED
@@ -6,7 +6,8 @@ image:https://github.com/relaton/relaton/actions/workflows/rake.yml/badge.svg["B
6
6
  Gem for importing and caching bibliographic references to technical standards.
7
7
  Every standards-body flavor ships inside this single gem; flavor code loads on
8
8
  demand via `autoload`, and the `Relaton::Db` API routes a reference to the right
9
- flavor by its prefix. The command-line interface, `relaton-cli`, is a separate gem.
9
+ flavor by its prefix. The command-line interface,
10
+ link:gems/relaton-cli/docs/README.adoc[`relaton-cli`], is a separate gem.
10
11
 
11
12
  == Installation
12
13
 
@@ -420,6 +421,56 @@ db.load_entry("ISO(ISO 19011)")
420
421
  => nil
421
422
  ----
422
423
 
424
+ == Organization metadata
425
+
426
+ Besides bibliographic references, Relaton exposes a lightweight store of
427
+ standards-development-organization (SDO) metadata: an organization's names (with
428
+ translations) and its logos. This is a separate, non-flavor concern -- it does
429
+ not go through `Relaton::Db` -- reached through the top-level
430
+ `Relaton.organization` method.
431
+
432
+ The data (names, translations and logo variants) is served as an `index.yaml`
433
+ manifest by the https://github.com/relaton/relaton-data-sdo[`relaton-data-sdo`]
434
+ data repository, downloaded on first use and cached under `~/.relaton/sdo`.
435
+
436
+ [source,ruby]
437
+ ----
438
+ org = Relaton.organization("ISO") # case-insensitive; nil if unknown
439
+
440
+ # Names (relaton-db#132) -- default, or a translation by language code
441
+ # (passed positionally or as the language: keyword)
442
+ org.name # => "International Organization for Standardization"
443
+ org.name("fr") # => "Organisation internationale de normalisation"
444
+ org.name(language: "fr") # => "Organisation internationale de normalisation"
445
+
446
+ # Logos (metanorma#346). A single organization can have several logo variants,
447
+ # distinguished by style (the primary discriminator), format and size.
448
+ # All arguments are optional; omitting one matches any value.
449
+ org.logo_query(format: "eps", size: "700x300", style: "default")
450
+ # => [#<Relaton::Sdo::Logo style="default" format="eps" size="700x300" url=...>, ...]
451
+
452
+ org.logo_query # => every logo variant for the organization
453
+
454
+ # Fetch one logo. Returns nil if nothing matches, the sole match if one does,
455
+ # and raises Relaton::Sdo::Error if the filter is ambiguous.
456
+ logo = org.logo(format: "eps", style: "default")
457
+ logo.content # raw binary content (fetched lazily, memoized)
458
+ logo.data_uri # "data:application/postscript;base64,..." -- ready to embed
459
+ logo.save # write to a filename derived from the URL; returns the path
460
+ logo.save("/tmp/iso.eps") # ...or to a custom path
461
+ ----
462
+
463
+ The store is configurable -- the manifest URL, the cache directory, a swappable
464
+ storage backend (e.g. an S3-backed object), and the cache TTL:
465
+
466
+ [source,ruby]
467
+ ----
468
+ Relaton::Sdo.configure do |config|
469
+ config.url = "https://example.org/my-index.yaml"
470
+ config.storage_dir = "/var/cache/relaton-sdo"
471
+ end
472
+ ----
473
+
423
474
  == Layout
424
475
 
425
476
  ----
@@ -430,6 +481,7 @@ lib/
430
481
  ├── version.rb # Relaton::VERSION — the single source of truth
431
482
  ├── db.rb, db/ # Relaton::Db: registry, cache, workers pool
432
483
  ├── core/ bib/ index/ logger/ # shared infrastructure
484
+ ├── sdo/ # Relaton.organization: SDO org/logo store (non-flavor)
433
485
  └── iso/ iec/ ietf/ … 3gpp/ # ~29 flavor plugins
434
486
  spec/<flavor>/ # each flavor's spec suite
435
487
  grammar/ # shared RelaxNG test schemas (test-only, not shipped)
@@ -480,6 +532,10 @@ link:lib/relaton/core/README.adoc[core],
480
532
  link:lib/relaton/index/README.adoc[index],
481
533
  link:lib/relaton/logger/README.adoc[logger].
482
534
 
535
+ Maintaining your own dataset? See
536
+ link:docs/data-repository-format.adoc[Relaton data repository format] for the
537
+ repository layout, index file specification, and GitHub Pages publishing contract.
538
+
483
539
 
484
540
  == Development
485
541
 
data/bin/console CHANGED
@@ -6,6 +6,5 @@ require "relaton"
6
6
  # You can add fixtures and/or initialization code here to make experimenting
7
7
  # with your gem easier. You can also use a different console, if you like.
8
8
 
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
9
  require "pry"
11
10
  Pry.start
@@ -2,15 +2,20 @@
2
2
 
3
3
  module Relaton
4
4
  module ThreeGpp
5
- # Methods for search IANA standards.
5
+ # Methods for search 3GPP standards.
6
6
  module Bibliography
7
7
  SOURCE = "https://raw.githubusercontent.com/relaton/relaton-data-3gpp/v2/"
8
8
 
9
9
  # @param text [String]
10
- # @return [RelatonBib::BibliographicItem]
11
- def search(text) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
12
- index = Relaton::Index.find_or_create "3GPP", url: "#{SOURCE}#{INDEXFILE}.zip", file: "#{INDEXFILE}.yaml"
13
- row = index.search(text.sub(/^3GPP\s/, "")).min_by { |r| r[:id] }
10
+ # @return [Relaton::ThreeGpp::ItemData, nil]
11
+ def search(text)
12
+ # An unrecognized reference raises; like ISO and ETSI we let it
13
+ # propagate — relaton-cli rescues Pubid::Errors::Error and renders
14
+ # "… is not a recognized standards identifier", and API callers rescue
15
+ # it themselves. The rescue below lists transport errors only, so it
16
+ # does not swallow the parse error.
17
+ pubid = ::Pubid::Tgpp::Identifier.parse text.to_s.strip
18
+ row = best_match pubid
14
19
  return unless row
15
20
 
16
21
  url = "#{SOURCE}#{row[:file]}"
@@ -26,10 +31,10 @@ module Relaton
26
31
  raise Relaton::RequestError, e.message
27
32
  end
28
33
 
29
- # @param ref [String] the W3C standard Code to look up
34
+ # @param ref [String] the 3GPP standard Code to look up
30
35
  # @param year [String, NilClass] not used
31
36
  # @param opts [Hash] options
32
- # @return [RelatonBib::BibliographicItem]
37
+ # @return [Relaton::ThreeGpp::ItemData, nil]
33
38
  def get(ref, _year = nil, _opts = {})
34
39
  Util.info "Fetching from Relaton repository ...", key: ref
35
40
  result = search(ref)
@@ -42,6 +47,76 @@ module Relaton
42
47
  result
43
48
  end
44
49
 
50
+ private
51
+
52
+ #
53
+ # Find the index row for a reference, newest version first.
54
+ #
55
+ # Passing the pubid to `Index::Type#search` is what enables the binary
56
+ # search on `id.root.number` — `Type#search_candidates` narrows only when
57
+ # its argument is NOT a String, so the plain text this used to pass
58
+ # disabled the narrowing however the index was built. With 88,464 rows in
59
+ # 3,767 number buckets that is the whole point of the index-v2 migration.
60
+ #
61
+ # The rows are selected with pubid's subset match `pubid === row`, the
62
+ # default of `Index::Type#search`. `release` and `version` are 3GPP's
63
+ # only optional components: when the reference omits one, it matches
64
+ # any value, so a bare `3GPP TS 23.207` still finds the `REL-19/19.0.0`
65
+ # row. pubid declares `suffix` and `parts` strict for 3GPP, because they
66
+ # are part of the document code (`TS 29.198-04-1`, `TR 00.01U`), not
67
+ # qualifiers. The document type is the identifier's class, and `===`
68
+ # requires the same class, so `TS 23.207` and `TR 23.207` never match.
69
+ #
70
+ # @param pubid [::Pubid::Tgpp::Identifier]
71
+ # @return [Hash, nil] the winning index row (`{ id:, file: }`)
72
+ #
73
+ def best_match(pubid)
74
+ index.search(pubid)
75
+ .max_by { |r| [version_key(r[:id].version), r[:id].to_s, r[:file]] }
76
+ end
77
+
78
+ #
79
+ # Order key for a 3GPP version — the highest version wins.
80
+ #
81
+ # Versions are dotted strings, so they must be compared segment by
82
+ # segment as integers rather than as text: `19.0.0` is newer than
83
+ # `4.0.0` but loses as a string. This is a deliberate behaviour change.
84
+ # The previous key, `min_by { r[:id] }` over raw strings, returned
85
+ # `REL-10/10.0.0` for `3GPP TS 23.207` — neither the newest nor the
86
+ # oldest of its 37 rows, just whichever sorted first as text. Scored
87
+ # against each document's own publication date over 870 documents in 59
88
+ # multi-row groups, this key picks the newest published document 51/59
89
+ # (86%) of the time; the old one managed 1/59 (2%).
90
+ #
91
+ # No release key is layered on top. Ranking releases numerically is wrong
92
+ # (the chronological order is Ph1, Ph2, REL-96 … REL-99, Release 2000,
93
+ # UMTS, REL-4 … REL-21, so REL-99 would outrank REL-19) and a
94
+ # hand-maintained 27-token table would disagree with this key on only 9
95
+ # of 3,982 multi-row groups — all draft TRs carried into a later release
96
+ # at a lower version, where the release answer is not clearly better.
97
+ #
98
+ # An absent version sorts below every present one. Ties break on the
99
+ # rendered id and then the file path, because the index sort is not
100
+ # stable.
101
+ #
102
+ # @param version [String, nil]
103
+ # @return [Array<Integer>]
104
+ #
105
+ def version_key(version)
106
+ version.to_s.split(".").map(&:to_i)
107
+ end
108
+
109
+ # The index is pubid-backed: `pubid_class:` is what makes Relaton::Index
110
+ # deserialize the rows into identifiers, sort them by `id.root.number`,
111
+ # and let `Type#search` bsearch. Without it the rows stay raw hashes,
112
+ # `FileIO#sorted` stays false, and every lookup scans all 88,464 of them.
113
+ def index
114
+ Relaton::Index.find_or_create(
115
+ "3GPP", url: "#{SOURCE}#{INDEXFILE}.zip", file: "#{INDEXFILE}.yaml",
116
+ pubid_class: ::Pubid::Tgpp::Identifier
117
+ )
118
+ end
119
+
45
120
  extend Bibliography
46
121
  end
47
122
  end
@@ -14,8 +14,16 @@ module Relaton
14
14
  Util.error msg
15
15
  end
16
16
 
17
+ # The index is pubid-backed: `pubid_class:` makes Relaton::Index sort the
18
+ # rows by `id.root.number` on save and serialize each id to its
19
+ # `_type: pubid:3gpp:*` hash. Without it a v1-shaped file would be
20
+ # written under a v2 name, with no error and no narrowing — FileIO#save
21
+ # only calls `to_hash` when the value is an instance of `pubid_class`.
17
22
  def index
18
- @index ||= Relaton::Index.find_or_create "3gpp", file: "#{INDEXFILE}.yaml"
23
+ @index ||= Relaton::Index.find_or_create(
24
+ "3gpp", file: "#{INDEXFILE}.yaml",
25
+ pubid_class: ::Pubid::Tgpp::Identifier
26
+ )
19
27
  end
20
28
 
21
29
  #
@@ -104,17 +112,57 @@ module Relaton
104
112
  return unless bib
105
113
 
106
114
  bib1 = bib
107
- file = output_file(bib1.docnumber)
115
+ # A DISTINCT docnumber that sanitizes to the same filename must not be
116
+ # merged into the other document — give it a path of its own. Only a
117
+ # genuine duplicate (same docnumber, same path) still merges.
118
+ file = unique_output_file(bib1.docnumber)
119
+ # A reserved path only ever belongs to one docnumber, so a hit here is
120
+ # the same document again and still merges. Checked FIRST: a
121
+ # disambiguated path stays != output_file forever, so gating the merge
122
+ # on that comparison would drop the merge enrichment on every repeat of
123
+ # a disambiguated docnumber.
108
124
  if @files.include? file
109
125
  bib1 = merge_duplication bib1, file
110
126
  Util.warn "File #{file} already exists. Document: #{bib.docnumber}" if bib1.nil?
111
127
  else
128
+ if file != output_file(bib1.docnumber)
129
+ Util.warn "File #{output_file bib1.docnumber} already exists. " \
130
+ "Document: #{bib.docnumber}. Writing #{file} instead."
131
+ end
112
132
  @files << file
113
- index.add_or_update bib1.docnumber, file
133
+ index_primary bib1.docidentifier&.detect(&:primary), file
114
134
  end
115
135
  File.write file, serialize(bib1), encoding: "UTF-8" unless bib1.nil?
116
136
  end
117
137
 
138
+ #
139
+ # Add a document's primary id to the index.
140
+ #
141
+ # The pubid is already parsed on `Docidentifier`, so index it directly
142
+ # rather than re-parsing `docnumber`. `pubid.to_s` (no publisher token)
143
+ # reproduces the `docnumber` string this used to store byte-for-byte, so
144
+ # the index key does not move; `docnumber` still names the output file.
145
+ #
146
+ # An id pubid cannot rebuild is recorded in `@errors` — the inherited
147
+ # `report_errors` logs a String value as the message — and the row is
148
+ # skipped rather than indexed unparsed: `Relaton::Index` rejects the
149
+ # WHOLE index if a single row fails to deserialize, and its sort calls
150
+ # `.root.number` on every id. The data file is still written, so the
151
+ # document is unindexed, never lost. (The W3C precedent.)
152
+ #
153
+ # @param [Docidentifier, nil] docid the document's primary identifier
154
+ # @param [String] file path the document was written to
155
+ #
156
+ def index_primary(docid, file)
157
+ if docid&.pubid
158
+ index.add_or_update docid.pubid, file
159
+ return
160
+ end
161
+
162
+ id = docid&.content || file
163
+ @errors[id.to_s] = "Unparseable primary id `#{id}` was not indexed (#{file})"
164
+ end
165
+
118
166
  #
119
167
  # Merge duplication
120
168
  #
@@ -0,0 +1,114 @@
1
+ module Relaton
2
+ module ThreeGpp
3
+ # Structured 3GPP document identifier. `content` stays the canonical string
4
+ # (e.g. "3GPP TS 29.198-04-1:REL-5/5.0.0") and the parsed
5
+ # `Pubid::Tgpp::Identifier` is kept alongside it in `@pubid`.
6
+ #
7
+ # Without this subclass 3GPP items inherit `Bib::Docidentifier`, whose
8
+ # `remove_part!` / `remove_date!` / `to_all_parts!` all raise
9
+ # NotImplementedError — so `ItemData#to_all_parts` and
10
+ # `#to_most_recent_reference`, which broadcast to every docidentifier,
11
+ # raised on every 3GPP item.
12
+ class Docidentifier < Bib::Docidentifier
13
+ attr_reader :pubid
14
+
15
+ # Capture the inherited (LocalizedMarkedUpString) content setter before
16
+ # overriding #content=, so #refresh_content! can write the re-rendered
17
+ # string back WITHOUT re-parsing (a re-parse would rebuild @pubid from
18
+ # the string and discard the in-place mutations just made).
19
+ alias_method :store_content, :content=
20
+
21
+ def initialize(attrs = {}, options = {})
22
+ pubid = attrs.is_a?(Hash) ? attrs.delete(:pubid) : nil
23
+ attrs[:content] ||= render(pubid) if pubid
24
+ super
25
+ @pubid = pubid if pubid
26
+ end
27
+
28
+ def content=(value)
29
+ super
30
+ return unless value
31
+
32
+ @pubid = begin
33
+ # Rescue LoadError so a missing pubid gem degrades to a plain string,
34
+ # and StandardError so non-3GPP or unparseable content just leaves
35
+ # @pubid nil rather than raising. Deserialization reaches this setter
36
+ # without the flavor entry file necessarily having been loaded, hence
37
+ # the local require.
38
+ require "pubid"
39
+ ::Pubid::Tgpp::Identifier.parse(value.to_s)
40
+ rescue LoadError, StandardError
41
+ nil
42
+ end
43
+ end
44
+
45
+ # `Pubid::Tgpp::Identifier` models number/suffix/parts/release/version.
46
+ # It has no date, and `suffix` is folded into the document code, so:
47
+ #
48
+ # * #remove_part! clears `parts` — a real, separable component here
49
+ # (unlike IALA's, which is a no-op): "TS 29.198-04-1" -> "TS 29.198".
50
+ # * #remove_date! clears `release` AND `version`. 3GPP carries no date;
51
+ # those two are its version discriminators — the same pair
52
+ # `Bibliography#ignored` treats as omittable — so clearing both gives
53
+ # the version-agnostic ("most recent") reference "TS 23.207".
54
+ # * #to_all_parts! does both, then wraps `@pubid` in pubid's `AllParts`.
55
+ # `content` stays the plain stripped id — the Tgpp renderer has no
56
+ # "(all parts)" marker, and `content` is cached from the pre-wrap
57
+ # pubid on purpose. `#pubid` itself, read directly, now answers
58
+ # `all_parts? == true` and renders WITH pubid's generic marker
59
+ # (`#to_s`), since it's the wrapper.
60
+ #
61
+ # All three no-op safely when @pubid is nil (parse failed, or the pubid
62
+ # gem is unavailable), so ItemData#to_all_parts and
63
+ # #to_most_recent_reference never raise on 3GPP items.
64
+
65
+ def remove_part!
66
+ clear_attrs! :parts
67
+ end
68
+
69
+ def remove_date!
70
+ clear_attrs! :release, :version
71
+ end
72
+
73
+ def to_all_parts!
74
+ return if !@pubid || @pubid.all_parts?
75
+
76
+ # `#exclude` (no args) rebuilds a full independent copy — `remove_part!`
77
+ # / `remove_date!` mutate `@pubid` in place, so a bare reference here
78
+ # would lose the original part/version to that mutation too.
79
+ original = @pubid.exclude
80
+ remove_part!
81
+ remove_date!
82
+ @pubid = original.to_all_parts
83
+ end
84
+
85
+ private
86
+
87
+ def clear_attrs!(*attrs)
88
+ return unless @pubid
89
+
90
+ cleared = attrs.select { |attr| @pubid.respond_to?("#{attr}=") }
91
+ return if cleared.empty?
92
+
93
+ # `parts` is a collection: empty it rather than nil it, so #code keeps
94
+ # rendering through `parts.map` instead of tripping over nil.
95
+ cleared.each do |attr|
96
+ @pubid.public_send("#{attr}=", attr == :parts ? [] : nil)
97
+ end
98
+ refresh_content!
99
+ end
100
+
101
+ def refresh_content!
102
+ store_content(render(@pubid)) if @pubid
103
+ end
104
+
105
+ # `Pubid::Tgpp::Identifier#to_s` defaults to OMITTING the "3GPP " token —
106
+ # deliberately, so it reproduces the index id. The stored docidentifier
107
+ # content carries the prefix ("3GPP TS 23.207:REL-4/4.0.0"), so every
108
+ # render here must opt back in or a mutation would silently strip it.
109
+ def render(pubid)
110
+ pubid.to_s(with_publisher: true)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -1,5 +1,6 @@
1
1
  require_relative "item_data"
2
2
  require_relative "ext"
3
+ require_relative "docidentifier"
3
4
 
4
5
  module Relaton
5
6
  module ThreeGpp
@@ -7,6 +8,11 @@ module Relaton
7
8
  model ItemData
8
9
 
9
10
  attribute :ext, Ext
11
+ # Shadow the Bib::Docidentifier declaration in Bib::ItemShared so items
12
+ # carry the pubid-backed 3GPP identifier. Bibitem and Bibdata subclass
13
+ # Item, so this one declaration covers all three.
14
+ attribute :docidentifier, Docidentifier, collection: true,
15
+ initialize_empty: true
10
16
  end
11
17
  end
12
18
  end
@@ -94,7 +94,7 @@ module Relaton
94
94
  #
95
95
  def parse_docid
96
96
  @errors[:docid] &&= @row[0].nil? || @row[0].empty?
97
- [Bib::Docidentifier.new(type: "3GPP", content: "3GPP #{number}", primary: true)]
97
+ [Docidentifier.new(type: "3GPP", content: "3GPP #{number}", primary: true)]
98
98
  end
99
99
 
100
100
  #
@@ -10,6 +10,7 @@ module Relaton
10
10
  @prefix = "3GPP"
11
11
  @defaultprefix = %r{^3GPP\s}
12
12
  @idtype = "3GPP"
13
+ @pubid_flavor = :Tgpp # global prefixes sourced from Pubid::Tgpp.prefixes
13
14
  @datasets = %w[status-smg-3GPP status-smg-3GPP-force]
14
15
  end
15
16
 
@@ -61,7 +62,9 @@ module Relaton
61
62
  #
62
63
  def remove_index_file
63
64
  require_relative "../3gpp"
64
- Relaton::Index.find_or_create("3GPP", url: true, file: "#{INDEXFILE}.yaml").remove_file
65
+ Relaton::Index.find_or_create(
66
+ "3GPP", url: true, file: "#{INDEXFILE}.yaml"
67
+ ).remove_file
65
68
  end
66
69
  end
67
70
  end
data/lib/relaton/3gpp.rb CHANGED
@@ -1,4 +1,8 @@
1
1
  require "net/http"
2
+ # pubid loads with the flavor, for the ::Pubid::Tgpp::Identifier that the
3
+ # index code names. Processor#remove_index_file names no pubid class: the
4
+ # delete never reads the index (see lib/relaton/index/CLAUDE.md).
5
+ require "pubid"
2
6
  require "relaton/index"
3
7
  require "relaton/core"
4
8
  require "relaton/bib"
@@ -11,7 +15,7 @@ require_relative "3gpp/bibliography"
11
15
 
12
16
  module Relaton
13
17
  module ThreeGpp
14
- INDEXFILE = "index-v1".freeze
18
+ INDEXFILE = "index-v2".freeze
15
19
 
16
20
  # Returns hash of XML reammar
17
21
  # @return [String]
@@ -0,0 +1,8 @@
1
+ module Relaton
2
+ module Adobe
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 Adobe
3
+ class Bibitem < Item
4
+ model ItemData
5
+ include Bib::BibitemShared
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,92 @@
1
+ require "net/http"
2
+
3
+ module Relaton
4
+ module Adobe
5
+ # Retrieval front-end for the Adobe flavor. Parses a reference with
6
+ # Pubid::Adobe, looks it up in the `relaton-data-adobe` index, fetches the
7
+ # matching YAML record, and deserializes it into a Relaton::Adobe::Item.
8
+ # Modelled on Relaton::Oiml::Bibliography (pubid-backed, index via
9
+ # `pubid_class`); adapted because Adobe identifiers carry no edition
10
+ # year/language, so there is no per-edition selection — each reference maps
11
+ # to a single record.
12
+ module Bibliography
13
+ ENDPOINT = "https://raw.githubusercontent.com/relaton/relaton-data-adobe/main/".freeze
14
+
15
+ class << self
16
+ # Search for an Adobe publication by its identifier.
17
+ #
18
+ # @param text [String, Pubid::Adobe::Identifier] the Adobe reference to
19
+ # look up (e.g. "Adobe Technical Note #5014", "ATN5014",
20
+ # "Adobe Publication adobe-glyph-list")
21
+ # @param _year [String, nil] unused (Adobe ids carry no edition year)
22
+ # @param _opts [Hash] options (unused)
23
+ # @return [Relaton::Adobe::Item, nil]
24
+ def search(text, _year = nil, _opts = {})
25
+ pubid = pubid_for(text) or return
26
+ Util.info "Fetching from Relaton repository ...", key: pubid.to_s
27
+ # Tech notes narrow by number via binary search; publications have no
28
+ # number, so pass nil to scan (Relaton::Index::Type#search returns all
29
+ # rows for a nil id, then applies the block).
30
+ query = pubid.root.number ? pubid : nil
31
+ row = index.search(query) { |r| pubid_match?(r[:id], pubid) }.first
32
+ unless row
33
+ Util.info "Not found.", key: pubid.to_s
34
+ return
35
+ end
36
+
37
+ uri = URI("#{ENDPOINT}#{row[:file]}")
38
+ resp = Net::HTTP.get_response uri
39
+ unless resp.code == "200"
40
+ raise Relaton::RequestError, "Could not access #{uri}: HTTP #{resp.code}"
41
+ end
42
+
43
+ item = Relaton::Adobe::Item.from_yaml resp.body
44
+ Util.info "Found: `#{item.docidentifier.first&.content}`", key: pubid.to_s
45
+ item.tap { |i| i.fetched = Date.today.to_s }
46
+ rescue SocketError, Errno::EINVAL, Errno::ECONNRESET, EOFError,
47
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
48
+ Net::ProtocolError, Net::ReadTimeout, OpenSSL::SSL::SSLError,
49
+ Errno::ETIMEDOUT => e
50
+ raise Relaton::RequestError, "Could not access #{uri}: #{e.message}"
51
+ end
52
+
53
+ def get(ref, year = nil, opts = {})
54
+ search(ref, year, opts)
55
+ end
56
+
57
+ private
58
+
59
+ # Coerce a reference to a Pubid::Adobe identifier. A non-canonical or
60
+ # non-Adobe string (e.g. a title-cased "Adobe Glyph List") is a graceful
61
+ # miss (logged, returns nil) rather than a raised Parslet error — the
62
+ # ref reached here only because it matched the flavor's prefix regex.
63
+ def pubid_for(text)
64
+ return text unless text.is_a?(String)
65
+
66
+ ::Pubid::Adobe.parse(text)
67
+ rescue StandardError
68
+ Util.info "Not found.", key: text
69
+ nil
70
+ end
71
+
72
+ def index
73
+ Relaton::Index.find_or_create(
74
+ :adobe,
75
+ url: "#{ENDPOINT}#{INDEXFILE}.zip",
76
+ file: "#{INDEXFILE}.yaml",
77
+ pubid_class: ::Pubid::Adobe::Identifier,
78
+ )
79
+ end
80
+
81
+ # Identity match on the human render: pubid renders a tech note as
82
+ # `Adobe TN <n>` (dropping the filename slug, so `ATN5014` and
83
+ # `Adobe Technical Note #5014` both match a record stored with its slug)
84
+ # and a publication as `Adobe Publication <slug>` including its version.
85
+ # Both `row_id` and `query` are Pubid::Adobe identifiers.
86
+ def pubid_match?(row_id, query)
87
+ row_id.to_s == query.to_s
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,49 @@
1
+ module Relaton
2
+ module Adobe
3
+ # Structured Adobe document identifier. The docid string is the canonical
4
+ # form (e.g. "Adobe TN 5014", "Adobe Publication adobe-glyph-list"); its
5
+ # `content` is soft-parsed into a Pubid::Adobe identifier kept in `@pubid`
6
+ # (a TechNote or Publication), mirroring Relaton::Oiml::Docidentifier.
7
+ #
8
+ # Parsing is soft: a non-Adobe or non-canonical string (e.g. a title-cased
9
+ # "Adobe Glyph List") leaves `@pubid` nil rather than raising, so the plain
10
+ # `content` string still round-trips.
11
+ class Docidentifier < Bib::Docidentifier
12
+ attr_reader :pubid
13
+
14
+ def initialize(attrs = {}, options = {})
15
+ pubid = attrs.is_a?(Hash) ? attrs.delete(:pubid) : nil
16
+ attrs[:content] ||= pubid.to_s if pubid
17
+ super
18
+ @pubid = pubid if pubid
19
+ end
20
+
21
+ def content=(value)
22
+ super
23
+ @pubid = ::Pubid::Adobe.parse(value) if value
24
+ rescue StandardError
25
+ @pubid = nil
26
+ end
27
+
28
+ # Adobe identifiers carry no part/date/all-parts component, so the base
29
+ # class's abstract mutation hooks (which otherwise raise NotImplementedError)
30
+ # are implemented as safe no-ops over the wrapped pubid: they keep
31
+ # `Bib::ItemData#to_most_recent_reference` / `#to_all_parts` from raising on
32
+ # Adobe items, and never change the rendered `content`.
33
+ def remove_part!
34
+ @pubid&.part = nil
35
+ end
36
+
37
+ def remove_date!
38
+ return unless @pubid
39
+
40
+ @pubid.date = nil
41
+ self.content = @pubid.to_s
42
+ end
43
+
44
+ def to_all_parts!
45
+ @pubid &&= @pubid.to_all_parts
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,14 @@
1
+ module Relaton
2
+ module Adobe
3
+ # Adobe publication document types. The value is carried in the
4
+ # inherited `content` attribute (e.g. `tech-note`, `publication`).
5
+ # The list mirrors the categories Adobe publishes through this
6
+ # dataset (font tech notes and named publications).
7
+ class Doctype < Bib::Doctype
8
+ TYPES = %w[
9
+ tech-note
10
+ publication
11
+ ].freeze
12
+ end
13
+ end
14
+ end