Skip to content

Instantly share code, notes, and snippets.

@pi314
Forked from Pysis868/gfm-render
Last active February 23, 2025 15:38
Show Gist options
  • Save pi314/27f6a2cf9343fd92ffadafbd4093b5a8 to your computer and use it in GitHub Desktop.
Save pi314/27f6a2cf9343fd92ffadafbd4093b5a8 to your computer and use it in GitHub Desktop.
Offline renderer for GitHub flavoured markdown
#!/usr/bin/env ruby
# Renders Markdown content offline by converting it to HTML using GitHub's own redcarpet renderer software, and their additional CSS.
# Does require a download of the additional GitHub CSS to a local cache.
# Replaces grip for preferred offline functionality: https://github.com/joeyespo/grip/issues/35
# Dependencies: redcarpet
require 'optparse'
options = {
cache: false,
replace: false
}
OptionParser.new do |opts|
opts.banner = 'Usage: [cat <file.md> |] gfm-render [options] <file.md>'
opts.on('-c', '--cache', 'Only checks to locally cache the additional GitHub CSS file, if it does not exist already.') do
options[:cache] = true
end
opts.on('-f', '--force', 'Sames as -r|--replace.') do
options[:replace] = true
end
opts.on('-h', '--help', "Displays this usage text.") do
puts opts
exit
end
opts.on('-r', '--replace', 'Forces a re-download of the additional GitHub CSS file.') do
options[:replace] = true
end
end.parse!(ARGV)
gh_markdown_css_filename = File.join(ENV["XDG_CACHE_HOME"] || "#{ENV['HOME']}/.cache", "gfm-render/github-markdown.css")
unless File.exist?(gh_markdown_css_filename) && !options[:replace]
require 'net/http'
require 'pathname'
CSS_URL = 'https://raw.githubusercontent.com/sindresorhus/github-markdown-css/main/github-markdown.css'
STDERR.puts "Downloading \"#{CSS_URL}\" to the cache at \"#{gh_markdown_css_filename}\"."
Pathname.new(gh_markdown_css_filename).dirname.mkpath
File.open(gh_markdown_css_filename, "w") do |css|
css.write(Net::HTTP.get(URI(CSS_URL)))
end
end
if options[:cache]
return
end
gh_markdown_css = File.read(gh_markdown_css_filename)
if !ARGV[0] || ARGV[0] == "-"
input = STDIN
else
input = File.open(ARGV[0])
end
if !ARGV[1]
if input == "<STDIN>"
output = STDOUT
else
output = File.open(File.basename(ARGV[0], File.extname(ARGV[0])) + ".html", "w")
end
elsif ARGV[1] == "-"
output = STDOUT
else
output = File.open(ARGV[1], "w")
end
File.join(ENV["XDG_CACHE_HOME"] || "#{ENV['HOME']}/.cache", "gfm-render")
require 'redcarpet'
require 'erb'
class RenderWithTaskLists < Redcarpet::Render::HTML
def list_item(text, list_type)
if text.start_with?("[x]", "[X]")
text[0..2] = %(<input type="checkbox" class="task-list-item-checkbox" disabled="" checked="checked">)
%(<li class="task-list-item">#{text}</li>)
elsif text.start_with?("[ ]")
text[0..2] = %(<input type="checkbox" class="task-list-item-checkbox" disabled="">)
%(<li class="task-list-item">#{text}</li>)
else
%(<li>#{text}</li>)
end
end
end
markdown_renderer = Redcarpet::Markdown.new(
RenderWithTaskLists.new(with_toc_data: true),
no_intra_emphasis: true,
autolink: true,
tables: true,
fenced_code_blocks: true,
lax_spacing: true,
)
html_output = markdown_renderer.render(input.read)
name = input.is_a?(File) && File.basename(input) || "<stdin>"
erb = ERB.new(DATA.read)
output.write(erb.result)
__END__
<html>
<head>
<meta charset="utf-8">
<title>Rendered <%= name %></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
<%= gh_markdown_css %>
@media (prefers-color-scheme: dark) {
body {
color: #c9d1d9;
background-color: #0d1117;
}
}
@media (prefers-color-scheme: light) {
body {
color: #24292f;
background-color: #ffffff;
}
}
.markdown-body {
box-sizing: border-box;
min-width: 200px;
max-width: 980px;
margin: 1 auto;
padding: 45px;
border: 1px solid #d0d7de;
border-radius: 6px;
}
@media (max-width: 767px) {
.markdown-body {
padding: 15px;
}
}
</style>
</head>
<body>
<article class="markdown-body">
<%= html_output %>
</article>
</body>
</html>
@pi314
Copy link
Author

pi314 commented Feb 14, 2025

Revision #7

  • Change shebang to #!/usr/bin/env ruby to use ruby installed by user.
  • Render normal list items without class="task-list-item" so their list-style-type: disc; would not be hidden

@pi314
Copy link
Author

pi314 commented Feb 14, 2025

Revision #8

  • Replace tabs in the HTML template with 8 characters for indent

@pi314
Copy link
Author

pi314 commented Feb 14, 2025

Revision #9

  • Set with_toc_data to true for section links to work

Mom I'm writing Ruby!!!

@pi314
Copy link
Author

pi314 commented Feb 23, 2025

Revision #10

  • Try to inference output file name from input file name (e.g. if only one argument README.md is provided, output file is set to README.html)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment