'
14 | end
15 |
16 | it 'sets the title' do
17 | expect(subject).to include 'Octodown Preview'
18 | end
19 |
20 | it 'injects Github CSS' do
21 | css = File.read assets_dir('github.css')
22 | expect(subject).to include css
23 | end
24 |
25 | it 'injects higlighting CSS' do
26 | css = File.read assets_dir('highlight.css')
27 | expect(subject).to include css
28 | end
29 |
30 | it 'does not include jQuery lol' do
31 | expect(subject).not_to include 'jquery'
32 | end
33 |
34 | it 'includes correct websocket address in js' do
35 | expect(subject).to include 'new ReconnectingWebSocket("ws://localhost:8887"'
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 Ian Ker-Seymer
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/tasks/styles.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'open-uri'
4 | require 'fileutils'
5 | require 'tempfile'
6 |
7 | task :styles do
8 | begin
9 | FileUtils.mkdir 'tmp'
10 | download_deps
11 | compile_less
12 | ensure
13 | FileUtils.remove_dir 'tmp'
14 | end
15 | end
16 |
17 | def deps
18 | {
19 | 'markdown-preview-default' =>
20 | 'markdown-preview/master/styles/markdown-preview-default.less',
21 | 'syntax-variables' =>
22 | 'atom/master/static/variables/syntax-variables.less'
23 | }
24 | end
25 |
26 | def download_deps
27 | host = 'https://raw.githubusercontent.com/atom/'
28 |
29 | deps.each do |k, v|
30 | File.open("tmp/#{k}.less", 'w') do |out_file|
31 | open(host + v, 'r') do |in_file|
32 | out_file << in_file.read
33 | end
34 | end
35 | end
36 | end
37 |
38 | def compile_less
39 | tmp = 'tmp/github.css'
40 | out_file = 'assets/atom.css'
41 | `lessc tmp/markdown-preview-default.less > #{tmp}`
42 |
43 | File.open out_file, 'w' do |file|
44 | css = File.read(tmp).gsub(/markdown-preview/, 'markdown-body')
45 | file << css
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/lib/renderer/github_markdown_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | include Octodown::Renderer
4 | describe GithubMarkdown do
5 | subject { GithubMarkdown.render dummy_file }
6 |
7 | it 'create HTML from markdown file' do
8 | expect(subject).to include '
Hello world!
'
9 | end
10 |
11 | it 'highlights the code' do
12 | expect(subject).to include 'class="highlight'
13 | end
14 |
15 | it 'properly recognizes stdin' do
16 | allow(STDIN).to receive(:read).and_return 'Mic check... 1, 2, 3.'
17 |
18 | expect(GithubMarkdown.render(STDIN)).to include 'Mic check... 1, 2, 3.'
19 | end
20 |
21 | let :md_factory do
22 | lambda do |params|
23 | GithubMarkdown.render(
24 | dummy_file,
25 | opts.merge(gfm: params[:gfm])
26 | )
27 | end
28 | end
29 |
30 | it 'renders hard-wraps' do
31 | expect(md_factory[gfm: true]).to include ' '
32 | end
33 |
34 | describe 'local file linking' do
35 | it 'includes the local file from correct location' do
36 | dirname = "#{File.dirname dummy_path}/test.txt"
37 | expect(subject).to include 'some-file'
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/assets/highlight.css:
--------------------------------------------------------------------------------
1 | .highlight table td{padding:5px}.highlight table pre{margin:0}.highlight .cm{color:#998;font-style:italic}.highlight .cp{color:#999;font-weight:700}.highlight .c1{color:#998;font-style:italic}.highlight .cs{color:#999;font-weight:700;font-style:italic}.highlight .c,.highlight .cd{color:#998;font-style:italic}.highlight .err{color:#a61717;background-color:#e3d2d2}.highlight .gd{color:#000;background-color:#fdd}.highlight .ge{color:#000;font-style:italic}.highlight .gr{color:#a00}.highlight .gh{color:#999}.highlight .gi{color:#000;background-color:#dfd}.highlight .go{color:#888}.highlight .gp{color:#555}.highlight .gs{font-weight:700}.highlight .gu{color:#aaa}.highlight .gt{color:#a00}.highlight .kc,.highlight .kd,.highlight .kn,.highlight .kp,.highlight .kr{color:#000;font-weight:700}.highlight .kt{color:#458;font-weight:700}.highlight .k,.highlight .kv{color:#000;font-weight:700}.highlight .il,.highlight .m,.highlight .mb,.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo,.highlight .mx{color:#099}.highlight .s2,.highlight .sb,.highlight .sc,.highlight .sd,.highlight .se,.highlight .sh,.highlight .si,.highlight .sx{color:#d14}.highlight .sr{color:#009926}.highlight .s1{color:#d14}.highlight .ss{color:#990073}.highlight .s{color:#d14}.highlight .na{color:teal}.highlight .bp{color:#999}.highlight .nb{color:#0086B3}.highlight .nc{color:#458;font-weight:700}.highlight .no{color:teal}.highlight .nd{color:#3c5d5d;font-weight:700}.highlight .ni{color:purple}.highlight .ne,.highlight .nf,.highlight .nl{color:#900;font-weight:700}.highlight .nn{color:#555}.highlight .nt{color:navy}.highlight .nv,.highlight .vc,.highlight .vg,.highlight .vi{color:teal}.highlight .o,.highlight .ow{color:#000;font-weight:700}.highlight .w{color:#bbb}.highlight{background-color:#f8f8f8}
2 |
--------------------------------------------------------------------------------
/.rubocop_todo.yml:
--------------------------------------------------------------------------------
1 | # This configuration was generated by
2 | # `rubocop --auto-gen-config`
3 | # on 2020-02-18 19:49:30 -0500 using RuboCop version 0.80.0.
4 | # The point is for the user to remove these configuration records
5 | # one by one as the offenses are removed from the code base.
6 | # Note that changes in the inspected code, or installation of new
7 | # versions of RuboCop, may require this file to be generated again.
8 |
9 | # Offense count: 1
10 | # Configuration parameters: Include.
11 | # Include: **/*.gemspec
12 | Gemspec/RequiredRubyVersion:
13 | Exclude:
14 | - 'octodown.gemspec'
15 |
16 | # Offense count: 1
17 | # Configuration parameters: CountComments, ExcludedMethods.
18 | # ExcludedMethods: refine
19 | Metrics/BlockLength:
20 | Max: 29
21 |
22 | # Offense count: 1
23 | # Configuration parameters: CountComments, ExcludedMethods.
24 | Metrics/MethodLength:
25 | Max: 12
26 |
27 | # Offense count: 1
28 | # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
29 | # AllowedNames: io, id, to, by, on, in, at, ip, db, os, pp
30 | Naming/MethodParameterName:
31 | Exclude:
32 | - 'lib/octodown/renderer/server.rb'
33 |
34 | # Offense count: 1
35 | Security/Open:
36 | Exclude:
37 | - 'tasks/styles.rake'
38 |
39 | # Offense count: 2
40 | Style/CommentedKeyword:
41 | Exclude:
42 | - 'lib/octodown/renderer/server.rb'
43 |
44 | # Offense count: 3
45 | Style/MixinUsage:
46 | Exclude:
47 | - 'spec/lib/renderer/github_markdown_spec.rb'
48 | - 'spec/lib/renderer/html_spec.rb'
49 | - 'spec/lib/renderer/server_spec.rb'
50 |
51 | # Offense count: 1
52 | # Cop supports --auto-correct.
53 | # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
54 | # URISchemes: http, https
55 | Layout/LineLength:
56 | Max: 81
57 |
--------------------------------------------------------------------------------
/lib/octodown/renderer/github_markdown.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rouge'
4 | require 'html/pipeline'
5 | require 'task_list/filter'
6 | require 'octodown/renderer/renderable'
7 |
8 | module Octodown
9 | module Renderer
10 | class GithubMarkdown
11 | include HTML
12 | include Renderable
13 |
14 | attr_reader :options, :file, :logger
15 |
16 | def initialize(file, options = {})
17 | @file = file
18 | @options = options
19 | @logger = options[:logger]
20 | end
21 |
22 | def content
23 | if file == STDIN
24 | buffer = file.read
25 | else
26 | begin
27 | File.open(file.path, 'r') { |f| buffer = f.read }
28 | rescue Errno::ENOENT
29 | logger.warn 'Something went wrong when trying to open the file'
30 | end
31 | end
32 | pipeline.call(buffer ||= 'could not read changes')[:output].to_s
33 | end
34 |
35 | private
36 |
37 | def context
38 | {
39 | asset_root: 'https://github.githubassets.com/images/icons/',
40 | server: options[:presenter] == :server,
41 | original_document_root: document_root,
42 | scope: 'highlight',
43 | gfm: options[:gfm] || false
44 | }
45 | end
46 |
47 | def pipeline
48 | Pipeline.new [
49 | Pipeline::MarkdownFilter,
50 | Pipeline::SyntaxHighlightFilter,
51 | Support::RelativeRootFilter,
52 | Pipeline::ImageMaxWidthFilter,
53 | Pipeline::MentionFilter,
54 | Pipeline::EmojiFilter,
55 | TaskList::Filter
56 | ], context
57 | end
58 |
59 | def document_root
60 | case file
61 | when STDIN then Dir.pwd
62 | else File.dirname File.expand_path(file.path)
63 | end
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/lib/octodown/support/relative_root_filter.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'uri'
4 |
5 | module Octodown
6 | module Support
7 | class RelativeRootFilter < HTML::Pipeline::Filter
8 | attr_accessor :root, :server
9 |
10 | def call
11 | @root = context[:original_document_root]
12 | @server = context[:server]
13 |
14 | filter_images doc.search('img')
15 | filter_links doc.search('a[href]')
16 | end
17 |
18 | private
19 |
20 | def relative_path_from_document_root(root, src)
21 | server ? src : File.join(root, src).to_s
22 | end
23 |
24 | def http_uri?(src)
25 | parsed_uri = begin
26 | URI.parse src
27 | rescue URI::InvalidURIError
28 | src
29 | end
30 |
31 | parsed_uri.is_a? URI::HTTP
32 | end
33 |
34 | # TODO: These two methods are highly similar and can be refactored, but
35 | # I'm can't find the right abstraction at the moment that isn't a total
36 | # hack involving bizarre object references and mutation
37 |
38 | def filter_images(images)
39 | images.each do |img|
40 | src = img['src']
41 |
42 | next if src.nil?
43 |
44 | src.strip!
45 |
46 | unless http_uri? src
47 | path = relative_path_from_document_root root, src
48 | img['src'] = path
49 | end
50 | end
51 |
52 | doc
53 | end
54 |
55 | def filter_links(links)
56 | links.each do |a|
57 | src = a.attributes['href'].value
58 |
59 | next if src.nil?
60 |
61 | src.strip!
62 |
63 | unless http_uri? src
64 | path = relative_path_from_document_root root, src
65 | a.attributes['href'].value = path
66 | end
67 | end
68 |
69 | doc
70 | end
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/lib/octodown/renderer/html.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'erb'
4 | require 'octodown/renderer/renderable'
5 |
6 | module Octodown
7 | module Renderer
8 | class HTML
9 | include Octodown::Support
10 | include Renderable
11 |
12 | attr_reader :rendered_markdown, :filepath, :options, :logger
13 |
14 | def initialize(rendered_markdown, options = {})
15 | @logger = options[:logger]
16 | @rendered_markdown = rendered_markdown
17 | @options = options
18 | @filepath = File.join parent_dir, 'template', 'octodown.html.erb'
19 | end
20 |
21 | def content
22 | template_text = File.read filepath
23 | erb_template = ERB.new template_text
24 | erb_template.result binding
25 | end
26 |
27 | def title
28 | 'Octodown Preview'
29 | end
30 |
31 | def stylesheet
32 | stylesheet = "#{options[:style]}.css"
33 | inject_html_node_with_file_content assets_dir(stylesheet), :style
34 | end
35 |
36 | def highlight_stylesheet
37 | inject_html_node_with_file_content assets_dir('highlight.css'), :style
38 | end
39 |
40 | def host
41 | "ws://localhost:#{options[:port]}".dump
42 | end
43 |
44 | def present
45 | if options[:no_open]
46 | logger.warn('--no-open argument was used so no browser will be opened')
47 | else
48 | Launchy.open PersistentTempfile.create(content, :html).path
49 | end
50 | end
51 |
52 | private
53 |
54 | def inject_html_node_with_file_content(name, tag)
55 | "<#{tag}>#{File.read name}#{tag}>"
56 | end
57 |
58 | def assets_dir(*args)
59 | File.join Octodown.root, 'assets', args
60 | end
61 |
62 | def parent_dir
63 | current_file = File.dirname __FILE__
64 | File.expand_path '..', current_file
65 | end
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/octodown.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | lib = File.expand_path('lib', __dir__)
4 |
5 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6 | require 'octodown/version'
7 | Gem::Specification.new do |spec|
8 | spec.name = 'octodown'
9 | spec.version = Octodown::VERSION
10 | spec.authors = ['Ian Ker-Seymer']
11 | spec.email = ['i.kerseymer@gmail.com']
12 | spec.summary = 'GitHub Markdown straight from your shell.'
13 | spec.homepage = 'https://github.com/ianks/octodown'
14 | spec.license = 'MIT'
15 | spec.required_ruby_version = '>= 2.2.5'
16 |
17 | spec.files = Dir['{lib,assets,bin}/**/**'].reject { |f| f.end_with?('.gif') }
18 | spec.executables << 'octodown'
19 | spec.require_paths = ['lib']
20 |
21 | spec.add_dependency 'commonmarker', '~> 0.17'
22 | spec.add_dependency 'deckar01-task_list', '~> 2.0'
23 | spec.add_dependency 'faye-websocket', '~> 0.10'
24 | spec.add_dependency 'gemoji', '>= 2', '< 4'
25 | spec.add_dependency 'html-pipeline', '>= 2.8', '< 2.13'
26 | spec.add_dependency 'launchy', '~> 2.4', '>= 2.4.3'
27 | spec.add_dependency 'listen', '~> 3.7'
28 | spec.add_dependency 'puma', '>= 3.7', '< 5.0'
29 | spec.add_dependency 'rack', '~> 2.0'
30 | spec.add_dependency 'rouge', '~> 3.1'
31 | spec.add_dependency 'tty-prompt', '~> 0.16'
32 |
33 | spec.add_development_dependency 'bundler', '~> 2.0'
34 | spec.add_development_dependency 'octokit'
35 | spec.add_development_dependency 'rack-test', '~> 1.0'
36 | spec.add_development_dependency 'rake', '~> 13.0'
37 | spec.add_development_dependency 'rspec', '~> 3.3'
38 | spec.add_development_dependency 'rspec-retry'
39 | spec.add_development_dependency 'rubocop', '~> 0.55'
40 | end
41 |
--------------------------------------------------------------------------------
/bin/octodown:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'octodown'
5 | require 'octodown/support/file_chooser'
6 | require 'octodown/support/logger'
7 | require 'optparse'
8 | require 'launchy'
9 | # Default options
10 | options = {
11 | port: 8887,
12 | presenter: :server,
13 | style: :github,
14 | stdin: false,
15 | logger: Octodown::Support::Logger.build,
16 | no_open: false
17 | }
18 |
19 | OptionParser.new do |opts|
20 | opts.banner = 'Usage: octodown [options]'
21 |
22 | opts.on_tail '--version', 'Show version' do
23 | puts "octodown v#{Octodown::VERSION}"
24 | exit
25 | end
26 |
27 | opts.on(
28 | '-s', '--style [STYLE]', %i[github atom], 'Choose style (atom, github)'
29 | ) do |s|
30 | options[:style] = s
31 | end
32 |
33 | opts.on '--stdin', 'Read from STDIN' do
34 | options[:stdin] = true
35 | end
36 |
37 | opts.on '--quiet', 'Shhhh.. keep quiet' do
38 | options[:logger].level = Logger::FATAL
39 | end
40 |
41 | opts.on '--debug', 'Debug the gem' do
42 | ENV['LISTEN_GEM_DEBUGGING'] = '2'
43 | options[:logger].level = Logger::DEBUG
44 | end
45 |
46 | opts.on '-r', '--raw', 'Print raw HTML to STDOUT' do
47 | options[:presenter] = :raw
48 | end
49 |
50 | opts.on '-l', '--live-reload', 'Start a LiveReload server' do
51 | options[:presenter] = :server
52 | end
53 |
54 | opts.on '-h', '--html', 'Render to HTML' do
55 | options[:presenter] = :html
56 | end
57 |
58 | opts.on(
59 | '-P', '--port [PORT]', "LiveReload port (default: #{options[:port]})"
60 | ) do |port|
61 | options[:presenter] = :server
62 | options[:port] = port.to_i
63 | end
64 |
65 | opts.on_tail '-h', '--help', 'Show this message' do
66 | puts opts
67 | exit
68 | end
69 |
70 | opts.on '--no-open', 'Do not open the browser' do
71 | options[:no_open] = true
72 | end
73 | end.parse!
74 |
75 | options[:file] = if ARGF.file == STDIN && options[:stdin]
76 | STDIN
77 | elsif ARGF.file != STDIN
78 | ARGF.file
79 | else
80 | Octodown::FileChooser.new(logger: options[:logger]).call
81 | end
82 |
83 | Octodown.call options
84 |
--------------------------------------------------------------------------------
/spec/lib/renderer/server_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'faye/websocket'
4 |
5 | include Octodown::Renderer
6 | class Dud < StandardError; end
7 |
8 | describe Server do
9 | let(:content) { File.read dummy_path }
10 | let(:app) { subject.app }
11 | let(:options) { opts.merge(file: File.new(dummy_path)) }
12 |
13 | subject do
14 | Server.new content, options
15 | end
16 |
17 | after(:each) do
18 | options[:file].close
19 | end
20 |
21 | before do
22 | allow_any_instance_of(Server).to receive(:maybe_launch_browser)
23 | .and_return true
24 | end
25 |
26 | it 'serves a Rack app' do
27 | expect(Rack::Handler::Puma).to receive(:run)
28 |
29 | subject.present
30 | end
31 |
32 | it 'register the listener' do
33 | allow(Rack::Handler::Puma).to receive(:run).and_return true
34 | expect(Octodown::Support::Services::Riposter).to receive :call
35 |
36 | subject.present
37 | end
38 |
39 | it 'generates HTML for each request' do
40 | get '/'
41 |
42 | expect(last_response).to be_ok
43 | expect(last_response.body).to include '
Hello world!
'
44 | end
45 |
46 | it 'regenerates HTML for each request' do
47 | get '/'
48 |
49 | expect(last_response.body).to include '