markdownator 0.1.0 → 0.1.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
- data/CHANGELOG.md +30 -5
- data/Gemfile +0 -1
- data/README.md +2 -3
- data/lib/markdownator/converters/html.rb +7 -3
- data/lib/markdownator/converters/html_renderer.rb +190 -0
- data/lib/markdownator/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4799d3266ce18fa6adff8a3264b255a8bcaa2974888e260542adacd709566f2a
|
|
4
|
+
data.tar.gz: 50f573d19ff4b5407220fe4e8d2b50ebbc481c7d983183e24032ed6a8e4671f4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71873a123d242b1ff45147fc28eb50300c2dfb82d92dd5939be352dbeb05bbf9ae97c1c7003c5edf606e918176f5fa2dcb53b3f2ef2b0c3e9437a3fdb3faad81
|
|
7
|
+
data.tar.gz: 2c7aaf6871850fa39e323e32f7da9fe3870e06159fda18e4f04e268231214efd717c88f0cd41d291a07964b705e653cac677459a1921a1258545a68e72fd273e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
1
8
|
## [Unreleased]
|
|
2
9
|
|
|
10
|
+
## [0.1.1] - 2026-06-13
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- HTML (and EPUB) conversion now renders Markdown directly from the Nokogiri
|
|
15
|
+
node tree, dropping the `reverse_markdown` dependency (it was only a thin
|
|
16
|
+
layer over Nokogiri).
|
|
17
|
+
|
|
3
18
|
## [0.1.0] - 2026-06-12
|
|
4
19
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Converter-registry engine (`Markdownator.convert`, `.convert_local`,
|
|
23
|
+
`.convert_stream`) dispatching local paths, URLs, and IO streams to the first
|
|
24
|
+
converter that accepts the stream.
|
|
25
|
+
- Converters for plain text, HTML, CSV, JSON, XML, DOCX, XLSX, PPTX, PDF, EPUB,
|
|
26
|
+
ZIP (recursive), and image metadata.
|
|
27
|
+
- Optional, lazily loaded format dependencies with a helpful
|
|
28
|
+
`MissingDependencyError` when a required gem is absent; zero hard runtime
|
|
29
|
+
dependencies.
|
|
30
|
+
- Pluggable LLM image-captioner hook (off by default).
|
|
31
|
+
|
|
32
|
+
[Unreleased]: https://github.com/alexrupom/markdownator/compare/v0.1.1...HEAD
|
|
33
|
+
[0.1.1]: https://github.com/alexrupom/markdownator/compare/v0.1.0...v0.1.1
|
|
34
|
+
[0.1.0]: https://github.com/alexrupom/markdownator/releases/tag/v0.1.0
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -13,13 +13,13 @@ libraries **lazily**, so you only install the gems for the formats you actually
|
|
|
13
13
|
| Plain text / Markdown | `.txt`, `.md` | — (built in) |
|
|
14
14
|
| CSV | `.csv` | — (built in) |
|
|
15
15
|
| JSON | `.json` | — (built in) |
|
|
16
|
-
| HTML | `.html`, `.htm` | `
|
|
16
|
+
| HTML | `.html`, `.htm` | `nokogiri` |
|
|
17
17
|
| XML | `.xml` | `nokogiri` |
|
|
18
18
|
| Word | `.docx` | `rubyzip`, `nokogiri` |
|
|
19
19
|
| Excel | `.xlsx` | `roo` |
|
|
20
20
|
| PowerPoint | `.pptx` | `rubyzip`, `nokogiri` |
|
|
21
21
|
| PDF | `.pdf` | `pdf-reader` |
|
|
22
|
-
| EPUB | `.epub` | `rubyzip`, `nokogiri
|
|
22
|
+
| EPUB | `.epub` | `rubyzip`, `nokogiri` |
|
|
23
23
|
| ZIP (recurses) | `.zip` | `rubyzip` |
|
|
24
24
|
| Images (metadata) | `.jpg`, `.png`, `.tiff`, … | `exifr` (for EXIF) |
|
|
25
25
|
|
|
@@ -39,7 +39,6 @@ gem "pdf-reader" # PDF
|
|
|
39
39
|
gem "roo" # XLSX
|
|
40
40
|
gem "rubyzip" # DOCX, PPTX, EPUB, ZIP
|
|
41
41
|
gem "nokogiri" # HTML, XML, DOCX, PPTX, EPUB
|
|
42
|
-
gem "reverse_markdown" # HTML, EPUB
|
|
43
42
|
gem "exifr" # image EXIF
|
|
44
43
|
```
|
|
45
44
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "html_renderer"
|
|
4
|
+
|
|
3
5
|
module Markdownator
|
|
4
6
|
module Converters
|
|
5
|
-
# Converts HTML into Markdown
|
|
7
|
+
# Converts HTML into Markdown by walking the Nokogiri node tree.
|
|
6
8
|
class Html < Base
|
|
7
9
|
def accepts?(_io, stream_info)
|
|
8
10
|
matches?(stream_info, extensions: %w[html htm], mimetypes: %w[text/html application/xhtml+xml])
|
|
@@ -15,8 +17,10 @@ module Markdownator
|
|
|
15
17
|
|
|
16
18
|
# Shared so other container converters (EPUB) can reuse HTML conversion.
|
|
17
19
|
def self.html_to_markdown(html)
|
|
18
|
-
Markdownator.require_optional("
|
|
19
|
-
|
|
20
|
+
Markdownator.require_optional("nokogiri", feature: "HTML conversion")
|
|
21
|
+
doc = Nokogiri::HTML(html)
|
|
22
|
+
root = doc.at_css("body") || doc.root || doc
|
|
23
|
+
HtmlRenderer.new.render(root)
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
def self.extract_title(html)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markdownator
|
|
4
|
+
module Converters
|
|
5
|
+
# Walks a Nokogiri HTML node tree and renders Markdown. A focused,
|
|
6
|
+
# dependency-free replacement for reverse_markdown: HTML conversion needs
|
|
7
|
+
# only Nokogiri (which reverse_markdown depended on anyway).
|
|
8
|
+
class HtmlRenderer
|
|
9
|
+
# Elements that introduce their own block (line-separated) content.
|
|
10
|
+
BLOCK_TAGS = %w[
|
|
11
|
+
address article aside blockquote details div dl figcaption figure
|
|
12
|
+
footer form h1 h2 h3 h4 h5 h6 header hr main nav ol p pre section
|
|
13
|
+
table ul
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
# Elements whose contents are dropped entirely.
|
|
17
|
+
SKIP_TAGS = %w[script style head title noscript template].freeze
|
|
18
|
+
|
|
19
|
+
def render(node)
|
|
20
|
+
blocks_to_string(render_blocks(node))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Renders the children of +node+ into an array of block strings, grouping
|
|
26
|
+
# consecutive inline content into paragraphs.
|
|
27
|
+
def render_blocks(node)
|
|
28
|
+
blocks = []
|
|
29
|
+
buffer = +""
|
|
30
|
+
|
|
31
|
+
node.children.each do |child|
|
|
32
|
+
if block?(child)
|
|
33
|
+
push_paragraph(blocks, buffer)
|
|
34
|
+
buffer = +""
|
|
35
|
+
block = render_block(child)
|
|
36
|
+
blocks << block unless block.nil? || block.empty?
|
|
37
|
+
elsif !skip?(child)
|
|
38
|
+
buffer << render_inline(child)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
push_paragraph(blocks, buffer)
|
|
42
|
+
blocks
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def render_block(node)
|
|
46
|
+
case node.name
|
|
47
|
+
when /\Ah([1-6])\z/ then "#{"#" * Regexp.last_match(1).to_i} #{inline_of(node)}"
|
|
48
|
+
when "ul" then render_list(node, ordered: false)
|
|
49
|
+
when "ol" then render_list(node, ordered: true)
|
|
50
|
+
when "pre" then render_pre(node)
|
|
51
|
+
when "blockquote" then render_blockquote(node)
|
|
52
|
+
when "table" then render_table(node)
|
|
53
|
+
when "dl" then render_definition_list(node)
|
|
54
|
+
when "hr" then "---"
|
|
55
|
+
else blocks_to_string(render_blocks(node)) # div, section, p, unknown blocks
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def render_inline(node)
|
|
60
|
+
return normalize(node.text) if node.text?
|
|
61
|
+
return "" if node.comment? || skip?(node)
|
|
62
|
+
|
|
63
|
+
case node.name
|
|
64
|
+
when "strong", "b" then emphasis(node, "**")
|
|
65
|
+
when "em", "i" then emphasis(node, "_")
|
|
66
|
+
when "del", "s", "strike" then emphasis(node, "~~")
|
|
67
|
+
when "code" then inline_code(node)
|
|
68
|
+
when "a" then render_link(node)
|
|
69
|
+
when "img" then render_image(node)
|
|
70
|
+
when "br" then "\n"
|
|
71
|
+
else inline_of(node)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def render_link(node)
|
|
76
|
+
href = node["href"].to_s.strip
|
|
77
|
+
text = inline_of(node)
|
|
78
|
+
text = href if text.empty?
|
|
79
|
+
href.empty? ? text : "[#{text}](#{href})"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def render_image(node)
|
|
83
|
+
"![#{node["alt"].to_s.strip}](#{node["src"].to_s.strip})"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def render_list(node, ordered:)
|
|
87
|
+
index = 0
|
|
88
|
+
list_items(node).map do |li|
|
|
89
|
+
index += 1
|
|
90
|
+
marker = ordered ? "#{index}." : "-"
|
|
91
|
+
indent = " " * (marker.length + 1)
|
|
92
|
+
lines = blocks_to_string(render_blocks(li)).split("\n")
|
|
93
|
+
first = lines.shift.to_s
|
|
94
|
+
rest = lines.map { |line| line.empty? ? "" : "#{indent}#{line}" }
|
|
95
|
+
(["#{marker} #{first}"] + rest).join("\n")
|
|
96
|
+
end.join("\n")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def render_pre(node)
|
|
100
|
+
code = node.at_css("code") || node
|
|
101
|
+
language = code["class"].to_s[/(?:language|lang)-(\w+)/, 1].to_s
|
|
102
|
+
"```#{language}\n#{code.text.chomp}\n```"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def render_blockquote(node)
|
|
106
|
+
blocks_to_string(render_blocks(node)).split("\n").map do |line|
|
|
107
|
+
line.empty? ? ">" : "> #{line}"
|
|
108
|
+
end.join("\n")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def render_table(node)
|
|
112
|
+
rows = node.css("tr").map do |tr|
|
|
113
|
+
tr.css("th, td").map { |cell| inline_of(cell).gsub("|", "\\|") }
|
|
114
|
+
end
|
|
115
|
+
rows.reject!(&:empty?)
|
|
116
|
+
return "" if rows.empty?
|
|
117
|
+
|
|
118
|
+
width = rows.map(&:length).max
|
|
119
|
+
rows.each { |row| row.fill("", row.length...width) }
|
|
120
|
+
header, *body = rows
|
|
121
|
+
lines = ["| #{header.join(" | ")} |", "| #{Array.new(width, "---").join(" | ")} |"]
|
|
122
|
+
body.each { |row| lines << "| #{row.join(" | ")} |" }
|
|
123
|
+
lines.join("\n")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def render_definition_list(node)
|
|
127
|
+
node.element_children.map do |child|
|
|
128
|
+
text = inline_of(child)
|
|
129
|
+
next if text.empty?
|
|
130
|
+
|
|
131
|
+
child.name == "dt" ? "**#{text}**" : ": #{text}"
|
|
132
|
+
end.compact.join("\n")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# --- helpers ---------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def inline_code(node)
|
|
138
|
+
text = node.text
|
|
139
|
+
fence = text.include?("`") ? "`` " : "`"
|
|
140
|
+
close = text.include?("`") ? " ``" : "`"
|
|
141
|
+
"#{fence}#{text}#{close}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def emphasis(node, marker)
|
|
145
|
+
inner = inline_of(node)
|
|
146
|
+
inner.empty? ? "" : "#{marker}#{inner}#{marker}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Inline content of a node, with surrounding whitespace collapsed.
|
|
150
|
+
def inline_of(node)
|
|
151
|
+
clean_inline(node.children.map { |child| render_inline(child) }.join)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def list_items(node)
|
|
155
|
+
node.element_children.select { |child| child.name == "li" }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def push_paragraph(blocks, buffer)
|
|
159
|
+
text = clean_block(buffer)
|
|
160
|
+
blocks << text unless text.empty?
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def blocks_to_string(blocks)
|
|
164
|
+
blocks.reject(&:empty?).join("\n\n")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Collapses source whitespace (including newlines) to single spaces, so
|
|
168
|
+
# only explicit <br> newlines survive.
|
|
169
|
+
def normalize(text)
|
|
170
|
+
text.gsub(/\s+/, " ")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def clean_inline(text)
|
|
174
|
+
text.gsub(/[ \t]{2,}/, " ").strip
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def clean_block(text)
|
|
178
|
+
text.gsub(/ *\n */, "\n").gsub(/[ \t]{2,}/, " ").gsub(/\n{3,}/, "\n\n").strip
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def block?(node)
|
|
182
|
+
node.element? && BLOCK_TAGS.include?(node.name)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def skip?(node)
|
|
186
|
+
node.element? && SKIP_TAGS.include?(node.name)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
data/lib/markdownator/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: markdownator
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- alexrupom
|
|
@@ -33,6 +33,7 @@ files:
|
|
|
33
33
|
- lib/markdownator/converters/docx.rb
|
|
34
34
|
- lib/markdownator/converters/epub.rb
|
|
35
35
|
- lib/markdownator/converters/html.rb
|
|
36
|
+
- lib/markdownator/converters/html_renderer.rb
|
|
36
37
|
- lib/markdownator/converters/image.rb
|
|
37
38
|
- lib/markdownator/converters/json.rb
|
|
38
39
|
- lib/markdownator/converters/pdf.rb
|