coradoc-markdown 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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/lib/coradoc/markdown/errors.rb +28 -0
  4. data/lib/coradoc/markdown/model/abbreviation.rb +27 -0
  5. data/lib/coradoc/markdown/model/attribute_list.rb +98 -0
  6. data/lib/coradoc/markdown/model/base.rb +86 -0
  7. data/lib/coradoc/markdown/model/blockquote.rb +21 -0
  8. data/lib/coradoc/markdown/model/code.rb +11 -0
  9. data/lib/coradoc/markdown/model/code_block.rb +24 -0
  10. data/lib/coradoc/markdown/model/definition_item.rb +24 -0
  11. data/lib/coradoc/markdown/model/definition_list.rb +47 -0
  12. data/lib/coradoc/markdown/model/definition_term.rb +21 -0
  13. data/lib/coradoc/markdown/model/document.rb +39 -0
  14. data/lib/coradoc/markdown/model/emphasis.rb +11 -0
  15. data/lib/coradoc/markdown/model/extension.rb +92 -0
  16. data/lib/coradoc/markdown/model/footnote.rb +31 -0
  17. data/lib/coradoc/markdown/model/footnote_reference.rb +22 -0
  18. data/lib/coradoc/markdown/model/heading.rb +44 -0
  19. data/lib/coradoc/markdown/model/highlight.rb +18 -0
  20. data/lib/coradoc/markdown/model/horizontal_rule.rb +16 -0
  21. data/lib/coradoc/markdown/model/image.rb +19 -0
  22. data/lib/coradoc/markdown/model/link.rb +19 -0
  23. data/lib/coradoc/markdown/model/list.rb +22 -0
  24. data/lib/coradoc/markdown/model/list_item.rb +29 -0
  25. data/lib/coradoc/markdown/model/math.rb +50 -0
  26. data/lib/coradoc/markdown/model/paragraph.rb +28 -0
  27. data/lib/coradoc/markdown/model/strikethrough.rb +18 -0
  28. data/lib/coradoc/markdown/model/strong.rb +11 -0
  29. data/lib/coradoc/markdown/model/table.rb +13 -0
  30. data/lib/coradoc/markdown/model/text.rb +15 -0
  31. data/lib/coradoc/markdown/parser/ast_processor.rb +543 -0
  32. data/lib/coradoc/markdown/parser/block_parser.rb +745 -0
  33. data/lib/coradoc/markdown/parser/html_entities.rb +2149 -0
  34. data/lib/coradoc/markdown/parser/inline_parser.rb +274 -0
  35. data/lib/coradoc/markdown/parser/parslet_extras.rb +215 -0
  36. data/lib/coradoc/markdown/parser.rb +11 -0
  37. data/lib/coradoc/markdown/parser_util.rb +90 -0
  38. data/lib/coradoc/markdown/serializer.rb +199 -0
  39. data/lib/coradoc/markdown/toc_generator.rb +215 -0
  40. data/lib/coradoc/markdown/transform/from_core_model.rb +325 -0
  41. data/lib/coradoc/markdown/transform/text_extraction.rb +19 -0
  42. data/lib/coradoc/markdown/transform/to_core_model.rb +287 -0
  43. data/lib/coradoc/markdown/transformer.rb +463 -0
  44. data/lib/coradoc/markdown/version.rb +7 -0
  45. data/lib/coradoc/markdown.rb +190 -0
  46. metadata +173 -0
@@ -0,0 +1,463 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parslet'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ autoload :ParserUtil, "#{__dir__}/parser_util"
8
+
9
+ # Transformer converts Parslet AST into Markdown Document Model objects.
10
+ #
11
+ # This transformer takes the raw output from the BlockParser/InlineParser
12
+ # and converts it into semantic model objects (Heading, Paragraph, etc.)
13
+ #
14
+ class Transformer < Parslet::Transform
15
+ # ATX Heading: # Heading
16
+ rule(heading: simple(:heading), text: simple(:text)) do
17
+ Heading.new(level: heading.to_s.length, text: text.to_s.strip)
18
+ end
19
+
20
+ # ATX Heading without text (just #)
21
+ rule(heading: simple(:heading)) do
22
+ Heading.new(level: heading.to_s.length, text: '')
23
+ end
24
+
25
+ # Thematic break (horizontal rule)
26
+ rule(hr: simple(:hr)) do
27
+ HorizontalRule.new(style: '---')
28
+ end
29
+
30
+ # Code block (fenced or indented)
31
+ rule(code_block: sequence(:lines)) do
32
+ code = lines.map { |l| l.is_a?(Hash) ? (l[:ln] || '') : l.to_s }.join("\n")
33
+ CodeBlock.new(code: "#{code.rstrip}\n")
34
+ end
35
+
36
+ rule(code_block: simple(:line)) do
37
+ code = line.is_a?(Hash) ? (line[:ln] || '') : line.to_s
38
+ CodeBlock.new(code: "#{code}\n")
39
+ end
40
+
41
+ # Block quote
42
+ rule(block_quote: subtree(:content)) do
43
+ # Recursively transform block quote content
44
+ transformed = content.is_a?(Array) ? content.map { |c| transform_element(c) } : [transform_element(content)]
45
+ text = transformed.map { |c| c.is_a?(Base) && c.class.attributes.key?(:text) ? c.text : c.to_s }.join("\n")
46
+ Blockquote.new(content: text)
47
+ end
48
+
49
+ # Paragraph
50
+ rule(p: { ln: simple(:text) }) do
51
+ Paragraph.new(text: text.to_s)
52
+ end
53
+
54
+ rule(p: sequence(:lines)) do
55
+ text = lines.map { |l| l.is_a?(Hash) ? (l[:ln] || '') : l.to_s }.join("\n")
56
+ Paragraph.new(text: text)
57
+ end
58
+
59
+ # Setext heading (underline style)
60
+ rule(text: subtree(:text_content), heading: simple(:heading)) do
61
+ # Extract text from the text_content
62
+ lines = text_content.is_a?(Array) ? text_content : [text_content]
63
+ text = lines.map do |l|
64
+ case l
65
+ when Hash then l[:ln] || ''
66
+ else l.to_s
67
+ end
68
+ end.join("\n")
69
+
70
+ level = heading.to_s.start_with?('=') ? 1 : 2
71
+ Heading.new(level: level, text: text.strip)
72
+ end
73
+
74
+ # Inline elements
75
+ rule(text: simple(:text)) do
76
+ Text.new(content: text.to_s)
77
+ end
78
+
79
+ rule(code: simple(:code)) do
80
+ Code.new(text: code.to_s)
81
+ end
82
+
83
+ rule(emph: subtree(:content)) do
84
+ text = extract_text(content)
85
+ Emphasis.new(text: text)
86
+ end
87
+
88
+ rule(strong: subtree(:content)) do
89
+ text = extract_text(content)
90
+ Strong.new(text: text)
91
+ end
92
+
93
+ # ===== KRAMDOWN EXTENSIONS =====
94
+
95
+ # IAL (Inline Attribute List): {:.class #id key="value"}
96
+ rule(ial: simple(:ial_content)) do
97
+ AttributeList.parse("{:#{ial_content}}")
98
+ end
99
+
100
+ # ALD (Attribute List Definition): {:name: #id .class}
101
+ rule(ald_name: simple(:name), ial: simple(:ial_content)) do
102
+ AttributeList.new(
103
+ name: name.to_s,
104
+ **parse_ial_content(ial_content.to_s)
105
+ )
106
+ end
107
+
108
+ # Extension (self-closing): {::toc /} or {::options key="value" /}
109
+ rule(extension: {
110
+ ext_name: simple(:name),
111
+ ext_options: simple(:opts)
112
+ }) do
113
+ Extension.new(
114
+ name: name.to_s,
115
+ options: parse_extension_options(opts.to_s)
116
+ )
117
+ end
118
+
119
+ rule(extension: {
120
+ ext_name: simple(:name),
121
+ ext_options: simple(:opts),
122
+ ext_body: simple(:body)
123
+ }) do
124
+ Extension.new(
125
+ name: name.to_s,
126
+ options: parse_extension_options(opts.to_s),
127
+ content: body.to_s
128
+ )
129
+ end
130
+
131
+ # Block math: $$...$$
132
+ rule(math_content: simple(:content)) do
133
+ Math.block(content.to_s.strip)
134
+ end
135
+
136
+ # Fallback for unmatched elements
137
+ rule(simple(:value)) do
138
+ Text.new(content: value.to_s)
139
+ end
140
+
141
+ class << self
142
+ # ALD storage - maps name to AttributeList
143
+ attr_accessor :ald_registry
144
+
145
+ # Transform AST into a Document model
146
+ #
147
+ # @param ast [Array] The parsed AST from BlockParser
148
+ # @return [Coradoc::Markdown::Document] The document model
149
+ def transform_document(ast)
150
+ @ald_registry = {}
151
+ blocks = Array(ast).map { |element| transform_element(element) }.compact
152
+ Document.new(blocks: blocks)
153
+ end
154
+
155
+ # Transform a single element
156
+ def transform_element(element)
157
+ return nil if element.nil?
158
+
159
+ case element
160
+ when Hash
161
+ # Handle ALD first (register it)
162
+ if element.key?(:ald_name)
163
+ register_ald(element)
164
+ return nil
165
+ end
166
+
167
+ # Handle IAL on its own line (reference or standalone)
168
+ if element.key?(:ial) && !element.key?(:p) && !element.key?(:heading)
169
+ ial = parse_ial_element(element[:ial])
170
+ # Check if it's a reference to an ALD
171
+ return @ald_registry[ial] if ial.is_a?(String) && @ald_registry.key?(ial)
172
+
173
+ return ial
174
+ end
175
+
176
+ # Handle extension
177
+ return transform_extension(element[:extension]) if element.key?(:extension)
178
+
179
+ # Handle extension (direct key)
180
+ return transform_extension(element) if element.key?(:ext_name)
181
+
182
+ # Handle math
183
+ return Math.block(extract_text(element[:math_content])) if element.key?(:math_content)
184
+
185
+ # Handle footnote reference (inline)
186
+ return FootnoteReference.new(id: element[:fn_ref].to_s) if element.key?(:fn_ref)
187
+
188
+ # Try to transform using rules
189
+ transformed = try_transform(element)
190
+ return transformed if transformed
191
+
192
+ # If no rule matches, try to extract text
193
+ if element.key?(:ln)
194
+ Paragraph.new(text: element[:ln].to_s)
195
+ elsif element.key?(:text)
196
+ Text.new(content: element[:text].to_s)
197
+ end
198
+ when Array
199
+ # Transform each item
200
+ element.map { |e| transform_element(e) }.compact
201
+ when Parslet::Slice
202
+ Text.new(content: element.to_s)
203
+ else
204
+ Text.new(content: element.to_s)
205
+ end
206
+ end
207
+
208
+ # Register an ALD (Attribute List Definition)
209
+ def register_ald(element)
210
+ name = element[:ald_name].to_s
211
+ ial_content = element[:ial].to_s
212
+ attrs = parse_ial_content(ial_content)
213
+ @ald_registry[name] = AttributeList.new(name: name, **attrs)
214
+ end
215
+
216
+ # Transform extension element
217
+ def transform_extension(element)
218
+ name = element[:ext_name].to_s
219
+ opts = element[:ext_options]
220
+ # Handle empty array from parser
221
+ options = if opts.is_a?(Array) && opts.empty?
222
+ {}
223
+ elsif opts
224
+ parse_extension_options(opts.to_s)
225
+ else
226
+ {}
227
+ end
228
+ body = element[:ext_body]
229
+
230
+ Extension.new(
231
+ name: name,
232
+ options: options,
233
+ content: body&.to_s
234
+ )
235
+ end
236
+
237
+ # Try to transform using the defined rules
238
+ def try_transform(element)
239
+ return nil unless element.is_a?(Hash)
240
+
241
+ # Check for known patterns and transform them
242
+ if element.key?(:heading)
243
+ level = element[:heading].to_s.length
244
+ text = element[:text] ? element[:text].to_s.strip : ''
245
+ heading = Heading.new(level: level, text: text)
246
+ # Apply IAL if present
247
+ apply_ial_to_element(heading, element[:ial]) if element.key?(:ial)
248
+ return heading
249
+ end
250
+
251
+ return HorizontalRule.new(style: '---') if element.key?(:hr)
252
+
253
+ # Fenced code block with language info
254
+ if element.key?(:info) && element.key?(:code_block)
255
+ language = element[:info].to_s.strip
256
+ code = extract_code(element[:code_block])
257
+ code_block = CodeBlock.new(language: language, code: code)
258
+ apply_ial_to_element(code_block, element[:ial]) if element.key?(:ial)
259
+ return code_block
260
+ end
261
+
262
+ if element.key?(:code_block)
263
+ code = extract_code(element[:code_block])
264
+ code_block = CodeBlock.new(code: code)
265
+ apply_ial_to_element(code_block, element[:ial]) if element.key?(:ial)
266
+ return code_block
267
+ end
268
+
269
+ if element.key?(:block_quote)
270
+ content = element[:block_quote]
271
+ transformed = content.is_a?(Array) ? content.map { |c| transform_element(c) } : [transform_element(content)]
272
+ text = transformed.compact.map do |c|
273
+ c.is_a?(Base) && c.class.attributes.key?(:text) ? c.text : c.to_s
274
+ end.join("\n")
275
+ blockquote = Blockquote.new(content: text)
276
+ apply_ial_to_element(blockquote, element[:ial]) if element.key?(:ial)
277
+ return blockquote
278
+ end
279
+
280
+ if element.key?(:p)
281
+ text = extract_text_from_p(element[:p])
282
+ paragraph = Paragraph.new(text: text)
283
+ apply_ial_to_element(paragraph, element[:ial]) if element.key?(:ial)
284
+ return paragraph
285
+ end
286
+
287
+ # Definition list
288
+ return transform_definition_list(element[:dl]) if element.key?(:dl)
289
+
290
+ # Footnote definition
291
+ if element.key?(:fn_id)
292
+ content = if element[:fn_content_continued]
293
+ lines = [element[:fn_content]]
294
+ lines += Array(element[:fn_content_continued])
295
+ lines.map { |l| extract_text(l) }.join("\n")
296
+ else
297
+ extract_text(element[:fn_content])
298
+ end
299
+ return Footnote.new(id: element[:fn_id].to_s, content: content.strip)
300
+ end
301
+
302
+ # Abbreviation definition
303
+ if element.key?(:abbr_term)
304
+ return Abbreviation.new(
305
+ term: element[:abbr_term].to_s,
306
+ definition: element[:abbr_def].to_s.strip
307
+ )
308
+ end
309
+
310
+ nil
311
+ end
312
+
313
+ # Transform definition list
314
+ def transform_definition_list(dl_content)
315
+ # The parser outputs term and definition as separate items
316
+ # We need to group them: [{:def_term=>...}, {:def_content=>...}, ...]
317
+ items = []
318
+ current_term = nil
319
+ current_definitions = []
320
+
321
+ Array(dl_content).each do |item|
322
+ next unless item.is_a?(Hash)
323
+
324
+ if item.key?(:def_term)
325
+ # Save previous term if exists
326
+ if current_term
327
+ items << DefinitionTerm.new(
328
+ text: current_term.strip,
329
+ definitions: current_definitions
330
+ )
331
+ end
332
+
333
+ # Start new term
334
+ current_term = extract_text(item[:def_term])
335
+ current_definitions = []
336
+ elsif item.key?(:def_content)
337
+ # Add definition to current term
338
+ content = extract_text(item[:def_content])
339
+ current_definitions << DefinitionItem.new(content: content.strip)
340
+ end
341
+ end
342
+
343
+ # Don't forget the last term
344
+ if current_term
345
+ items << DefinitionTerm.new(
346
+ text: current_term.strip,
347
+ definitions: current_definitions
348
+ )
349
+ end
350
+
351
+ DefinitionList.new(items: items)
352
+ end
353
+
354
+ # Apply IAL attributes to an element
355
+ def apply_ial_to_element(element, ial_content)
356
+ attrs = parse_ial_content(ial_content.to_s)
357
+ element.id = attrs[:id] if attrs[:id]
358
+ element.classes = attrs[:classes] if attrs[:classes]
359
+ element.attributes = attrs[:attributes] if attrs[:attributes]
360
+ element
361
+ end
362
+
363
+ # Parse IAL content string into components
364
+ # Delegates to shared IalParser for consistent parsing
365
+ def parse_ial_content(content)
366
+ ParserUtil::IalParser.parse_to_hash(content)
367
+ end
368
+
369
+ # Parse extension options string into hash
370
+ def parse_extension_options(content)
371
+ return {} if content.nil? || content.empty?
372
+
373
+ result = {}
374
+ scanner = StringScanner.new(content.strip)
375
+
376
+ until scanner.eos?
377
+ scanner.skip(/\s+/)
378
+ break if scanner.eos?
379
+
380
+ if scanner.scan(/(\w[\w-]*)\s*=\s*/)
381
+ key = scanner[1]
382
+ value = if scanner.scan(/"([^"\\]*)"/)
383
+ scanner[1]
384
+ elsif scanner.scan(/'([^'\\]*)'/)
385
+ scanner[1]
386
+ elsif scanner.scan(/(\S+)/)
387
+ scanner[1]
388
+ else
389
+ ''
390
+ end
391
+ result[key] = value
392
+ else
393
+ # Skip unrecognized character to avoid infinite loop
394
+ scanner.scan(/./)
395
+ end
396
+ end
397
+
398
+ result
399
+ end
400
+
401
+ # Parse IAL element (can be a reference or full IAL)
402
+ def parse_ial_element(ial_content)
403
+ content = ial_content.to_s.strip
404
+ # Check if it's just a name reference (no . or #)
405
+ if content =~ /\A\w+\z/ && @ald_registry.key?(content)
406
+ @ald_registry[content]
407
+ else
408
+ attrs = parse_ial_content(content)
409
+ AttributeList.new(
410
+ id: attrs[:id],
411
+ classes: attrs[:classes],
412
+ attributes: attrs[:attributes]
413
+ )
414
+ end
415
+ end
416
+
417
+ # Extract text from paragraph structure
418
+ def extract_text_from_p(p)
419
+ case p
420
+ when Hash
421
+ p[:ln].to_s
422
+ when Array
423
+ p.map { |l| l.is_a?(Hash) ? l[:ln].to_s : l.to_s }.join("\n")
424
+ else
425
+ p.to_s
426
+ end
427
+ end
428
+
429
+ # Extract code from code_block structure
430
+ def extract_code(code_block)
431
+ case code_block
432
+ when Array
433
+ code_block.map { |l| l.is_a?(Hash) ? l[:ln].to_s : l.to_s }.join("\n")
434
+ when Hash
435
+ code_block[:ln].to_s
436
+ else
437
+ code_block.to_s
438
+ end
439
+ end
440
+
441
+ # Extract text content from nested structures
442
+ def extract_text(content)
443
+ case content
444
+ when Array
445
+ content.map { |c| extract_text(c) }.join
446
+ when Hash
447
+ if content.key?(:text)
448
+ content[:text].to_s
449
+ elsif content.key?(:ln)
450
+ content[:ln].to_s
451
+ else
452
+ content.values.map { |v| extract_text(v) }.join
453
+ end
454
+ when Parslet::Slice
455
+ content.to_s
456
+ else
457
+ content.to_s
458
+ end
459
+ end
460
+ end
461
+ end
462
+ end
463
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ VERSION = '1.0.0'
6
+ end
7
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ # coradoc-markdown - Markdown document model, parser, and serializer
4
+ #
5
+ # This gem provides Markdown support for the Coradoc document processing library.
6
+ # It includes:
7
+ # - Markdown Document Model (Coradoc::Markdown::*)
8
+ # - Markdown Parser (CommonMark-compliant, Parslet-based)
9
+ # - Markdown Serializer (round-trip capable)
10
+ # - Kramdown extensions support (IAL, ALD, math, TOC)
11
+ #
12
+ # @example Basic usage
13
+ # require 'coradoc/markdown'
14
+ #
15
+ # # Parse Markdown content
16
+ # document = Coradoc::Markdown.parse("# Title\n\nContent")
17
+ #
18
+ # # Serialize back to Markdown
19
+ # output = Coradoc::Markdown.serialize(document)
20
+
21
+ require 'parslet'
22
+ require 'lutaml/model'
23
+
24
+ module Coradoc
25
+ module Markdown
26
+ # Error classes
27
+ autoload :Errors, 'coradoc/markdown/errors'
28
+
29
+ # Autoload model classes
30
+ autoload :Base, 'coradoc/markdown/model/base'
31
+ autoload :Document, 'coradoc/markdown/model/document'
32
+ autoload :Heading, 'coradoc/markdown/model/heading'
33
+ autoload :Paragraph, 'coradoc/markdown/model/paragraph'
34
+ autoload :Text, 'coradoc/markdown/model/text'
35
+ autoload :List, 'coradoc/markdown/model/list'
36
+ autoload :ListItem, 'coradoc/markdown/model/list_item'
37
+ autoload :CodeBlock, 'coradoc/markdown/model/code_block'
38
+ autoload :Blockquote, 'coradoc/markdown/model/blockquote'
39
+ autoload :Link, 'coradoc/markdown/model/link'
40
+ autoload :Image, 'coradoc/markdown/model/image'
41
+ autoload :HorizontalRule, 'coradoc/markdown/model/horizontal_rule'
42
+ autoload :Table, 'coradoc/markdown/model/table'
43
+ autoload :Emphasis, 'coradoc/markdown/model/emphasis'
44
+ autoload :Strong, 'coradoc/markdown/model/strong'
45
+ autoload :Code, 'coradoc/markdown/model/code'
46
+ autoload :DefinitionList, 'coradoc/markdown/model/definition_list'
47
+ autoload :DefinitionTerm, 'coradoc/markdown/model/definition_term'
48
+ autoload :DefinitionItem, 'coradoc/markdown/model/definition_item'
49
+ autoload :Footnote, 'coradoc/markdown/model/footnote'
50
+ autoload :FootnoteReference, 'coradoc/markdown/model/footnote_reference'
51
+ autoload :Abbreviation, 'coradoc/markdown/model/abbreviation'
52
+ autoload :AttributeList, 'coradoc/markdown/model/attribute_list'
53
+ autoload :Math, 'coradoc/markdown/model/math'
54
+ autoload :Extension, 'coradoc/markdown/model/extension'
55
+ autoload :Strikethrough, 'coradoc/markdown/model/strikethrough'
56
+ autoload :Highlight, 'coradoc/markdown/model/highlight'
57
+
58
+ # Serializer
59
+ autoload :Serializer, 'coradoc/markdown/serializer'
60
+
61
+ # TOC Generator
62
+ autoload :TocGenerator, 'coradoc/markdown/toc_generator'
63
+
64
+ # Transformer (AST to Model)
65
+ autoload :Transformer, 'coradoc/markdown/transformer'
66
+
67
+ # CoreModel transformers
68
+ module Transform
69
+ autoload :TextExtraction, 'coradoc/markdown/transform/text_extraction'
70
+ autoload :ToCoreModel, 'coradoc/markdown/transform/to_core_model'
71
+ autoload :FromCoreModel, 'coradoc/markdown/transform/from_core_model'
72
+ end
73
+
74
+ # Parser module namespace
75
+ module Parser
76
+ autoload :BlockParser, 'coradoc/markdown/parser/block_parser'
77
+ autoload :InlineParser, 'coradoc/markdown/parser/inline_parser'
78
+ autoload :ParsletExtras, 'coradoc/markdown/parser/parslet_extras'
79
+ autoload :HTML_ENTITIES, 'coradoc/markdown/parser/html_entities'
80
+ autoload :AstProcessor, 'coradoc/markdown/parser/ast_processor'
81
+ end
82
+
83
+ # Shared parser utilities
84
+ autoload :ParserUtil, 'coradoc/markdown/parser_util'
85
+
86
+ # Convenience accessors for kramdown extension models
87
+ class << self
88
+ # Access AttributeList class
89
+ def AttributeList
90
+ @AttributeList ||= const_get(:AttributeList)
91
+ end
92
+
93
+ # Access Math class
94
+ def Math
95
+ @Math ||= const_get(:Math)
96
+ end
97
+
98
+ # Access Extension class
99
+ def Extension
100
+ @Extension ||= const_get(:Extension)
101
+ end
102
+ end
103
+
104
+ class << self
105
+ # Parse Markdown content into a Document model
106
+ #
107
+ # @param content [String] The Markdown content to parse
108
+ # @param options [Hash] Parsing options
109
+ # @return [Coradoc::Markdown::Document] The parsed document model
110
+ def parse(content, _options = {})
111
+ ast = Parser::BlockParser.new.parse(content)
112
+ Transformer.transform_document(ast)
113
+ end
114
+
115
+ # Parse raw AST (for debugging)
116
+ #
117
+ # @param content [String] The Markdown content to parse
118
+ # @return [Array] The raw AST
119
+ def parse_ast(content)
120
+ Parser::BlockParser.new.parse(content)
121
+ end
122
+
123
+ # Parse Markdown from a file
124
+ #
125
+ # @param filename [String] Path to the Markdown file
126
+ # @param options [Hash] Parsing options (see #parse)
127
+ # @return [Array] The parsed AST
128
+ def from_file(filename, **options)
129
+ content = File.read(filename)
130
+ parse(content, **options)
131
+ end
132
+
133
+ # Parse inline Markdown content
134
+ #
135
+ # @param content [String] The inline Markdown content to parse
136
+ # @return [Array] The parsed inline elements
137
+ def parse_inline(content)
138
+ Parser::InlineParser.new.parse(content)
139
+ end
140
+
141
+ # Serialize a document model to Markdown string
142
+ #
143
+ # @param document [Coradoc::Markdown::Document] The document to serialize
144
+ # @param options [Hash] Serialization options
145
+ # @return [String] The Markdown output
146
+ def serialize(document, options = {})
147
+ Serializer.serialize(document, options)
148
+ end
149
+
150
+ # Check if this format can transform the given model to CoreModel
151
+ #
152
+ # @param model [Object] The model to check
153
+ # @return [Boolean] true if this format handles the model type
154
+ def handles_model?(model)
155
+ model.is_a?(Coradoc::Markdown::Base)
156
+ end
157
+
158
+ # Transform Markdown model to CoreModel
159
+ #
160
+ # @param document [Coradoc::Markdown::Document] The Markdown document
161
+ # @return [Coradoc::CoreModel::StructuralElement] The CoreModel document
162
+ def to_core_model(document)
163
+ Transform::ToCoreModel.transform(document)
164
+ end
165
+ alias to_core to_core_model
166
+
167
+ # Parse and transform to CoreModel in one step
168
+ #
169
+ # @param content [String] The Markdown content
170
+ # @return [Coradoc::CoreModel::StructuralElement] The CoreModel document
171
+ def parse_to_core(content)
172
+ doc = parse(content)
173
+ to_core_model(doc)
174
+ end
175
+
176
+ # Transform CoreModel to Markdown model
177
+ #
178
+ # @param core_document [Coradoc::CoreModel::StructuralElement] The CoreModel document
179
+ # @return [Coradoc::Markdown::Document] The Markdown document
180
+ def from_core_model(core_document)
181
+ Transform::FromCoreModel.transform(core_document)
182
+ end
183
+ end
184
+ end
185
+
186
+ # Register the Markdown format with Coradoc
187
+ register_format(:markdown, Markdown,
188
+ aliases: %w[md markdown mdown mkd],
189
+ extensions: %w[.md .markdown .mdown .mkd])
190
+ end