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.
- checksums.yaml +4 -4
- data/bin/relaton +20 -0
- data/lib/relaton/3gpp/processor.rb +1 -1
- data/lib/relaton/3gpp.rb +3 -3
- data/lib/relaton/adobe/processor.rb +1 -1
- data/lib/relaton/adobe.rb +3 -3
- data/lib/relaton/bib/item_data.rb +2 -2
- data/lib/relaton/bib/model/docidentifier.rb +17 -2
- data/lib/relaton/bib.rb +2 -2
- data/lib/relaton/bibcollection.rb +146 -0
- data/lib/relaton/bibdata.rb +92 -0
- data/lib/relaton/bipm/data_outcomes_parser.rb +6 -1
- data/lib/relaton/bipm/processor.rb +1 -1
- data/lib/relaton/bipm.rb +3 -3
- data/lib/relaton/bsi/processor.rb +1 -1
- data/lib/relaton/bsi.rb +1 -1
- data/lib/relaton/calconnect/data_fetcher.rb +2 -2
- data/lib/relaton/calconnect/model/item.rb +1 -1
- data/lib/relaton/calconnect/processor.rb +1 -1
- data/lib/relaton/calconnect.rb +2 -2
- data/lib/relaton/ccsds/data/fetcher.rb +1 -1
- data/lib/relaton/ccsds/model/item.rb +1 -1
- data/lib/relaton/ccsds/processor.rb +1 -1
- data/lib/relaton/ccsds/util.rb +1 -1
- data/lib/relaton/ccsds.rb +1 -1
- data/lib/relaton/cen/processor.rb +1 -1
- data/lib/relaton/cen.rb +2 -2
- data/lib/relaton/cie/data_fetcher.rb +3 -3
- data/lib/relaton/cie/processor.rb +1 -1
- data/lib/relaton/cie.rb +2 -2
- data/lib/relaton/cli/base_convertor.rb +134 -0
- data/lib/relaton/cli/command.rb +396 -0
- data/lib/relaton/cli/data_fetcher.rb +20 -0
- data/lib/relaton/cli/frontend/dist/404.html +18 -0
- data/lib/relaton/cli/frontend/dist/app.iife.js +1 -0
- data/lib/relaton/cli/frontend/dist/style.css +3 -0
- data/lib/relaton/cli/frontend/index.html +14 -0
- data/lib/relaton/cli/frontend/package-lock.json +2945 -0
- data/lib/relaton/cli/frontend/package.json +29 -0
- data/lib/relaton/cli/frontend/public/404.html +18 -0
- data/lib/relaton/cli/frontend/src/App.test.ts +501 -0
- data/lib/relaton/cli/frontend/src/App.vue +506 -0
- data/lib/relaton/cli/frontend/src/app.ts +90 -0
- data/lib/relaton/cli/frontend/src/components/DocumentDetail.test.ts +142 -0
- data/lib/relaton/cli/frontend/src/components/DocumentDetail.vue +228 -0
- data/lib/relaton/cli/frontend/src/components/DocumentRow.vue +88 -0
- data/lib/relaton/cli/frontend/src/components/Icon.test.ts +44 -0
- data/lib/relaton/cli/frontend/src/components/Icon.vue +55 -0
- data/lib/relaton/cli/frontend/src/components/Pagination.vue +26 -0
- data/lib/relaton/cli/frontend/src/dev/main.ts +100 -0
- data/lib/relaton/cli/frontend/src/dev/sample-data.json +71 -0
- data/lib/relaton/cli/frontend/src/lib/filter.test.ts +88 -0
- data/lib/relaton/cli/frontend/src/lib/filter.ts +95 -0
- data/lib/relaton/cli/frontend/src/lib/hydrate.test.ts +102 -0
- data/lib/relaton/cli/frontend/src/lib/hydrate.ts +55 -0
- data/lib/relaton/cli/frontend/src/lib/shards.test.ts +319 -0
- data/lib/relaton/cli/frontend/src/lib/shards.ts +183 -0
- data/lib/relaton/cli/frontend/src/lib/types.ts +148 -0
- data/lib/relaton/cli/frontend/src/lib/url.test.ts +151 -0
- data/lib/relaton/cli/frontend/src/lib/url.ts +111 -0
- data/lib/relaton/cli/frontend/src/shims-vue.d.ts +5 -0
- data/lib/relaton/cli/frontend/src/styles/index.css +27 -0
- data/lib/relaton/cli/frontend/src/test/fetch.ts +58 -0
- data/lib/relaton/cli/frontend/tsconfig.json +20 -0
- data/lib/relaton/cli/frontend/vite.config.ts +42 -0
- data/lib/relaton/cli/frontend/vitest.config.ts +11 -0
- data/lib/relaton/cli/frontend_assets.rb +58 -0
- data/lib/relaton/cli/full_text_search.rb +98 -0
- data/lib/relaton/cli/i18n.yaml +1 -0
- data/lib/relaton/cli/index_item_normalizer.rb +380 -0
- data/lib/relaton/cli/index_site_generator.rb +1043 -0
- data/lib/relaton/cli/relaton_file.rb +290 -0
- data/lib/relaton/cli/subcommand_collection.rb +263 -0
- data/lib/relaton/cli/subcommand_db.rb +81 -0
- data/lib/relaton/cli/templates/_document.liquid +111 -0
- data/lib/relaton/cli/templates/_index.liquid +61 -0
- data/lib/relaton/cli/templates/index/page.liquid +20 -0
- data/lib/relaton/cli/templates/index-style.css +1017 -0
- data/lib/relaton/cli/util.rb +8 -0
- data/lib/relaton/cli/xml_convertor.rb +47 -0
- data/lib/relaton/cli/xml_to_html_renderer.rb +117 -0
- data/lib/relaton/cli/yaml_convertor.rb +102 -0
- data/lib/relaton/cli.rb +103 -0
- data/lib/relaton/core/processor.rb +1 -1
- data/lib/relaton/core.rb +1 -1
- data/lib/relaton/db/registry.rb +1 -1
- data/lib/relaton/db.rb +1 -1
- data/lib/relaton/doi/CLAUDE.md +1 -1
- data/lib/relaton/doi/parser.rb +2 -1
- data/lib/relaton/doi/processor.rb +1 -1
- data/lib/relaton/doi.rb +4 -4
- data/lib/relaton/easc/processor.rb +1 -1
- data/lib/relaton/easc.rb +3 -3
- data/lib/relaton/ecma/data_fetcher.rb +1 -1
- data/lib/relaton/ecma/processor.rb +1 -1
- data/lib/relaton/ecma.rb +2 -2
- data/lib/relaton/element_finder.rb +29 -0
- data/lib/relaton/etsi/data_fetcher.rb +2 -2
- data/lib/relaton/etsi/item.rb +1 -1
- data/lib/relaton/etsi/processor.rb +2 -2
- data/lib/relaton/etsi.rb +1 -1
- data/lib/relaton/evs/processor.rb +1 -1
- data/lib/relaton/evs.rb +2 -2
- data/lib/relaton/gb/CLAUDE.md +1 -1
- data/lib/relaton/gb/bibliography.rb +1 -1
- data/lib/relaton/gb/processor.rb +1 -1
- data/lib/relaton/gb.rb +2 -2
- data/lib/relaton/gost/processor.rb +1 -1
- data/lib/relaton/gost.rb +3 -3
- data/lib/relaton/iala/processor.rb +1 -1
- data/lib/relaton/iala.rb +3 -3
- data/lib/relaton/iana/data_fetcher.rb +1 -1
- data/lib/relaton/iana/processor.rb +1 -1
- data/lib/relaton/iana.rb +2 -2
- data/lib/relaton/iec/data_fetcher.rb +1 -1
- data/lib/relaton/iec/processor.rb +1 -1
- data/lib/relaton/iec.rb +2 -2
- data/lib/relaton/ieee/processor.rb +1 -1
- data/lib/relaton/ieee.rb +2 -2
- data/lib/relaton/ietf/CLAUDE.md +1 -1
- data/lib/relaton/ietf/data_fetcher.rb +1 -1
- data/lib/relaton/ietf/processor.rb +1 -1
- data/lib/relaton/ietf.rb +2 -2
- data/lib/relaton/iho/CLAUDE.md +3 -3
- data/lib/relaton/iho/bibliography.rb +41 -27
- data/lib/relaton/iho/hash_parser_v1.rb +1 -1
- data/lib/relaton/iho/processor.rb +4 -3
- data/lib/relaton/iho.rb +2 -2
- data/lib/relaton/index.rb +1 -1
- data/lib/relaton/isbn/processor.rb +1 -1
- data/lib/relaton/isbn.rb +1 -1
- data/lib/relaton/iso/hash_parser_v1.rb +1 -1
- data/lib/relaton/iso/processor.rb +1 -1
- data/lib/relaton/iso.rb +3 -3
- data/lib/relaton/itu/processor.rb +1 -1
- data/lib/relaton/itu.rb +3 -3
- data/lib/relaton/jcgm/processor.rb +1 -1
- data/lib/relaton/jcgm.rb +2 -2
- data/lib/relaton/jis/processor.rb +1 -1
- data/lib/relaton/jis.rb +2 -2
- data/lib/relaton/nist/CLAUDE.md +11 -1
- data/lib/relaton/nist/README.adoc +7 -1
- data/lib/relaton/nist/hit_collection.rb +25 -1
- data/lib/relaton/nist/mods_parser.rb +32 -8
- data/lib/relaton/nist/processor.rb +1 -1
- data/lib/relaton/nist/pubs_export.rb +69 -11
- data/lib/relaton/nist.rb +2 -2
- data/lib/relaton/oasis/processor.rb +1 -1
- data/lib/relaton/oasis.rb +2 -2
- data/lib/relaton/ogc/CLAUDE.md +1 -1
- data/lib/relaton/ogc/processor.rb +1 -1
- data/lib/relaton/ogc.rb +2 -2
- data/lib/relaton/oiml/processor.rb +1 -1
- data/lib/relaton/oiml.rb +2 -2
- data/lib/relaton/omg/CLAUDE.md +1 -1
- data/lib/relaton/omg/processor.rb +1 -1
- data/lib/relaton/omg.rb +1 -1
- data/lib/relaton/plateau/CLAUDE.md +1 -1
- data/lib/relaton/plateau/data_fetcher.rb +1 -1
- data/lib/relaton/plateau/processor.rb +1 -1
- data/lib/relaton/plateau.rb +2 -2
- data/lib/relaton/render/general.rb +319 -0
- data/lib/relaton/render/iso690/csl_import.rb +129 -0
- data/lib/relaton/render/iso690/element.rb +153 -0
- data/lib/relaton/render/iso690/elements/access.rb +44 -0
- data/lib/relaton/render/iso690/elements/authorizer.rb +45 -0
- data/lib/relaton/render/iso690/elements/citeid.rb +39 -0
- data/lib/relaton/render/iso690/elements/component_part.rb +97 -0
- data/lib/relaton/render/iso690/elements/creator.rb +267 -0
- data/lib/relaton/render/iso690/elements/edition.rb +97 -0
- data/lib/relaton/render/iso690/elements/extent.rb +71 -0
- data/lib/relaton/render/iso690/elements/identifier.rb +46 -0
- data/lib/relaton/render/iso690/elements/location.rb +73 -0
- data/lib/relaton/render/iso690/elements/medium.rb +32 -0
- data/lib/relaton/render/iso690/elements/numeration.rb +26 -0
- data/lib/relaton/render/iso690/elements/production.rb +111 -0
- data/lib/relaton/render/iso690/elements/pub_date.rb +63 -0
- data/lib/relaton/render/iso690/elements/series.rb +66 -0
- data/lib/relaton/render/iso690/elements/size.rb +45 -0
- data/lib/relaton/render/iso690/elements/status.rb +30 -0
- data/lib/relaton/render/iso690/elements/stddoc.rb +40 -0
- data/lib/relaton/render/iso690/elements/title.rb +56 -0
- data/lib/relaton/render/iso690/elements/updated.rb +38 -0
- data/lib/relaton/render/iso690/elements.rb +67 -0
- data/lib/relaton/render/iso690/fields.rb +142 -0
- data/lib/relaton/render/iso690/i18n/en.yml +48 -0
- data/lib/relaton/render/iso690/i18n/fr.yml +14 -0
- data/lib/relaton/render/iso690/i18n/ja.yml +38 -0
- data/lib/relaton/render/iso690/i18n.rb +83 -0
- data/lib/relaton/render/iso690/identifier_kinds.rb +29 -0
- data/lib/relaton/render/iso690/index_renderer.rb +57 -0
- data/lib/relaton/render/iso690/kinds.rb +53 -0
- data/lib/relaton/render/iso690/renderer.rb +150 -0
- data/lib/relaton/render/iso690/rules/bipm.rb +186 -0
- data/lib/relaton/render/iso690/rules/jis.rb +103 -0
- data/lib/relaton/render/iso690/rules/nist.rb +384 -0
- data/lib/relaton/render/iso690/rules.rb +227 -0
- data/lib/relaton/render/iso690/style/locale.rb +59 -0
- data/lib/relaton/render/iso690/style/name_form.rb +87 -0
- data/lib/relaton/render/iso690/style/scheme.rb +33 -0
- data/lib/relaton/render/iso690/style/sort_rule.rb +25 -0
- data/lib/relaton/render/iso690/style/template_map.rb +40 -0
- data/lib/relaton/render/iso690/style/type_template.rb +45 -0
- data/lib/relaton/render/iso690/style.rb +227 -0
- data/lib/relaton/render/iso690/styles/apa.yml +45 -0
- data/lib/relaton/render/iso690/styles/author-date.yml +31 -0
- data/lib/relaton/render/iso690/styles/chicago.yml +49 -0
- data/lib/relaton/render/iso690/styles/ieee-sa.yml +45 -0
- data/lib/relaton/render/iso690/styles/jis-en.yml +68 -0
- data/lib/relaton/render/iso690/styles/jis-ja.yml +67 -0
- data/lib/relaton/render/iso690/styles/lncs.yml +54 -0
- data/lib/relaton/render/iso690/styles/mla.yml +56 -0
- data/lib/relaton/render/iso690/template.rb +248 -0
- data/lib/relaton/render/iso690.rb +28 -0
- data/lib/relaton/sae/CLAUDE.md +89 -0
- data/lib/relaton/sae/bibdata.rb +8 -0
- data/lib/relaton/sae/bibitem.rb +8 -0
- data/lib/relaton/sae/bibliography.rb +128 -0
- data/lib/relaton/sae/data_fetcher.rb +219 -0
- data/lib/relaton/sae/doi_code.rb +83 -0
- data/lib/relaton/sae/item.rb +10 -0
- data/lib/relaton/sae/item_data.rb +6 -0
- data/lib/relaton/sae/processor.rb +62 -0
- data/lib/relaton/sae/util.rb +9 -0
- data/lib/relaton/sae.rb +37 -0
- data/lib/relaton/un/CLAUDE.md +1 -1
- data/lib/relaton/un/processor.rb +1 -1
- data/lib/relaton/un.rb +1 -1
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton/w3c/bibliography.rb +1 -1
- data/lib/relaton/w3c/data_fetcher.rb +1 -1
- data/lib/relaton/w3c/processor.rb +1 -1
- data/lib/relaton/w3c.rb +2 -2
- data/lib/relaton/xsf/data_fetcher.rb +1 -1
- data/lib/relaton/xsf/processor.rb +1 -1
- data/lib/relaton/xsf.rb +2 -2
- data/lib/relaton-cli.rb +17 -0
- data/lib/relaton-render.rb +12 -0
- data/lib/relaton.rb +3 -2
- 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,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
|