coradoc 2.0.22 → 2.0.24
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 +4 -4
- data/lib/coradoc/coradoc.rb +35 -402
- data/lib/coradoc/core_model/base.rb +39 -0
- data/lib/coradoc/core_model/comment_block.rb +4 -0
- data/lib/coradoc/core_model/comment_line.rb +4 -0
- data/lib/coradoc/core_model/frontmatter/codec.rb +66 -19
- data/lib/coradoc/core_model/frontmatter.rb +4 -0
- data/lib/coradoc/core_model/inline_content.rb +77 -0
- data/lib/coradoc/core_model/inline_element.rb +52 -26
- data/lib/coradoc/core_model/list_block.rb +17 -0
- data/lib/coradoc/core_model/list_item.rb +21 -0
- data/lib/coradoc/core_model/output_artifact.rb +48 -0
- data/lib/coradoc/core_model/paragraph_block.rb +4 -0
- data/lib/coradoc/core_model/stem_block.rb +21 -0
- data/lib/coradoc/core_model/structural_element.rb +41 -0
- data/lib/coradoc/core_model/text_content.rb +4 -0
- data/lib/coradoc/core_model.rb +3 -0
- data/lib/coradoc/dispatch.rb +95 -0
- data/lib/coradoc/format_catalog.rb +83 -0
- data/lib/coradoc/introspection/element_counter.rb +48 -0
- data/lib/coradoc/introspection.rb +72 -0
- data/lib/coradoc/link_rewriter/identity.rb +13 -0
- data/lib/coradoc/link_rewriter/visitor.rb +157 -0
- data/lib/coradoc/link_rewriter.rb +37 -0
- data/lib/coradoc/pipeline.rb +108 -0
- data/lib/coradoc/relative_path.rb +32 -0
- data/lib/coradoc/version.rb +1 -1
- data/lib/coradoc.rb +7 -13
- metadata +13 -9
- data/lib/coradoc/document_builder.rb +0 -184
- data/lib/coradoc/document_manipulator.rb +0 -203
- data/lib/coradoc/input.rb +0 -22
- data/lib/coradoc/output.rb +0 -22
- data/lib/coradoc/processor_registry.rb +0 -50
- data/lib/coradoc/serializer/registry.rb +0 -150
- data/lib/coradoc/transform/base.rb +0 -21
- data/lib/coradoc/transform.rb +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bcce76c634ae51a857397a301fc2638950cb25e1a97048589ee5510f8a7aee40
|
|
4
|
+
data.tar.gz: 6cd3b391ae83e360ed282b40f778dad1c4290202b386b457b881e264ddca464e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f424dfb3267d2497dc46b9e764fab868083add0ef5cd82163aa89a35b884e75b81c7426a47d9c0b81abd15659dcc15b49f6f309b786848cba7c0aed42557cf3a
|
|
7
|
+
data.tar.gz: 07fe25f772cf02b7673f25f4bb41e9abb65e25631898af071407eb68961bba9c2a74c19c7d20d85544c722a4ad07404c4846d389278fb989d2a186caab36bdbb
|
data/lib/coradoc/coradoc.rb
CHANGED
|
@@ -52,402 +52,67 @@ module Coradoc
|
|
|
52
52
|
# @see Coradoc::UnsupportedFormatError Unsupported format errors
|
|
53
53
|
|
|
54
54
|
class << self
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def registry
|
|
59
|
-
@registry ||= Registry.new
|
|
60
|
-
end
|
|
55
|
+
# ---- Format registry (delegates to FormatCatalog) ----
|
|
56
|
+
|
|
57
|
+
def registry = FormatCatalog.registry
|
|
61
58
|
|
|
62
|
-
# Register a format gem
|
|
63
|
-
#
|
|
64
|
-
# @param format_name [Symbol] the format name (e.g., :asciidoc, :html, :markdown)
|
|
65
|
-
# @param format_module [Module] the format module
|
|
66
|
-
# @param options [Hash] optional configuration (e.g., extensions: [])
|
|
67
|
-
# @return [void]
|
|
68
59
|
def register_format(format_name, format_module, **options)
|
|
69
|
-
|
|
70
|
-
registry.register(format_name, format_module, options)
|
|
71
|
-
FormatModule.validate!(format_module, format_name)
|
|
60
|
+
FormatCatalog.register_format(format_name, format_module, **options)
|
|
72
61
|
end
|
|
73
62
|
|
|
74
|
-
|
|
75
|
-
#
|
|
76
|
-
# @param format_name [Symbol] the format name
|
|
77
|
-
# @return [Module, nil] the format module or nil if not found
|
|
78
|
-
def get_format(format_name)
|
|
79
|
-
registry.get(format_name)
|
|
80
|
-
end
|
|
63
|
+
def get_format(format_name) = FormatCatalog.get_format(format_name)
|
|
81
64
|
|
|
82
|
-
|
|
83
|
-
#
|
|
84
|
-
# @return [Array<Symbol>] list of registered format names
|
|
85
|
-
def registered_formats
|
|
86
|
-
registry.list
|
|
87
|
-
end
|
|
65
|
+
def registered_formats = FormatCatalog.registered_formats
|
|
88
66
|
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
# Graph mode is the only mode: +include::+ directives survive as
|
|
92
|
-
# +CoreModel::Include+ link nodes pointing at other files. NO file
|
|
93
|
-
# I/O happens during parse. The result is a single document that
|
|
94
|
-
# references other documents via Include edges — a text graph.
|
|
95
|
-
#
|
|
96
|
-
# To splice included content inline, call +Coradoc.resolve_includes+
|
|
97
|
-
# on the parsed document. This is an explicit, separate step so the
|
|
98
|
-
# caller controls when (and whether) file I/O happens.
|
|
99
|
-
#
|
|
100
|
-
# @param text [String] the document text to parse
|
|
101
|
-
# @param format [Symbol] the source format (:asciidoc, :html, :markdown)
|
|
102
|
-
# @return [Coradoc::CoreModel::Base, Object] the parsed document model
|
|
103
|
-
# @raise [UnsupportedFormatError] if the format is not registered
|
|
104
|
-
#
|
|
105
|
-
# @example Parse — Include directives stay as link nodes
|
|
106
|
-
# doc = Coradoc.parse(text, format: :asciidoc)
|
|
107
|
-
#
|
|
108
|
-
# @example Then flatten — splice included files inline
|
|
109
|
-
# flat = Coradoc.resolve_includes(doc, base_dir: Dir.pwd)
|
|
110
|
-
def parse(text, format:)
|
|
111
|
-
format_module = get_format(format)
|
|
112
|
-
unless format_module
|
|
113
|
-
raise UnsupportedFormatError,
|
|
114
|
-
"Format '#{format}' is not registered. " \
|
|
115
|
-
"Available formats: #{registered_formats.join(', ')}"
|
|
116
|
-
end
|
|
67
|
+
# ---- Pipeline (delegates to Pipeline) ----
|
|
117
68
|
|
|
118
|
-
|
|
119
|
-
result = format_module.parse_to_core(text)
|
|
120
|
-
Hooks.invoke(:after_parse, result, format: format)
|
|
121
|
-
end
|
|
69
|
+
def parse(text, format:) = Pipeline.parse(text, format: format)
|
|
122
70
|
|
|
123
|
-
|
|
124
|
-
#
|
|
125
|
-
# Walks the document tree and replaces every +CoreModel::Include+
|
|
126
|
-
# link node with the parsed content of its target file, recursing
|
|
127
|
-
# into the result. The original document is left unchanged; a new
|
|
128
|
-
# subtree is constructed.
|
|
129
|
-
#
|
|
130
|
-
# This is the explicit "flatten" step that turns a text graph into
|
|
131
|
-
# a single spliced document. Callers control:
|
|
132
|
-
# - +base_dir+ — where to root relative include paths
|
|
133
|
-
# - +missing_include+ — what to do when a target is missing
|
|
134
|
-
# - +max_depth+ — recursion cap
|
|
135
|
-
# - +allow_unsafe+ — opt out of path-traversal protection
|
|
136
|
-
# - +resolver+ — custom resolution strategy (e.g. HTTP, in-memory)
|
|
137
|
-
#
|
|
138
|
-
# @param document [Coradoc::CoreModel::Base] parsed document
|
|
139
|
-
# @param base_dir [String] base directory for relative include paths
|
|
140
|
-
# @param missing_include [Symbol] :error (default), :warn, :silent, :passthrough
|
|
141
|
-
# @param max_depth [Integer] recursion cap (default 64)
|
|
142
|
-
# @param allow_unsafe [Boolean] disable path-traversal protection
|
|
143
|
-
# @param resolver [Object, nil] custom resolver. Defaults to
|
|
144
|
-
# +Coradoc::IncludeResolver::Filesystem+ rooted at +base_dir+.
|
|
145
|
-
# @return [Coradoc::CoreModel::Base] new document with includes expanded
|
|
146
|
-
# @raise [Coradoc::IncludeNotFoundError] when a target is missing
|
|
147
|
-
# and policy is :error
|
|
148
|
-
# @raise [Coradoc::IncludeDepthExceededError] when +max_depth+ is hit
|
|
149
|
-
# @raise [Coradoc::CircularIncludeError] when an include cycle is detected
|
|
150
|
-
#
|
|
151
|
-
# @example
|
|
152
|
-
# doc = Coradoc.parse(text, format: :asciidoc)
|
|
153
|
-
# flat = Coradoc.resolve_includes(doc, base_dir: Dir.pwd)
|
|
154
|
-
def resolve_includes(document, base_dir:,
|
|
155
|
-
missing_include: :error,
|
|
156
|
-
max_depth: Coradoc::ResolveIncludes::DEFAULT_MAX_DEPTH,
|
|
157
|
-
allow_unsafe: false,
|
|
158
|
-
resolver: nil)
|
|
159
|
-
resolver = Coradoc::IncludeResolver.coerce(
|
|
160
|
-
resolver,
|
|
161
|
-
base_dir: base_dir,
|
|
162
|
-
allow_unsafe: allow_unsafe
|
|
163
|
-
)
|
|
164
|
-
Coradoc::ResolveIncludes.call(
|
|
165
|
-
document,
|
|
166
|
-
resolver: resolver,
|
|
167
|
-
base_dir: base_dir,
|
|
168
|
-
missing_include: missing_include,
|
|
169
|
-
max_depth: max_depth
|
|
170
|
-
)
|
|
171
|
-
end
|
|
71
|
+
def resolve_includes(document, **) = Pipeline.resolve_includes(document, **)
|
|
172
72
|
|
|
173
|
-
|
|
174
|
-
#
|
|
175
|
-
# This is the main entry point for format conversion. It handles the
|
|
176
|
-
# complete pipeline: parse -> transform to CoreModel -> transform to target -> serialize
|
|
177
|
-
#
|
|
178
|
-
# @param text [String] the source document text
|
|
179
|
-
# @param from [Symbol] the source format (:asciidoc, :html, :markdown)
|
|
180
|
-
# @param to [Symbol] the target format (:asciidoc, :html, :markdown)
|
|
181
|
-
# @param options [Hash] additional options for the conversion
|
|
182
|
-
# @return [String] the converted document text
|
|
183
|
-
# @raise [UnsupportedFormatError] if a format is not registered
|
|
184
|
-
#
|
|
185
|
-
# @example Convert AsciiDoc to HTML
|
|
186
|
-
# html = Coradoc.convert(adoc_text, from: :asciidoc, to: :html)
|
|
187
|
-
#
|
|
188
|
-
# @example Convert HTML to AsciiDoc
|
|
189
|
-
# adoc = Coradoc.convert(html_text, from: :html, to: :asciidoc)
|
|
190
|
-
def convert(text, from:, to:, **)
|
|
191
|
-
# Parse to CoreModel
|
|
192
|
-
core = parse(text, format: from)
|
|
193
|
-
|
|
194
|
-
# Convert to target format
|
|
195
|
-
serialize(core, to: to, **)
|
|
196
|
-
end
|
|
73
|
+
def rewrite_links(...) = Pipeline.rewrite_links(...)
|
|
197
74
|
|
|
198
|
-
|
|
199
|
-
#
|
|
200
|
-
# @param model [Object] a format-specific model
|
|
201
|
-
# @return [Coradoc::CoreModel::Base] the CoreModel representation
|
|
202
|
-
def to_core(model)
|
|
203
|
-
return model if model.is_a?(CoreModel::Base)
|
|
75
|
+
def convert(text, **) = Pipeline.convert(text, **)
|
|
204
76
|
|
|
205
|
-
|
|
206
|
-
next unless format_module.handles_model?(model)
|
|
77
|
+
def to_core(model) = Pipeline.to_core(model)
|
|
207
78
|
|
|
208
|
-
|
|
209
|
-
end
|
|
79
|
+
def serialize(model, **) = Pipeline.serialize(model, **)
|
|
210
80
|
|
|
211
|
-
|
|
212
|
-
end
|
|
81
|
+
def build(...) = Pipeline.build(...)
|
|
213
82
|
|
|
214
|
-
|
|
215
|
-
#
|
|
216
|
-
# @param model [Coradoc::CoreModel::Base] the CoreModel to serialize
|
|
217
|
-
# @param to [Symbol] the target format
|
|
218
|
-
# @param options [Hash] additional options
|
|
219
|
-
# @return [String] the serialized document
|
|
220
|
-
def serialize(model, to:, **)
|
|
221
|
-
format_module = get_format(to)
|
|
222
|
-
raise UnsupportedFormatError, "Format '#{to}' is not registered" unless format_module
|
|
223
|
-
|
|
224
|
-
model = Hooks.invoke(:before_serialize, model, format: to)
|
|
225
|
-
result = format_module.serialize(model, **)
|
|
226
|
-
Hooks.invoke(:after_serialize, result, format: to)
|
|
227
|
-
end
|
|
83
|
+
def parse_file(path, **) = Pipeline.parse_file(path, **)
|
|
228
84
|
|
|
229
|
-
|
|
230
|
-
#
|
|
231
|
-
# @param document [Coradoc::CoreModel::Base] the document to manipulate
|
|
232
|
-
# @return [DocumentManipulator] a new manipulator instance
|
|
233
|
-
#
|
|
234
|
-
# @example Chainable document manipulation
|
|
235
|
-
# html = Coradoc.manipulate(doc)
|
|
236
|
-
# .transform_text(&:upcase)
|
|
237
|
-
# .add_toc
|
|
238
|
-
# .to_html
|
|
239
|
-
def manipulate(document)
|
|
240
|
-
DocumentManipulator.new(document)
|
|
241
|
-
end
|
|
85
|
+
def convert_file(path, **) = Pipeline.convert_file(path, **)
|
|
242
86
|
|
|
243
|
-
#
|
|
244
|
-
#
|
|
245
|
-
# @param filename [String] Filename or extension to detect
|
|
246
|
-
# @return [Symbol, nil] the detected format symbol
|
|
247
|
-
#
|
|
248
|
-
# @example
|
|
249
|
-
# Coradoc.detect_format("document.adoc") # => :asciidoc
|
|
250
|
-
# Coradoc.detect_format("file.md") # => :markdown
|
|
251
|
-
def detect_format(filename)
|
|
252
|
-
ext = File.extname(filename).downcase
|
|
253
|
-
registry.each_key do |name|
|
|
254
|
-
opts = registry.options_for(name)
|
|
255
|
-
return name if opts[:extensions]&.include?(ext)
|
|
256
|
-
end
|
|
257
|
-
nil
|
|
258
|
-
end
|
|
87
|
+
# ---- Format detection (delegates to FormatCatalog) ----
|
|
259
88
|
|
|
260
|
-
|
|
261
|
-
#
|
|
262
|
-
# Handles both text formats (reads file content) and binary formats
|
|
263
|
-
# (passes file path directly to the format module).
|
|
264
|
-
#
|
|
265
|
-
# @param path [String] path to the document file
|
|
266
|
-
# @param format [Symbol, nil] source format (auto-detected if nil)
|
|
267
|
-
# @return [Coradoc::CoreModel::Base] the parsed CoreModel document
|
|
268
|
-
# @raise [UnsupportedFormatError] if format is not detected or registered
|
|
269
|
-
#
|
|
270
|
-
# @example
|
|
271
|
-
# doc = Coradoc.parse_file("document.adoc")
|
|
272
|
-
# doc = Coradoc.parse_file("report.docx", format: :docx)
|
|
273
|
-
def parse_file(path, format: nil)
|
|
274
|
-
raise FileNotFoundError, path unless File.exist?(path)
|
|
89
|
+
def detect_format(filename) = FormatCatalog.detect_format(filename)
|
|
275
90
|
|
|
276
|
-
|
|
277
|
-
raise UnsupportedFormatError, "Could not detect format for: #{path}" unless source_format
|
|
91
|
+
def binary_format?(format) = FormatCatalog.binary_format?(format)
|
|
278
92
|
|
|
279
|
-
|
|
280
|
-
raise UnsupportedFormatError, "Format '#{source_format}' is not registered" unless format_module
|
|
93
|
+
def normalize_format(name) = FormatCatalog.normalize_format(name)
|
|
281
94
|
|
|
282
|
-
|
|
283
|
-
format_module.parse_to_core(path)
|
|
284
|
-
else
|
|
285
|
-
content = File.read(path)
|
|
286
|
-
content = Hooks.invoke(:before_parse, content, format: source_format)
|
|
287
|
-
result = format_module.parse_file_to_core(path, content)
|
|
288
|
-
Hooks.invoke(:after_parse, result, format: source_format)
|
|
289
|
-
end
|
|
290
|
-
end
|
|
95
|
+
def serialize_format?(format) = FormatCatalog.serialize_format?(format)
|
|
291
96
|
|
|
292
|
-
|
|
293
|
-
#
|
|
294
|
-
# @param path [String] path to the source document file
|
|
295
|
-
# @param from [Symbol, nil] source format (auto-detected if nil)
|
|
296
|
-
# @param to [Symbol] target format
|
|
297
|
-
# @param options [Hash] additional options
|
|
298
|
-
# @return [String] the converted document text
|
|
299
|
-
#
|
|
300
|
-
# @example
|
|
301
|
-
# html = Coradoc.convert_file("document.adoc", to: :html)
|
|
302
|
-
# adoc = Coradoc.convert_file("report.docx", to: :asciidoc)
|
|
303
|
-
def convert_file(path, to:, from: nil, **)
|
|
304
|
-
source_format = from || detect_format(path)
|
|
305
|
-
raise UnsupportedFormatError, "Could not detect format for: #{path}" unless source_format
|
|
306
|
-
|
|
307
|
-
core = parse_file(path, format: source_format)
|
|
308
|
-
serialize(core, to: to, **)
|
|
309
|
-
end
|
|
97
|
+
def parse_format?(format) = FormatCatalog.parse_format?(format)
|
|
310
98
|
|
|
311
|
-
|
|
312
|
-
#
|
|
313
|
-
# @param format [Symbol] the format to check
|
|
314
|
-
# @return [Boolean] true if the format is binary
|
|
315
|
-
def binary_format?(format)
|
|
316
|
-
opts = registry.options_for(format)
|
|
317
|
-
opts&.fetch(:binary, false) == true
|
|
318
|
-
end
|
|
99
|
+
def format_capabilities = FormatCatalog.capabilities
|
|
319
100
|
|
|
320
|
-
|
|
321
|
-
#
|
|
322
|
-
# Handles common aliases like "adoc" → :asciidoc, "md" → :markdown.
|
|
323
|
-
#
|
|
324
|
-
# @param name [String, Symbol, nil] the format name to normalize
|
|
325
|
-
# @return [Symbol, nil] the normalized format symbol, or nil
|
|
326
|
-
def normalize_format(name)
|
|
327
|
-
return nil unless name
|
|
328
|
-
|
|
329
|
-
key = name.to_s.downcase
|
|
330
|
-
registry.each_key do |fmt_name|
|
|
331
|
-
opts = registry.options_for(fmt_name)
|
|
332
|
-
return fmt_name if opts[:aliases]&.include?(key)
|
|
333
|
-
end
|
|
334
|
-
key.to_sym
|
|
335
|
-
end
|
|
101
|
+
def resolve_output_format(output_file, **) = FormatCatalog.resolve_output_format(output_file, **)
|
|
336
102
|
|
|
337
|
-
#
|
|
338
|
-
#
|
|
339
|
-
# @param format [Symbol] the format to check
|
|
340
|
-
# @return [Boolean] true if the format can serialize
|
|
341
|
-
def serialize_format?(format)
|
|
342
|
-
mod = get_format(format)
|
|
343
|
-
return false unless mod
|
|
103
|
+
# ---- Introspection (delegates to Introspection) ----
|
|
344
104
|
|
|
345
|
-
|
|
346
|
-
end
|
|
105
|
+
def file_info(path) = Introspection.file_info(path)
|
|
347
106
|
|
|
348
|
-
|
|
349
|
-
#
|
|
350
|
-
# @param format [Symbol] the format to check
|
|
351
|
-
# @return [Boolean] true if the format can parse
|
|
352
|
-
def parse_format?(format)
|
|
353
|
-
mod = get_format(format)
|
|
354
|
-
return false unless mod
|
|
107
|
+
def validate_file(path, **) = Introspection.validate_file(path, **)
|
|
355
108
|
|
|
356
|
-
|
|
357
|
-
end
|
|
109
|
+
def document_stats(doc) = Introspection.document_stats(doc)
|
|
358
110
|
|
|
359
|
-
|
|
360
|
-
#
|
|
361
|
-
# Returns a hash mapping each format name to its capabilities
|
|
362
|
-
# (parse: bool, serialize: bool). Useful for CLI display and introspection.
|
|
363
|
-
#
|
|
364
|
-
# @return [Hash<Symbol, Hash<Symbol, Boolean>>]
|
|
365
|
-
def format_capabilities
|
|
366
|
-
registered_formats.each_with_object({}) do |name, caps|
|
|
367
|
-
caps[name] = {
|
|
368
|
-
parse: parse_format?(name),
|
|
369
|
-
serialize: serialize_format?(name)
|
|
370
|
-
}
|
|
371
|
-
end
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
# Resolve the output format from a filename, with a default
|
|
375
|
-
#
|
|
376
|
-
# @param output_file [String, nil] output filename to detect from
|
|
377
|
-
# @param default [Symbol] default format when detection fails (default: :html)
|
|
378
|
-
# @return [Symbol] the resolved format
|
|
379
|
-
def resolve_output_format(output_file, default: :html)
|
|
380
|
-
return default unless output_file
|
|
111
|
+
def describe_element(elem) = Introspection.describe_element(elem)
|
|
381
112
|
|
|
382
|
-
|
|
383
|
-
end
|
|
113
|
+
# ---- Utilities that stay on the top-level façade ----
|
|
384
114
|
|
|
385
|
-
#
|
|
386
|
-
#
|
|
387
|
-
# @param path [String] path to the file
|
|
388
|
-
# @return [Hash] metadata including :size, :format, and :lines (for text formats)
|
|
389
|
-
def file_info(path)
|
|
390
|
-
fmt = detect_format(path)
|
|
391
|
-
info = { size: File.size(path), format: fmt }
|
|
392
|
-
info[:lines] = File.foreach(path).count unless binary_format?(fmt)
|
|
393
|
-
info
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
# Validate a document file
|
|
397
|
-
#
|
|
398
|
-
# Parses the file and validates against auto-generated schema.
|
|
399
|
-
# Returns a Coradoc::Validation::Result.
|
|
400
|
-
#
|
|
401
|
-
# @param path [String] path to the document file
|
|
402
|
-
# @param format [Symbol, nil] source format (auto-detected if nil)
|
|
403
|
-
# @return [Coradoc::Validation::Result] validation result
|
|
404
|
-
# @raise [UnsupportedFormatError] if format is not detected or registered
|
|
405
|
-
def validate_file(path, format: nil)
|
|
406
|
-
doc = parse_file(path, format: format)
|
|
407
|
-
|
|
408
|
-
schema = Validation::SchemaGenerator.generate(doc.class)
|
|
409
|
-
return schema.validate(doc) if schema
|
|
410
|
-
|
|
411
|
-
Validation::Result.new
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
# Gather statistics about a parsed document
|
|
415
|
-
#
|
|
416
|
-
# @param doc [CoreModel::Base] parsed document
|
|
417
|
-
# @return [Hash] statistics including element counts, title, etc.
|
|
418
|
-
def document_stats(doc)
|
|
419
|
-
stats = {}
|
|
420
|
-
|
|
421
|
-
stats[:title] = doc.title if doc.title
|
|
422
|
-
|
|
423
|
-
if doc.is_a?(CoreModel::StructuralElement)
|
|
424
|
-
stats[:child_count] = count_elements(doc)
|
|
425
|
-
stats[:element_counts] = count_element_types(doc)
|
|
426
|
-
end
|
|
427
|
-
|
|
428
|
-
stats
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
# Describe an element for display
|
|
432
|
-
#
|
|
433
|
-
# @param elem [Object] element to describe
|
|
434
|
-
# @return [String] human-readable description
|
|
435
|
-
def describe_element(elem)
|
|
436
|
-
return elem.to_s unless elem.is_a?(CoreModel::Base)
|
|
437
|
-
|
|
438
|
-
type = elem.class.name.split('::').last
|
|
439
|
-
if elem.title
|
|
440
|
-
"#{type}: #{elem.title}"
|
|
441
|
-
elsif elem.is_a?(CoreModel::Block) && elem.content
|
|
442
|
-
preview = elem.content.to_s[0..50]
|
|
443
|
-
preview += '...' if elem.content.to_s.length > 50
|
|
444
|
-
"#{type}: #{preview}"
|
|
445
|
-
else
|
|
446
|
-
type
|
|
447
|
-
end
|
|
448
|
-
end
|
|
449
|
-
|
|
450
|
-
# Strip unicode whitespace from a string
|
|
115
|
+
# Strip unicode whitespace from a string.
|
|
451
116
|
#
|
|
452
117
|
# @param string [String] the string to strip
|
|
453
118
|
# @param only [Symbol, nil] what to strip: :begin, :end, or nil for both
|
|
@@ -464,38 +129,6 @@ module Coradoc
|
|
|
464
129
|
string.sub(/^\p{Zs}+/, '').sub(/\p{Zs}+$/, '')
|
|
465
130
|
end
|
|
466
131
|
end
|
|
467
|
-
|
|
468
|
-
private
|
|
469
|
-
|
|
470
|
-
def count_elements(doc)
|
|
471
|
-
return 0 unless doc.is_a?(CoreModel::StructuralElement)
|
|
472
|
-
|
|
473
|
-
doc.children.sum do |child|
|
|
474
|
-
1 + (child.is_a?(CoreModel::StructuralElement) ? count_elements(child) : 0)
|
|
475
|
-
end
|
|
476
|
-
end
|
|
477
|
-
|
|
478
|
-
def count_element_types(doc)
|
|
479
|
-
counts = Hash.new(0)
|
|
480
|
-
visitor = Class.new(Visitor::Base) do
|
|
481
|
-
define_method(:visit) do |element|
|
|
482
|
-
if element.is_a?(CoreModel::Base)
|
|
483
|
-
has_element_type = element.is_a?(CoreModel::StructuralElement) || element.is_a?(CoreModel::Block)
|
|
484
|
-
type_key = if has_element_type && element.element_type
|
|
485
|
-
element.element_type
|
|
486
|
-
else
|
|
487
|
-
element.class.name.split('::').last
|
|
488
|
-
.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '')
|
|
489
|
-
end
|
|
490
|
-
counts[type_key] += 1
|
|
491
|
-
end
|
|
492
|
-
super(element)
|
|
493
|
-
end
|
|
494
|
-
end.new
|
|
495
|
-
visitor.visit(doc)
|
|
496
|
-
counts.reject! { |_, v| v.zero? }
|
|
497
|
-
counts
|
|
498
|
-
end
|
|
499
132
|
end
|
|
500
133
|
|
|
501
134
|
autoload :Error, "#{__dir__}/errors"
|
|
@@ -508,15 +141,15 @@ module Coradoc
|
|
|
508
141
|
autoload :FormatModule, "#{__dir__}/format_module"
|
|
509
142
|
autoload :CoreModel, "#{__dir__}/core_model"
|
|
510
143
|
autoload :Registry, "#{__dir__}/registry"
|
|
511
|
-
autoload :Transform, "#{__dir__}/transform"
|
|
512
|
-
autoload :Input, "#{__dir__}/input"
|
|
513
|
-
autoload :Output, "#{__dir__}/output"
|
|
514
|
-
autoload :DocumentManipulator, "#{__dir__}/document_manipulator"
|
|
515
144
|
autoload :Visitor, "#{__dir__}/visitor"
|
|
516
145
|
autoload :PerformanceRegression, "#{__dir__}/performance_regression"
|
|
517
146
|
autoload :IncludeResolver, "#{__dir__}/include_resolver"
|
|
518
147
|
autoload :IncludeSelectors, "#{__dir__}/include_selectors"
|
|
519
148
|
autoload :ResolveIncludes, "#{__dir__}/resolve_includes"
|
|
149
|
+
autoload :Pipeline, "#{__dir__}/pipeline"
|
|
150
|
+
autoload :FormatCatalog, "#{__dir__}/format_catalog"
|
|
151
|
+
autoload :Introspection, "#{__dir__}/introspection"
|
|
152
|
+
autoload :Dispatch, "#{__dir__}/dispatch"
|
|
520
153
|
end
|
|
521
154
|
|
|
522
155
|
# Format gems self-register via Coradoc.register_format when they are required.
|
|
@@ -41,6 +41,28 @@ module Coradoc
|
|
|
41
41
|
# @return [Array<MetadataEntry>] additional metadata entries
|
|
42
42
|
attribute :metadata_entries, MetadataEntry, collection: true
|
|
43
43
|
|
|
44
|
+
# Construct an instance and yield it for in-place mutation.
|
|
45
|
+
#
|
|
46
|
+
# This is the programmatic-construction entry point for CoreModel
|
|
47
|
+
# nodes. It calls +new+ exactly as a caller would, then yields
|
|
48
|
+
# the resulting instance for append-style construction. No new
|
|
49
|
+
# class hierarchy, no +method_missing+ — the block operates on
|
|
50
|
+
# the real model object.
|
|
51
|
+
#
|
|
52
|
+
# Per-class fluent helpers (e.g., +ListBlock#add_item+,
|
|
53
|
+
# +ListItem#add_text+) compose naturally with +build+:
|
|
54
|
+
#
|
|
55
|
+
# list = ListBlock.build do |ul|
|
|
56
|
+
# children.each { |c| ul.add_item { |li| li.add_link(c[:slug], text: c[:title]) } }
|
|
57
|
+
# end
|
|
58
|
+
#
|
|
59
|
+
# Without a block, +build(**attrs)+ is identical to +new(**attrs)+.
|
|
60
|
+
def self.build(**attrs)
|
|
61
|
+
instance = new(**attrs)
|
|
62
|
+
yield instance if block_given?
|
|
63
|
+
instance
|
|
64
|
+
end
|
|
65
|
+
|
|
44
66
|
# Get all metadata as a hash, or a specific metadata value by key
|
|
45
67
|
# @overload metadata
|
|
46
68
|
# @return [Hash] All metadata as key-value pairs
|
|
@@ -145,6 +167,23 @@ module Coradoc
|
|
|
145
167
|
visitor.visit(self)
|
|
146
168
|
end
|
|
147
169
|
|
|
170
|
+
# True when this node counts as "real body content" for the
|
|
171
|
+
# purposes of empty-document detection and similar structural
|
|
172
|
+
# queries. Default is true; metadata and ephemeral nodes
|
|
173
|
+
# (FrontmatterBlock, CommentBlock, CommentLine) override to
|
|
174
|
+
# false. Polymorphic dispatch keeps the predicate open for
|
|
175
|
+
# future "skip-me" types — no central walker to edit (OCP).
|
|
176
|
+
def body_content?
|
|
177
|
+
true
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# True when this node is structurally present but carries no
|
|
181
|
+
# visible characters. Default is false; inline text and
|
|
182
|
+
# paragraph blocks override to inspect their text content.
|
|
183
|
+
def whitespace_only?
|
|
184
|
+
false
|
|
185
|
+
end
|
|
186
|
+
|
|
148
187
|
private
|
|
149
188
|
|
|
150
189
|
# List of attributes to compare for semantic equivalence
|