lib/relaton_bib/bibliographic_item.rb
# frozen_string_literal: true
require "relaton_bib/typed_uri"
require "relaton_bib/document_identifier"
require "relaton_bib/copyright_association"
require "relaton_bib/formatted_string"
require "relaton_bib/contribution_info"
require "relaton_bib/bibliographic_date"
require "relaton_bib/series"
require "relaton_bib/document_status"
require "relaton_bib/organization"
require "relaton_bib/document_relation_collection"
require "relaton_bib/typed_title_string"
require "relaton_bib/formatted_ref"
require "relaton_bib/medium"
require "relaton_bib/classification"
require "relaton_bib/validity"
require "relaton_bib/document_relation"
require "relaton_bib/bib_item_locality"
require "relaton_bib/xml_parser"
require "relaton_bib/bibtex_parser"
require "relaton_bib/biblio_note"
require "relaton_bib/biblio_version"
require "relaton_bib/workers_pool"
require "relaton_bib/hash_converter"
require "relaton_bib/place"
require "relaton_bib/structured_identifier"
require "relaton_bib/editorial_group"
require "relaton_bib/ics"
require "relaton_bib/bibliographic_size"
require "relaton_bib/edition"
module RelatonBib
# Bibliographic item
class BibliographicItem
include RelatonBib
TYPES = %W[article book booklet manual proceedings presentation
thesis techreport standard unpublished map electronic\sresource
audiovisual film video broadcast software graphic_work music
patent inbook incollection inproceedings journal website
webresource dataset archival social_media alert message
conversation misc internal].freeze
# @return [Boolean, nil]
attr_accessor :all_parts
# @return [String, nil]
attr_reader :id, :type, :docnumber, :subdoctype
# @return [RelatonBib::DocumentType] document type
attr_reader :doctype
# @return [RelatonBib::Edition, nil] edition
attr_reader :edition
# @!attribute [r] title
# @return [RelatonBib::TypedTitleStringCollection]
# @return [Array<RelatonBib::TypedUri>]
attr_reader :link
# @return [Array<RelatonBib::DocumentIdentifier>]
attr_reader :docidentifier
# @return [Array<RelatonBib::BibliographicDate>]
attr_writer :date
# @return [Array<RelatonBib::ContributionInfo>]
attr_reader :contributor
# @return [Array<RelatonBib::BibliographicItem::Version>]
attr_reader :version
# @return [RelatonBib::BiblioNoteCollection]
attr_reader :biblionote
# @return [Array<String>] language Iso639 code
attr_reader :language
# @return [Array<String>] script Iso15924 code
attr_reader :script
# @return [RelatonBib::FormattedRef, nil]
attr_reader :formattedref
# @!attribute [r] abstract
# @return [Array<RelatonBib::FormattedString>]
# @return [RelatonBib::DocumentStatus, nil]
attr_reader :status
# @return [Array<RelatonBib::CopyrightAssociation>]
attr_reader :copyright
# @return [RelatonBib::DocRelationCollection]
attr_reader :relation
# @return [Array<RelatonBib::Series>]
attr_reader :series
# @return [RelatonBib::Medium, nil]
attr_reader :medium
# @return [Array<RelatonBib::Place>]
attr_reader :place
# @return [Array<RelatonBib::Locality, RelatonBib::LocalityStack>]
attr_reader :extent
# @return [Array<Strig>]
attr_reader :accesslocation, :license
# @return [Array<Relaton::Classification>]
attr_reader :classification
# @return [RelatonBib:Validity, nil]
attr_reader :validity
# @return [Date]
attr_accessor :fetched
# @return [Array<RelatonBib::LocalizedString>]
attr_reader :keyword
# @return [RelatonBib::EditorialGroup, nil]
attr_reader :editorialgroup
# @return [Array<RelatonBib:ICS>]
attr_reader :ics
# @return [RelatonBib::StructuredIdentifierCollection]
attr_reader :structuredidentifier
# @return [RelatonBib::BibliographicSize, nil]
attr_reader :size
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# @param id [String, nil]
# @param title [RelatonBib::TypedTitleStringCollection,
# Array<Hash, RelatonBib::TypedTitleString>]
# @param formattedref [RelatonBib::FormattedRef, nil]
# @param type [String, nil]
# @param docid [Array<RelatonBib::DocumentIdentifier>]
# @param docnumber [String, nil]
# @param language [Arra<String>]
# @param script [Array<String>]
# @param docstatus [RelatonBib::DocumentStatus, nil]
# @param edition [RelatonBib::Edition, String, Integer, Float, nil]
# @param version [Array<RelatonBib::BibliographicItem::Version>]
# @param biblionote [RelatonBib::BiblioNoteCollection]
# @param series [Array<RelatonBib::Series>]
# @param medium [RelatonBib::Medium, nil]
# @param place [Array<String, RelatonBib::Place>]
# @param extent [Array<RelatonBib::Locality, RelatonBib::LocalityStack>]
# @param accesslocation [Array<String>]
# @param classification [Array<RelatonBib::Classification>]
# @param validity [RelatonBib:Validity, nil]
# @param fetched [Date, nil] default nil
# @param keyword [Array<String>]
# @param doctype [RelatonBib::DocumentType]
# @param subdoctype [String]
# @param editorialgroup [RelatonBib::EditorialGroup, nil]
# @param ics [Array<RelatonBib::ICS>]
# @param structuredidentifier [RelatonBib::StructuredIdentifierCollection]
# @param size [RelatonBib::BibliographicSize, nil]
#
# @param copyright [Array<Hash, RelatonBib::CopyrightAssociation>]
# @option copyright [Array<Hash, RelatonBib::ContributionInfo>] :owner
# @option copyright [String] :from
# @option copyright [String, nil] :to
# @option copyright [String, nil] :scope
#
# @param date [Array<Hash, RelatonBib::BibliographicDate>]
# @option date [String] :type
# @option date [String, nil] :from required if :on is nil
# @option date [String, nil] :to
# @option date [String, nil] :on required if :from is nil
#
# @param contributor [Array<Hash, RelatonBib::ContributionInfo>]
# @option contributor [RealtonBib::Organization, RelatonBib::Person] :entity
# @option contributor [String] :type
# @option contributor [String] :from
# @option contributor [String] :to
# @option contributor [String] :abbreviation
# @option contributor [Array<Array<String,Array<String>>>] :role
#
# @param abstract [Array<Hash, RelatonBib::FormattedString>]
# @option abstract [String] :content
# @option abstract [String] :language
# @option abstract [String] :script
# @option abstract [String] :format
#
# @param relation [Array<Hash>]
# @option relation [String] :type
# @option relation [RelatonBib::BibliographicItem,
# RelatonIso::IsoBibliographicItem] :bibitem
# @option relation [Array<RelatonBib::Locality,
# RelatonBib::LocalityStack>] :locality
# @option relation [Array<RelatonBib::SourceLocality,
# RelatonBib::SourceLocalityStack>] :source_locality
#
# @param link [Array<Hash, RelatonBib::TypedUri>]
# @option link [String] :type
# @option link [String] :content
def initialize(**args)
if args[:type] && !TYPES.include?(args[:type])
Util.warn %{Type `#{args[:type]}` is invalid.}
end
@title = if args[:title].is_a?(TypedTitleStringCollection)
args[:title]
else
TypedTitleStringCollection.new(args[:title])
end
@date = (args[:date] || []).map do |d|
d.is_a?(Hash) ? BibliographicDate.new(**d) : d
end
@contributor = (args[:contributor] || []).map do |c|
if c.is_a? Hash
e = c[:entity].is_a?(Hash) ? Organization.new(**c[:entity]) : c[:entity]
ContributionInfo.new(entity: e, role: c[:role])
else c
end
end
@abstract = (args[:abstract] || []).map do |a|
a.is_a?(Hash) ? FormattedString.new(**a) : a
end
@copyright = args.fetch(:copyright, []).map do |c|
c.is_a?(Hash) ? CopyrightAssociation.new(**c) : c
end
@docidentifier = args[:docid] || []
@formattedref = args[:formattedref] if title.empty?
@id = args[:id] || makeid(nil, false)
@type = args[:type]
@docnumber = args[:docnumber]
@edition = case args[:edition]
when Hash then Edition.new(**args[:edition])
when String, Integer, Float
Edition.new(content: args[:edition].to_s)
when Edition then args[:edition]
end
@version = args.fetch :version, []
@biblionote = args.fetch :biblionote, BiblioNoteCollection.new([])
@language = args.fetch :language, []
@script = args.fetch :script, []
@status = args[:docstatus]
@relation = DocRelationCollection.new(args[:relation] || [])
@link = args.fetch(:link, []).map do |s|
case s
when Hash then TypedUri.new(**s)
when String then TypedUri.new(content: s)
else s
end
end
@series = args.fetch :series, []
@medium = args[:medium]
@place = args.fetch(:place, []).map do |pl|
pl.is_a?(String) ? Place.new(name: pl) : pl
end
@extent = args[:extent] || []
@size = args[:size]
@accesslocation = args.fetch :accesslocation, []
@classification = args.fetch :classification, []
@validity = args[:validity]
# we should pass the fetched arg from scrappers
@fetched = args.fetch :fetched, nil
@keyword = (args[:keyword] || []).map do |kw|
case kw
when Hash then LocalizedString.new(kw[:content], kw[:language], kw[:script])
when String then LocalizedString.new(kw)
else kw
end
end
@license = args.fetch :license, []
@doctype = args[:doctype]
@subdoctype = args[:subdoctype]
@editorialgroup = args[:editorialgroup]
@ics = args.fetch :ics, []
@structuredidentifier = args[:structuredidentifier]
end
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
#
# Fetch schema version
#
# @return [String] schema version
#
def schema
@schema ||= schema_versions["relaton-models"]
end
#
# Read schema versions from file
#
# @return [Hash{String=>String}] schema versions
#
def schema_versions
JSON.parse File.read(File.join(__dir__, "../../grammars/versions.json"))
end
# @param hash [Hash]
# @return [RelatonBipm::BipmBibliographicItem]
def self.from_hash(hash)
item_hash = Object.const_get(name.split("::")[0])::HashConverter.hash_to_bib(hash)
new(**item_hash)
end
#
# @param [String, nil] type if nil return all dates else return dates with type
#
# @return [Array<RelatonBib::BibliographicDate>]
#
def date(type: nil)
type ? @date.select { |d| d.type == type } : @date
end
# @param lang [String] language code Iso639
# @return [RelatonBib::FormattedString, Array<RelatonBib::FormattedString>]
def abstract(lang: nil)
if lang
@abstract.detect { |a| a.language&.include? lang }
else
@abstract
end
end
# @param identifier [RelatonBib::DocumentIdentifier, nil]
# @param attribute [Boolean, nil]
# @return [String]
def makeid(identifier, attribute)
return nil if attribute && !@id_attribute
identifier ||= @docidentifier.reject { |i| i.type == "DOI" }[0]
return unless identifier
idstr = identifier.id.gsub(/[:\/]/, "-").gsub(/[\s\(\)]/, "")
idstr.strip
end
# @param identifier [RelatonBib::DocumentIdentifier]
# @param options [Hash]
# @option options [boolean, nil] :no_year
# @option options [boolean, nil] :all_parts
# @return [String]
def shortref(identifier, **opts) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/PerceivedComplexity
pubdate = date.select { |d| d.type == "published" || d.type == "issued" }
year = if opts[:no_year] || pubdate.empty? then ""
else ":#{pubdate&.first&.on(:year)}"
end
year += ": All Parts" if opts[:all_parts] || @all_parts
"#{makeid(identifier, false)}#{year}"
end
# @param opts [Hash]
# @option opts [Nokogiri::XML::Builder] :builder XML builder
# @option opts [Boolean] :bibdata
# @option opts [Symbol, nil] :date_format (:short), :full
# @option opts [String, Symbol] :lang language
# @return [String] XML
def to_xml(**opts, &block)
if opts[:builder]
render_xml(**opts, &block)
else
Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
render_xml builder: xml, **opts, &block
end.doc.root.to_xml
end
end
#
# Render BibXML (RFC)
#
# @param [Nokogiri::XML::Builder, nil] builder
# @param [Boolean] include_keywords (false)
#
# @return [String, Nokogiri::XML::Builder::NodeBuilder] XML
#
def to_bibxml(builder = nil, include_keywords: false)
Renderer::BibXML.new(self).render builder: builder, include_keywords: include_keywords
end
# @return [Hash]
def to_hash(embedded: false) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
hash = {}
hash["schema-version"] = schema unless embedded
hash["id"] = id if id
hash["title"] = title.to_hash if title&.any?
hash["link"] = single_element_array(link) if link&.any?
hash["type"] = type if type
hash["docid"] = single_element_array(docidentifier) if docidentifier&.any?
hash["docnumber"] = docnumber if docnumber
hash["date"] = single_element_array(date) if date&.any?
if contributor&.any?
hash["contributor"] = single_element_array(contributor)
end
hash["edition"] = edition.to_hash if edition
hash["version"] = version.map &:to_hash if version.any?
hash["revdate"] = revdate if revdate
hash["biblionote"] = single_element_array(biblionote) if biblionote&.any?
hash["language"] = single_element_array(language) if language&.any?
hash["script"] = single_element_array(script) if script&.any?
hash["formattedref"] = formattedref.to_hash if formattedref
hash["abstract"] = single_element_array(abstract) if abstract&.any?
hash["docstatus"] = status.to_hash if status
hash["copyright"] = single_element_array(copyright) if copyright&.any?
hash["relation"] = single_element_array(relation) if relation&.any?
hash["series"] = single_element_array(series) if series&.any?
hash["medium"] = medium.to_hash if medium
hash["place"] = single_element_array(place) if place&.any?
hash["extent"] = single_element_array(extent) if extent&.any?
hash["size"] = size.to_hash if size&.any?
if accesslocation&.any?
hash["accesslocation"] = single_element_array(accesslocation)
end
if classification&.any?
hash["classification"] = single_element_array(classification)
end
hash["validity"] = validity.to_hash if validity
hash["fetched"] = fetched.to_s if fetched
hash["keyword"] = single_element_array(keyword) if keyword&.any?
hash["license"] = single_element_array(license) if license&.any?
hash["doctype"] = doctype.to_hash if doctype
hash["subdoctype"] = subdoctype if subdoctype
if editorialgroup&.presence?
hash["editorialgroup"] = editorialgroup.to_hash
end
hash["ics"] = single_element_array ics if ics.any?
if structuredidentifier&.presence?
hash["structuredidentifier"] = structuredidentifier.to_hash
end
hash["ext"] = { "schema-version" => ext_schema } if !embedded && respond_to?(:ext_schema) && ext_schema
hash
end
#
# Reander BibTeX
#
# @param bibtex [BibTeX::Bibliography, nil]
#
# @return [String]
#
def to_bibtex(bibtex = nil)
Renderer::BibtexBuilder.build(self, bibtex).to_s
end
#
# Render citeproc
#
# @param bibtex [BibTeX::Bibliography, nil]
#
# @return [Hash] citeproc
#
def to_citeproc(bibtex = nil)
Renderer::BibtexBuilder.build(self, bibtex).to_citeproc.map do |cp|
cp.transform_keys(&:to_s)
end
end
# @param lang [String, nil] language code Iso639
# @return [RelatonIsoBib::TypedTitleStringCollection]
def title(lang: nil)
@title.lang lang
end
# @param type [Symbol] type of url, can be :src/:obp/:rss
# @return [String, nil]
def url(type = :src)
@link.detect { |s| s.type == type.to_s }&.content&.to_s
end
def abstract=(value)
@abstract = value
end
def deep_clone
dump = Marshal.dump self
Marshal.load dump # rubocop:disable Security/MarshalLoad
end
def disable_id_attribute
@id_attribute = false
end
# remove title part components and abstract
def to_all_parts # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength,Metrics/PerceivedComplexity
me = deep_clone
me.disable_id_attribute
me.relation << DocumentRelation.new(type: "instanceOf", bibitem: self)
me.language.each do |l|
me.title.delete_title_part!
ttl = me.title.select do |t|
t.type != "main" && t.title.language&.include?(l)
end
next if ttl.empty?
tm_en = ttl.map { |t| t.title.content }.join " – "
me.title.detect do |t|
t.type == "main" && t.title.language&.include?(l)
end&.title&.content = tm_en
end
me.abstract = []
me.docidentifier.each(&:remove_part)
me.docidentifier.each(&:all_parts)
me.structuredidentifier.remove_part
me.structuredidentifier.all_parts
me.docidentifier.each &:remove_date
me.structuredidentifier&.remove_date
me.all_parts = true
me
end
# convert ISO:yyyy reference to reference to most recent
# instance of reference, removing date-specific infomration:
# date of publication, abstracts. Make dated reference Instance relation
# of the redacated document
def to_most_recent_reference
me = deep_clone
disable_id_attribute
me.relation << DocumentRelation.new(type: "instanceOf", bibitem: self)
me.abstract = []
me.date = []
me.docidentifier.each &:remove_date
me.structuredidentifier&.remove_date
me.id&.sub!(/-[12]\d\d\d/, "")
me
end
# If revision_date exists then returns it else returns published date or nil
# @return [String, nil]
def revdate # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
@revdate ||= if (v = version.detect &:revision_date)
v.revision_date
else
date.detect { |d| d.type == "published" }&.on&.to_s
end
end
# @param prefix [String]
# @return [String]
def to_asciibib(prefix = "") # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
pref = prefix.empty? ? prefix : "#{prefix}."
out = prefix.empty? ? "[%bibitem]\n== {blank}\n" : ""
out += "#{pref}id:: #{id}\n" if id
out += "#{pref}fetched:: #{fetched}\n" if fetched
title.each { |t| out += t.to_asciibib(prefix, title.size) }
out += "#{pref}type:: #{type}\n" if type
docidentifier.each do |di|
out += di.to_asciibib prefix, docidentifier.size
end
out += "#{pref}docnumber:: #{docnumber}\n" if docnumber
out += edition.to_asciibib(prefix) if edition
language.each { |l| out += "#{pref}language:: #{l}\n" }
script.each { |s| out += "#{pref}script:: #{s}\n" }
version.each { |v| out += v.to_asciibib prefix, version.size }
biblionote&.each { |b| out += b.to_asciibib prefix, biblionote.size }
out += status.to_asciibib prefix if status
date.each { |d| out += d.to_asciibib prefix, date.size }
abstract.each do |a|
out += a.to_asciibib "#{pref}abstract", abstract.size
end
copyright.each { |c| out += c.to_asciibib prefix, copyright.size }
link.each { |l| out += l.to_asciibib prefix, link.size }
out += medium.to_asciibib prefix if medium
place.each { |pl| out += pl.to_asciibib prefix, place.size }
extent.each { |ex| out += ex.to_asciibib "#{pref}extent", extent.size }
out += size.to_asciibib pref if size
accesslocation.each { |al| out += "#{pref}accesslocation:: #{al}\n" }
classification.each do |cl|
out += cl.to_asciibib prefix, classification.size
end
out += validity.to_asciibib prefix if validity
contributor.each do |c|
out += c.to_asciibib "contributor.*", contributor.size
end
out += relation.to_asciibib prefix if relation
series.each { |s| out += s.to_asciibib prefix, series.size }
out += doctype.to_asciibib prefix if doctype
out += "#{pref}subdoctype:: #{subdoctype}\n" if subdoctype
out += "#{pref}formattedref:: #{formattedref}\n" if formattedref
keyword.each { |kw| out += kw.to_asciibib "#{pref}keyword", keyword.size }
out += editorialgroup.to_asciibib prefix if editorialgroup
ics.each { |i| out += i.to_asciibib prefix, ics.size }
out += structuredidentifier.to_asciibib prefix if structuredidentifier
out
end
private
# @param opts [Hash]
# @option opts [Nokogiri::XML::Builder] :builder XML builder
# @option opts [Boolean] bibdata
# @option opts [Symbol, nil] :date_format (:short), :full
# @option opts [String] :lang language
def render_xml(**opts) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
root = opts[:bibdata] ? :bibdata : :bibitem
xml = opts[:builder].send(root) do |builder| # rubocop:disable Metrics/BlockLength
builder.fetched fetched if fetched
title.to_xml(**opts)
formattedref&.to_xml builder
link.each { |s| s.to_xml builder }
docidentifier.each { |di| di.to_xml(**opts) }
builder.docnumber docnumber if docnumber
date.each { |d| d.to_xml builder, **opts }
contributor.each do |c|
builder.contributor do
c.role.each { |r| r.to_xml(**opts) }
c.to_xml(**opts)
end
end
edition&.to_xml builder
version.each { |v| v.to_xml builder }
biblionote.to_xml(**opts)
opts[:note]&.each do |n|
builder.note(n[:text], format: "text/plain", type: n[:type])
end
language.each { |l| builder.language l }
script.each { |s| builder.script s }
abstr = abstract.select { |ab| ab.language&.include? opts[:lang] }
abstr = abstract unless abstr.any?
abstr.each { |a| builder.abstract { a.to_xml(builder) } }
status&.to_xml builder
copyright&.each { |c| c.to_xml(**opts) }
relation.each { |r| r.to_xml builder, **opts }
series.each { |s| s.to_xml builder }
medium&.to_xml builder
place.each { |pl| pl.to_xml builder }
extent.each { |e| builder.extent { e.to_xml builder } }
size&.to_xml builder
accesslocation.each { |al| builder.accesslocation al }
license.each { |l| builder.license l }
classification.each { |cls| cls.to_xml builder }
kwrd = keyword.select { |kw| kw.language&.include? opts[:lang] }
kwrd = keyword unless kwrd.any?
kwrd.each { |kw| builder.keyword { kw.to_xml(builder) } }
validity&.to_xml builder
if block_given? then yield builder
elsif opts[:bibdata] && (doctype || editorialgroup || ics&.any? ||
structuredidentifier&.presence?)
ext = builder.ext do |b|
doctype.to_xml b if doctype
b.subdoctype subdoctype if subdoctype
editorialgroup&.to_xml b
ics.each { |i| i.to_xml b }
structuredidentifier&.to_xml b
end
ext["schema-version"] = ext_schema if !opts[:embedded] && respond_to?(:ext_schema) && ext_schema
end
end
xml[:id] = id if id && !opts[:bibdata] && !opts[:embedded]
xml[:type] = type if type
xml["schema-version"] = schema unless opts[:embedded]
xml
end
end
end