markly 0.14.1 → 0.15.1
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
- checksums.yaml.gz.sig +0 -0
- data/context/abstract-syntax-tree.md +95 -0
- data/context/getting-started.md +101 -0
- data/context/headings.md +116 -0
- data/context/index.yaml +20 -0
- data/lib/markly/node.rb +6 -7
- data/lib/markly/renderer/headings.rb +81 -0
- data/lib/markly/renderer/html.rb +12 -9
- data/lib/markly/version.rb +1 -1
- data/readme.md +10 -0
- data/releases.md +8 -0
- data.tar.gz.sig +0 -0
- metadata +6 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 712a0a7ad856598eb83b20c370e7613896596af5c76cc2379a368382802f09db
|
|
4
|
+
data.tar.gz: f775598d6fc6e4d5e340bf68518816d3bd23bc44facd209fcd59bd4f0d429639
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f13cbaaba186b5716efb05567721ea024633f0c353f7296721406dd7d4f7918625e5518ae4836371a784808ae99de8d4831bf938df6595ed26da462398c6e39f
|
|
7
|
+
data.tar.gz: a92d4ee382baaf2bea98defccfd318142f0fdccf42ff5fca5293c17bf611e432d21e178f3d69a272f0c021309d3725454e93ea401d7473495ff4620490071d5d
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Abstract Syntax Tree
|
|
2
|
+
|
|
3
|
+
This guide explains how to use Markly's abstract syntax tree (AST) to parse and manipulate Markdown documents.
|
|
4
|
+
|
|
5
|
+
## Parsing
|
|
6
|
+
|
|
7
|
+
You can parse Markdown to a `Document` node using `Markly.parse`:
|
|
8
|
+
|
|
9
|
+
~~~ ruby
|
|
10
|
+
require 'markly'
|
|
11
|
+
|
|
12
|
+
document = Markly.parse('*Hello* world')
|
|
13
|
+
|
|
14
|
+
pp document
|
|
15
|
+
~~~
|
|
16
|
+
|
|
17
|
+
This will print out the following:
|
|
18
|
+
|
|
19
|
+
~~~
|
|
20
|
+
#<Markly::Node(document):
|
|
21
|
+
source_position={:start_line=>1, :start_column=>1, :end_line=>1, :end_column=>13}
|
|
22
|
+
children=[#<Markly::Node(paragraph):
|
|
23
|
+
source_position={:start_line=>1, :start_column=>1, :end_line=>1, :end_column=>13}
|
|
24
|
+
children=[#<Markly::Node(emph):
|
|
25
|
+
source_position={:start_line=>1, :start_column=>1, :end_line=>1, :end_column=>7}
|
|
26
|
+
children=[#<Markly::Node(text): source_position={:start_line=>1, :start_column=>2, :end_line=>1, :end_column=>6}, string_content="Hello">]>,
|
|
27
|
+
#<Markly::Node(text): source_position={:start_line=>1, :start_column=>8, :end_line=>1, :end_column=>13}, string_content=" world">]>]>
|
|
28
|
+
~~~
|
|
29
|
+
|
|
30
|
+
As you can see, a document consists of a root node, which contains several children, they themselves containing children, and so on. We refer to this as the abstract syntax tree (AST).
|
|
31
|
+
|
|
32
|
+
## Example: Walking the AST
|
|
33
|
+
|
|
34
|
+
You can use `walk` or `each` to iterate over nodes:
|
|
35
|
+
|
|
36
|
+
- `walk` will iterate on a node and recursively iterate on a node's children.
|
|
37
|
+
- `each` will iterate on a node and its children, but no further.
|
|
38
|
+
|
|
39
|
+
<!-- end list -->
|
|
40
|
+
|
|
41
|
+
``` ruby
|
|
42
|
+
require 'markly'
|
|
43
|
+
|
|
44
|
+
document = Markly.parse("# The site\n\n [GitHub](https://www.github.com)")
|
|
45
|
+
|
|
46
|
+
# Walk tree and print out URLs for links:
|
|
47
|
+
document.walk do |node|
|
|
48
|
+
if node.type == :link
|
|
49
|
+
puts "URL = #{node.url}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Capitalize all regular text in headers:
|
|
54
|
+
document.walk do |node|
|
|
55
|
+
if node.type == :header
|
|
56
|
+
node.each do |subnode|
|
|
57
|
+
if subnode.type == :text
|
|
58
|
+
subnode.string_content = subnode.string_content.upcase
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Transform links to regular text:
|
|
65
|
+
document.walk do |node|
|
|
66
|
+
if node.type == :link
|
|
67
|
+
node.insert_before(node.first_child)
|
|
68
|
+
node.delete
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Creating a Custom Renderer
|
|
74
|
+
|
|
75
|
+
You can also derive a class from {ruby Markly::Renderer::HTML} class. Using a pure Ruby renderer is slower, but allows you to customize the output. For example:
|
|
76
|
+
|
|
77
|
+
``` ruby
|
|
78
|
+
class MyHtmlRenderer < Markly::Renderer::HTML
|
|
79
|
+
def initialize
|
|
80
|
+
super
|
|
81
|
+
@header_id = 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def header(node)
|
|
85
|
+
block do
|
|
86
|
+
out("<h", node.header_level, " id=\"", @header_id, "\">",
|
|
87
|
+
:children, "</h", node.header_level, ">")
|
|
88
|
+
@header_id += 1
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
my_renderer = MyHtmlRenderer.new
|
|
94
|
+
puts my_renderer.render(document)
|
|
95
|
+
```
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
This guide explains now to install and use Markly.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem to your project:
|
|
8
|
+
|
|
9
|
+
$ bundle add markly
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Markly's most basic usage is to convert Markdown to HTML. You can do this in a few ways:
|
|
14
|
+
|
|
15
|
+
~~~ ruby
|
|
16
|
+
require 'markly'
|
|
17
|
+
|
|
18
|
+
Markly.render_html('Hi *there*')
|
|
19
|
+
# <p>Hi <em>there</em></p>\n
|
|
20
|
+
~~~
|
|
21
|
+
|
|
22
|
+
You can also parse a string to receive a `Document` node. You can then print that node to HTML, iterate over the children, and other fun node stuff. For example:
|
|
23
|
+
|
|
24
|
+
~~~ ruby
|
|
25
|
+
require 'markly'
|
|
26
|
+
|
|
27
|
+
document = Markly.parse('*Hello* world')
|
|
28
|
+
puts(document.to_html) # <p>Hi <em>there</em></p>\n
|
|
29
|
+
|
|
30
|
+
document.walk do |node|
|
|
31
|
+
puts node.type # [:document, :paragraph, :text, :emph, :text]
|
|
32
|
+
end
|
|
33
|
+
~~~
|
|
34
|
+
|
|
35
|
+
## Options
|
|
36
|
+
|
|
37
|
+
Markly accepts integer flags which control how the Markdown is parsed and rendered.
|
|
38
|
+
|
|
39
|
+
### Parse Options
|
|
40
|
+
|
|
41
|
+
| Name | Description
|
|
42
|
+
| ------------------------------------ | -----------
|
|
43
|
+
| `Markly::DEFAULT` | The default parsing system.
|
|
44
|
+
| `Markly::UNSAFE` | Allow raw/custom HTML and unsafe links.
|
|
45
|
+
| `Markly::FOOTNOTES` | Parse footnotes.
|
|
46
|
+
| `Markly::LIBERAL_HTML_TAG` | Support liberal parsing of inline HTML tags.
|
|
47
|
+
| `Markly::SMART` | Use smart punctuation (curly quotes, etc.).
|
|
48
|
+
| `Markly::STRIKETHROUGH_DOUBLE_TILDE` | Parse strikethroughs by double tildes (compatibility with [redcarpet](https://github.com/vmg/redcarpet))
|
|
49
|
+
| `Markly::VALIDATE_UTF8` | Replace illegal sequences with the replacement character `U+FFFD`.
|
|
50
|
+
|
|
51
|
+
### Render Options
|
|
52
|
+
|
|
53
|
+
| Name | Description |
|
|
54
|
+
| --------------------------------------- | --------------------------------------------------------------- |
|
|
55
|
+
| `Markly::DEFAULT` | The default rendering system. |
|
|
56
|
+
| `Markly::UNSAFE` | Allow raw/custom HTML and unsafe links. |
|
|
57
|
+
| `Markly::GITHUB_PRE_LANG` | Use GitHub-style `<pre lang>` for fenced code blocks. |
|
|
58
|
+
| `Markly::HARD_BREAKS` | Treat `\n` as hardbreaks (by adding `<br/>`). |
|
|
59
|
+
| `Markly::NO_BREAKS` | Translate `\n` in the source to a single whitespace. |
|
|
60
|
+
| `Markly::SOURCE_POSITION` | Include source position in rendered HTML. |
|
|
61
|
+
| `Markly::TABLE_PREFER_STYLE_ATTRIBUTES` | Use `style` insted of `align` for table cells. |
|
|
62
|
+
| `Markly::FULL_INFO_STRING` | Include full info strings of code blocks in separate attribute. |
|
|
63
|
+
|
|
64
|
+
### Passing Options
|
|
65
|
+
|
|
66
|
+
To apply a single option, pass it in as a flags option:
|
|
67
|
+
|
|
68
|
+
``` ruby
|
|
69
|
+
Markly.parse("\"Hello,\" said the spider.", flags: Markly::SMART)
|
|
70
|
+
# <p>“Hello,” said the spider.</p>\n
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
To have multiple options applied, `|` (or) the flags together:
|
|
74
|
+
|
|
75
|
+
``` ruby
|
|
76
|
+
Markly.render_html("\"'Shelob' is my name.\"", flags: Markly::HARD_BREAKS|Markly::SOURCE_POSITION)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Extensions
|
|
80
|
+
|
|
81
|
+
Both `render_html` and `parse` take an optional `extensions:` argument defining the extensions you want enabled as your CommonMark document is being processed:
|
|
82
|
+
|
|
83
|
+
``` ruby
|
|
84
|
+
Markly.render_html("<script>hi</script>", flags: Markly::UNSAFE, extensions: [:tagfilter])
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The documentation for these extensions are [defined in this spec](https://github.github.com/gfm/), and the rationale is provided [in this blog post](https://githubengineering.com/a-formal-spec-for-github-markdown/).
|
|
88
|
+
|
|
89
|
+
The available extensions are:
|
|
90
|
+
|
|
91
|
+
- `:table` - This provides support for tables.
|
|
92
|
+
- `:tasklist` - This provides support for task list items.
|
|
93
|
+
- `:strikethrough` - This provides support for strikethroughs.
|
|
94
|
+
- `:autolink` - This provides support for automatically converting URLs to anchor tags.
|
|
95
|
+
- `:tagfilter` - This escapes [several "unsafe" HTML tags](https://github.github.com/gfm/#disallowed-raw-html-extension-), causing them to not have any effect.
|
|
96
|
+
|
|
97
|
+
## Developing Locally
|
|
98
|
+
|
|
99
|
+
After cloning the repo:
|
|
100
|
+
|
|
101
|
+
$ bake build test
|
data/context/headings.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Headings
|
|
2
|
+
|
|
3
|
+
This guide explains how to work with headings in Markly, including extracting them for navigation and handling duplicate heading text.
|
|
4
|
+
|
|
5
|
+
## Unique ID Generation
|
|
6
|
+
|
|
7
|
+
When rendering HTML with `ids: true`, duplicate heading text automatically gets unique IDs to avoid collisions. This is particularly useful when multiple sections have the same title (e.g., multiple "Deployment" sections under different parent headings).
|
|
8
|
+
|
|
9
|
+
``` ruby
|
|
10
|
+
markdown = <<~MARKDOWN
|
|
11
|
+
## Kubernetes
|
|
12
|
+
|
|
13
|
+
### Deployment
|
|
14
|
+
|
|
15
|
+
## Systemd
|
|
16
|
+
|
|
17
|
+
### Deployment
|
|
18
|
+
MARKDOWN
|
|
19
|
+
|
|
20
|
+
renderer = Markly::Renderer::HTML.new(ids: true)
|
|
21
|
+
html = renderer.render(Markly.parse(markdown))
|
|
22
|
+
|
|
23
|
+
# Generates:
|
|
24
|
+
# <section id="kubernetes">...</section>
|
|
25
|
+
# <section id="deployment">...</section>
|
|
26
|
+
# <section id="systemd">...</section>
|
|
27
|
+
# <section id="deployment-2">...</section>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The first occurrence gets the clean ID, subsequent duplicates get numbered suffixes (`-2`, `-3`, etc.).
|
|
31
|
+
|
|
32
|
+
## Extracting Headings for Table of Contents
|
|
33
|
+
|
|
34
|
+
The `Headings` class can extract headings for building navigation or table of contents:
|
|
35
|
+
|
|
36
|
+
``` ruby
|
|
37
|
+
document = Markly.parse(markdown)
|
|
38
|
+
headings = Markly::Renderer::Headings.extract(document, min_level: 2, max_level: 3)
|
|
39
|
+
|
|
40
|
+
headings.each do |heading|
|
|
41
|
+
puts "#{heading.level}: #{heading.text} (#{heading.anchor})"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Output:
|
|
45
|
+
# 2: Kubernetes (kubernetes)
|
|
46
|
+
# 3: Deployment (deployment)
|
|
47
|
+
# 2: Systemd (systemd)
|
|
48
|
+
# 3: Deployment (deployment-2)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Each `Heading` object has:
|
|
52
|
+
- `level` - The heading level (1-6)
|
|
53
|
+
- `text` - The plain text content
|
|
54
|
+
- `anchor` - The unique ID/anchor
|
|
55
|
+
- `node` - The original Markly AST node
|
|
56
|
+
|
|
57
|
+
### Level Filtering
|
|
58
|
+
|
|
59
|
+
Use `min_level` and `max_level` to filter which heading levels to extract:
|
|
60
|
+
|
|
61
|
+
``` ruby
|
|
62
|
+
# Only extract h2 and h3 headings
|
|
63
|
+
headings = Markly::Renderer::Headings.extract(document, min_level: 2, max_level: 3)
|
|
64
|
+
|
|
65
|
+
# Only h1 headings
|
|
66
|
+
headings = Markly::Renderer::Headings.extract(document, min_level: 1, max_level: 1)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Custom Heading Strategies
|
|
70
|
+
|
|
71
|
+
For advanced use cases, you can provide a custom `Headings` instance to the HTML renderer:
|
|
72
|
+
|
|
73
|
+
### Sharing State Across Documents
|
|
74
|
+
|
|
75
|
+
To ensure IDs remain unique across multiple documents:
|
|
76
|
+
|
|
77
|
+
``` ruby
|
|
78
|
+
# Share heading state across multiple documents
|
|
79
|
+
headings = Markly::Renderer::Headings.new
|
|
80
|
+
renderer = Markly::Renderer::HTML.new(headings: headings)
|
|
81
|
+
|
|
82
|
+
doc1_html = renderer.render(Markly.parse(doc1_markdown))
|
|
83
|
+
doc2_html = renderer.render(Markly.parse(doc2_markdown))
|
|
84
|
+
# IDs remain unique across both documents
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Custom ID Generation
|
|
88
|
+
|
|
89
|
+
Subclass `Headings` to implement alternative ID generation strategies:
|
|
90
|
+
|
|
91
|
+
``` ruby
|
|
92
|
+
class HierarchicalHeadings < Markly::Renderer::Headings
|
|
93
|
+
def initialize
|
|
94
|
+
super
|
|
95
|
+
@parent_context = []
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def anchor_for(node)
|
|
99
|
+
base = base_anchor_for(node)
|
|
100
|
+
|
|
101
|
+
# Custom logic: could incorporate parent heading context
|
|
102
|
+
# to generate IDs like "kubernetes-deployment" instead of "deployment-2"
|
|
103
|
+
|
|
104
|
+
if @ids.key?(base)
|
|
105
|
+
@ids[base] += 1
|
|
106
|
+
"#{base}-#{@ids[base]}"
|
|
107
|
+
else
|
|
108
|
+
@ids[base] = 1
|
|
109
|
+
base
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
renderer = Markly::Renderer::HTML.new(headings: HierarchicalHeadings.new)
|
|
115
|
+
```
|
|
116
|
+
|
data/context/index.yaml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Automatically generated context index for Utopia::Project guides.
|
|
2
|
+
# Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
|
|
3
|
+
---
|
|
4
|
+
description: CommonMark parser and renderer. Written in C, wrapped in Ruby.
|
|
5
|
+
metadata:
|
|
6
|
+
documentation_uri: https://ioquatix.github.io/markly/
|
|
7
|
+
funding_uri: https://github.com/sponsors/ioquatix/
|
|
8
|
+
source_code_uri: https://github.com/ioquatix/markly.git
|
|
9
|
+
files:
|
|
10
|
+
- path: getting-started.md
|
|
11
|
+
title: Getting Started
|
|
12
|
+
description: This guide explains now to install and use Markly.
|
|
13
|
+
- path: abstract-syntax-tree.md
|
|
14
|
+
title: Abstract Syntax Tree
|
|
15
|
+
description: This guide explains how to use Markly's abstract syntax tree (AST)
|
|
16
|
+
to parse and manipulate Markdown documents.
|
|
17
|
+
- path: headings.md
|
|
18
|
+
title: Headings
|
|
19
|
+
description: This guide explains how to work with headings in Markly, including
|
|
20
|
+
extracting them for navigation and handling duplicate heading text.
|
data/lib/markly/node.rb
CHANGED
|
@@ -29,7 +29,7 @@ module Markly
|
|
|
29
29
|
|
|
30
30
|
# Public: An iterator that "walks the tree," descending into children recursively.
|
|
31
31
|
#
|
|
32
|
-
#
|
|
32
|
+
# block - A {Proc} representing the action to take for each child
|
|
33
33
|
def walk(&block)
|
|
34
34
|
return enum_for(:walk) unless block_given?
|
|
35
35
|
|
|
@@ -41,7 +41,7 @@ module Markly
|
|
|
41
41
|
|
|
42
42
|
# Public: Convert the node to an HTML string.
|
|
43
43
|
#
|
|
44
|
-
#
|
|
44
|
+
# flags - A {Symbol} or {Array of Symbol}s indicating the render options
|
|
45
45
|
# extensions - An {Array of Symbol}s indicating the extensions to use
|
|
46
46
|
#
|
|
47
47
|
# Returns a {String}.
|
|
@@ -51,7 +51,7 @@ module Markly
|
|
|
51
51
|
|
|
52
52
|
# Public: Convert the node to a CommonMark string.
|
|
53
53
|
#
|
|
54
|
-
#
|
|
54
|
+
# flags - A {Symbol} or {Array of Symbol}s indicating the render options
|
|
55
55
|
# width - Column to wrap the output at
|
|
56
56
|
#
|
|
57
57
|
# Returns a {String}.
|
|
@@ -63,7 +63,7 @@ module Markly
|
|
|
63
63
|
|
|
64
64
|
# Public: Convert the node to a plain text string.
|
|
65
65
|
#
|
|
66
|
-
#
|
|
66
|
+
# flags - A {Symbol} or {Array of Symbol}s indicating the render options
|
|
67
67
|
# width - Column to wrap the output at
|
|
68
68
|
#
|
|
69
69
|
# Returns a {String}.
|
|
@@ -106,7 +106,6 @@ module Markly
|
|
|
106
106
|
|
|
107
107
|
# Replace a section (header + content) with a new node.
|
|
108
108
|
#
|
|
109
|
-
# @parameter title [String] the title of the section to replace.
|
|
110
109
|
# @parameter new_node [Markly::Node] the node to replace the section with.
|
|
111
110
|
# @parameter replace_header [Boolean] whether to replace the header itself or not.
|
|
112
111
|
# @parameter remove_subsections [Boolean] whether to remove subsections or not.
|
|
@@ -132,7 +131,7 @@ module Markly
|
|
|
132
131
|
|
|
133
132
|
# Append the given node after the current node.
|
|
134
133
|
#
|
|
135
|
-
# It's okay to provide a document node,
|
|
134
|
+
# It's okay to provide a document node, its children will be appended.
|
|
136
135
|
#
|
|
137
136
|
# @parameter node [Markly::Node] the node to append.
|
|
138
137
|
def append_after(node)
|
|
@@ -151,7 +150,7 @@ module Markly
|
|
|
151
150
|
|
|
152
151
|
# Append the given node before the current node.
|
|
153
152
|
#
|
|
154
|
-
# It's okay to provide a document node,
|
|
153
|
+
# It's okay to provide a document node, its children will be appended.
|
|
155
154
|
#
|
|
156
155
|
# @parameter node [Markly::Node] the node to append.
|
|
157
156
|
def append_before(node)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Markly
|
|
7
|
+
module Renderer
|
|
8
|
+
# Extracts headings from a markdown document with unique anchor IDs.
|
|
9
|
+
# Handles duplicate heading text by appending counters (e.g., "deployment", "deployment-2", "deployment-3").
|
|
10
|
+
class Headings
|
|
11
|
+
def initialize
|
|
12
|
+
@ids = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Generate a unique anchor for a node.
|
|
16
|
+
# @parameter node [Markly::Node] The heading node
|
|
17
|
+
# @returns [String] A unique anchor ID
|
|
18
|
+
def anchor_for(node)
|
|
19
|
+
base = base_anchor_for(node)
|
|
20
|
+
|
|
21
|
+
if @ids.key?(base)
|
|
22
|
+
@ids[base] += 1
|
|
23
|
+
"#{base}-#{@ids[base]}"
|
|
24
|
+
else
|
|
25
|
+
@ids[base] = 1
|
|
26
|
+
base
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Extract all headings from a document root with unique anchors.
|
|
31
|
+
# @parameter root [Markly::Node] The document root node
|
|
32
|
+
# @parameter min_level [Integer] Minimum heading level to extract (default: 1)
|
|
33
|
+
# @parameter max_level [Integer] Maximum heading level to extract (default: 6)
|
|
34
|
+
# @returns [Array<Heading>] Array of heading objects with unique anchors
|
|
35
|
+
def extract(root, min_level: 1, max_level: 6)
|
|
36
|
+
headings = []
|
|
37
|
+
root.walk do |node|
|
|
38
|
+
if node.type == :header
|
|
39
|
+
level = node.header_level
|
|
40
|
+
next if level < min_level || level > max_level
|
|
41
|
+
|
|
42
|
+
headings << Heading.new(
|
|
43
|
+
node: node,
|
|
44
|
+
level: level,
|
|
45
|
+
text: node.to_plaintext.chomp,
|
|
46
|
+
anchor: anchor_for(node)
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
headings
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Class method for convenience - creates a new instance and extracts headings.
|
|
54
|
+
# @parameter root [Markly::Node] The document root node
|
|
55
|
+
# @parameter min_level [Integer] Minimum heading level to extract (default: 1)
|
|
56
|
+
# @parameter max_level [Integer] Maximum heading level to extract (default: 6)
|
|
57
|
+
# @returns [Array<Heading>] Array of heading objects with unique anchors
|
|
58
|
+
def self.extract(root, min_level: 1, max_level: 6)
|
|
59
|
+
new.extract(root, min_level: min_level, max_level: max_level)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Generate a base anchor from a node's text content.
|
|
65
|
+
# @parameter node [Markly::Node] The heading node
|
|
66
|
+
# @returns [String] The base anchor (lowercase, hyphenated)
|
|
67
|
+
def base_anchor_for(node)
|
|
68
|
+
text = node.to_plaintext.chomp.downcase
|
|
69
|
+
text.gsub(/\s+/, "-")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Represents a heading extracted from a document.
|
|
74
|
+
# @attribute node [Markly::Node] The original heading node
|
|
75
|
+
# @attribute level [Integer] The heading level (1-6)
|
|
76
|
+
# @attribute text [String] The plain text content of the heading
|
|
77
|
+
# @attribute anchor [String] The unique anchor ID for this heading
|
|
78
|
+
Heading = Struct.new(:node, :level, :text, :anchor, keyword_init: true)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
data/lib/markly/renderer/html.rb
CHANGED
|
@@ -9,15 +9,18 @@
|
|
|
9
9
|
# Copyright, 2020-2025, by Samuel Williams.
|
|
10
10
|
|
|
11
11
|
require_relative "generic"
|
|
12
|
+
require_relative "headings"
|
|
12
13
|
require "cgi"
|
|
13
14
|
|
|
14
15
|
module Markly
|
|
15
16
|
module Renderer
|
|
16
17
|
class HTML < Generic
|
|
17
|
-
def initialize(ids: false, tight: false, **options)
|
|
18
|
+
def initialize(ids: false, headings: nil, tight: false, **options)
|
|
18
19
|
super(**options)
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
# Initialize heading tracker if IDs are enabled
|
|
22
|
+
@headings = headings || (ids ? Headings.new : nil)
|
|
23
|
+
|
|
21
24
|
@section = nil
|
|
22
25
|
@tight = tight
|
|
23
26
|
|
|
@@ -32,8 +35,8 @@ module Markly
|
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
def id_for(node)
|
|
35
|
-
if @
|
|
36
|
-
anchor =
|
|
38
|
+
if @headings
|
|
39
|
+
anchor = @headings.anchor_for(node)
|
|
37
40
|
return " id=\"#{CGI.escape_html anchor}\""
|
|
38
41
|
end
|
|
39
42
|
end
|
|
@@ -54,7 +57,7 @@ module Markly
|
|
|
54
57
|
|
|
55
58
|
def header(node)
|
|
56
59
|
block do
|
|
57
|
-
if @
|
|
60
|
+
if @headings
|
|
58
61
|
out("</section>") if @section
|
|
59
62
|
@section = true
|
|
60
63
|
out("<section#{id_for(node)}>")
|
|
@@ -253,10 +256,10 @@ module Markly
|
|
|
253
256
|
end
|
|
254
257
|
|
|
255
258
|
TABLE_CELL_ALIGNMENT = {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
259
|
+
left: ' align="left"',
|
|
260
|
+
right: ' align="right"',
|
|
261
|
+
center: ' align="center"'
|
|
262
|
+
}.freeze
|
|
260
263
|
|
|
261
264
|
def table_cell(node)
|
|
262
265
|
align = TABLE_CELL_ALIGNMENT.fetch(@alignments[@column_index], "")
|
data/lib/markly/version.rb
CHANGED
data/readme.md
CHANGED
|
@@ -22,10 +22,20 @@ Please see the [project documentation](https://ioquatix.github.io/markly/) for m
|
|
|
22
22
|
|
|
23
23
|
- [Abstract Syntax Tree](https://ioquatix.github.io/markly/guides/abstract-syntax-tree/index) - This guide explains how to use Markly's abstract syntax tree (AST) to parse and manipulate Markdown documents.
|
|
24
24
|
|
|
25
|
+
- [Headings](https://ioquatix.github.io/markly/guides/headings/index) - This guide explains how to work with headings in Markly, including extracting them for navigation and handling duplicate heading text.
|
|
26
|
+
|
|
25
27
|
## Releases
|
|
26
28
|
|
|
27
29
|
Please see the [project releases](https://ioquatix.github.io/markly/releases/index) for all releases.
|
|
28
30
|
|
|
31
|
+
### v0.15.1
|
|
32
|
+
|
|
33
|
+
- Add agent context.
|
|
34
|
+
|
|
35
|
+
### v0.15.0
|
|
36
|
+
|
|
37
|
+
- Introduced `Markly::Renderer::Headings` class for extracting headings from markdown documents with automatic duplicate ID resolution. When rendering HTML with `ids: true`, duplicate heading text now automatically gets unique IDs (`deployment`, `deployment-2`, `deployment-3`). The `Headings` class can also be used to extract headings for building navigation or table of contents.
|
|
38
|
+
|
|
29
39
|
### v0.14.0
|
|
30
40
|
|
|
31
41
|
- Expose `Markly::Renderer::HTML.anchor_for` method to generate URL-safe anchors from headers.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.15.1
|
|
4
|
+
|
|
5
|
+
- Add agent context.
|
|
6
|
+
|
|
7
|
+
## v0.15.0
|
|
8
|
+
|
|
9
|
+
- Introduced `Markly::Renderer::Headings` class for extracting headings from markdown documents with automatic duplicate ID resolution. When rendering HTML with `ids: true`, duplicate heading text now automatically gets unique IDs (`deployment`, `deployment-2`, `deployment-3`). The `Headings` class can also be used to extract headings for building navigation or table of contents.
|
|
10
|
+
|
|
3
11
|
## v0.14.0
|
|
4
12
|
|
|
5
13
|
- Expose `Markly::Renderer::HTML.anchor_for` method to generate URL-safe anchors from headers.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: markly
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.15.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Garen Torikian
|
|
@@ -62,6 +62,10 @@ extensions:
|
|
|
62
62
|
- ext/markly/extconf.rb
|
|
63
63
|
extra_rdoc_files: []
|
|
64
64
|
files:
|
|
65
|
+
- context/abstract-syntax-tree.md
|
|
66
|
+
- context/getting-started.md
|
|
67
|
+
- context/headings.md
|
|
68
|
+
- context/index.yaml
|
|
65
69
|
- ext/markly/arena.c
|
|
66
70
|
- ext/markly/autolink.c
|
|
67
71
|
- ext/markly/autolink.h
|
|
@@ -138,6 +142,7 @@ files:
|
|
|
138
142
|
- lib/markly/node.rb
|
|
139
143
|
- lib/markly/node/inspect.rb
|
|
140
144
|
- lib/markly/renderer/generic.rb
|
|
145
|
+
- lib/markly/renderer/headings.rb
|
|
141
146
|
- lib/markly/renderer/html.rb
|
|
142
147
|
- lib/markly/version.rb
|
|
143
148
|
- license.md
|
metadata.gz.sig
CHANGED
|
Binary file
|