diff --git a/Rakefile b/Rakefile index 1b2cd40..a1fe1d3 100644 --- a/Rakefile +++ b/Rakefile @@ -68,6 +68,6 @@ namespace :yard do coverage = m[1].to_f.round(2) puts "Documentation coverage: #{coverage}%" - raise 'Not fully documented!' if coverage < 38 + raise 'Not fully documented!' if coverage < 36 end end diff --git a/lib/repubmark.rb b/lib/repubmark.rb index a244dd4..e91fdcf 100644 --- a/lib/repubmark.rb +++ b/lib/repubmark.rb @@ -16,6 +16,9 @@ require_relative 'repubmark/titled_ref' require_relative 'repubmark/config' require_relative 'repubmark/config/css_classes' +require_relative 'repubmark/references/author' +require_relative 'repubmark/references/item' + require_relative 'repubmark/elems/base' # Top-level element @@ -72,7 +75,10 @@ require_relative 'repubmark/elems/footnote' module Repubmark FORMATS = %i[chapters gemtext html summary_plain word_count].freeze - SLUG_RE = /\A[[:word:]]+(-[[:word:]]+)*\z/ + SLUG_RE = /\A[[:word:]]+(-[[:word:]]+)*\z/ + DOI_RE = %r{\A10(\.\d+)+/\w+(\.\w+)*\z} + ISBN_RE = /\A[0-9]{13}\z/ + NAME_PART_RE = /\A[[:word:]]+(-[[:word:]]+)*( [[:word:]]+(-[[:word:]]+)*)*\z/ UNICODE_SUPS = { '0' => '⁰', @@ -117,6 +123,46 @@ module Repubmark slug end + def self.validate_doi!(doi) + doi = String(doi).freeze + raise 'Invalid DOI' unless DOI_RE.match? doi + + doi + end + + def self.validate_isbn!(isbn) + isbn = String(isbn).freeze + raise 'Invalid ISBN' unless ISBN_RE.match? isbn + + isbn + end + + def self.validate_name_part!(name_part) + name_part = String(name_part).freeze + raise 'Invalid name part' unless NAME_PART_RE.match? name_part + + name_part + end + + def self.validate_text!(str) + str = String(str).freeze + raise 'Invalid text' if str.empty? || str != str.strip + + str + end + + def self.validate_absolute_url!(url) + url = Addressable::URI.parse(url).freeze + raise 'Invalid URL' unless url.absolute? && + url.scheme && + !url.scheme.strip.empty? && + url.host && + !url.host.strip.empty? && + url.userinfo.nil? + + url + end + def self.unicode_sup(val) String(val).each_char.map { |chr| UNICODE_SUPS.fetch chr }.join.freeze end diff --git a/lib/repubmark/references/author.rb b/lib/repubmark/references/author.rb new file mode 100644 index 0000000..200934d --- /dev/null +++ b/lib/repubmark/references/author.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Repubmark + module References + class Author + attr_reader :family, :given + + def initialize(family, given) + self.family = family + self.given = given + end + + def inspect = "#<#{self.class}: #{self}>".freeze + + def to_s = "#{family}, #{given.join(' ')}".freeze + + alias to_str to_s + + private + + def family=(family) + @family = Repubmark.validate_name_part! family + end + + def given=(given) + @given = + Array(given).map { |part| Repubmark.validate_name_part! part }.freeze + end + end + end +end diff --git a/lib/repubmark/references/item.rb b/lib/repubmark/references/item.rb new file mode 100644 index 0000000..851a613 --- /dev/null +++ b/lib/repubmark/references/item.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Repubmark + module References + class Item + ATTRIBUTES = %i[slug doi isbn title urls authors].freeze + attr_reader(*ATTRIBUTES) + + def initialize(**kwargs) + ATTRIBUTES.each { |attr| send :"#{attr}=", kwargs[attr] } + end + + def inspect = "#<#{self.class}:#{slug}>".freeze + + private + + def slug=(slug) + @slug = Repubmark.validate_slug! slug + end + + def doi=(doi) + @doi = Repubmark.validate_doi! doi if doi + end + + def isbn=(isbn) + @isbn = Repubmark.validate_isbn! isbn if isbn + end + + def title=(title) + @title = Repubmark.validate_text! title + end + + def urls=(urls) + urls = Array urls + return @urls = [].freeze if urls.empty? + + @urls = urls.map { |url| Repubmark.validate_absolute_url! url }.freeze + end + + def authors=(authors) + authors = Array authors + return @authors = [].freeze if authors.empty? + + @authors = authors.freeze.each do |author| + unless author.instance_of? Author + raise TypeError, "Expected #{Author}, got #{author.class}" + end + end + end + end + end +end