html-to-markdown 2.29.0-arm64-darwin
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/.bundle/config +2 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +29 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +223 -0
- data/README.md +345 -0
- data/Rakefile +32 -0
- data/Steepfile +26 -0
- data/bin/benchmark.rb +232 -0
- data/exe/html-to-markdown +6 -0
- data/html-to-markdown-rb.gemspec +99 -0
- data/lib/html_to_markdown/cli.rb +21 -0
- data/lib/html_to_markdown/cli_proxy.rb +74 -0
- data/lib/html_to_markdown/version.rb +5 -0
- data/lib/html_to_markdown.rb +211 -0
- data/lib/html_to_markdown_rb.bundle +0 -0
- data/sig/html_to_markdown/cli.rbs +24 -0
- data/sig/html_to_markdown/cli_proxy.rbs +48 -0
- data/sig/html_to_markdown.rbs +498 -0
- data/sig/open3.rbs +12 -0
- data/spec/cli_proxy_spec.rb +42 -0
- data/spec/convert_spec.rb +77 -0
- data/spec/convert_with_tables_spec.rb +194 -0
- data/spec/metadata_extraction_spec.rb +437 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/visitor_issue_187_spec.rb +605 -0
- data/spec/visitor_spec.rb +1149 -0
- metadata +80 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module HtmlToMarkdown
|
|
2
|
+
module CLI
|
|
3
|
+
# Module method (module_function creates both module and instance methods)
|
|
4
|
+
#
|
|
5
|
+
# Run the CLI with the given arguments
|
|
6
|
+
#
|
|
7
|
+
# @param argv Command-line arguments (defaults to ARGV)
|
|
8
|
+
# @param stdout Output stream for standard output
|
|
9
|
+
# @param stderr Output stream for standard error
|
|
10
|
+
# @return Exit code (0 for success, non-zero for failure)
|
|
11
|
+
def self.run: (
|
|
12
|
+
?Array[String] argv,
|
|
13
|
+
?stdout: IO,
|
|
14
|
+
?stderr: IO
|
|
15
|
+
) -> Integer
|
|
16
|
+
|
|
17
|
+
# Instance method version (created by module_function)
|
|
18
|
+
def run: (
|
|
19
|
+
?Array[String] argv,
|
|
20
|
+
?stdout: IO,
|
|
21
|
+
?stderr: IO
|
|
22
|
+
) -> Integer
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module HtmlToMarkdown
|
|
2
|
+
module CLIProxy
|
|
3
|
+
# Base error class
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# Error when CLI binary is not found
|
|
8
|
+
class MissingBinaryError < Error
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Error when CLI execution fails
|
|
12
|
+
class CLIExecutionError < Error
|
|
13
|
+
attr_reader stderr: String
|
|
14
|
+
attr_reader status: Integer?
|
|
15
|
+
|
|
16
|
+
def initialize: (String message, stderr: String, status: Integer?) -> void
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Module methods (module_function creates both module and instance methods)
|
|
20
|
+
|
|
21
|
+
# Execute CLI with given arguments
|
|
22
|
+
def self.call: (Array[String] argv) -> String
|
|
23
|
+
|
|
24
|
+
# Find the CLI binary in search paths
|
|
25
|
+
def self.find_cli_binary: () -> Pathname
|
|
26
|
+
|
|
27
|
+
# Get root path of the gem
|
|
28
|
+
def self.root_path: () -> Pathname
|
|
29
|
+
|
|
30
|
+
# Get lib path of the gem
|
|
31
|
+
def self.lib_path: () -> Pathname
|
|
32
|
+
|
|
33
|
+
# Get search paths for CLI binary
|
|
34
|
+
def self.search_paths: (String binary_name) -> Array[Pathname]
|
|
35
|
+
|
|
36
|
+
# Get error message for missing binary
|
|
37
|
+
def self.missing_binary_message: () -> String
|
|
38
|
+
|
|
39
|
+
# Instance method versions (created by module_function)
|
|
40
|
+
|
|
41
|
+
def call: (Array[String] argv) -> String
|
|
42
|
+
def find_cli_binary: () -> Pathname
|
|
43
|
+
def root_path: () -> Pathname
|
|
44
|
+
def lib_path: () -> Pathname
|
|
45
|
+
def search_paths: (String binary_name) -> Array[Pathname]
|
|
46
|
+
def missing_binary_message: () -> String
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
# Type definitions for HtmlToMarkdown Ruby gem
|
|
2
|
+
module HtmlToMarkdown
|
|
3
|
+
VERSION: String
|
|
4
|
+
|
|
5
|
+
# Opaque handle for reusable conversion options
|
|
6
|
+
class Options
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Visitor context information passed to visitor callbacks
|
|
10
|
+
class NodeContext
|
|
11
|
+
attr_reader node_type: Symbol
|
|
12
|
+
attr_reader tag_name: String
|
|
13
|
+
attr_reader attributes: Hash[String, String]
|
|
14
|
+
attr_reader depth: Integer
|
|
15
|
+
attr_reader index_in_parent: Integer
|
|
16
|
+
attr_reader parent_tag: String | nil
|
|
17
|
+
attr_reader is_inline: bool
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Result of a visitor callback
|
|
21
|
+
type visitor_result = {
|
|
22
|
+
type: :continue,
|
|
23
|
+
} | {
|
|
24
|
+
type: :custom,
|
|
25
|
+
output: String,
|
|
26
|
+
} | {
|
|
27
|
+
type: :skip,
|
|
28
|
+
} | {
|
|
29
|
+
type: :preserve_html,
|
|
30
|
+
} | {
|
|
31
|
+
type: :error,
|
|
32
|
+
message: String,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type heading_style = :underlined | :atx | :atx_closed
|
|
36
|
+
type list_indent_type = :spaces | :tabs
|
|
37
|
+
type highlight_style = :double_equal | :html | :bold | :none
|
|
38
|
+
type whitespace_mode = :normalized | :strict
|
|
39
|
+
type newline_style = :spaces | :backslash
|
|
40
|
+
type code_block_style = :indented | :backticks | :tildes
|
|
41
|
+
type output_format = :markdown | :djot
|
|
42
|
+
type preprocessing_preset = :minimal | :standard | :aggressive
|
|
43
|
+
|
|
44
|
+
type preprocessing_options = {
|
|
45
|
+
enabled?: bool,
|
|
46
|
+
preset?: preprocessing_preset,
|
|
47
|
+
remove_navigation?: bool,
|
|
48
|
+
remove_forms?: bool
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type conversion_options = {
|
|
52
|
+
heading_style?: heading_style,
|
|
53
|
+
list_indent_type?: list_indent_type,
|
|
54
|
+
list_indent_width?: Integer,
|
|
55
|
+
bullets?: String,
|
|
56
|
+
strong_em_symbol?: String,
|
|
57
|
+
escape_asterisks?: bool,
|
|
58
|
+
escape_underscores?: bool,
|
|
59
|
+
escape_misc?: bool,
|
|
60
|
+
escape_ascii?: bool,
|
|
61
|
+
code_language?: String,
|
|
62
|
+
autolinks?: bool,
|
|
63
|
+
default_title?: bool,
|
|
64
|
+
br_in_tables?: bool,
|
|
65
|
+
hocr_spatial_tables?: bool,
|
|
66
|
+
highlight_style?: highlight_style,
|
|
67
|
+
extract_metadata?: bool,
|
|
68
|
+
whitespace_mode?: whitespace_mode,
|
|
69
|
+
strip_newlines?: bool,
|
|
70
|
+
wrap?: bool,
|
|
71
|
+
wrap_width?: Integer,
|
|
72
|
+
convert_as_inline?: bool,
|
|
73
|
+
sub_symbol?: String,
|
|
74
|
+
sup_symbol?: String,
|
|
75
|
+
newline_style?: newline_style,
|
|
76
|
+
code_block_style?: code_block_style,
|
|
77
|
+
keep_inline_images_in?: Array[String],
|
|
78
|
+
preprocessing?: preprocessing_options,
|
|
79
|
+
encoding?: String,
|
|
80
|
+
debug?: bool,
|
|
81
|
+
strip_tags?: Array[String],
|
|
82
|
+
preserve_tags?: Array[String],
|
|
83
|
+
output_format?: output_format,
|
|
84
|
+
skip_images?: bool
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type inline_image_config = {
|
|
88
|
+
max_decoded_size_bytes?: Integer,
|
|
89
|
+
filename_prefix?: String?,
|
|
90
|
+
capture_svg?: bool,
|
|
91
|
+
infer_dimensions?: bool
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type inline_image_format = "png" | "jpeg" | "gif" | "bmp" | "webp" | "svg" | String
|
|
95
|
+
|
|
96
|
+
type inline_image_source = "img_data_uri" | "svg_element"
|
|
97
|
+
|
|
98
|
+
type inline_image = {
|
|
99
|
+
data: String,
|
|
100
|
+
format: inline_image_format,
|
|
101
|
+
filename: String?,
|
|
102
|
+
description: String?,
|
|
103
|
+
dimensions: [Integer, Integer]?,
|
|
104
|
+
source: inline_image_source,
|
|
105
|
+
attributes: Hash[String, String]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type inline_image_warning = {
|
|
109
|
+
index: Integer,
|
|
110
|
+
message: String
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type html_extraction = {
|
|
114
|
+
markdown: String,
|
|
115
|
+
inline_images: Array[inline_image],
|
|
116
|
+
warnings: Array[inline_image_warning]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type metadata_config = {
|
|
120
|
+
extract_document?: bool,
|
|
121
|
+
extract_headers?: bool,
|
|
122
|
+
extract_links?: bool,
|
|
123
|
+
extract_images?: bool,
|
|
124
|
+
extract_structured_data?: bool,
|
|
125
|
+
max_structured_data_size?: Integer
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type text_direction = "ltr" | "rtl" | "auto" | nil
|
|
129
|
+
|
|
130
|
+
type document_metadata = {
|
|
131
|
+
title: String?,
|
|
132
|
+
description: String?,
|
|
133
|
+
keywords: Array[String],
|
|
134
|
+
author: String?,
|
|
135
|
+
canonical_url: String?,
|
|
136
|
+
base_href: String?,
|
|
137
|
+
language: String?,
|
|
138
|
+
text_direction: text_direction,
|
|
139
|
+
open_graph: Hash[String, String],
|
|
140
|
+
twitter_card: Hash[String, String],
|
|
141
|
+
meta_tags: Hash[String, String]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
type header_metadata = {
|
|
145
|
+
level: Integer,
|
|
146
|
+
text: String,
|
|
147
|
+
id: String?,
|
|
148
|
+
depth: Integer,
|
|
149
|
+
html_offset: Integer
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
type link_type = "anchor" | "internal" | "external" | "email" | "phone" | "other"
|
|
153
|
+
|
|
154
|
+
type link_metadata = {
|
|
155
|
+
href: String,
|
|
156
|
+
text: String,
|
|
157
|
+
title: String?,
|
|
158
|
+
link_type: link_type,
|
|
159
|
+
rel: Array[String],
|
|
160
|
+
attributes: Hash[String, String]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
type image_type = "data_uri" | "inline_svg" | "external" | "relative"
|
|
164
|
+
|
|
165
|
+
type image_metadata = {
|
|
166
|
+
src: String,
|
|
167
|
+
alt: String?,
|
|
168
|
+
title: String?,
|
|
169
|
+
dimensions: [Integer, Integer]?,
|
|
170
|
+
image_type: image_type,
|
|
171
|
+
attributes: Hash[String, String]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
type structured_data = {
|
|
175
|
+
data_type: "json_ld" | "microdata" | "rdfa",
|
|
176
|
+
raw_json: String,
|
|
177
|
+
schema_type: String?
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
type extended_metadata = {
|
|
181
|
+
document: document_metadata,
|
|
182
|
+
headers: Array[header_metadata],
|
|
183
|
+
links: Array[link_metadata],
|
|
184
|
+
images: Array[image_metadata],
|
|
185
|
+
structured_data: Array[structured_data]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
type table_data = {
|
|
189
|
+
cells: Array[Array[String]],
|
|
190
|
+
markdown: String,
|
|
191
|
+
is_header_row: Array[bool]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
type table_extraction_result = {
|
|
195
|
+
content: String,
|
|
196
|
+
metadata: extended_metadata?,
|
|
197
|
+
tables: Array[table_data]
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Native methods (implemented in Rust via Magnus/rb-sys)
|
|
201
|
+
# These are aliased from the Rust extension and available as both module and instance methods
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
def self.native_convert: (String html, conversion_options? options) -> String
|
|
205
|
+
def self.native_options: (conversion_options? options_hash) -> Options
|
|
206
|
+
def self.native_convert_with_options: (String html, Options options_handle) -> String
|
|
207
|
+
def self.native_convert_with_inline_images_handle: (
|
|
208
|
+
String html,
|
|
209
|
+
Options options_handle,
|
|
210
|
+
inline_image_config? image_config
|
|
211
|
+
) -> html_extraction
|
|
212
|
+
def self.native_convert_with_inline_images: (
|
|
213
|
+
String html,
|
|
214
|
+
conversion_options? options,
|
|
215
|
+
inline_image_config? image_config
|
|
216
|
+
) -> html_extraction
|
|
217
|
+
def self.native_convert_with_metadata_handle: (
|
|
218
|
+
String html,
|
|
219
|
+
Options options_handle,
|
|
220
|
+
metadata_config? metadata_config
|
|
221
|
+
) -> [String, extended_metadata]
|
|
222
|
+
def self.native_convert_with_metadata: (
|
|
223
|
+
String html,
|
|
224
|
+
conversion_options? options,
|
|
225
|
+
metadata_config? metadata_config
|
|
226
|
+
) -> [String, extended_metadata]
|
|
227
|
+
def self.native_convert_with_visitor: (
|
|
228
|
+
String html,
|
|
229
|
+
conversion_options? options,
|
|
230
|
+
visitor? visitor
|
|
231
|
+
) -> String
|
|
232
|
+
def self.native_convert_with_tables: (
|
|
233
|
+
String html,
|
|
234
|
+
conversion_options? options,
|
|
235
|
+
metadata_config? metadata_config
|
|
236
|
+
) -> table_extraction_result
|
|
237
|
+
|
|
238
|
+
def native_convert: (String html, conversion_options? options) -> String
|
|
239
|
+
def native_options: (conversion_options? options_hash) -> Options
|
|
240
|
+
def native_convert_with_options: (String html, Options options_handle) -> String
|
|
241
|
+
def native_convert_with_inline_images_handle: (
|
|
242
|
+
String html,
|
|
243
|
+
Options options_handle,
|
|
244
|
+
inline_image_config? image_config
|
|
245
|
+
) -> html_extraction
|
|
246
|
+
def native_convert_with_inline_images: (
|
|
247
|
+
String html,
|
|
248
|
+
conversion_options? options,
|
|
249
|
+
inline_image_config? image_config
|
|
250
|
+
) -> html_extraction
|
|
251
|
+
def native_convert_with_metadata_handle: (
|
|
252
|
+
String html,
|
|
253
|
+
Options options_handle,
|
|
254
|
+
metadata_config? metadata_config
|
|
255
|
+
) -> [String, extended_metadata]
|
|
256
|
+
def native_convert_with_metadata: (
|
|
257
|
+
String html,
|
|
258
|
+
conversion_options? options,
|
|
259
|
+
metadata_config? metadata_config
|
|
260
|
+
) -> [String, extended_metadata]
|
|
261
|
+
def native_convert_with_visitor: (
|
|
262
|
+
String html,
|
|
263
|
+
conversion_options? options,
|
|
264
|
+
visitor? visitor
|
|
265
|
+
) -> String
|
|
266
|
+
def native_convert_with_tables: (
|
|
267
|
+
String html,
|
|
268
|
+
conversion_options? options,
|
|
269
|
+
metadata_config? metadata_config
|
|
270
|
+
) -> table_extraction_result
|
|
271
|
+
|
|
272
|
+
# Visitor interface for customizing conversion behavior
|
|
273
|
+
type visitor = Object
|
|
274
|
+
|
|
275
|
+
public
|
|
276
|
+
|
|
277
|
+
# Convert HTML to Markdown with optional configuration and visitor
|
|
278
|
+
#
|
|
279
|
+
# The optional visitor parameter allows customization of conversion behavior for specific elements.
|
|
280
|
+
# When both options and visitor are provided, the visitor can override default conversions.
|
|
281
|
+
#
|
|
282
|
+
# Args:
|
|
283
|
+
# html: HTML string to convert
|
|
284
|
+
# options: Optional conversion configuration
|
|
285
|
+
# visitor: Optional visitor object for customizing conversion
|
|
286
|
+
#
|
|
287
|
+
# Returns:
|
|
288
|
+
# markdown: String - Converted markdown output
|
|
289
|
+
#
|
|
290
|
+
# Example:
|
|
291
|
+
# markdown = HtmlToMarkdown.convert(html, { wrap: true }, my_visitor)
|
|
292
|
+
def self.convert: (String html, ?conversion_options options, ?visitor visitor) -> String
|
|
293
|
+
|
|
294
|
+
# Create a reusable options handle for performance
|
|
295
|
+
def self.options: (?conversion_options options_hash) -> Options
|
|
296
|
+
|
|
297
|
+
# Convert HTML using a pre-built options handle
|
|
298
|
+
def self.convert_with_options: (String html, Options options_handle) -> String
|
|
299
|
+
def self.convert_with_inline_images_handle: (
|
|
300
|
+
String html,
|
|
301
|
+
Options options_handle,
|
|
302
|
+
?inline_image_config image_config
|
|
303
|
+
) -> html_extraction
|
|
304
|
+
|
|
305
|
+
# Convert HTML with inline image extraction
|
|
306
|
+
#
|
|
307
|
+
# Optionally accepts a visitor for customizing conversion behavior.
|
|
308
|
+
#
|
|
309
|
+
# Args:
|
|
310
|
+
# html: HTML string to convert
|
|
311
|
+
# options: Optional conversion configuration
|
|
312
|
+
# image_config: Optional inline image extraction configuration
|
|
313
|
+
# visitor: Optional visitor object for customizing conversion
|
|
314
|
+
#
|
|
315
|
+
# Returns:
|
|
316
|
+
# html_extraction: Hash containing markdown, inline_images array, and warnings array
|
|
317
|
+
#
|
|
318
|
+
# Example:
|
|
319
|
+
# result = HtmlToMarkdown.convert_with_inline_images(html, { wrap: true }, image_config, my_visitor)
|
|
320
|
+
def self.convert_with_inline_images: (
|
|
321
|
+
String html,
|
|
322
|
+
?conversion_options options,
|
|
323
|
+
?inline_image_config image_config,
|
|
324
|
+
?visitor visitor
|
|
325
|
+
) -> html_extraction
|
|
326
|
+
|
|
327
|
+
# Convert HTML to Markdown with a custom visitor (deprecated)
|
|
328
|
+
#
|
|
329
|
+
# DEPRECATED: Use convert() with the optional visitor parameter instead.
|
|
330
|
+
# This method is maintained for backward compatibility.
|
|
331
|
+
#
|
|
332
|
+
# All convert functions now accept optional visitors:
|
|
333
|
+
# - convert(html, options, visitor)
|
|
334
|
+
# - convert_with_inline_images(html, options, image_config, visitor)
|
|
335
|
+
# - convert_with_metadata(html, options, metadata_config, visitor)
|
|
336
|
+
#
|
|
337
|
+
# The visitor object can implement any of the following methods:
|
|
338
|
+
# - visit_element_start(ctx) -> visitor_result
|
|
339
|
+
# - visit_element_end(ctx, output) -> visitor_result
|
|
340
|
+
# - visit_text(ctx, text) -> visitor_result
|
|
341
|
+
# - visit_link(ctx, href, text, title) -> visitor_result
|
|
342
|
+
# - visit_image(ctx, src, alt, title) -> visitor_result
|
|
343
|
+
# - visit_heading(ctx, level, text, id) -> visitor_result
|
|
344
|
+
# - visit_code_block(ctx, lang, code) -> visitor_result
|
|
345
|
+
# - visit_code_inline(ctx, code) -> visitor_result
|
|
346
|
+
# - visit_list_item(ctx, ordered, marker, text) -> visitor_result
|
|
347
|
+
# - visit_list_start(ctx, ordered) -> visitor_result
|
|
348
|
+
# - visit_list_end(ctx, ordered, output) -> visitor_result
|
|
349
|
+
# - visit_table_start(ctx) -> visitor_result
|
|
350
|
+
# - visit_table_row(ctx, cells, is_header) -> visitor_result
|
|
351
|
+
# - visit_table_end(ctx, output) -> visitor_result
|
|
352
|
+
# - visit_blockquote(ctx, content, depth) -> visitor_result
|
|
353
|
+
# - visit_strong(ctx, text) -> visitor_result
|
|
354
|
+
# - visit_emphasis(ctx, text) -> visitor_result
|
|
355
|
+
# - visit_strikethrough(ctx, text) -> visitor_result
|
|
356
|
+
# - visit_underline(ctx, text) -> visitor_result
|
|
357
|
+
# - visit_subscript(ctx, text) -> visitor_result
|
|
358
|
+
# - visit_superscript(ctx, text) -> visitor_result
|
|
359
|
+
# - visit_mark(ctx, text) -> visitor_result
|
|
360
|
+
# - visit_line_break(ctx) -> visitor_result
|
|
361
|
+
# - visit_horizontal_rule(ctx) -> visitor_result
|
|
362
|
+
# - visit_custom_element(ctx, tag_name, html) -> visitor_result
|
|
363
|
+
# - visit_definition_list_start(ctx) -> visitor_result
|
|
364
|
+
# - visit_definition_term(ctx, text) -> visitor_result
|
|
365
|
+
# - visit_definition_description(ctx, text) -> visitor_result
|
|
366
|
+
# - visit_definition_list_end(ctx, output) -> visitor_result
|
|
367
|
+
# - visit_form(ctx, action, method) -> visitor_result
|
|
368
|
+
# - visit_input(ctx, input_type, name, value) -> visitor_result
|
|
369
|
+
# - visit_button(ctx, text) -> visitor_result
|
|
370
|
+
# - visit_audio(ctx, src) -> visitor_result
|
|
371
|
+
# - visit_video(ctx, src) -> visitor_result
|
|
372
|
+
# - visit_iframe(ctx, src) -> visitor_result
|
|
373
|
+
# - visit_details(ctx, open) -> visitor_result
|
|
374
|
+
# - visit_summary(ctx, text) -> visitor_result
|
|
375
|
+
# - visit_figure_start(ctx) -> visitor_result
|
|
376
|
+
# - visit_figcaption(ctx, text) -> visitor_result
|
|
377
|
+
# - visit_figure_end(ctx, output) -> visitor_result
|
|
378
|
+
#
|
|
379
|
+
# Each method should return a Hash with at least :type key:
|
|
380
|
+
# { type: :continue } - Continue with default behavior
|
|
381
|
+
# { type: :custom, output: "..." } - Replace with custom markdown
|
|
382
|
+
# { type: :skip } - Skip this element entirely
|
|
383
|
+
# { type: :preserve_html } - Keep original HTML
|
|
384
|
+
# { type: :error, message: "..." } - Stop conversion with error
|
|
385
|
+
#
|
|
386
|
+
# Args:
|
|
387
|
+
# html: HTML string to convert
|
|
388
|
+
# options: Optional conversion configuration
|
|
389
|
+
# visitor: Visitor object that responds to visitor callback methods
|
|
390
|
+
#
|
|
391
|
+
# Returns:
|
|
392
|
+
# markdown: String - Converted markdown output
|
|
393
|
+
#
|
|
394
|
+
# Example:
|
|
395
|
+
# class MyVisitor
|
|
396
|
+
# def visit_link(ctx, href, text, title = nil)
|
|
397
|
+
# { type: :custom, output: "[#{text}](#{href})" }
|
|
398
|
+
# end
|
|
399
|
+
# end
|
|
400
|
+
#
|
|
401
|
+
# HtmlToMarkdown.convert_with_visitor(html, visitor: MyVisitor.new)
|
|
402
|
+
def self.convert_with_visitor: (String html, ?conversion_options options, visitor: visitor) -> String
|
|
403
|
+
|
|
404
|
+
# Convert HTML to Markdown with metadata extraction
|
|
405
|
+
#
|
|
406
|
+
# Extracts comprehensive metadata (headers, links, images, structured data) during conversion.
|
|
407
|
+
# Optionally accepts a visitor for customizing conversion behavior.
|
|
408
|
+
#
|
|
409
|
+
# Args:
|
|
410
|
+
# html: HTML string to convert
|
|
411
|
+
# options: Optional conversion configuration
|
|
412
|
+
# metadata_config: Optional metadata extraction configuration
|
|
413
|
+
# visitor: Optional visitor object for customizing conversion
|
|
414
|
+
#
|
|
415
|
+
# Returns:
|
|
416
|
+
# Array containing:
|
|
417
|
+
# - [0] markdown: String - Converted markdown output
|
|
418
|
+
# - [1] metadata: Hash - Extracted metadata with document, headers, links, images, structured_data
|
|
419
|
+
#
|
|
420
|
+
# The metadata hash contains:
|
|
421
|
+
# - document: Document-level metadata (title, description, lang, etc.)
|
|
422
|
+
# - headers: List of header elements with hierarchy
|
|
423
|
+
# - links: List of extracted hyperlinks with classification
|
|
424
|
+
# - images: List of extracted images with metadata
|
|
425
|
+
# - structured_data: List of JSON-LD, Microdata, or RDFa blocks
|
|
426
|
+
#
|
|
427
|
+
# Example:
|
|
428
|
+
# html = '<html lang="en"><head><title>Test</title></head><body><h1>Hello</h1></body></html>'
|
|
429
|
+
# markdown, metadata = HtmlToMarkdown.convert_with_metadata(html)
|
|
430
|
+
# puts "Title: #{metadata['document']['title']}"
|
|
431
|
+
# puts "Headers: #{metadata['headers'].length}"
|
|
432
|
+
#
|
|
433
|
+
# Example with visitor:
|
|
434
|
+
# markdown, metadata = HtmlToMarkdown.convert_with_metadata(html, options, metadata_config, my_visitor)
|
|
435
|
+
def self.convert_with_metadata: (
|
|
436
|
+
String html,
|
|
437
|
+
?conversion_options options,
|
|
438
|
+
?metadata_config metadata_config,
|
|
439
|
+
?visitor visitor
|
|
440
|
+
) -> [String, extended_metadata]
|
|
441
|
+
def self.convert_with_metadata_handle: (
|
|
442
|
+
String html,
|
|
443
|
+
Options options_handle,
|
|
444
|
+
?metadata_config metadata_config
|
|
445
|
+
) -> [String, extended_metadata]
|
|
446
|
+
|
|
447
|
+
# Convert HTML and extract tables as structured data
|
|
448
|
+
#
|
|
449
|
+
# Args:
|
|
450
|
+
# html: HTML string to convert
|
|
451
|
+
# options: Optional conversion configuration
|
|
452
|
+
# metadata_config: Optional metadata extraction configuration
|
|
453
|
+
#
|
|
454
|
+
# Returns:
|
|
455
|
+
# table_extraction_result: Hash containing content, metadata, and tables array
|
|
456
|
+
#
|
|
457
|
+
# Example:
|
|
458
|
+
# result = HtmlToMarkdown.convert_with_tables(html)
|
|
459
|
+
# puts result[:tables].length
|
|
460
|
+
def self.convert_with_tables: (
|
|
461
|
+
String html,
|
|
462
|
+
?conversion_options options,
|
|
463
|
+
?metadata_config metadata_config
|
|
464
|
+
) -> table_extraction_result
|
|
465
|
+
|
|
466
|
+
# Instance method versions (created by module_function)
|
|
467
|
+
def convert: (String html, ?conversion_options options, ?visitor visitor) -> String
|
|
468
|
+
def options: (?conversion_options options_hash) -> Options
|
|
469
|
+
def convert_with_options: (String html, Options options_handle) -> String
|
|
470
|
+
def convert_with_inline_images_handle: (
|
|
471
|
+
String html,
|
|
472
|
+
Options options_handle,
|
|
473
|
+
?inline_image_config image_config
|
|
474
|
+
) -> html_extraction
|
|
475
|
+
def convert_with_inline_images: (
|
|
476
|
+
String html,
|
|
477
|
+
?conversion_options options,
|
|
478
|
+
?inline_image_config image_config,
|
|
479
|
+
?visitor visitor
|
|
480
|
+
) -> html_extraction
|
|
481
|
+
def convert_with_visitor: (String html, ?conversion_options options, visitor: visitor) -> String
|
|
482
|
+
def convert_with_metadata: (
|
|
483
|
+
String html,
|
|
484
|
+
?conversion_options options,
|
|
485
|
+
?metadata_config metadata_config,
|
|
486
|
+
?visitor visitor
|
|
487
|
+
) -> [String, extended_metadata]
|
|
488
|
+
def convert_with_metadata_handle: (
|
|
489
|
+
String html,
|
|
490
|
+
Options options_handle,
|
|
491
|
+
?metadata_config metadata_config
|
|
492
|
+
) -> [String, extended_metadata]
|
|
493
|
+
def convert_with_tables: (
|
|
494
|
+
String html,
|
|
495
|
+
?conversion_options options,
|
|
496
|
+
?metadata_config metadata_config
|
|
497
|
+
) -> table_extraction_result
|
|
498
|
+
end
|
data/sig/open3.rbs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Type signature for Open3 standard library
|
|
2
|
+
module Open3
|
|
3
|
+
# Execute command and capture stdout, stderr, and status
|
|
4
|
+
#
|
|
5
|
+
# @param cmd Command to execute
|
|
6
|
+
# @param args Command arguments
|
|
7
|
+
# @return Array containing stdout (String), stderr (String), and status (Process::Status)
|
|
8
|
+
def self.capture3: (
|
|
9
|
+
String cmd,
|
|
10
|
+
*String args
|
|
11
|
+
) -> [String, String, Process::Status]
|
|
12
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'html_to_markdown/cli_proxy'
|
|
5
|
+
require 'html_to_markdown/cli'
|
|
6
|
+
require 'stringio'
|
|
7
|
+
|
|
8
|
+
RSpec.describe HtmlToMarkdown::CLIProxy do
|
|
9
|
+
describe '.call' do
|
|
10
|
+
it 'executes the CLI binary' do
|
|
11
|
+
begin
|
|
12
|
+
binary = described_class.find_cli_binary
|
|
13
|
+
rescue HtmlToMarkdown::CLIProxy::MissingBinaryError
|
|
14
|
+
skip 'CLI binary not built'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
expect(binary).to be_file
|
|
18
|
+
|
|
19
|
+
output = described_class.call(['--version'])
|
|
20
|
+
expect(output).to include(HtmlToMarkdown::VERSION)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe HtmlToMarkdown::CLI do
|
|
25
|
+
it 'writes CLI output to stdout' do
|
|
26
|
+
begin
|
|
27
|
+
HtmlToMarkdown::CLIProxy.find_cli_binary
|
|
28
|
+
rescue HtmlToMarkdown::CLIProxy::MissingBinaryError
|
|
29
|
+
skip 'CLI binary not built'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
stdout = StringIO.new
|
|
33
|
+
stderr = StringIO.new
|
|
34
|
+
|
|
35
|
+
exit_code = described_class.run(['--version'], stdout: stdout, stderr: stderr)
|
|
36
|
+
|
|
37
|
+
expect(exit_code).to eq(0)
|
|
38
|
+
expect(stdout.string).to include(HtmlToMarkdown::VERSION)
|
|
39
|
+
expect(stderr.string).to be_empty
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe HtmlToMarkdown do
|
|
6
|
+
describe '.convert' do
|
|
7
|
+
it 'converts simple headings' do
|
|
8
|
+
expect(described_class.convert('<h1>Hello</h1>')).to eq("# Hello\n")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'accepts options hash' do
|
|
12
|
+
result = described_class.convert(
|
|
13
|
+
'<h1>Hello</h1>',
|
|
14
|
+
heading_style: :atx_closed,
|
|
15
|
+
default_title: true
|
|
16
|
+
)
|
|
17
|
+
expect(result).to include('Hello')
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe '.convert_with_inline_images' do
|
|
22
|
+
it 'returns inline images metadata' do
|
|
23
|
+
html = '<p><img src="data:image/png;base64,ZmFrZQ==" alt="fake"></p>'
|
|
24
|
+
extraction = described_class.convert_with_inline_images(html)
|
|
25
|
+
expect(extraction).to include(:markdown, :inline_images, :warnings)
|
|
26
|
+
expect(extraction[:inline_images].first[:description]).to eq('fake')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe '.options' do
|
|
31
|
+
it 'returns a reusable options handle' do
|
|
32
|
+
handle = described_class.options(heading_style: :atx_closed)
|
|
33
|
+
expect(handle).to be_a(HtmlToMarkdown::Options)
|
|
34
|
+
result = described_class.convert_with_options('<h1>Hello</h1>', handle)
|
|
35
|
+
expect(result).to include('# Hello #')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe 'panic handling' do
|
|
40
|
+
context 'when a Rust panic would occur' do
|
|
41
|
+
it 'catches panics in convert method' do
|
|
42
|
+
malformed_html = "#{'<' * 100_000}div#{'>' * 100_000}"
|
|
43
|
+
|
|
44
|
+
begin
|
|
45
|
+
result = described_class.convert(malformed_html)
|
|
46
|
+
expect(result).to be_a(String)
|
|
47
|
+
rescue RuntimeError => e
|
|
48
|
+
expect(e.message).to match(/html-to-markdown panic during conversion/)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'catches panics in convert_with_options method' do
|
|
53
|
+
malformed_html = "#{'<' * 100_000}div#{'>' * 100_000}"
|
|
54
|
+
handle = described_class.options(heading_style: :atx)
|
|
55
|
+
|
|
56
|
+
begin
|
|
57
|
+
result = described_class.convert_with_options(malformed_html, handle)
|
|
58
|
+
expect(result).to be_a(String)
|
|
59
|
+
rescue RuntimeError => e
|
|
60
|
+
expect(e.message).to match(/html-to-markdown panic during conversion/)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'catches panics in convert_with_inline_images method' do
|
|
65
|
+
malformed_html = "#{'<' * 100_000}div#{'>' * 100_000}"
|
|
66
|
+
|
|
67
|
+
begin
|
|
68
|
+
result = described_class.convert_with_inline_images(malformed_html)
|
|
69
|
+
expect(result).to be_a(Hash)
|
|
70
|
+
expect(result).to include(:markdown, :inline_images, :warnings)
|
|
71
|
+
rescue RuntimeError => e
|
|
72
|
+
expect(e.message).to match(/html-to-markdown panic during conversion/)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|