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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/lib/coradoc/markdown/errors.rb +28 -0
- data/lib/coradoc/markdown/model/abbreviation.rb +27 -0
- data/lib/coradoc/markdown/model/attribute_list.rb +98 -0
- data/lib/coradoc/markdown/model/base.rb +86 -0
- data/lib/coradoc/markdown/model/blockquote.rb +21 -0
- data/lib/coradoc/markdown/model/code.rb +11 -0
- data/lib/coradoc/markdown/model/code_block.rb +24 -0
- data/lib/coradoc/markdown/model/definition_item.rb +24 -0
- data/lib/coradoc/markdown/model/definition_list.rb +47 -0
- data/lib/coradoc/markdown/model/definition_term.rb +21 -0
- data/lib/coradoc/markdown/model/document.rb +39 -0
- data/lib/coradoc/markdown/model/emphasis.rb +11 -0
- data/lib/coradoc/markdown/model/extension.rb +92 -0
- data/lib/coradoc/markdown/model/footnote.rb +31 -0
- data/lib/coradoc/markdown/model/footnote_reference.rb +22 -0
- data/lib/coradoc/markdown/model/heading.rb +44 -0
- data/lib/coradoc/markdown/model/highlight.rb +18 -0
- data/lib/coradoc/markdown/model/horizontal_rule.rb +16 -0
- data/lib/coradoc/markdown/model/image.rb +19 -0
- data/lib/coradoc/markdown/model/link.rb +19 -0
- data/lib/coradoc/markdown/model/list.rb +22 -0
- data/lib/coradoc/markdown/model/list_item.rb +29 -0
- data/lib/coradoc/markdown/model/math.rb +50 -0
- data/lib/coradoc/markdown/model/paragraph.rb +28 -0
- data/lib/coradoc/markdown/model/strikethrough.rb +18 -0
- data/lib/coradoc/markdown/model/strong.rb +11 -0
- data/lib/coradoc/markdown/model/table.rb +13 -0
- data/lib/coradoc/markdown/model/text.rb +15 -0
- data/lib/coradoc/markdown/parser/ast_processor.rb +543 -0
- data/lib/coradoc/markdown/parser/block_parser.rb +745 -0
- data/lib/coradoc/markdown/parser/html_entities.rb +2149 -0
- data/lib/coradoc/markdown/parser/inline_parser.rb +274 -0
- data/lib/coradoc/markdown/parser/parslet_extras.rb +215 -0
- data/lib/coradoc/markdown/parser.rb +11 -0
- data/lib/coradoc/markdown/parser_util.rb +90 -0
- data/lib/coradoc/markdown/serializer.rb +199 -0
- data/lib/coradoc/markdown/toc_generator.rb +215 -0
- data/lib/coradoc/markdown/transform/from_core_model.rb +325 -0
- data/lib/coradoc/markdown/transform/text_extraction.rb +19 -0
- data/lib/coradoc/markdown/transform/to_core_model.rb +287 -0
- data/lib/coradoc/markdown/transformer.rb +463 -0
- data/lib/coradoc/markdown/version.rb +7 -0
- data/lib/coradoc/markdown.rb +190 -0
- 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,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,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
|