bridgetown-html-to-markdown 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 52d1a9575cba29d63575e50ed5295e36b5dc1d25f1e06bd24b2d322d1ae143e5
4
+ data.tar.gz: 258b703bc73609e771810e4abfb3bf74e1a4c39528226e4aba5107b1921bd92f
5
+ SHA512:
6
+ metadata.gz: 2c7c8169b978df01be6805a3e2cfa076e20307c35d7fcbed07e1fd1c943558822c438a23618ed6bc52be50d1554b230752d6ef7f909b9266f83bbaffa83f2cb7
7
+ data.tar.gz: e58159f11a5a382a4c01cd04340222c4b3b2cf7b3d0c2a0122d8ce687aadc858ef155db06ef14b2f77e751bf37ff055f60f458c5a97f0375131b31add4f5b872
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-09-07
9
+
10
+ ### Added
11
+ - Automated generation of `.md` Markdown files for every rendered `.html` file upon site build.
12
+ - Dual file generation for nested pages (e.g. `categories/index.md` and `categories.md`).
13
+ - HTML cleanup pipeline before conversion:
14
+ - Custom regex and string pattern replacements.
15
+ - Dialog stripping with configurable selector preservation (`exclude_dialogs`, `preserve_dialog_selectors`).
16
+ - SVG label extraction (`aria-label`, `alt`, `<title>`) and inline SVG stripping (`extract_svg_labels`).
17
+ - Spacing cleanup between inline tags and adjacent links (`clean_spacing`).
18
+ - File inclusion and exclusion filters (`include_files`, `exclude_files`).
19
+ - Explicit UTF-8 encoding for file operations and registration of `text/markdown; charset=utf-8` MIME type for Rack servers.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Marc Heiligers
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.
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # BridgetownHtmlToMarkdown
2
+
3
+ A [Bridgetown](https://www.bridgetownrb.com/) plugin that automatically generates a `.md` Markdown file for every rendered `.html` file during the site build process, powered by the fast Rust-backed [`html-to-markdown`](https://github.com/thewhodidthis/html-to-markdown) gem.
4
+
5
+ ## Features
6
+
7
+ - Converts rendered HTML pages to Markdown on `:site, :post_write`.
8
+ - Generates dual paths for nested pages (e.g., `categories/index.html` creates both `categories/index.md` and `categories.md`).
9
+ - Strips interactive dialogs/lightboxes while allowing selective preservation.
10
+ - Extracts SVG accessibility labels (`aria-label`, `alt`, `<title>`) and strips noisy inline SVG vector data.
11
+ - Cleans spacing around inline formatting and adjacent navigation links.
12
+ - Supports custom regex/string replacements prior to Markdown conversion.
13
+ - Registers `text/markdown; charset=utf-8` MIME type for Rack-based Bridgetown servers to prevent character encoding issues in browsers.
14
+
15
+ ## Installation
16
+
17
+ Add the gem to your Bridgetown application:
18
+
19
+ ```bash
20
+ bundle add bridgetown-html-to-markdown
21
+ ```
22
+
23
+ Or add it directly to your `Gemfile`:
24
+
25
+ ```ruby
26
+ gem "bridgetown-html-to-markdown", "~> 0.1.0"
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ In your Bridgetown site's `config/initializers.rb`:
32
+
33
+ ```ruby
34
+ init :"bridgetown-html-to-markdown"
35
+ ```
36
+
37
+ ### Configuration Options
38
+
39
+ You can pass options directly to `init`:
40
+
41
+ ```ruby
42
+ init :"bridgetown-html-to-markdown",
43
+ # Custom string or regex replacements before markdown conversion
44
+ replacements: {
45
+ /<section class="interactive-tool".*?<\/section>/m => "<p>Please see our catalog for details.</p>"
46
+ },
47
+
48
+ # Glob patterns of files to exclude (default includes 404.html, 500.html, search verification files)
49
+ exclude_files: ["google*.html", "404.html", "500.html", "BingSiteAuth.xml"],
50
+
51
+ # Glob patterns of files to include even if they match exclude_files
52
+ include_files: ["404.html"],
53
+
54
+ # Strip <dialog> elements (default: true)
55
+ exclude_dialogs: true,
56
+
57
+ # Preserve specific dialogs by CSS selector (id or class)
58
+ preserve_dialog_selectors: ["#contact-dialog"],
59
+
60
+ # Extract aria-label, alt, or <title> from SVGs and strip svg markup (default: true)
61
+ extract_svg_labels: true,
62
+
63
+ # Clean spacing between adjacent inline tags and links (default: true)
64
+ clean_spacing: true
65
+ ```
66
+
67
+ Alternatively, options can be set in `config/bridgetown.config.yml`:
68
+
69
+ ```yaml
70
+ html_to_markdown:
71
+ exclude_dialogs: true
72
+ clean_spacing: true
73
+ ```
74
+
75
+ ## Development
76
+
77
+ After checking out the repository, run `bin/setup` to install dependencies. Then run `bundle exec rake` to run tests and code style checks.
78
+
79
+ ## License
80
+
81
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "html_to_markdown"
4
+
5
+ module BridgetownHtmlToMarkdown
6
+ class Builder < Bridgetown::Builder
7
+ DEFAULT_EXCLUDED_FILES = [
8
+ "google*.html",
9
+ "404.html",
10
+ "500.html",
11
+ "yandex_*.html",
12
+ "BingSiteAuth.xml"
13
+ ].freeze
14
+
15
+ def initialize(name = nil, current_site = nil)
16
+ current_site ||= Bridgetown::Current.site || Struct.new(:config).new(Bridgetown::Configuration.new)
17
+ super(name, current_site)
18
+ end
19
+
20
+ def build
21
+ hook :site, :post_write do |site|
22
+ convert_site_html(site)
23
+ end
24
+ end
25
+
26
+ def convert_site_html(site, converter: HtmlToMarkdown)
27
+ options = html_to_markdown_options(site)
28
+ html_files = Dir.glob(File.join(site.dest, "**", "*.html"))
29
+
30
+ html_files.each do |html_path|
31
+ next if excluded_file?(html_path, dest_dir: site.dest, options: options)
32
+
33
+ convert_html_file(html_path, dest_dir: site.dest, converter: converter, options: options)
34
+ end
35
+ end
36
+
37
+ def convert_html_file(html_path, dest_dir: nil, converter: HtmlToMarkdown, options: {})
38
+ md_paths = destination_md_paths(html_path, dest_dir: dest_dir)
39
+ html_content = File.read(html_path, encoding: "UTF-8")
40
+ markdown = convert_string(html_content, converter: converter, options: options)
41
+
42
+ md_paths.each do |md_path|
43
+ File.write(md_path, markdown, encoding: "UTF-8")
44
+
45
+ if Bridgetown.respond_to?(:logger)
46
+ rel_path = dest_dir ? md_path.delete_prefix(dest_dir.to_s) : md_path
47
+ Bridgetown.logger.info("HtmlToMarkdown:", "Generated #{rel_path}")
48
+ end
49
+ end
50
+
51
+ md_paths
52
+ rescue StandardError => e
53
+ Bridgetown.logger.error("HtmlToMarkdown:", "Failed to convert #{html_path}: #{e.message}") if Bridgetown.respond_to?(:logger)
54
+ nil
55
+ end
56
+
57
+ def convert_string(html, converter: HtmlToMarkdown, options: {})
58
+ cleaned_html = clean_html(html, options: options)
59
+
60
+ result = if defined?(HtmlToMarkdown::ConversionOptions) && converter == HtmlToMarkdown
61
+ conv_opts = HtmlToMarkdown::ConversionOptions.new(strip_tags: ["svg"])
62
+ converter.convert(cleaned_html, conv_opts)
63
+ else
64
+ converter.convert(cleaned_html)
65
+ end
66
+
67
+ result.respond_to?(:content) ? result.content : result.to_s
68
+ end
69
+
70
+ def clean_html(html, options: {})
71
+ cleaned = html.to_s
72
+
73
+ # Custom replacements first
74
+ replacements = options[:replacements] || {}
75
+ replacements.each do |target, replacement|
76
+ pattern = if target.is_a?(Regexp)
77
+ target
78
+ elsif target.is_a?(String) && target =~ /\A\(\?([a-z-]*):(.*)\)\z/m
79
+ flags = 0
80
+ active_flags = Regexp.last_match(1).split("-").first || ""
81
+ flags |= Regexp::MULTILINE if active_flags.include?("m")
82
+ flags |= Regexp::IGNORECASE if active_flags.include?("i")
83
+ Regexp.new(Regexp.last_match(2), flags)
84
+ else
85
+ target
86
+ end
87
+
88
+ cleaned = cleaned.gsub(pattern, replacement)
89
+ end
90
+
91
+ # Clean dialogs if exclude_dialogs is enabled (default true)
92
+ if options.fetch(:exclude_dialogs, true)
93
+ preserve_selectors = Array(options[:preserve_dialog_selectors])
94
+ cleaned = clean_dialogs(cleaned, preserve_selectors: preserve_selectors)
95
+ end
96
+
97
+ # Extract or strip SVGs if enabled (default true)
98
+ cleaned = extract_or_strip_svgs(cleaned) if options.fetch(:extract_svg_labels, true)
99
+
100
+ # Clean spacing between adjacent tags if enabled (default true)
101
+ cleaned = clean_spacing(cleaned) if options.fetch(:clean_spacing, true)
102
+
103
+ cleaned
104
+ end
105
+
106
+ def clean_dialogs(html, preserve_selectors: [])
107
+ html.to_s.gsub(%r{<dialog\b([^>]*?)>(.*?)</dialog>}mi) do |match|
108
+ attrs = Regexp.last_match(1) || ""
109
+ preserved = preserve_selectors.any? do |sel|
110
+ if sel.start_with?("#")
111
+ attrs =~ /\bid\s*=\s*["']#{Regexp.escape(sel[1..])}["']/i
112
+ elsif sel.start_with?(".")
113
+ attrs =~ /\bclass\s*=\s*["'][^"']*\b#{Regexp.escape(sel[1..])}\b[^"']*["']/i
114
+ else
115
+ attrs.include?(sel)
116
+ end
117
+ end
118
+
119
+ preserved ? match : ""
120
+ end
121
+ end
122
+
123
+ def clean_spacing(html)
124
+ # Ensure whitespace between adjacent inline formatting tags
125
+ cleaned = html.to_s.gsub(%r{(</(?:strong|b|em|i|small)>)\s*(<(?:strong|b|em|i|small)>)}i, '\1 \2')
126
+ # Ensure line break between adjacent anchor tags in navigation/headers
127
+ cleaned.gsub(%r{(</a>)\s*(<a\b)}i, "\\1<br>\\2")
128
+ end
129
+
130
+ def excluded_file?(html_path, dest_dir: nil, options: {})
131
+ file_name = File.basename(html_path)
132
+ rel_path = dest_dir ? html_path.delete_prefix(dest_dir.to_s).delete_prefix("/") : file_name
133
+
134
+ include_patterns = Array(options[:include_files])
135
+ return false if include_patterns.any? { |pat| File.fnmatch?(pat, file_name) || File.fnmatch?(pat, rel_path) }
136
+
137
+ exclude_patterns = options.key?(:exclude_files) ? Array(options[:exclude_files]) : DEFAULT_EXCLUDED_FILES
138
+ exclude_patterns.any? { |pat| File.fnmatch?(pat, file_name) || File.fnmatch?(pat, rel_path) }
139
+ end
140
+
141
+ def html_to_markdown_options(target_site = nil)
142
+ target_site ||= site
143
+ opts = {}
144
+ if target_site.respond_to?(:config) && target_site.config
145
+ init_params = target_site.config.dig(:init_params, :"bridgetown-html-to-markdown") ||
146
+ target_site.config.dig(:init_params, "bridgetown-html-to-markdown")
147
+ opts = target_site.config[:html_to_markdown] || init_params || {}
148
+ end
149
+ opts.transform_keys(&:to_sym)
150
+ end
151
+
152
+ def extract_or_strip_svgs(html)
153
+ html.to_s.gsub(%r{<svg\b([^>]*?)(?:/>|>(.*?)</svg>)}mi) do
154
+ attrs = Regexp.last_match(1) || ""
155
+ inner = Regexp.last_match(2) || ""
156
+
157
+ if attrs =~ /\baria-hidden\s*=\s*["']true["']/i
158
+ ""
159
+ elsif attrs =~ /\b(?:aria-label|alt)\s*=\s*["']([^"']+)["']/i
160
+ " #{::Regexp.last_match(1).strip} "
161
+ elsif inner =~ %r{<title\b[^>]*>(.*?)</title>}mi
162
+ " #{::Regexp.last_match(1).strip} "
163
+ else
164
+ ""
165
+ end
166
+ end
167
+ end
168
+
169
+ def destination_md_paths(html_path, dest_dir: nil)
170
+ primary_path = destination_md_path(html_path)
171
+ paths = [primary_path]
172
+
173
+ if File.basename(html_path) == "index.html"
174
+ dir = File.dirname(html_path)
175
+ is_root = if dest_dir
176
+ File.expand_path(dir) == File.expand_path(dest_dir)
177
+ else
178
+ [".", "/", ""].include?(dir)
179
+ end
180
+
181
+ paths << "#{dir}.md" unless is_root
182
+ end
183
+
184
+ paths
185
+ end
186
+
187
+ def destination_md_path(html_path)
188
+ html_path.sub(/\.html\z/, ".md")
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BridgetownHtmlToMarkdown
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bridgetown"
4
+ require_relative "bridgetown-html-to-markdown/version"
5
+ require_relative "bridgetown-html-to-markdown/builder"
6
+
7
+ begin
8
+ require "rack/mime"
9
+ Rack::Mime::MIME_TYPES[".md"] = "text/markdown; charset=utf-8"
10
+ Rack::Mime::MIME_TYPES[".markdown"] = "text/markdown; charset=utf-8"
11
+ rescue LoadError
12
+ # Rack not present
13
+ end
14
+
15
+ module BridgetownHtmlToMarkdown
16
+ end
17
+
18
+ Bridgetown.initializer :"bridgetown-html-to-markdown" do |config, **options|
19
+ if defined?(Rack::Mime)
20
+ Rack::Mime::MIME_TYPES[".md"] = "text/markdown; charset=utf-8"
21
+ Rack::Mime::MIME_TYPES[".markdown"] = "text/markdown; charset=utf-8"
22
+ end
23
+
24
+ if config.respond_to?(:set)
25
+ config.set :html_to_markdown, options
26
+ elsif config.respond_to?(:[]=)
27
+ config[:html_to_markdown] = options
28
+ end
29
+ config.builder(BridgetownHtmlToMarkdown::Builder) if config.respond_to?(:builder)
30
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bridgetown-html-to-markdown
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Marc Heiligers
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bridgetown
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 2.0.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 2.0.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: html-to-markdown
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ description: A Bridgetown plugin that converts rendered HTML files into Markdown files
41
+ alongside each HTML file using the html-to-markdown converter.
42
+ email:
43
+ - marc@eternal.co.za
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGELOG.md
49
+ - LICENSE.txt
50
+ - README.md
51
+ - lib/bridgetown-html-to-markdown.rb
52
+ - lib/bridgetown-html-to-markdown/builder.rb
53
+ - lib/bridgetown-html-to-markdown/version.rb
54
+ homepage: https://github.com/FASCINATION-works/bridgetown-html-to-markdown
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ source_code_uri: https://github.com/FASCINATION-works/bridgetown-html-to-markdown
59
+ changelog_uri: https://github.com/FASCINATION-works/bridgetown-html-to-markdown/blob/main/CHANGELOG.md
60
+ bug_tracker_uri: https://github.com/FASCINATION-works/bridgetown-html-to-markdown/issues
61
+ rubygems_mfa_required: 'true'
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 3.2.0
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubygems_version: 4.0.6
77
+ specification_version: 4
78
+ summary: Bridgetown plugin to generate a .md file for every html file when building
79
+ test_files: []