Module: Abqari::Importers::Support::HtmlToMarkdown
- Defined in:
- lib/abqari/importers/support/html_to_markdown.rb
Overview
Thin wrapper over reverse_markdown. The gem is lazy-required
so a site that never runs bin/import doesn't pay the load
cost on every build.
Conversion tuning:
* `unknown_tags: :pass_through` keeps unrecognised HTML
elements verbatim rather than dropping them — preserves
things like Substack-specific `<figcaption>` wrappers or
Ghost's `<bookmark>` cards. The user can clean those up
manually after import; dropping them silently loses
content.
* `github_flavored: true` emits GFM table syntax and
fenced code blocks, matching how Abqari posts are
authored (kramdown + GFM extensions).
Pre-clean pass: strip platform chrome before conversion so the resulting markdown doesn't contain "Subscribe now" CTAs, share buttons, etc. The selectors are passed in by the caller — each importer ships its own list because each platform's chrome is different.
Class Method Summary collapse
-
.convert(html, strip_selectors: []) ⇒ Object
Convert an HTML string to Markdown.
-
.parse_selector(selector) ⇒ Object
Decompose
'div.subscription-widget'→['div', 'subscription-widget']and'aside'→['aside', nil]. -
.pre_clean(html, selectors) ⇒ Object
Remove DOM subtrees matching any of
selectors. -
.require_reverse_markdown! ⇒ Object
reverse_markdownis an OPTIONAL dependency, loaded only here.
Class Method Details
.convert(html, strip_selectors: []) ⇒ Object
Convert an HTML string to Markdown. strip_selectors: is
an array of CSS selectors whose matching elements are
removed from the DOM before conversion. Empty array = no
pre-clean.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/abqari/importers/support/html_to_markdown.rb', line 33 def self.convert(html, strip_selectors: []) return '' if html.to_s.empty? require_reverse_markdown! html = pre_clean(html, strip_selectors) unless strip_selectors.empty? md = ReverseMarkdown.convert( html, unknown_tags: :pass_through, github_flavored: true ) # `reverse_markdown` sometimes emits 3+ consecutive blank # lines around block elements. Collapse to a single blank. md.gsub(/\n{3,}/, "\n\n").strip + "\n" end |
.parse_selector(selector) ⇒ Object
Decompose 'div.subscription-widget' → ['div', 'subscription-widget']
and 'aside' → ['aside', nil]. Returns [nil, nil] for
patterns we don't support (id selectors, attribute selectors,
descendant combinators) — those are caller errors and we
ignore them rather than crashing the whole import.
117 118 119 120 121 122 |
# File 'lib/abqari/importers/support/html_to_markdown.rb', line 117 def self.parse_selector(selector) return [nil, nil] unless selector.is_a?(String) m = selector.match(/\A([a-z][a-z0-9]*)(?:\.([a-z0-9_-]+))?\z/i) return [nil, nil] unless m [m[1], m[2]] end |
.pre_clean(html, selectors) ⇒ Object
Remove DOM subtrees matching any of selectors. We use a
regex-based pass for the simple cases reverse_markdown
already handles internally (and to avoid pulling in a
full HTML parser like Nokogiri). Patterns target whole
opening-to-closing-tag spans with a class match. Good
enough for the platform-chrome blocks we know about —
callers should keep the selectors simple (div.foo,
aside.bar).
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/abqari/importers/support/html_to_markdown.rb', line 89 def self.pre_clean(html, selectors) selectors.each do |sel| tag, klass = parse_selector(sel) next unless tag if klass # Match <tag …class="… klass …" …>…</tag> with the # class anywhere in the class attribute. pattern = %r{ <#{Regexp.escape(tag)}\b [^>]*\bclass\s*=\s*["'][^"']*\b#{Regexp.escape(klass)}\b[^"']*["'] [^>]*> .*? </#{Regexp.escape(tag)}> }mxi else pattern = %r{<#{Regexp.escape(tag)}\b[^>]*>.*?</#{Regexp.escape(tag)}>}mi end html = html.gsub(pattern, '') end html end |
.require_reverse_markdown! ⇒ Object
reverse_markdown is an OPTIONAL dependency, loaded only here.
It isn't in the gemspec's runtime dependencies because it pulls
nokogiri, which pulls racc — a chain that costs every install a
native-extension gem for a one-time migration feature most
sites never run. On Ruby 3.3 that chain is worse than heavy:
3.3 bundles racc 1.7.3, Bundler resolves to 1.8.1, and 1.8.1
ships no precompiled Linux binary, so a toolchain-free image
(ruby:3.3-slim, most deploy bases) fails bundle install
outright — on the version the gemspec advertises as its floor.
So: importing asks for the gem at the moment it's needed, rather than every user paying for it up front.
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/abqari/importers/support/html_to_markdown.rb', line 63 def self.require_reverse_markdown! require 'reverse_markdown' rescue LoadError raise Abqari::UserError, <<~MSG Content importing needs the `reverse_markdown` gem, which Abqari doesn't install by default (it pulls in nokogiri, which most sites never need). Install it, then re-run this import: gem install reverse_markdown Or add it to your site's Gemfile: gem 'reverse_markdown', '~> 2.1' MSG end |