coradoc 1.1.8 → 2.0.12

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.
Files changed (225) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -1
  3. data/Rakefile +3 -12
  4. data/exe/coradoc +21 -2
  5. data/lib/coradoc/cli.rb +185 -91
  6. data/lib/coradoc/configurable.rb +527 -0
  7. data/lib/coradoc/coradoc.rb +463 -0
  8. data/lib/coradoc/core_model/annotation_block.rb +57 -0
  9. data/lib/coradoc/core_model/base.rb +172 -0
  10. data/lib/coradoc/core_model/bibliography.rb +41 -0
  11. data/lib/coradoc/core_model/bibliography_entry.rb +48 -0
  12. data/lib/coradoc/core_model/block.rb +63 -0
  13. data/lib/coradoc/core_model/children_content.rb +53 -0
  14. data/lib/coradoc/core_model/comment_block.rb +10 -0
  15. data/lib/coradoc/core_model/definition_item.rb +46 -0
  16. data/lib/coradoc/core_model/definition_list.rb +28 -0
  17. data/lib/coradoc/core_model/element_attribute.rb +26 -0
  18. data/lib/coradoc/core_model/example_block.rb +10 -0
  19. data/lib/coradoc/core_model/footnote.rb +92 -0
  20. data/lib/coradoc/core_model/horizontal_rule_block.rb +10 -0
  21. data/lib/coradoc/core_model/id_generator.rb +16 -0
  22. data/lib/coradoc/core_model/image.rb +66 -0
  23. data/lib/coradoc/core_model/inline_element.rb +140 -0
  24. data/lib/coradoc/core_model/list_block.rb +135 -0
  25. data/lib/coradoc/core_model/list_item.rb +142 -0
  26. data/lib/coradoc/core_model/listing_block.rb +13 -0
  27. data/lib/coradoc/core_model/literal_block.rb +10 -0
  28. data/lib/coradoc/core_model/metadata.rb +79 -0
  29. data/lib/coradoc/core_model/open_block.rb +10 -0
  30. data/lib/coradoc/core_model/paragraph_block.rb +10 -0
  31. data/lib/coradoc/core_model/pass_block.rb +10 -0
  32. data/lib/coradoc/core_model/quote_block.rb +12 -0
  33. data/lib/coradoc/core_model/reviewer_block.rb +10 -0
  34. data/lib/coradoc/core_model/sidebar_block.rb +10 -0
  35. data/lib/coradoc/core_model/source_block.rb +10 -0
  36. data/lib/coradoc/core_model/structural_element.rb +94 -0
  37. data/lib/coradoc/core_model/table.rb +148 -0
  38. data/lib/coradoc/core_model/term.rb +53 -0
  39. data/lib/coradoc/core_model/text_content.rb +22 -0
  40. data/lib/coradoc/core_model/toc.rb +105 -0
  41. data/lib/coradoc/core_model/toc_generator.rb +151 -0
  42. data/lib/coradoc/core_model/verse_block.rb +12 -0
  43. data/lib/coradoc/core_model.rb +77 -0
  44. data/lib/coradoc/document_builder.rb +184 -0
  45. data/lib/coradoc/document_manipulator.rb +203 -0
  46. data/lib/coradoc/errors.rb +312 -0
  47. data/lib/coradoc/format_module.rb +49 -0
  48. data/lib/coradoc/hooks.rb +176 -0
  49. data/lib/coradoc/input.rb +17 -7
  50. data/lib/coradoc/logger.rb +54 -0
  51. data/lib/coradoc/output.rb +17 -6
  52. data/lib/coradoc/performance_regression.rb +109 -0
  53. data/lib/coradoc/processor_registry.rb +50 -0
  54. data/lib/coradoc/query.rb +455 -0
  55. data/lib/coradoc/registry.rb +156 -0
  56. data/lib/coradoc/serializer/registry.rb +150 -0
  57. data/lib/coradoc/transform.rb +11 -0
  58. data/lib/coradoc/validation.rb +646 -0
  59. data/lib/coradoc/version.rb +1 -1
  60. data/lib/coradoc/visitor.rb +283 -0
  61. data/lib/coradoc.rb +40 -19
  62. metadata +67 -277
  63. data/.editorconfig +0 -15
  64. data/.envrc +0 -1
  65. data/.irbrc +0 -1
  66. data/.pryrc.sample +0 -1
  67. data/.rubocop.yml +0 -14
  68. data/.rubocop_todo.yml +0 -179
  69. data/CHANGELOG.md +0 -9
  70. data/CODE_OF_CONDUCT.md +0 -84
  71. data/Dockerfile +0 -19
  72. data/Gemfile +0 -16
  73. data/LICENSE.txt +0 -21
  74. data/Makefile +0 -35
  75. data/README.Docker.adoc +0 -57
  76. data/README.adoc +0 -119
  77. data/coradoc.gemspec +0 -40
  78. data/docker-compose.yml +0 -14
  79. data/exe/reverse_adoc +0 -81
  80. data/exe/w2a +0 -60
  81. data/flake.lock +0 -114
  82. data/flake.nix +0 -135
  83. data/lib/coradoc/converter.rb +0 -144
  84. data/lib/coradoc/document.rb +0 -77
  85. data/lib/coradoc/element/admonition.rb +0 -18
  86. data/lib/coradoc/element/attribute.rb +0 -36
  87. data/lib/coradoc/element/attribute_list.rb +0 -138
  88. data/lib/coradoc/element/audio.rb +0 -33
  89. data/lib/coradoc/element/author.rb +0 -24
  90. data/lib/coradoc/element/base.rb +0 -92
  91. data/lib/coradoc/element/bibliography.rb +0 -24
  92. data/lib/coradoc/element/bibliography_entry.rb +0 -24
  93. data/lib/coradoc/element/block/core.rb +0 -76
  94. data/lib/coradoc/element/block/example.rb +0 -23
  95. data/lib/coradoc/element/block/listing.rb +0 -21
  96. data/lib/coradoc/element/block/literal.rb +0 -21
  97. data/lib/coradoc/element/block/open.rb +0 -22
  98. data/lib/coradoc/element/block/pass.rb +0 -21
  99. data/lib/coradoc/element/block/quote.rb +0 -19
  100. data/lib/coradoc/element/block/reviewer_comment.rb +0 -19
  101. data/lib/coradoc/element/block/side.rb +0 -19
  102. data/lib/coradoc/element/block/sourcecode.rb +0 -21
  103. data/lib/coradoc/element/block.rb +0 -17
  104. data/lib/coradoc/element/break.rb +0 -11
  105. data/lib/coradoc/element/comment_block.rb +0 -22
  106. data/lib/coradoc/element/comment_line.rb +0 -18
  107. data/lib/coradoc/element/document_attributes.rb +0 -33
  108. data/lib/coradoc/element/header.rb +0 -22
  109. data/lib/coradoc/element/image/block_image.rb +0 -32
  110. data/lib/coradoc/element/image/core.rb +0 -58
  111. data/lib/coradoc/element/image/inline_image.rb +0 -12
  112. data/lib/coradoc/element/image.rb +0 -10
  113. data/lib/coradoc/element/include.rb +0 -18
  114. data/lib/coradoc/element/inline/anchor.rb +0 -19
  115. data/lib/coradoc/element/inline/attribute_reference.rb +0 -19
  116. data/lib/coradoc/element/inline/bold.rb +0 -25
  117. data/lib/coradoc/element/inline/cross_reference.rb +0 -46
  118. data/lib/coradoc/element/inline/footnote.rb +0 -24
  119. data/lib/coradoc/element/inline/hard_line_break.rb +0 -11
  120. data/lib/coradoc/element/inline/highlight.rb +0 -25
  121. data/lib/coradoc/element/inline/italic.rb +0 -25
  122. data/lib/coradoc/element/inline/link.rb +0 -42
  123. data/lib/coradoc/element/inline/monospace.rb +0 -25
  124. data/lib/coradoc/element/inline/quotation.rb +0 -20
  125. data/lib/coradoc/element/inline/small.rb +0 -19
  126. data/lib/coradoc/element/inline/span.rb +0 -37
  127. data/lib/coradoc/element/inline/subscript.rb +0 -20
  128. data/lib/coradoc/element/inline/superscript.rb +0 -20
  129. data/lib/coradoc/element/inline/underline.rb +0 -19
  130. data/lib/coradoc/element/inline.rb +0 -23
  131. data/lib/coradoc/element/list/core.rb +0 -51
  132. data/lib/coradoc/element/list/definition.rb +0 -29
  133. data/lib/coradoc/element/list/ordered.rb +0 -17
  134. data/lib/coradoc/element/list/unordered.rb +0 -17
  135. data/lib/coradoc/element/list.rb +0 -13
  136. data/lib/coradoc/element/list_item.rb +0 -98
  137. data/lib/coradoc/element/list_item_definition.rb +0 -32
  138. data/lib/coradoc/element/paragraph.rb +0 -37
  139. data/lib/coradoc/element/revision.rb +0 -27
  140. data/lib/coradoc/element/section.rb +0 -62
  141. data/lib/coradoc/element/table.rb +0 -91
  142. data/lib/coradoc/element/tag.rb +0 -19
  143. data/lib/coradoc/element/term.rb +0 -22
  144. data/lib/coradoc/element/text_element.rb +0 -92
  145. data/lib/coradoc/element/title.rb +0 -62
  146. data/lib/coradoc/element/video.rb +0 -50
  147. data/lib/coradoc/generator.rb +0 -19
  148. data/lib/coradoc/input/adoc.rb +0 -30
  149. data/lib/coradoc/input/docx.rb +0 -64
  150. data/lib/coradoc/input/html/LICENSE.txt +0 -25
  151. data/lib/coradoc/input/html/README.adoc +0 -308
  152. data/lib/coradoc/input/html/cleaner.rb +0 -142
  153. data/lib/coradoc/input/html/config.rb +0 -77
  154. data/lib/coradoc/input/html/converters/a.rb +0 -52
  155. data/lib/coradoc/input/html/converters/aside.rb +0 -16
  156. data/lib/coradoc/input/html/converters/audio.rb +0 -29
  157. data/lib/coradoc/input/html/converters/base.rb +0 -108
  158. data/lib/coradoc/input/html/converters/blockquote.rb +0 -22
  159. data/lib/coradoc/input/html/converters/br.rb +0 -15
  160. data/lib/coradoc/input/html/converters/bypass.rb +0 -81
  161. data/lib/coradoc/input/html/converters/code.rb +0 -23
  162. data/lib/coradoc/input/html/converters/div.rb +0 -19
  163. data/lib/coradoc/input/html/converters/dl.rb +0 -62
  164. data/lib/coradoc/input/html/converters/drop.rb +0 -26
  165. data/lib/coradoc/input/html/converters/em.rb +0 -21
  166. data/lib/coradoc/input/html/converters/figure.rb +0 -25
  167. data/lib/coradoc/input/html/converters/h.rb +0 -42
  168. data/lib/coradoc/input/html/converters/head.rb +0 -23
  169. data/lib/coradoc/input/html/converters/hr.rb +0 -15
  170. data/lib/coradoc/input/html/converters/ignore.rb +0 -20
  171. data/lib/coradoc/input/html/converters/img.rb +0 -110
  172. data/lib/coradoc/input/html/converters/li.rb +0 -17
  173. data/lib/coradoc/input/html/converters/mark.rb +0 -19
  174. data/lib/coradoc/input/html/converters/markup.rb +0 -31
  175. data/lib/coradoc/input/html/converters/math.rb +0 -38
  176. data/lib/coradoc/input/html/converters/ol.rb +0 -65
  177. data/lib/coradoc/input/html/converters/p.rb +0 -23
  178. data/lib/coradoc/input/html/converters/pass_through.rb +0 -17
  179. data/lib/coradoc/input/html/converters/pre.rb +0 -55
  180. data/lib/coradoc/input/html/converters/q.rb +0 -16
  181. data/lib/coradoc/input/html/converters/strong.rb +0 -20
  182. data/lib/coradoc/input/html/converters/sub.rb +0 -22
  183. data/lib/coradoc/input/html/converters/sup.rb +0 -22
  184. data/lib/coradoc/input/html/converters/table.rb +0 -319
  185. data/lib/coradoc/input/html/converters/td.rb +0 -81
  186. data/lib/coradoc/input/html/converters/text.rb +0 -32
  187. data/lib/coradoc/input/html/converters/th.rb +0 -18
  188. data/lib/coradoc/input/html/converters/tr.rb +0 -22
  189. data/lib/coradoc/input/html/converters/video.rb +0 -29
  190. data/lib/coradoc/input/html/converters.rb +0 -59
  191. data/lib/coradoc/input/html/errors.rb +0 -14
  192. data/lib/coradoc/input/html/html_converter.rb +0 -168
  193. data/lib/coradoc/input/html/plugin.rb +0 -131
  194. data/lib/coradoc/input/html/plugins/plateau.rb +0 -213
  195. data/lib/coradoc/input/html/postprocessor.rb +0 -220
  196. data/lib/coradoc/input/html.rb +0 -61
  197. data/lib/coradoc/legacy_parser.rb +0 -200
  198. data/lib/coradoc/oscal.rb +0 -99
  199. data/lib/coradoc/output/adoc.rb +0 -19
  200. data/lib/coradoc/output/coradoc_tree_debug.rb +0 -21
  201. data/lib/coradoc/parser/asciidoc/admonition.rb +0 -24
  202. data/lib/coradoc/parser/asciidoc/attribute_list.rb +0 -89
  203. data/lib/coradoc/parser/asciidoc/base.rb +0 -87
  204. data/lib/coradoc/parser/asciidoc/bibliography.rb +0 -29
  205. data/lib/coradoc/parser/asciidoc/block.rb +0 -94
  206. data/lib/coradoc/parser/asciidoc/citation.rb +0 -30
  207. data/lib/coradoc/parser/asciidoc/content.rb +0 -64
  208. data/lib/coradoc/parser/asciidoc/document_attributes.rb +0 -25
  209. data/lib/coradoc/parser/asciidoc/header.rb +0 -29
  210. data/lib/coradoc/parser/asciidoc/inline.rb +0 -195
  211. data/lib/coradoc/parser/asciidoc/list.rb +0 -115
  212. data/lib/coradoc/parser/asciidoc/paragraph.rb +0 -54
  213. data/lib/coradoc/parser/asciidoc/section.rb +0 -61
  214. data/lib/coradoc/parser/asciidoc/table.rb +0 -32
  215. data/lib/coradoc/parser/asciidoc/term.rb +0 -41
  216. data/lib/coradoc/parser/asciidoc/text.rb +0 -158
  217. data/lib/coradoc/parser/base.rb +0 -40
  218. data/lib/coradoc/parser.rb +0 -11
  219. data/lib/coradoc/reverse_adoc.rb +0 -18
  220. data/lib/coradoc/transformer.rb +0 -476
  221. data/lib/coradoc/util.rb +0 -12
  222. data/lib/reverse_adoc.rb +0 -20
  223. data/utils/inspect_asciidoc.rb +0 -29
  224. data/utils/parser_analyzer.rb +0 -66
  225. data/utils/round_trip.rb +0 -53
@@ -0,0 +1,527 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ # Global configuration system for Coradoc.
5
+ #
6
+ # Provides centralized configuration management with support for:
7
+ # - Configuration files (.coradoc.yml)
8
+ # - Environment-specific settings
9
+ # - Per-module configuration merging
10
+ # - Validation of configuration values
11
+ #
12
+ # @example Basic configuration
13
+ # Coradoc.configure do |config|
14
+ # config.default_format = :asciidoc
15
+ # config.cache.enabled = true
16
+ # config.cache.max_size = 1000
17
+ # end
18
+ #
19
+ # @example Environment-specific configuration
20
+ # Coradoc.configure do |config|
21
+ # config.environment = ENV.fetch("RACK_ENV", "development")
22
+ # config.cache.enabled = config.production?
23
+ # end
24
+ #
25
+ # @example Loading from file
26
+ # Coradoc::Configuration.load_file(".coradoc.yml")
27
+ #
28
+ module Configurable
29
+ # Configuration error
30
+ class ConfigurationError < Coradoc::Error; end
31
+
32
+ # Base class for configuration sections
33
+ class ConfigSection
34
+ # @return [Hash] Raw configuration values
35
+ attr_reader :options
36
+
37
+ def self.symbolize_keys(hash)
38
+ return {} unless hash.is_a?(Hash)
39
+
40
+ hash.transform_keys(&:to_sym)
41
+ end
42
+
43
+ # Create a configuration section
44
+ #
45
+ # @param options [Hash] Configuration options
46
+ def initialize(options = {})
47
+ @options = symbolize_keys(options)
48
+ after_initialize
49
+ end
50
+
51
+ # Hook for subclass initialization
52
+ def after_initialize; end
53
+
54
+ # Get a configuration value
55
+ #
56
+ # @param key [Symbol] Configuration key
57
+ # @return [Object] Configuration value
58
+ def [](key)
59
+ @options[key]
60
+ end
61
+
62
+ # Set a configuration value
63
+ #
64
+ # @param key [Symbol] Configuration key
65
+ # @param value [Object] Configuration value
66
+ def []=(key, value)
67
+ @options[key] = value
68
+ end
69
+
70
+ # Merge options into this section
71
+ #
72
+ # @param other [Hash, ConfigSection] Options to merge
73
+ # @return [void]
74
+ def merge!(other)
75
+ other_options = other.is_a?(ConfigSection) ? other.options : other
76
+ other_options = symbolize_keys(other_options)
77
+ @options.merge!(other_options)
78
+ # Apply merged options to accessors
79
+ apply_options(other_options)
80
+ end
81
+
82
+ # Convert to hash
83
+ #
84
+ # @return [Hash]
85
+ def to_h
86
+ @options.transform_values do |v|
87
+ v.is_a?(ConfigSection) ? v.to_h : v
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def symbolize_keys(hash)
94
+ self.class.symbolize_keys(hash)
95
+ end
96
+
97
+ # Apply options to instance variables
98
+ # Override in subclasses for custom handling
99
+ def apply_options(options)
100
+ options.each do |key, value|
101
+ setter = "#{key}="
102
+ instance_variable_set("@#{key}", value) if public_methods.include?(setter.to_sym)
103
+ end
104
+ end
105
+ end
106
+
107
+ # Cache configuration section
108
+ class CacheConfig < ConfigSection
109
+ # @return [Boolean] Whether caching is enabled
110
+ attr_accessor :enabled
111
+
112
+ # @return [Integer] Maximum cache size (number of entries)
113
+ attr_accessor :max_size
114
+
115
+ # @return [Integer] Cache TTL in seconds (0 = no expiry)
116
+ attr_accessor :ttl
117
+
118
+ # @return [Symbol] Cache storage backend (:memory, :file, :redis)
119
+ attr_accessor :backend
120
+
121
+ # @return [String, nil] Cache directory for file backend
122
+ attr_accessor :cache_dir
123
+
124
+ def initialize(options = {})
125
+ super
126
+ @enabled = @options.fetch(:enabled, true)
127
+ @max_size = @options.fetch(:max_size, 1000)
128
+ @ttl = @options.fetch(:ttl, 0)
129
+ @backend = @options.fetch(:backend, :memory)
130
+ @cache_dir = @options.fetch(:cache_dir, nil)
131
+ end
132
+ end
133
+
134
+ # Parser configuration section
135
+ class ParserConfig < ConfigSection
136
+ # @return [Boolean] Enable parser caching
137
+ attr_accessor :cache_enabled
138
+
139
+ # @return [Boolean] Strict parsing mode
140
+ attr_accessor :strict_mode
141
+
142
+ # @return [Boolean] Include source location in AST
143
+ attr_accessor :include_source_loc
144
+
145
+ # @return [Integer] Maximum nesting depth
146
+ attr_accessor :max_nesting_depth
147
+
148
+ def initialize(options = {})
149
+ super
150
+ @cache_enabled = @options.fetch(:cache_enabled, true)
151
+ @strict_mode = @options.fetch(:strict_mode, false)
152
+ @include_source_loc = @options.fetch(:include_source_loc, true)
153
+ @max_nesting_depth = @options.fetch(:max_nesting_depth, 100)
154
+ end
155
+ end
156
+
157
+ # Transformer configuration section
158
+ class TransformerConfig < ConfigSection
159
+ # @return [Boolean] Enable transformation caching
160
+ attr_accessor :cache_enabled
161
+
162
+ # @return [Boolean] Preserve unknown elements
163
+ attr_accessor :preserve_unknown
164
+
165
+ # @return [Boolean] Validate after transformation
166
+ attr_accessor :validate_output
167
+
168
+ # @return [Array<Symbol>] Enabled transformers
169
+ attr_accessor :enabled_transformers
170
+
171
+ def initialize(options = {})
172
+ super
173
+ @cache_enabled = @options.fetch(:cache_enabled, true)
174
+ @preserve_unknown = @options.fetch(:preserve_unknown, true)
175
+ @validate_output = @options.fetch(:validate_output, false)
176
+ @enabled_transformers = @options.fetch(:enabled_transformers, [])
177
+ end
178
+ end
179
+
180
+ # Output configuration section
181
+ class OutputConfig < ConfigSection
182
+ # @return [Symbol] Default output format
183
+ attr_accessor :default_format
184
+
185
+ # @return [Boolean] Pretty print output
186
+ attr_accessor :pretty_print
187
+
188
+ # @return [Integer] Line width for text output
189
+ attr_accessor :line_width
190
+
191
+ # @return [String] Indentation string
192
+ attr_accessor :indent
193
+
194
+ # @return [Boolean] Include metadata in output
195
+ attr_accessor :include_metadata
196
+
197
+ def initialize(options = {})
198
+ super
199
+ @default_format = @options.fetch(:default_format, :html)
200
+ @pretty_print = @options.fetch(:pretty_print, true)
201
+ @line_width = @options.fetch(:line_width, 80)
202
+ @indent = @options.fetch(:indent, ' ')
203
+ @include_metadata = @options.fetch(:include_metadata, false)
204
+ end
205
+ end
206
+
207
+ # Logging configuration section
208
+ class LoggingConfig < ConfigSection
209
+ # @return [Symbol] Log level (:debug, :info, :warn, :error)
210
+ attr_accessor :level
211
+
212
+ # @return [Boolean] Include timestamps
213
+ attr_accessor :timestamps
214
+
215
+ # @return [IO, nil] Log output destination
216
+ attr_accessor :output
217
+
218
+ # @return [Boolean] Colorize output
219
+ attr_accessor :colorize
220
+
221
+ def initialize(options = {})
222
+ super
223
+ @level = @options.fetch(:level, :info)
224
+ @timestamps = @options.fetch(:timestamps, true)
225
+ @output = @options.fetch(:output, $stderr)
226
+ @colorize = @options.fetch(:colorize, true)
227
+ end
228
+ end
229
+
230
+ # Main configuration class
231
+ class Configuration
232
+ # @return [String] Current environment
233
+ attr_accessor :environment
234
+
235
+ # @return [CacheConfig] Cache configuration
236
+ attr_reader :cache
237
+
238
+ # @return [ParserConfig] Parser configuration
239
+ attr_reader :parser
240
+
241
+ # @return [TransformerConfig] Transformer configuration
242
+ attr_reader :transformer
243
+
244
+ # @return [OutputConfig] Output configuration
245
+ attr_reader :output
246
+
247
+ # @return [LoggingConfig] Logging configuration
248
+ attr_reader :logging
249
+
250
+ # @return [Hash] Custom configuration values
251
+ attr_reader :custom
252
+
253
+ # Create a new configuration
254
+ #
255
+ # @param options [Hash] Configuration options
256
+ def initialize(options = {})
257
+ options = symbolize_keys(options)
258
+ @environment = options.fetch(:environment, detect_environment)
259
+ @cache = CacheConfig.new(options[:cache] || {})
260
+ @parser = ParserConfig.new(options[:parser] || {})
261
+ @transformer = TransformerConfig.new(options[:transformer] || {})
262
+ @output = OutputConfig.new(options[:output] || {})
263
+ @logging = LoggingConfig.new(options[:logging] || {})
264
+ @custom = options.fetch(:custom, {})
265
+ end
266
+
267
+ # Check if running in development environment
268
+ #
269
+ # @return [Boolean]
270
+ def development?
271
+ @environment == 'development'
272
+ end
273
+
274
+ # Check if running in production environment
275
+ #
276
+ # @return [Boolean]
277
+ def production?
278
+ @environment == 'production'
279
+ end
280
+
281
+ # Check if running in test environment
282
+ #
283
+ # @return [Boolean]
284
+ def test?
285
+ @environment == 'test'
286
+ end
287
+
288
+ # Get a custom configuration value
289
+ #
290
+ # @param key [Symbol] Configuration key
291
+ # @return [Object] Configuration value
292
+ def [](key)
293
+ @custom[key.to_sym]
294
+ end
295
+
296
+ # Set a custom configuration value
297
+ #
298
+ # @param key [Symbol] Configuration key
299
+ # @param value [Object] Configuration value
300
+ def []=(key, value)
301
+ @custom[key.to_sym] = value
302
+ end
303
+
304
+ # Merge another configuration into this one
305
+ #
306
+ # @param other [Configuration, Hash] Configuration to merge
307
+ # @return [void]
308
+ def merge!(other)
309
+ case other
310
+ when Configuration
311
+ @environment = other.environment if other.environment != detect_environment
312
+ @cache.merge!(other.cache)
313
+ @parser.merge!(other.parser)
314
+ @transformer.merge!(other.transformer)
315
+ @output.merge!(other.output)
316
+ @logging.merge!(other.logging)
317
+ @custom.merge!(other.custom)
318
+ when Hash
319
+ merge_hash(other)
320
+ end
321
+ end
322
+
323
+ # Create a copy of this configuration
324
+ #
325
+ # @return [Configuration]
326
+ def dup
327
+ self.class.new(to_h)
328
+ end
329
+
330
+ # Convert configuration to hash
331
+ #
332
+ # @return [Hash]
333
+ def to_h
334
+ {
335
+ environment: @environment,
336
+ cache: @cache.to_h,
337
+ parser: @parser.to_h,
338
+ transformer: @transformer.to_h,
339
+ output: @output.to_h,
340
+ logging: @logging.to_h,
341
+ custom: @custom.dup
342
+ }
343
+ end
344
+
345
+ # Load configuration from a YAML file
346
+ #
347
+ # @param path [String] Path to configuration file
348
+ # @return [Configuration] Loaded configuration
349
+ # @raise [ConfigurationError] If file cannot be loaded
350
+ def self.load_file(path)
351
+ require 'yaml'
352
+
353
+ raise ConfigurationError, "Configuration file not found: #{path}" unless File.exist?(path)
354
+
355
+ begin
356
+ yaml_content = YAML.load_file(path)
357
+ new(yaml_content || {})
358
+ rescue Psych::SyntaxError => e
359
+ raise ConfigurationError, "Invalid YAML in #{path}: #{e.message}"
360
+ end
361
+ end
362
+
363
+ # Load configuration from environment variables
364
+ #
365
+ # @param prefix [String] Environment variable prefix
366
+ # @return [Configuration] Configuration from environment
367
+ def self.load_environment(prefix = 'CORADOC')
368
+ options = {}
369
+
370
+ # Parse CORADOC_CACHE_ENABLED=true style variables
371
+ ENV.each do |key, value|
372
+ next unless key.start_with?("#{prefix}_")
373
+
374
+ # Convert CORADOC_CACHE_ENABLED to [:cache, :enabled]
375
+ parts = key.sub("#{prefix}_", '').downcase.split('_')
376
+ next if parts.length < 2
377
+
378
+ section = parts.first.to_sym
379
+ setting = parts[1..].join('_').to_sym
380
+
381
+ options[section] ||= {}
382
+ options[section][setting] = parse_env_value(value)
383
+ end
384
+
385
+ # Special handling for CORADOC_ENV
386
+ options[:environment] = ENV["#{prefix}_ENV"] if ENV["#{prefix}_ENV"]
387
+
388
+ new(options)
389
+ end
390
+
391
+ # Reset configuration to defaults
392
+ #
393
+ # @return [void]
394
+ def reset!
395
+ @environment = detect_environment
396
+ @cache = CacheConfig.new
397
+ @parser = ParserConfig.new
398
+ @transformer = TransformerConfig.new
399
+ @output = OutputConfig.new
400
+ @logging = LoggingConfig.new
401
+ @custom = {}
402
+ end
403
+
404
+ # Validate configuration
405
+ #
406
+ # @return [Array<String>] List of validation errors
407
+ def validate
408
+ errors = []
409
+
410
+ errors << 'cache.max_size must be positive' if @cache.max_size <= 0
411
+ errors << 'cache.ttl must be non-negative' if @cache.ttl.negative?
412
+ errors << 'parser.max_nesting_depth must be positive' if @parser.max_nesting_depth <= 0
413
+ errors << 'output.line_width must be positive' if @output.line_width <= 0
414
+
415
+ errors << 'cache.backend must be :memory, :file, or :redis' unless %i[memory file
416
+ redis].include?(@cache.backend)
417
+
418
+ errors << 'logging.level must be :debug, :info, :warn, or :error' unless %i[debug info warn
419
+ error].include?(@logging.level)
420
+
421
+ errors
422
+ end
423
+
424
+ # Check if configuration is valid
425
+ #
426
+ # @return [Boolean]
427
+ def valid?
428
+ validate.empty?
429
+ end
430
+
431
+ private
432
+
433
+ def detect_environment
434
+ ENV.fetch('RACK_ENV', ENV.fetch('RAILS_ENV', 'development'))
435
+ end
436
+
437
+ def symbolize_keys(hash)
438
+ ConfigSection.symbolize_keys(hash)
439
+ end
440
+
441
+ def merge_hash(hash)
442
+ hash = symbolize_keys(hash)
443
+
444
+ @environment = hash[:environment] if hash.key?(:environment)
445
+ @cache.merge!(hash[:cache]) if hash.key?(:cache)
446
+ @parser.merge!(hash[:parser]) if hash.key?(:parser)
447
+ @transformer.merge!(hash[:transformer]) if hash.key?(:transformer)
448
+ @output.merge!(hash[:output]) if hash.key?(:output)
449
+ @logging.merge!(hash[:logging]) if hash.key?(:logging)
450
+ @custom.merge!(symbolize_keys(hash[:custom] || {}))
451
+ end
452
+
453
+ def self.parse_env_value(value)
454
+ case value.downcase
455
+ when 'true', 'yes', '1'
456
+ true
457
+ when 'false', 'no', '0'
458
+ false
459
+ when /^\d+$/
460
+ value.to_i
461
+ when /^\d+\.\d+$/
462
+ value.to_f
463
+ else
464
+ value
465
+ end
466
+ end
467
+ private_class_method :parse_env_value
468
+ end
469
+
470
+ class << self
471
+ # Get current global configuration
472
+ #
473
+ # @return [Configuration]
474
+ def configuration
475
+ @configuration ||= Configuration.new
476
+ end
477
+
478
+ # Set global configuration
479
+ #
480
+ # @param config [Configuration] Configuration to set
481
+ # @return [void]
482
+ attr_writer :configuration
483
+
484
+ # Configure Coradoc
485
+ #
486
+ # @yield [Configuration] Block receives configuration object
487
+ # @return [void]
488
+ def configure
489
+ yield configuration if block_given?
490
+ end
491
+
492
+ # Reset configuration to defaults
493
+ #
494
+ # @return [void]
495
+ def reset_configuration!
496
+ @configuration = Configuration.new
497
+ end
498
+
499
+ # Load configuration from file
500
+ #
501
+ # @param path [String] Path to configuration file
502
+ # @return [void]
503
+ def load_configuration(path)
504
+ config = Configuration.load_file(path)
505
+ configuration.merge!(config)
506
+ end
507
+ end
508
+ end
509
+
510
+ # Include Configurable in main module for easy access
511
+ extend Configurable
512
+
513
+ # Shortcut to configuration
514
+ #
515
+ # @return [Configuration]
516
+ def self.config
517
+ Configurable.configuration
518
+ end
519
+
520
+ # Shortcut to configure
521
+ #
522
+ # @yield [Configuration]
523
+ # @return [void]
524
+ def self.configure(&block)
525
+ Configurable.configure(&block) if block_given?
526
+ end
527
+ end