markdownr 0.5.9 → 0.5.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84700eb63ad3d72b16419ce7e3d1df58fd543e20b28db82197bd65336e1ae5d9
4
- data.tar.gz: 6043776aefb91dcc08d280fec07cd06b9f2f2a4061c380ec7a856d37838b312a
3
+ metadata.gz: 49c6809e18e2fe51a4e65446e185529edd529e61bb77e6eea79696b734ebdb8b
4
+ data.tar.gz: 9d533023489082e69413b4e718c27efa05076f1da863f25d9d68de26bd3ec090
5
5
  SHA512:
6
- metadata.gz: cdd4ccae742493f6bf4d8e8960e22b91e984ad2aa9954c89131a9bb98d22af6d5e6181177b7e29764b86f0267f42624c2df19ce8e7477e9bfe6c0620a1d41144
7
- data.tar.gz: c1984de4fb7097acf566bdb530d287e4e500ff151525773a6b258014d35582eb83269573c47882cb0fe232ab56fcde83c588fe50377b3e7782827a94fc895f95
6
+ metadata.gz: a2602173ee41f8d6034e410a8deba4fb11f5f3149e39c410dbc6366349dc93ba0bec519c6860e4970a7f14ed20dd5f8eeadf03d6452e821993e0d0450bebf19e
7
+ data.tar.gz: a5dc9a0bb156371fddcbdcdab626ee36b82079a175c56d967f7d4ed1f746d5f191bf709efbb7c0d4cb4f8419d072f51b1d92c8e39e90c8e643c220ebe57d4cbf
@@ -0,0 +1,5 @@
1
+ FROM ruby:3.4-slim
2
+ RUN apt-get update && apt-get install -y build-essential && gem install markdownr
3
+ WORKDIR /data
4
+ CMD ["markdownr", "--behind-proxy", "-b", "0.0.0.0", "/data"]
5
+
@@ -0,0 +1,12 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ IMAGE="$DOCKER_HUB_USERNAME/markdownr:latest"
5
+
6
+ echo "$DOCKER_HUB_TOKEN" | docker login -u "$DOCKER_HUB_USERNAME" --password-stdin
7
+
8
+ docker build --no-cache -f bin/Dockerfile.markdownr -t "$IMAGE" .
9
+
10
+ docker push "$IMAGE"
11
+
12
+ docker logout
data/bin/markdownr CHANGED
@@ -23,6 +23,14 @@ OptionParser.new do |opts|
23
23
  options[:allow_robots] = true
24
24
  end
25
25
 
26
+ opts.on("--behind-proxy", "Trust X-Forwarded-For for client IP (use when behind a reverse proxy like Caddy)") do
27
+ options[:behind_proxy] = true
28
+ end
29
+
30
+ opts.on("-i", "--index-file FILENAME", "Render this file instead of the directory listing when present (e.g. index.md, moc.md)") do |f|
31
+ options[:index_file] = f
32
+ end
33
+
26
34
  opts.on("--no-link-tooltips", "Disable preview tooltips for local markdown links") do
27
35
  options[:link_tooltips] = false
28
36
  end
@@ -46,8 +54,10 @@ unless File.directory?(dir)
46
54
  end
47
55
 
48
56
  MarkdownServer::App.set :root_dir, dir
57
+ MarkdownServer::App.set :behind_proxy, options[:behind_proxy] || false
49
58
  MarkdownServer::App.set :custom_title, options[:title]
50
59
  MarkdownServer::App.set :allow_robots, options[:allow_robots] || false
60
+ MarkdownServer::App.set :index_file, options[:index_file]
51
61
  MarkdownServer::App.set :link_tooltips, options.fetch(:link_tooltips, true)
52
62
  MarkdownServer::App.set :hard_wrap, options.fetch(:hard_wrap, true)
53
63
  MarkdownServer::App.set :port, options[:port]
@@ -9,6 +9,7 @@ require "cgi"
9
9
  require "pathname"
10
10
  require "set"
11
11
  require "net/http"
12
+ require "base64"
12
13
 
13
14
  module MarkdownServer
14
15
  class App < Sinatra::Base
@@ -20,11 +21,15 @@ module MarkdownServer
20
21
  set :root_dir, Dir.pwd
21
22
  set :custom_title, nil
22
23
  set :allow_robots, false
24
+ set :index_file, nil
23
25
  set :link_tooltips, true
24
26
  set :hard_wrap, true
25
27
  set :show_exceptions, false
26
28
  set :protection, false
27
29
  set :host_authorization, { permitted_hosts: [] }
30
+ set :behind_proxy, false
31
+ set :session_secret, ENV.fetch("MARKDOWNR_SESSION_SECRET", SecureRandom.hex(64))
32
+ set :sessions, key: "markdownr_session", same_site: :strict, httponly: true
28
33
  end
29
34
 
30
35
  helpers do
@@ -721,6 +726,23 @@ module MarkdownServer
721
726
  info_html + infl_html + usage_html + conc_html
722
727
  end
723
728
 
729
+ def inline_directory_html(dir_path, relative_dir)
730
+ entries = Dir.entries(dir_path).reject { |e| e.start_with?(".") || EXCLUDED.include?(e) }
731
+ items = entries.map do |name|
732
+ stat = File.stat(File.join(dir_path, name)) rescue next
733
+ is_dir = stat.directory?
734
+ href = "/browse/" + (relative_dir.empty? ? "" : relative_dir + "/") +
735
+ encode_path_component(name) + (is_dir ? "/" : "")
736
+ { name: name, is_dir: is_dir, href: href }
737
+ end.compact.sort_by { |i| [i[:is_dir] ? 0 : 1, i[:name].downcase] }
738
+
739
+ rows = items.map do |i|
740
+ %(<li><a href="#{h(i[:href])}"><span class="icon">#{icon_for(i[:name], i[:is_dir])}</span> ) +
741
+ %(#{h(i[:name])}#{i[:is_dir] ? "/" : ""}</a></li>)
742
+ end.join
743
+ %(<ul class="dir-listing">#{rows}</ul>)
744
+ end
745
+
724
746
  def compile_regexes(query)
725
747
  words = query.split(/\s+/).reject(&:empty?)
726
748
  return nil if words.empty?
@@ -729,6 +751,43 @@ module MarkdownServer
729
751
  raise RegexpError, e.message
730
752
  end
731
753
 
754
+ def client_ip
755
+ if settings.behind_proxy
756
+ fwd = env["HTTP_X_FORWARDED_FOR"].to_s.split(",").map(&:strip).first
757
+ fwd && !fwd.empty? ? fwd : env["REMOTE_ADDR"]
758
+ else
759
+ env["REMOTE_ADDR"]
760
+ end
761
+ end
762
+
763
+ def setup_config
764
+ @setup_config ||= begin
765
+ path = File.join(root_dir, ".setup.yml")
766
+ (File.exist?(path) && YAML.safe_load(File.read(path))) || {}
767
+ rescue StandardError
768
+ {}
769
+ end
770
+ end
771
+
772
+ def admin?
773
+ return true if session[:admin]
774
+
775
+ adm = setup_config["admin"]
776
+ return false unless adm.is_a?(Hash)
777
+
778
+ return true if adm["ip"].to_s.strip == client_ip
779
+
780
+ if adm["user"] && adm["pw"]
781
+ auth = request.env["HTTP_AUTHORIZATION"].to_s
782
+ if auth.start_with?("Basic ")
783
+ user, pw = Base64.decode64(auth[6..]).split(":", 2)
784
+ return true if user == adm["user"].to_s && pw == adm["pw"].to_s
785
+ end
786
+ end
787
+
788
+ false
789
+ end
790
+
732
791
  end
733
792
 
734
793
  # Routes
@@ -746,6 +805,41 @@ module MarkdownServer
746
805
  redirect "/browse/"
747
806
  end
748
807
 
808
+ get "/setup-info" do
809
+ @title = "Setup Info"
810
+ @client_ip = client_ip
811
+ @is_admin = admin?
812
+ @served_path = root_dir if @is_admin
813
+ @public_config = setup_config.reject { |k, _| k == "admin" }
814
+ erb :setup_info
815
+ end
816
+
817
+ get "/admin/login" do
818
+ @title = "Admin Login"
819
+ @error = session.delete(:login_error)
820
+ @return_to = params[:return_to]
821
+ erb :admin_login
822
+ end
823
+
824
+ post "/admin/login" do
825
+ adm = setup_config["admin"]
826
+ if adm.is_a?(Hash) &&
827
+ params[:username] == adm["user"].to_s &&
828
+ params[:password] == adm["pw"].to_s
829
+ session[:admin] = true
830
+ return_to = params[:return_to].to_s
831
+ redirect(return_to.start_with?("/") ? return_to : "/")
832
+ else
833
+ session[:login_error] = "Invalid username or password."
834
+ redirect "/admin/login"
835
+ end
836
+ end
837
+
838
+ get "/admin/logout" do
839
+ session.clear
840
+ redirect "/"
841
+ end
842
+
749
843
  get "/browse/?*" do
750
844
  requested = params["splat"].first.to_s
751
845
  requested = requested.chomp("/")
@@ -757,7 +851,14 @@ module MarkdownServer
757
851
  end
758
852
 
759
853
  if File.directory?(real_path)
760
- render_directory(real_path, requested)
854
+ index = settings.index_file
855
+ index_path = index && File.join(real_path, index)
856
+ if index_path && File.file?(index_path)
857
+ index_rel = requested.empty? ? index : "#{requested}/#{index}"
858
+ render_file(index_path, index_rel)
859
+ else
860
+ render_directory(real_path, requested)
861
+ end
761
862
  else
762
863
  render_file(real_path, requested)
763
864
  end
@@ -1022,6 +1123,11 @@ module MarkdownServer
1022
1123
  @meta, body = parse_frontmatter(content)
1023
1124
  @current_wiki_dir = File.dirname(real_path)
1024
1125
  @content = render_markdown(body)
1126
+ relative_dir = File.dirname(relative_path)
1127
+ relative_dir = "" if relative_dir == "."
1128
+ listing = -> { inline_directory_html(File.dirname(real_path), relative_dir) }
1129
+ @content.gsub!("<p>{{directory}}</p>") { listing.call }
1130
+ @content.gsub!("<p>{{admin-directory}}</p>") { admin? ? listing.call : "" }
1025
1131
  @toc = extract_toc(@content)
1026
1132
  @has_toc = @toc.length > 1
1027
1133
  erb :markdown
@@ -1,3 +1,3 @@
1
1
  module MarkdownServer
2
- VERSION = "0.5.9"
2
+ VERSION = "0.5.11"
3
3
  end
@@ -0,0 +1,24 @@
1
+ <div class="title-bar">
2
+ <h1 class="page-title">Admin Login</h1>
3
+ </div>
4
+
5
+ <div class="md-content" style="max-width: 360px;">
6
+ <% if @error %>
7
+ <p style="color: #b33; margin-bottom: 1rem;"><%= h(@error) %></p>
8
+ <% end %>
9
+
10
+ <form method="post" action="/admin/login">
11
+ <input type="hidden" name="return_to" value="<%= h(@return_to.to_s) %>">
12
+ <table class="meta-table" style="margin-bottom: 1rem;">
13
+ <tr>
14
+ <th class="blb-th">Username</th>
15
+ <td><input type="text" name="username" autofocus style="width:100%;padding:2px 4px;"></td>
16
+ </tr>
17
+ <tr>
18
+ <th class="blb-th">Password</th>
19
+ <td><input type="password" name="password" style="width:100%;padding:2px 4px;"></td>
20
+ </tr>
21
+ </table>
22
+ <button type="submit" style="padding:4px 16px;">Log in</button>
23
+ </form>
24
+ </div>
data/views/layout.erb CHANGED
@@ -1219,6 +1219,7 @@
1219
1219
  var touchStartX = 0;
1220
1220
  var touchStartY = 0;
1221
1221
  var touchCurrentX = 0;
1222
+ var touchStartedOnTable = false;
1222
1223
  var isDragging = false;
1223
1224
 
1224
1225
  function openDrawer() {
@@ -1277,6 +1278,7 @@
1277
1278
  touchStartY = e.touches[0].clientY;
1278
1279
  touchCurrentX = touchStartX;
1279
1280
  isDragging = false;
1281
+ touchStartedOnTable = !!e.target.closest('table');
1280
1282
  }, { passive: true });
1281
1283
 
1282
1284
  document.addEventListener('touchmove', function(e) {
@@ -1315,7 +1317,8 @@
1315
1317
  if (dy > 80) return; // Too vertical
1316
1318
 
1317
1319
  if (!isOpen && dx < -threshold) {
1318
- // Swipe left — open drawer
1320
+ // Swipe left — open drawer (not when swipe started on a table)
1321
+ if (touchStartedOnTable) return;
1319
1322
  openDrawer();
1320
1323
  } else if (isOpen && dx > threshold) {
1321
1324
  // Swipe right — close drawer
@@ -0,0 +1,28 @@
1
+ <div class="title-bar">
2
+ <h1 class="page-title">Setup Info</h1>
3
+ </div>
4
+
5
+ <div class="md-content">
6
+ <% if @is_admin %>
7
+ <p><strong>You are an administrator.</strong></p>
8
+ <% end %>
9
+
10
+ <table class="meta-table">
11
+ <tr><th>Your IP</th><td><%= h(@client_ip) %></td></tr>
12
+ <% if @served_path %>
13
+ <tr><th>Serving</th><td><%= h(@served_path) %></td></tr>
14
+ <% end %>
15
+ </table>
16
+
17
+ <% unless @public_config.empty? %>
18
+ <h3>Configuration</h3>
19
+ <table class="meta-table">
20
+ <% @public_config.each do |key, value| %>
21
+ <tr>
22
+ <th><%= h(key.to_s) %></th>
23
+ <td><%= h(value.to_s) %></td>
24
+ </tr>
25
+ <% end %>
26
+ </table>
27
+ <% end %>
28
+ </div>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: markdownr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.9
4
+ version: 0.5.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Dunn
@@ -100,15 +100,19 @@ executables:
100
100
  extensions: []
101
101
  extra_rdoc_files: []
102
102
  files:
103
+ - bin/Dockerfile.markdownr
104
+ - bin/build-and-push-to-docker
103
105
  - bin/markdownr
104
106
  - lib/markdown_server.rb
105
107
  - lib/markdown_server/app.rb
106
108
  - lib/markdown_server/version.rb
109
+ - views/admin_login.erb
107
110
  - views/directory.erb
108
111
  - views/layout.erb
109
112
  - views/markdown.erb
110
113
  - views/raw.erb
111
114
  - views/search.erb
115
+ - views/setup_info.erb
112
116
  homepage: https://github.com/brianmd/markdown-server
113
117
  licenses:
114
118
  - MIT