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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e985a5efd633ee5e945c01d6f9465316f36ed682e8a65fa1c6c33304f8af453c
4
+ data.tar.gz: ece9f3be5276f6ef0c1c325fa59a3f43452472b8aa5e2350ad690381b5b7a173
5
+ SHA512:
6
+ metadata.gz: 531bc6952f8a55ccbd3b86190ced905b60242bebe6dd65873fee5cc0715fcdaa802147473fb897fb818bdec611937e48eb3b995db4844cd4c7c08a4f575bdbf7
7
+ data.tar.gz: 022055b32982e6a661bcbcca4e6044e8ccb4f36d265d7f4bbe1575c07e9a424acb711d8dc532a8ba49bc4afd2d56423f0c9be127bb9a996d1cdd5c9e1e738d26
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Abu Nashir
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ module Errors
6
+ # Base error class for Markdown errors
7
+ # Inherits from Coradoc::Error for unified error handling
8
+ class Error < Coradoc::Error
9
+ end
10
+
11
+ # Raised when Markdown parsing fails
12
+ class ParseError < Error
13
+ end
14
+
15
+ # Raised when Markdown serialization fails
16
+ class SerializationError < Error
17
+ end
18
+
19
+ # Raised when an unsupported Markdown feature is encountered
20
+ class UnsupportedFeatureError < Error
21
+ end
22
+
23
+ # Raised when Markdown-to-CoreModel transformation fails
24
+ class TransformationError < Error
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # Abbreviation model representing an abbreviation definition.
6
+ #
7
+ # Kramdown syntax:
8
+ # `*[ABC]: A Big Corporation`
9
+ #
10
+ # When the abbreviation appears in the text, it will be wrapped
11
+ # with an <abbr> tag with the definition as the title.
12
+ #
13
+ # @example Abbreviation definition
14
+ # abbr = Coradoc::Markdown::Abbreviation.new(
15
+ # term: "ABC",
16
+ # definition: "A Big Corporation"
17
+ # )
18
+ #
19
+ class Abbreviation < Base
20
+ # The abbreviation term
21
+ attribute :term, :string
22
+
23
+ # The full definition/explanation
24
+ attribute :definition, :string
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # Represents an Inline Attribute List (IAL) or Attribute List Definition (ALD)
6
+ #
7
+ # IAL syntax: {:.class #id key="value"}
8
+ # ALD syntax: {:name: #id .class key="value"}
9
+ #
10
+ # Examples:
11
+ # {:.highlight} - adds class "highlight"
12
+ # {#introduction} - sets id to "introduction"
13
+ # {:key="value"} - sets attribute key="value"
14
+ # {:name: #id .class} - defines ALD named "name"
15
+ #
16
+ class AttributeList < Base
17
+ attribute :id, :string
18
+ attribute :classes, :string, collection: true, default: []
19
+ attribute :attributes, :hash, default: {}
20
+ attribute :name, :string # For ALD - the reference name
21
+
22
+ # Parse an IAL string into an AttributeList
23
+ # @param str [String] The IAL string (e.g., '{:.class #id key="val"}')
24
+ # @return [AttributeList] Parsed attribute list
25
+ def self.parse(str)
26
+ return nil if str.nil? || str.empty?
27
+
28
+ # Remove surrounding braces
29
+ content = str.strip.gsub(/\A\{?:?|\}?\z/, '')
30
+
31
+ attr_list = new
32
+
33
+ # Check for ALD (has a name before colon)
34
+ if content =~ /\A(\w+):\s*/
35
+ attr_list.name = ::Regexp.last_match(1)
36
+ content = ::Regexp.last_match.post_match
37
+ end
38
+
39
+ # Parse the content
40
+ parse_attributes(content, attr_list)
41
+
42
+ attr_list
43
+ end
44
+
45
+ # Merge another AttributeList into this one
46
+ # @param other [AttributeList] The other attribute list to merge
47
+ # @return [AttributeList] self for chaining
48
+ def merge!(other)
49
+ return self unless other
50
+
51
+ self.id = other.id if other.id
52
+ self.classes = (classes + other.classes).uniq
53
+ self.attributes = attributes.merge(other.attributes)
54
+ self
55
+ end
56
+
57
+ # Create a merged copy
58
+ # @param other [AttributeList] The other attribute list to merge
59
+ # @return [AttributeList] New merged attribute list
60
+ def merge(other)
61
+ dup.merge!(other)
62
+ end
63
+
64
+ # Check if this has any attributes
65
+ # @return [Boolean]
66
+ def empty?
67
+ id.nil? && classes.empty? && attributes.empty?
68
+ end
69
+
70
+ # Convert to Markdown IAL syntax
71
+ # @return [String]
72
+ def to_md
73
+ return '' if empty?
74
+
75
+ parts = []
76
+ parts << "##{id}" if id
77
+ parts += classes.map { |c| ".#{c}" }
78
+ parts += attributes.map { |k, v| %(#{k}="#{v}") }
79
+
80
+ "{:#{parts.join(' ')}}"
81
+ end
82
+
83
+ def self.parse_attributes(content, attr_list)
84
+ # Use shared IalParser for consistent parsing
85
+ ParserUtil::IalParser.tokenize(content).each do |token|
86
+ case token[:type]
87
+ when :class
88
+ attr_list.classes << token[:value]
89
+ when :id
90
+ attr_list.id = token[:value]
91
+ when :attribute
92
+ attr_list.attributes[token[:key]] = token[:value]
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # Base class for all Markdown model objects.
6
+ #
7
+ # The Base class provides common functionality for all document model elements,
8
+ # including serialization support and tree traversal.
9
+ #
10
+ class Base < Lutaml::Model::Serializable
11
+ attribute :id, :string
12
+
13
+ # Attribute list for IAL (Inline Attribute List) support
14
+ # This allows attaching classes, id, and other attributes to elements
15
+ # Example: {:.highlight #intro data-role="main"}
16
+ attribute :attribute_list, :string
17
+
18
+ # Classes from IAL (for convenience)
19
+ attribute :classes, :string, collection: true
20
+
21
+ # Additional attributes from IAL
22
+ attribute :attributes, :hash, default: {}
23
+
24
+ # Visit pattern for traversing the document tree
25
+ def self.visit(element, &block)
26
+ return element if element.nil?
27
+
28
+ element = yield element, :pre
29
+ element = case element
30
+ when self
31
+ element.visit(&block)
32
+ when Array
33
+ element.map { |child| visit(child, &block) }.flatten.compact
34
+ when Hash
35
+ result = {}
36
+ element.each { |k, v| result[k] = visit(v, &block) }
37
+ result
38
+ else
39
+ element
40
+ end
41
+ yield element, :post
42
+ end
43
+
44
+ def visit(&block)
45
+ self.class.attributes.each_key do |attr_name|
46
+ child = public_send(attr_name)
47
+ result = self.class.visit(child, &block)
48
+ public_send(:"#{attr_name}=", result) if result != child
49
+ end
50
+ self
51
+ end
52
+
53
+ # Serialize polymorphic content to Markdown string
54
+ def serialize_content(content)
55
+ case content
56
+ when Array
57
+ content.map { |elem| serialize_content(elem) }.join
58
+ when String
59
+ content
60
+ when nil
61
+ ''
62
+ else
63
+ if content.is_a?(Base)
64
+ content.to_md
65
+ else
66
+ raise ArgumentError,
67
+ "Cannot serialize #{content.class.name} to Markdown. " \
68
+ 'Expected String or Base subclass.'
69
+ end
70
+ end
71
+ end
72
+
73
+ # Does a shallow attribute dump of the object
74
+ def to_h
75
+ self.class.attributes.keys.each_with_object({}) do |attribute, acc|
76
+ acc[attribute] = public_send(attribute)
77
+ end
78
+ end
79
+
80
+ # Serialize this model element to Markdown
81
+ def to_md
82
+ Coradoc::Markdown::Serializer.serialize(self)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # Blockquote model representing a Markdown blockquote (> prefix).
6
+ #
7
+ # @example Create a blockquote
8
+ # quote = Coradoc::Markdown::Blockquote.new(
9
+ # content: "This is a quoted text."
10
+ # )
11
+ #
12
+ class Blockquote < Base
13
+ attribute :content, :string
14
+
15
+ def initialize(content: '')
16
+ super()
17
+ @content = content
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # Code model representing inline code (`code`).
6
+ #
7
+ class Code < Base
8
+ attribute :text, :string
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # CodeBlock model representing a fenced code block.
6
+ #
7
+ # @example Create a code block
8
+ # code = Coradoc::Markdown::CodeBlock.new(
9
+ # language: "ruby",
10
+ # code: "puts 'Hello World'"
11
+ # )
12
+ #
13
+ class CodeBlock < Base
14
+ attribute :language, :string
15
+ attribute :code, :string
16
+
17
+ def initialize(language: nil, code: '')
18
+ super()
19
+ @language = language
20
+ @code = code
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # DefinitionItem model representing a single definition in a definition list.
6
+ #
7
+ # Each definition item contains the definition content and can have
8
+ # nested blocks (paragraphs, code blocks, lists, etc.)
9
+ #
10
+ # @example Simple definition
11
+ # defn = Coradoc::Markdown::DefinitionItem.new(content: "A Markdown parser")
12
+ #
13
+ class DefinitionItem < Base
14
+ # The definition content (text or nested blocks)
15
+ attribute :content, :string
16
+
17
+ # Inline content can be an array of text/inline elements
18
+ attribute :inline_content, :string, collection: true
19
+
20
+ # Nested block content (paragraphs, code blocks, lists, etc.)
21
+ attribute :blocks, :string, collection: true
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # DefinitionList model representing a Kramdown definition list.
6
+ #
7
+ # Definition lists consist of terms followed by one or more definitions.
8
+ # The syntax uses `:` to start a definition.
9
+ #
10
+ # @example Simple definition list
11
+ # list = Coradoc::Markdown::DefinitionList.new(
12
+ # items: [
13
+ # Coradoc::Markdown::DefinitionTerm.new(
14
+ # text: "kramdown",
15
+ # definitions: [
16
+ # Coradoc::Markdown::DefinitionItem.new(content: "A Markdown parser")
17
+ # ]
18
+ # )
19
+ # ]
20
+ # )
21
+ #
22
+ # Syntax:
23
+ # term
24
+ # : definition content
25
+ #
26
+ # multiple terms
27
+ # : first definition
28
+ # : second definition
29
+ #
30
+ class DefinitionList < Base
31
+ # Terms with their definitions
32
+ attribute :items, Coradoc::Markdown::DefinitionTerm, collection: true
33
+
34
+ # Serialize to Markdown
35
+ def to_md
36
+ items.map do |term|
37
+ term_text = term.text.to_s
38
+ defs = term.definitions.map do |defn|
39
+ content = defn.content.to_s
40
+ ": #{content}"
41
+ end.join("\n")
42
+ "#{term_text}\n#{defs}"
43
+ end.join("\n\n")
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # DefinitionTerm model representing a term in a definition list.
6
+ #
7
+ # A term can have multiple definitions and can span multiple lines.
8
+ # Terms can also have IAL attributes attached.
9
+ #
10
+ # @example Simple term
11
+ # term = Coradoc::Markdown::DefinitionTerm.new(text: "kramdown")
12
+ #
13
+ class DefinitionTerm < Base
14
+ # The term text content
15
+ attribute :text, :string
16
+
17
+ # Definitions for this term
18
+ attribute :definitions, Coradoc::Markdown::DefinitionItem, collection: true, default: []
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # Document model representing a Markdown document.
6
+ #
7
+ # The Document class is the main container for parsed Markdown content.
8
+ # It holds the document's blocks (headings, paragraphs, lists, etc.).
9
+ #
10
+ # @example Create a new document
11
+ # doc = Coradoc::Markdown::Document.new(
12
+ # blocks: [
13
+ # Coradoc::Markdown::Heading.new(level: 1, text: "My Document"),
14
+ # Coradoc::Markdown::Paragraph.new(text: "Hello World")
15
+ # ]
16
+ # )
17
+ #
18
+ class Document < Base
19
+ attribute :blocks, Coradoc::Markdown::Base, collection: true
20
+
21
+ # @param [Integer] index The index of the block to retrieve
22
+ # @return [Coradoc::Markdown::Base] The block at the specified index
23
+ def [](index)
24
+ blocks[index]
25
+ end
26
+
27
+ # @param [Integer] index The index of the block to set
28
+ # @param [Coradoc::Markdown::Base] value The block to set
29
+ def []=(index, value)
30
+ blocks[index] = value
31
+ end
32
+
33
+ # Create a document from an array of blocks
34
+ def self.from_ast(elements)
35
+ new(blocks: elements)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # Emphasis model representing italic text (*text* or _text_).
6
+ #
7
+ class Emphasis < Base
8
+ attribute :text, :string
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # Represents a kramdown extension
6
+ #
7
+ # Extension syntax: {::extension_name options /}
8
+ # Common extensions:
9
+ # {::toc} - table of contents
10
+ # {::options key="value" /} - parser options
11
+ # {::comment}content{:/} - comment
12
+ # {::nomarkdown}content{:/} - raw HTML passthrough
13
+ #
14
+ class Extension < Base
15
+ attribute :name, :string
16
+ attribute :options, :hash, default: {}
17
+ attribute :content, :string # For block extensions with content
18
+ attribute :body, :string # Alias for content
19
+
20
+ # Known extension types
21
+ TYPES = {
22
+ toc: :toc,
23
+ options: :options,
24
+ comment: :comment,
25
+ nomarkdown: :nomarkdown,
26
+ ignore: :ignore,
27
+ if: :conditional,
28
+ endif: :conditional
29
+ }.freeze
30
+
31
+ # Create a TOC extension
32
+ # @param options [Hash] TOC options (levels, etc.)
33
+ # @return [Extension]
34
+ def self.toc(options = {})
35
+ new(name: :toc, options: options)
36
+ end
37
+
38
+ # Create an options extension
39
+ # @param options [Hash] Parser options
40
+ # @return [Extension]
41
+ def self.options(options = {})
42
+ new(name: :options, options: options)
43
+ end
44
+
45
+ # Create a comment extension
46
+ # @param content [String] Comment content
47
+ # @return [Extension]
48
+ def self.comment(content = '')
49
+ new(name: :comment, content: content)
50
+ end
51
+
52
+ # Create a nomarkdown extension (passthrough)
53
+ # @param content [String] Raw content to pass through
54
+ # @return [Extension]
55
+ def self.nomarkdown(content)
56
+ new(name: :nomarkdown, content: content)
57
+ end
58
+
59
+ # Check if this is a specific extension type
60
+ # @param type [Symbol] The extension type
61
+ # @return [Boolean]
62
+ def type?(type)
63
+ name.to_sym == type
64
+ end
65
+
66
+ # Check if this is a self-closing extension
67
+ # @return [Boolean]
68
+ def self_closing?
69
+ content.nil? || content.empty?
70
+ end
71
+
72
+ # Convert to Markdown
73
+ # @return [String]
74
+ def to_md
75
+ opts = options.empty? ? '' : " #{options_to_s}"
76
+ if self_closing?
77
+ "{::#{name}#{opts} /}"
78
+ else
79
+ "{::#{name}#{opts}}#{content}{:/}"
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def options_to_s
86
+ options.map do |k, v|
87
+ %(#{k}="#{v}")
88
+ end.join(' ')
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # Footnote model representing a footnote definition or reference.
6
+ #
7
+ # Kramdown syntax:
8
+ # - Reference: `[^1]` or `[^name]`
9
+ # - Definition: `[^1]: Footnote text`
10
+ #
11
+ # @example Footnote definition
12
+ # fn = Coradoc::Markdown::Footnote.new(
13
+ # id: "1",
14
+ # content: "This is a footnote"
15
+ # )
16
+ #
17
+ class Footnote < Base
18
+ # The footnote identifier (number or name)
19
+ attribute :id, :string
20
+
21
+ # The footnote content
22
+ attribute :content, :string
23
+
24
+ # Inline content (can be array of elements)
25
+ attribute :inline_content, :string, collection: true
26
+
27
+ # Reference back to where this footnote is used
28
+ attribute :backlink, :boolean, default: true
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # FootnoteReference model representing an inline footnote reference.
6
+ #
7
+ # Syntax: `[^name]` anywhere in text
8
+ #
9
+ # @example
10
+ # ref = Coradoc::Markdown::FootnoteReference.new(id: "1")
11
+ #
12
+ class FootnoteReference < Base
13
+ # The footnote identifier
14
+ attribute :id, :string
15
+
16
+ # Serialize to Markdown
17
+ def to_md
18
+ "[^#{id}]"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # Heading model representing a Markdown heading (# to ######).
6
+ #
7
+ # @example Create a heading
8
+ # heading = Coradoc::Markdown::Heading.new(level: 1, text: "Title")
9
+ #
10
+ class Heading < Base
11
+ attribute :level, :integer, default: 1
12
+ attribute :text, :string
13
+
14
+ def initialize(level: 1, text: '')
15
+ super()
16
+ @level = level
17
+ @text = text
18
+ end
19
+
20
+ # Generate an auto ID from the heading text
21
+ #
22
+ # @return [String] A slugified version of the text suitable for use as an ID
23
+ # @example
24
+ # Heading.new(text: "Hello World!").auto_id #=> "hello-world"
25
+ def auto_id
26
+ return '' if text.nil? || text.empty?
27
+
28
+ # Downcase, replace non-alphanumeric with hyphens, collapse multiple hyphens
29
+ slug = text.to_s
30
+ .downcase
31
+ .gsub(/[^a-z0-9]+/, '-')
32
+ .gsub(/^-+|-+$/, '')
33
+ slug.empty? ? 'section' : slug
34
+ end
35
+
36
+ # Get the ID for this heading (uses explicit id if set, otherwise auto_id)
37
+ #
38
+ # @return [String] The ID to use for this heading
39
+ def heading_id
40
+ id || auto_id
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ module Coradoc
5
+ module Markdown
6
+ # Represents highlighted text using == syntax (extended Markdown).
7
+ #
8
+ # Example: ==highlighted text==
9
+ #
10
+ class Highlight < Base
11
+ attribute :text, :string
12
+
13
+ def to_md
14
+ "==#{text}=="
15
+ end
16
+ end
17
+ end
18
+ end