adocconf 1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 114bb62e62dfd948dff12053c7d99d761a946dd79cab5237d09139d3355609d5
4
+ data.tar.gz: 9f20a3d92931504bf01918cb74204607fe558b660e29cdb99b861d9f476ac2dd
5
+ SHA512:
6
+ metadata.gz: 79a3a5fd799bfba457d73f3b9b182a5e426e3a90156421154cb12ccf6da4fd85783773696c4930a82b376395cb0ed907cf2c3e80624e7720cfad450e738f97da
7
+ data.tar.gz: bddd93ffefbbca8091c3ea06ae376421adee8b252dbc2f13bdfc2796a0b2ea408c849bfd8bb00a0a30d4e7db755f1c353e74ee7d2d9f6661a24c60ef05ef5e23
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nathan Hammer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/exe/adocconf ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
+ require "adocconf"
5
+ exit Adocconf::CLI.start(ARGV)
@@ -0,0 +1,28 @@
1
+ module Adocconf
2
+ class CLI
3
+ def self.start(argv)
4
+ command = argv.shift
5
+
6
+ case command
7
+ when "parse"
8
+ parse_command(argv)
9
+ else
10
+ warn "Unknown command: #{command}"
11
+ 1
12
+ end
13
+ rescue Adocconf::Error => e
14
+ warn "error: #{e.message}"
15
+ 1
16
+ end
17
+
18
+ def self.parse_command(argv)
19
+ path = argv.shift
20
+ raise ParseError, "No input file provided" if path.nil? || path.strip.empty?
21
+
22
+ result = Parser.new.parse_file(path)
23
+
24
+ puts JSON.pretty_generate(result)
25
+ 0
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,7 @@
1
+ module Adocconf
2
+ class Error < StandardError; end
3
+ class DuplicateKeyError < Error; end
4
+ class InvalidStructureError < Error; end
5
+ class UnsupportedNodeError < Error; end
6
+ class ParseError < Error; end
7
+ end
@@ -0,0 +1,199 @@
1
+ module Adocconf
2
+ class Extractor
3
+ def initialize(document)
4
+ @document = document
5
+ end
6
+
7
+ def extract
8
+ extract_node(@document)
9
+ end
10
+
11
+ def extract_node(node)
12
+ sections = child_sections(node)
13
+
14
+ if sections.any?
15
+ extract_container(node, sections)
16
+ else
17
+ extract_value_section(node)
18
+ end
19
+ end
20
+
21
+ def child_sections(node)
22
+ return [] unless node.respond_to?(:blocks)
23
+ node.blocks.select { |block| block.context == :section }
24
+ end
25
+
26
+ def non_section_blocks(node)
27
+ return [] unless node.respond_to?(:blocks)
28
+ node.blocks.reject { |block| block.context == :section }
29
+ end
30
+
31
+ def extract_container(_node, sections)
32
+ result = {}
33
+
34
+ sections.each do |section|
35
+ key = slugify(section.title)
36
+
37
+ if result.key?(key)
38
+ raise DuplicateKeyError, "Duplicate section key: #{key}"
39
+ end
40
+
41
+ result[key] = extract_node(section)
42
+ end
43
+
44
+ result
45
+ end
46
+
47
+ def extract_term_text(term)
48
+ if term.respond_to?(:text)
49
+ term.text.to_s
50
+ else
51
+ term.to_s
52
+ end
53
+ end
54
+
55
+ def extract_description_text(description)
56
+ if description.respond_to?(:text)
57
+ description.text.to_s.strip
58
+ elsif description.respond_to?(:blocks) && description.blocks&.any?
59
+ raise UnsupportedNodeError, "Complex description list values are not supported"
60
+ else
61
+ ""
62
+ end
63
+ end
64
+
65
+ def extract_value_section(node)
66
+ blocks = non_section_blocks(node)
67
+ values = []
68
+
69
+ blocks.each do |block|
70
+ case block.context
71
+ when :dlist
72
+ values << extract_dlist(block)
73
+ when :ulist
74
+ values << extract_ulist(block)
75
+ when :table
76
+ values << extract_table(block)
77
+ when :paragraph
78
+ # ignored by spec
79
+ when :open, :example, :listing, :literal, :quote, :verse, :stem, :sidebar, :image, :audio, :video
80
+ handle_unsupported(block)
81
+ else
82
+ handle_unsupported(block)
83
+ end
84
+ end
85
+
86
+ merge_values(values)
87
+ end
88
+
89
+ def extract_dlist(block)
90
+ result = {}
91
+
92
+ block.items.each do |terms, description|
93
+ if terms.nil? || terms.empty?
94
+ raise InvalidStructureError, "Description list item is missing a term"
95
+ end
96
+
97
+ key = extract_term_text(terms.first).strip
98
+
99
+ if result.key?(key)
100
+ raise DuplicateKeyError, "Duplicate key in description list: #{key}"
101
+ end
102
+
103
+ result[key] = extract_description_text(description)
104
+ end
105
+
106
+ result
107
+ end
108
+
109
+ def extract_dlist_term(item)
110
+ terms = item.respond_to?(:terms) ? item.terms : []
111
+ raise InvalidStructureError, "Description list item is missing a term" if terms.nil? || terms.empty?
112
+
113
+ term = terms.first
114
+ term.respond_to?(:text) ? term.text : term.to_s
115
+ end
116
+
117
+ def extract_dlist_value(item)
118
+ return item.text.strip if item.respond_to?(:text) && item.text
119
+
120
+ raise UnsupportedNodeError, "Complex description list values are not supported" if item.respond_to?(:blocks) && item.blocks&.any?
121
+
122
+ ""
123
+ end
124
+
125
+ def extract_ulist(block)
126
+ block.items.map do |item|
127
+ nested_blocks = item.respond_to?(:blocks) ? item.blocks : []
128
+ nested_lists = nested_blocks.select { |b| b.context == :ulist || b.context == :olist }
129
+
130
+ if nested_lists.any?
131
+ raise UnsupportedNodeError, "Nested lists are not supported"
132
+ end
133
+
134
+ item.text.to_s.strip
135
+ end
136
+ end
137
+
138
+ def extract_table(block)
139
+ head_rows = block.rows[:head] || []
140
+ body_rows = block.rows[:body] || []
141
+
142
+ raise InvalidStructureError, "Table must have a header row" if head_rows.empty?
143
+
144
+ headers = head_rows.first.map { |cell| cell.text.to_s.strip }
145
+ raise InvalidStructureError, "Table header cannot be empty" if headers.empty?
146
+
147
+ body_rows.map do |row|
148
+ if row.length != headers.length
149
+ raise InvalidStructureError,
150
+ "Table row width mismatch: expected #{headers.length}, got #{row.length}"
151
+ end
152
+
153
+ result = {}
154
+ headers.zip(row).each do |header, cell|
155
+ result[header] = cell.text.to_s.strip
156
+ end
157
+ result
158
+ end
159
+ end
160
+
161
+ def merge_values(values)
162
+ return {} if values.empty?
163
+ return values.first if values.length == 1
164
+
165
+ if values.all? { |value| value.is_a?(Hash) }
166
+ merged = {}
167
+
168
+ values.each do |hash|
169
+ hash.each do |key, value|
170
+ if merged.key?(key)
171
+ raise DuplicateKeyError, "Duplicate merged key: #{key}"
172
+ end
173
+
174
+ merged[key] = value
175
+ end
176
+ end
177
+
178
+ return merged
179
+ end
180
+
181
+ if values.all? { |value| value.is_a?(Array) }
182
+ return values.flatten(1)
183
+ end
184
+
185
+ raise InvalidStructureError, "Mixed value types in section are not supported"
186
+ end
187
+
188
+ def slugify(title)
189
+ value = Slugifier.call(title)
190
+ raise InvalidStructureError, "Section title produced an empty slug" if value.empty?
191
+
192
+ value
193
+ end
194
+
195
+ def handle_unsupported(block)
196
+ raise UnsupportedNodeError, "Unsupported block context: #{block.context}"
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,14 @@
1
+ module Adocconf
2
+ class Parser
3
+ def parse_file(path)
4
+ raise ParseError, "File not found: #{path}" unless File.file?(path)
5
+
6
+ doc = Asciidoctor.load_file(path, safe: :safe, parse: true)
7
+ Extractor.new(doc).extract
8
+ rescue Adocconf::Error
9
+ raise
10
+ rescue StandardError => e
11
+ raise ParseError, e.message
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module Adocconf
2
+ module Slugifier
3
+ module_function
4
+ def call(value)
5
+ value.to_s
6
+ .downcase
7
+ .strip
8
+ .gsub(/[^a-z0-9]+/, "_")
9
+ .gsub(/-+/, "_")
10
+ .gsub(/\A-+|-+\z/, "")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module Adocconf
2
+ VERSION = "1.0.0"
3
+ end
data/lib/adocconf.rb ADDED
@@ -0,0 +1,9 @@
1
+ require "asciidoctor"
2
+ require "json"
3
+
4
+ require_relative "adocconf/version"
5
+ require_relative "adocconf/errors"
6
+ require_relative "adocconf/slugify"
7
+ require_relative "adocconf/extract"
8
+ require_relative "adocconf/parser"
9
+ require_relative "adocconf/cli"
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adocconf
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Nathan Hammer
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: asciidoctor
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ description: adocconf allows you to create both configuration using AsciiDoc.
41
+ executables:
42
+ - adocconf
43
+ extensions: []
44
+ extra_rdoc_files: []
45
+ files:
46
+ - LICENSE
47
+ - exe/adocconf
48
+ - lib/adocconf.rb
49
+ - lib/adocconf/cli.rb
50
+ - lib/adocconf/errors.rb
51
+ - lib/adocconf/extract.rb
52
+ - lib/adocconf/parser.rb
53
+ - lib/adocconf/slugify.rb
54
+ - lib/adocconf/version.rb
55
+ homepage: https://github.com/vphammer/adocconf
56
+ licenses:
57
+ - MIT
58
+ metadata: {}
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '3.1'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 4.0.8
74
+ specification_version: 4
75
+ summary: Create configuration using AsciiDoc
76
+ test_files: []