rfc-web-link 0.0.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.
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module RFC
6
+ module Web
7
+ module Link
8
+ # A HTTP header field decoder which adheres to RFC 8187.
9
+ class Decoder
10
+ def initialize delimiter: "'", default_encoding: Encoding::UTF_8, client: URI
11
+ @delimiter = delimiter
12
+ @default_encoding = default_encoding
13
+ @client = client
14
+ end
15
+
16
+ def call text
17
+ value, encoding, language = parse text
18
+
19
+ value = client.decode_uri_component(value)
20
+ .force_encoding(encoding || default_encoding)
21
+ .encode(default_encoding)
22
+
23
+ {value:, encoding:, language:}
24
+ rescue ArgumentError, NoMethodError
25
+ {value: nil, encoding:, language:}
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :delimiter, :default_encoding, :client
31
+
32
+ def parse text
33
+ case String(text).split delimiter
34
+ in [value] then [value, nil, nil]
35
+ in [encoding, language, value] then [value, encoding, language]
36
+ else [nil, nil, nil]
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RFC
4
+ module Web
5
+ module Link
6
+ # A HTTP header field encoder which adheres to RFC 8187.
7
+ class Encoder
8
+ def initialize pattern: /[^0-9a-zA-Z!\#$&+\-.^_`|~]/
9
+ @pattern = pattern
10
+ end
11
+
12
+ def call(text) = text.match?(pattern) ? convert_characters(text).join : text
13
+
14
+ private
15
+
16
+ attr_reader :pattern
17
+
18
+ def convert_characters text
19
+ text.each_char.map do |character|
20
+ next character unless character.match? pattern
21
+
22
+ character.bytes
23
+ .map { format "%%%02X", it }
24
+ .join
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module RFC
6
+ module Web
7
+ module Link
8
+ module Models
9
+ # Models a link.
10
+ Link = Data.define :uri, :pairs do
11
+ extend Forwardable
12
+
13
+ delegate %i[empty? include?] => :pairs
14
+
15
+ def initialize uri:, pairs: Set.new
16
+ super
17
+ end
18
+
19
+ def add pair
20
+ pairs.add pair
21
+ self
22
+ end
23
+
24
+ def append key, value, **attributes
25
+ pairs.add Pair[key:, value:, **attributes]
26
+ self
27
+ end
28
+
29
+ def has?(key) = pairs.any? { it.key == key.to_s }
30
+
31
+ def to_s(delimiter: "; ") = "<#{uri}>; #{pairs.join delimiter}"
32
+
33
+ alias_method :to_str, :to_s
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module RFC
6
+ module Web
7
+ module Link
8
+ module Models
9
+ # Models a list of links.
10
+ List = Data.define :links do
11
+ extend Forwardable
12
+
13
+ delegate %i[all? any? empty? find include? map none? one? size] => :links
14
+
15
+ def initialize links: Set.new
16
+ super
17
+ end
18
+
19
+ def add line
20
+ links.add line
21
+ self
22
+ end
23
+
24
+ def clear
25
+ links.clear
26
+ self
27
+ end
28
+
29
+ def each(&block) = block ? links.each(&block) : self
30
+
31
+ def reject(&) = with links: Set[*links.reject(&)]
32
+
33
+ def select(&) = with links: Set[*links.select(&)]
34
+
35
+ def to_s(delimiter: ", ") = links.join delimiter
36
+
37
+ alias_method :to_str, :to_s
38
+
39
+ private :links
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RFC
4
+ module Web
5
+ module Link
6
+ module Models
7
+ # Models the key, delimiter, and decoded value associated with a link.
8
+ Pair = Data.define :key, :delimiter, :value, :encoding, :language do
9
+ def initialize key:, value:, delimiter: "=", encoding: nil, language: nil
10
+ super key: key.to_s, value:, delimiter:, encoding:, language:
11
+ end
12
+
13
+ def encoded? = delimiter == "*="
14
+
15
+ def to_s key_map: {"relation" => "rel", "language" => "hreflang"}.freeze,
16
+ encoder: Encoder.new
17
+ transformed_key = key_map.fetch(key) { it }
18
+
19
+ if encoding
20
+ "#{transformed_key}#{delimiter}#{encoding}'#{language}'" \
21
+ "#{encoder.call value.encode(encoding)}"
22
+ else
23
+ "#{transformed_key}#{delimiter}#{value}"
24
+ end
25
+ end
26
+
27
+ alias_method :to_str, :to_s
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RFC
4
+ module Web
5
+ module Link
6
+ module Parsers
7
+ # Parses a header link into a list of records.
8
+ class Header
9
+ def initialize pattern: /link/i, list: List.new
10
+ @pattern = pattern
11
+ @list = list
12
+ end
13
+
14
+ def call headers, root_uri:
15
+ text = headers.find { |key, value| break value if key.match? pattern }
16
+ list.call text, root_uri:
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :pattern, :list
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RFC
4
+ module Web
5
+ module Link
6
+ module Parsers
7
+ # Parses a header line into a link record.
8
+ class Line
9
+ def initialize delimiter: /;\s*?/, pair: Pair.new, model: Models::Link
10
+ @delimiter = delimiter
11
+ @pair = pair
12
+ @model = model
13
+ end
14
+
15
+ def call text, root_uri:
16
+ link, *parts = text.split delimiter
17
+ pairs = process parts, root_uri
18
+
19
+ build_model link, pairs, root_uri
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :delimiter, :pair, :model
25
+
26
+ def process parts,
27
+ root_uri,
28
+ default: Hash.new { |nascence, lacuna| nascence[lacuna] = [] }
29
+ parts.each.with_object default do |part, all|
30
+ part = pair.call(part, root_uri:)
31
+ key = part.key
32
+
33
+ case part
34
+ in key: "hreflang" then all[key].append part
35
+ in delimiter: "*=" then all[key].clear.append part
36
+ else all[key].append part unless all.key? key
37
+ end
38
+ end
39
+ end
40
+
41
+ def build_model link, pairs, root_uri
42
+ model[uri: build_uri(link, root_uri:), pairs: Set[*pairs.values.flatten!]]
43
+ end
44
+
45
+ def build_uri value, root_uri:
46
+ value.delete_prefix("<")
47
+ .delete_suffix(">")
48
+ .then { it.start_with?("/") ? "#{root_uri}#{it}" : it }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module RFC
6
+ module Web
7
+ module Link
8
+ module Parsers
9
+ # Parses header links into a list of records.
10
+ class List
11
+ RELATION_PATTERN = /
12
+ (?<prefix>rel=") # Prefix.
13
+ .+ # One or more characters.
14
+ \s+? # One or more spaces, lazy.
15
+ .+ # One or more characters.
16
+ (?<suffix>") # Suffix.
17
+ /x
18
+
19
+ def initialize relation_pattern: RELATION_PATTERN, line: Line.new, list: Models::List.new
20
+ @scanner = StringScanner.new ""
21
+ @relation_pattern = relation_pattern
22
+ @line = line
23
+ @list = list
24
+ @comma = ","
25
+ @quote = %(")
26
+ end
27
+
28
+ def call text, root_uri:
29
+ text = String text
30
+ list.clear
31
+
32
+ return list unless text.start_with? "<"
33
+
34
+ scanner.string = text
35
+
36
+ build_list root_uri
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :scanner, :relation_pattern, :line, :list, :comma, :quote
42
+
43
+ def build_list root_uri, buffer: +"", lines: []
44
+ check scanner.getch, buffer, lines until scanner.eos?
45
+
46
+ lines.append(buffer.dup)
47
+ .each { maybe_split_by_relation it.strip, root_uri: }
48
+
49
+ list
50
+ end
51
+
52
+ def check character, buffer, lines
53
+ case character
54
+ when quote then enquote buffer
55
+ when comma
56
+ lines.append buffer.dup
57
+ buffer.clear
58
+ else buffer << character
59
+ end
60
+ end
61
+
62
+ def enquote buffer
63
+ start = scanner.pos
64
+ scanner.scan_until quote
65
+ buffer << %("#{scanner.pre_match[start..]}")
66
+ end
67
+
68
+ # rubocop:todo Metrics/AbcSize
69
+ def maybe_split_by_relation text, root_uri:
70
+ match = text.match relation_pattern
71
+
72
+ return list.add line.call(text, root_uri:) unless match
73
+
74
+ match.to_s
75
+ .delete_prefix(match[:prefix])
76
+ .delete_suffix(match[:suffix])
77
+ .split
78
+ .each { list.add line.call(text.sub(relation_pattern, "rel=#{it}"), root_uri:) }
79
+ end
80
+ # rubocop:enable Metrics/AbcSize
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RFC
4
+ module Web
5
+ module Link
6
+ module Parsers
7
+ # Parses a header key/value pair into a record.
8
+ class Pair
9
+ def initialize split_pattern: /(?<target>=)|(?<extended>\*=)/,
10
+ decoder: Decoder.new,
11
+ model: Models::Pair
12
+ @split_pattern = split_pattern
13
+ @decoder = decoder
14
+ @model = model
15
+ end
16
+
17
+ def call text, root_uri:
18
+ key, delimiter, value = text.split split_pattern
19
+ key.strip!
20
+
21
+ attributes = decoder.call value
22
+ value = attributes.delete :value
23
+ value = "#{root_uri}#{value}" if key == "anchor" && value.start_with?("/")
24
+ attributes[:value] = value
25
+
26
+ model[key:, delimiter:, **attributes]
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :split_pattern, :decoder, :model
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rfc/web/link/decoder"
4
+ require "rfc/web/link/encoder"
5
+ require "rfc/web/link/models/link"
6
+ require "rfc/web/link/models/list"
7
+ require "rfc/web/link/models/pair"
8
+ require "rfc/web/link/parsers/header"
9
+ require "rfc/web/link/parsers/line"
10
+ require "rfc/web/link/parsers/list"
11
+ require "rfc/web/link/parsers/pair"
12
+
13
+ module RFC
14
+ module Web
15
+ # Main namespace.
16
+ module Link
17
+ def self.new(**) = Parsers::Header.new(**)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "rfc-web-link"
5
+ spec.version = "0.0.0"
6
+ spec.authors = ["Brooke Kuhlmann"]
7
+ spec.email = ["brooke@alchemists.io"]
8
+ spec.homepage = "https://alchemists.io/projects/rfc-web-link"
9
+ spec.summary = "A RFC 8288 Web Linking implementation."
10
+ spec.license = "Hippocratic-2.1"
11
+
12
+ spec.metadata = {
13
+ "bug_tracker_uri" => "https://github.com/bkuhlmann/rfc-web-link/issues",
14
+ "changelog_uri" => "https://alchemists.io/projects/rfc-web-link/versions",
15
+ "homepage_uri" => "https://alchemists.io/projects/rfc-web-link",
16
+ "funding_uri" => "https://github.com/sponsors/bkuhlmann",
17
+ "label" => "RFC Web Link",
18
+ "rubygems_mfa_required" => "true",
19
+ "source_code_uri" => "https://github.com/bkuhlmann/rfc-web-link"
20
+ }
21
+
22
+ spec.signing_key = Gem.default_key_path
23
+ spec.cert_chain = [Gem.default_cert_path]
24
+
25
+ spec.required_ruby_version = ">= 4.0"
26
+
27
+ spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
28
+ spec.files = Dir["*.gemspec", "lib/**/*"]
29
+ end
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rfc-web-link
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brooke Kuhlmann
8
+ bindir: bin
9
+ cert_chain:
10
+ - |
11
+ -----BEGIN CERTIFICATE-----
12
+ MIIENjCCAp6gAwIBAgIBAzANBgkqhkiG9w0BAQsFADBBMQ8wDQYDVQQDDAZicm9v
13
+ a2UxGjAYBgoJkiaJk/IsZAEZFgphbGNoZW1pc3RzMRIwEAYKCZImiZPyLGQBGRYC
14
+ aW8wHhcNMjYwMzI1MTI0OTEyWhcNMjcwMzI1MTI0OTEyWjBBMQ8wDQYDVQQDDAZi
15
+ cm9va2UxGjAYBgoJkiaJk/IsZAEZFgphbGNoZW1pc3RzMRIwEAYKCZImiZPyLGQB
16
+ GRYCaW8wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCro8tj5/E1Hg88
17
+ f4qfiwPVd2zJQHvdYt4GHVvuHRRgx4HGhJuNp+4BId08RBn7V6V1MW6MY3kezRBs
18
+ M+7QOQ4b1xNLTvY7FYQB1wGK5a4x7TTokDrPYQxDB2jmsdDYCzVbIMrAvUfcecRi
19
+ khyGZCdByiiCl4fKv77P12tTT+NfsvXkLt/AYCGwjOUyGKTQ01Z6eC09T27GayPH
20
+ QQvIkakyFgcJtzSyGzs8bzK5q9u7wQ12MNTjJoXzW69lqp0oNvDylu81EiSUb5S6
21
+ QzzPxZBiRB1sgtbt1gUbVI262ZDq1gR+HxPFmp+Cgt7ZLIJZAtesQvtcMzseXpfn
22
+ hpmm0Sw22KGhRAy/mqHBRhDl5HqS1SJp2Ko3lcnpXeFResp0HNlt8NSu13vhC08j
23
+ GUHU9MyIXbFOsnp3K3ADrAVjPWop8EZkmUR3MV/CUm00w2cZHCSGiXl1KMpiVKvk
24
+ Ywr1gd2ZME4QLSo+EXUtLxDUa/W3xnBS8dBOuMMz02FPWYr3PN8CAwEAAaM5MDcw
25
+ CQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFAFgmv0tYMZnItuPycSM
26
+ F5wykJEVMA0GCSqGSIb3DQEBCwUAA4IBgQAG+ykjp+DIXSybGEtX+/ve974mYfN6
27
+ 8U7qcVfRM+qDSOZ+97iu30qUTbVAKIHlHCDKRn3SgOffDUB5VU2MsJBh/3TPKWBZ
28
+ anB/uzMcwOfru+qyA3b7ZFqZzRLWmR5FtPObFxc0gYMT3YvLNHk2Nb9Vjq/PoiGG
29
+ e75PXweDOokwDA5m1gMOz1rdp/dlGMXkSFQg94PPVyUKXgO4VzWTgePSDxOIL+v6
30
+ +OWV6AaEH9BaqxnmdA5ubi0L7bhl0gbN92FxpNO3kpTjww8kme856a+wCK3qyM5w
31
+ 7ZLbUexynDN0Au8eSpT2Bf6ztGmB1S9ffzDJsGX1/lkpMIB51e48Xe2+gzzOgemk
32
+ CdZaGupj6WkarnT8kh/cPtyA5ax4rGX6GOS8meGxzkv8Uy0JSEOYAp6wLfIisYZp
33
+ IJBIXIOkwKKJ0eB5YHrUSJxzpP4LlcIg/eTftaXmJdYjy+2VRrCZYDjfguyLmMjR
34
+ KR9w4/Fjvqy87kCHmxMWa6IL2Vzt1Clm2cA=
35
+ -----END CERTIFICATE-----
36
+ date: 1980-01-02 00:00:00.000000000 Z
37
+ dependencies: []
38
+ email:
39
+ - brooke@alchemists.io
40
+ executables: []
41
+ extensions: []
42
+ extra_rdoc_files:
43
+ - LICENSE.adoc
44
+ - README.adoc
45
+ files:
46
+ - LICENSE.adoc
47
+ - README.adoc
48
+ - lib/rfc/web/link.rb
49
+ - lib/rfc/web/link/decoder.rb
50
+ - lib/rfc/web/link/encoder.rb
51
+ - lib/rfc/web/link/models/link.rb
52
+ - lib/rfc/web/link/models/list.rb
53
+ - lib/rfc/web/link/models/pair.rb
54
+ - lib/rfc/web/link/parsers/header.rb
55
+ - lib/rfc/web/link/parsers/line.rb
56
+ - lib/rfc/web/link/parsers/list.rb
57
+ - lib/rfc/web/link/parsers/pair.rb
58
+ - rfc-web-link.gemspec
59
+ homepage: https://alchemists.io/projects/rfc-web-link
60
+ licenses:
61
+ - Hippocratic-2.1
62
+ metadata:
63
+ bug_tracker_uri: https://github.com/bkuhlmann/rfc-web-link/issues
64
+ changelog_uri: https://alchemists.io/projects/rfc-web-link/versions
65
+ homepage_uri: https://alchemists.io/projects/rfc-web-link
66
+ funding_uri: https://github.com/sponsors/bkuhlmann
67
+ label: RFC Web Link
68
+ rubygems_mfa_required: 'true'
69
+ source_code_uri: https://github.com/bkuhlmann/rfc-web-link
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '4.0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubygems_version: 4.0.19
85
+ specification_version: 4
86
+ summary: A RFC 8288 Web Linking implementation.
87
+ test_files: []
metadata.gz.sig ADDED
Binary file