synful 1.0.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.
Files changed (7) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE +21 -0
  4. data/README.md +66 -0
  5. data/bin/synful +291 -0
  6. data/synful.gemspec +16 -0
  7. metadata +76 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e868e7d2521446e9bee4d813d8e15580b5ccfb58f71c0b7fad2459d212585a13
4
+ data.tar.gz: 1b2b05e76f6718625c94b77fa43e55c3d808a2c3c22ded2a83c9f4a24d8d99e9
5
+ SHA512:
6
+ metadata.gz: f7c6834c135e52ed6aa55f5ea9422a566957a3b046f02a94ff9a32391c34f927513801a866f8d2ec919fc7b721dddc7ef80d9e314b4977d0bb971ef54fed7287
7
+ data.tar.gz: 50d63b3c5b7af27adbc8b4e38765a8c60d183d1cc2c413e3e1edf5a4982184dad28f090e9518ddcc5b46c26641d1be042f11197d83eb0728d0f628207158f87b
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Steve Shreeve
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # synful
2
+
3
+ Ruby utility that shows syntax highlighted code in your browser
4
+
5
+ ## Goals
6
+
7
+ 1. Provide an easy way to view and print syntax highlighted source code
8
+ 2. Enable file selection or rejection in an intuitive way
9
+
10
+ ## Overview
11
+
12
+ Synful is a command line utility that will render syntax highlighted
13
+ source code in your browser. Simply indicate the file or directory
14
+ path names with optional file extensions to include or exclude. Synful
15
+ will generate a nice index and an entry for each file and it will
16
+ serve up the content and launch you web browser to view it. Once the
17
+ request has been served, the server will close down and exit. If you'd
18
+ like to keep the server running so that you can make changes and hit
19
+ refresh, there is an option to do that also. All syntax highlighting
20
+ is made possible by the amazing Rouge library.
21
+
22
+ ## Examples
23
+
24
+ ```
25
+ $ synful winr/bin/winr censive/lib/censive.rb
26
+ ```
27
+
28
+ A more complicated example is:
29
+
30
+ ```
31
+ synful -f -x spec,ru lib test /tmp/example.rb -- -test/boring-results.txt
32
+ ```
33
+
34
+ This will keep `synful` in the foreground (the `-f` option) and will
35
+ exclude all files with the `.spec` or `.ru` extensions. It will also
36
+ include files from the `lib/` and `test/` directories as well as the
37
+ `/tmp/example.rb` file. Even though the `test/boring-results.txt` file
38
+ should be rendered, we have disabled it via the `-` (minus sign or dash)
39
+ in front of it's filename. Note that to use this type of negation, we
40
+ need to precede it with a "double-dash" `--` to tell `synful` that we
41
+ are done with the normal command options.
42
+
43
+ ## Install
44
+
45
+ Install via `rubygems` with:
46
+
47
+ ```
48
+ gem install synful
49
+ ```
50
+
51
+ ## Options
52
+
53
+ ```
54
+ $ synful -h
55
+
56
+ usage: synful [options] <paths ...> [--] <-paths_to_skip ...>
57
+ -b, --bypass Bypass (skip over) top comments (begin with "#") in files
58
+ -f, --foreground Stay in the foreground, allows more requests
59
+ -h, --help Show help and command usage
60
+ -i, --include <exts> Comma list of extensions to include (incompatible with -x)
61
+ -x, --exclude <exts> Comma list of extensions to exclude (incompatible with -i)
62
+ ```
63
+
64
+ ## License
65
+
66
+ This software is licensed under terms of the MIT License.
data/bin/synful ADDED
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # ============================================================================
4
+ # synful - Ruby utility that shows syntax highlighted code in your browser
5
+ #
6
+ # Author: Steve Shreeve (steve.shreeve@gmail.com)
7
+ # Date: Feb 18, 2023
8
+ #
9
+ # Thanks to Julie Evans for creating the amazing rouge library!
10
+ # ============================================================================
11
+ # GOALS:
12
+ # 1. Provide an easy way to view and print syntax highlighted source code
13
+ # 2. Enable file selection or rejection in an intuitive way
14
+ # ============================================================================
15
+ # TODO:
16
+ # 1. Ensure there is a section for a file, even when it has a bad encoding
17
+ # ============================================================================
18
+
19
+ require "optparse"
20
+
21
+ OptionParser.new.instance_eval do
22
+ @banner = "usage: #{program_name} [options] <paths ...> [--] <-paths_to_skip ...>"
23
+
24
+ on '-b' , '--bypass' , 'Bypass (skip over) top comments (begin with "#") in files'
25
+ on '-f' , '--foreground', 'Stay in the foreground, allows more requests'
26
+ on '-h' , '--help' , 'Show help and command usage' do Kernel.abort to_s; end
27
+ on '-i <exts>' , '--include' , 'Comma list of extensions to include (incompatible with -x)', Array
28
+ on '-x <exts>' , '--exclude' , 'Comma list of extensions to exclude (incompatible with -i)', Array
29
+
30
+ self
31
+ end.parse!(into: opts={}) rescue abort($!.message)
32
+
33
+ skip, want = ARGV.map(&:dup).partition {|item| item.delete_prefix!("-") }; ARGV.clear
34
+ want << "." if want.empty?
35
+ skip = skip.empty? ? nil : Set.new(skip)
36
+
37
+ nuke = opts[:bypass ]
38
+ fore = opts[:foreground]
39
+ keep = opts[:include ] # will become a Set or nil
40
+ deny = opts[:exclude ] # will become a Set or nil
41
+
42
+ if keep && deny
43
+ abort "#{File.basename $0}: include and exclude cannot be used together"
44
+ elsif keep
45
+ keep = Set.new(keep.map(&:downcase))
46
+ deny = nil
47
+ else
48
+ keep = nil
49
+ deny = Set.new(((deny || []) + %w[
50
+ ico jpg jpeg otf png pdf gif css svg eot ttf woff woff2 o a dylib
51
+ gem gz zip sqlite3 DS_Store
52
+ ]).map(&:downcase))
53
+ end
54
+
55
+ $skip, $want, $fore, $keep, $deny, $nuke = [skip, want, fore, keep, deny, nuke]
56
+
57
+ # ============================================================================
58
+
59
+ require "rouge"
60
+
61
+ $show = Rouge::Formatters::HTMLLegacy.new(line_numbers: true)
62
+
63
+ Rouge::Lexers::Ruby.filenames "*.rbx", "*.ru", "*.imba"
64
+ Rouge::Lexers::Sass.filenames "*.styl"
65
+ Rouge::Lexers::ERB.filenames "*.eco"
66
+
67
+ # ============================================================================
68
+
69
+ require "find"
70
+ require "set"
71
+ require "sinatra"
72
+
73
+ $stderr = $stderr.dup.reopen File.new("/dev/null", "w") # turn off logger
74
+
75
+ set :server, "webrick"
76
+
77
+ get "*" do
78
+ skip, want, fore, keep, deny = [$skip, $want, $fore, $keep, $deny]
79
+
80
+ init = Time.now.to_f
81
+
82
+ @list = want.inject([]) do |list, path|
83
+ if !File.readable?(path)
84
+ warn "unreadable '#{path}'"
85
+ elsif File.directory?(path)
86
+ Find.find(path) do |path|
87
+ path.delete_prefix!("./") #!# TODO: is this working as expected?
88
+ if !File.readable?(path) || path == "."
89
+ next
90
+ elsif File.directory?(path)
91
+ Find.prune if File.basename(path).start_with?(".") or skip&.include?(path)
92
+ elsif File.file?(path)
93
+ next if skip&.include?(path)
94
+ type = path[/(?<=\.)[^.\/]+\z/].to_s.downcase
95
+ list << path if keep ? keep.include?(type) : !deny.include?(type)
96
+ else
97
+ warn "unknown '#{path}'"
98
+ end
99
+ end
100
+ elsif File.file?(path)
101
+ list << path # requested explicitly
102
+ end
103
+ list
104
+ end.sort.uniq
105
+
106
+ # show filenames
107
+ time = Time.now.strftime("%Y-%m-%d %H:%M:%S")
108
+ STDERR.puts "\n[#{time}]\n\n", @list.map {|item| " • #{item}"}
109
+
110
+ # generate content and generation time
111
+ $err = false
112
+ body = erb :page
113
+ wait = Time.now.to_f - init
114
+ STDERR.puts "\nTime elapsed: %.2f" % wait
115
+
116
+ # send response
117
+ $fore or Thread.new { sleep wait; exit! } # how can we know when the request is "done?"
118
+ headers "Connection" => "close"
119
+ body
120
+ end
121
+
122
+ Thread.new do
123
+ sleep 0.1 until settings.running?
124
+ fork or exec "open 'http://localhost:#{settings.port}/'"
125
+ end
126
+
127
+ __END__
128
+
129
+ @@ layout
130
+ <!DOCTYPE html>
131
+ <html lang="en">
132
+ <head>
133
+ <meta charset="utf-8">
134
+ <title>Synful</title>
135
+ <link rel="icon" href="data:,">
136
+ <style type="text/css">
137
+ html body { font-size: 14px; }
138
+ body { font-family: Verdana; padding: 5px; }
139
+ a { text-decoration: none; }
140
+ h4 a { color: #fff; font-weight: bold; margin: 0; border: none; }
141
+ h4 a:hover { background: #333; }
142
+ h4 { background: black; color: white; padding: 5px; margin: 0; border: none; }
143
+ li a { color: #000; }
144
+ li a:hover { text-decoration: underline; color: #666; }
145
+ pre { margin: 0; font-family: Consolas, Menlo, monospace; }
146
+
147
+ .rouge-table { padding: 0; border-collapse: collapse; }
148
+ .rouge-table td { padding: 0; vertical-align: top; }
149
+ .rouge-table td:first-child { text-align: right; }
150
+ .rouge-table pre { padding: 0.7em; }
151
+
152
+ .codehilite { margin: 0; background: #fff; white-space: pre; overflow-x: hidden; }
153
+ .codehilite .lineno { margin: 0; color: #ddd; background: #f4f4f4; }
154
+ .codehilite .hll { background-color: #ffffcc }
155
+ .codehilite .c { color: #888888 } /* Comment */
156
+ .codehilite .k { color: #008800; font-weight: bold } /* Keyword */
157
+ .codehilite .o { color: #333333 } /* Operator */
158
+ .codehilite .cm { color: #888888 } /* Comment.Multiline */
159
+ .codehilite .cp { color: #557799 } /* Comment.Preproc */
160
+ .codehilite .c1 { color: #888888 } /* Comment.Single */
161
+ .codehilite .cs { color: #cc0000; font-weight: bold } /* Comment.Special */
162
+ .codehilite .gd { color: #a00000 } /* Generic.Deleted */
163
+ .codehilite .ge { font-style: italic } /* Generic.Emph */
164
+ .codehilite .gr { color: #ff0000 } /* Generic.Error */
165
+ .codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
166
+ .codehilite .gi { color: #00a000 } /* Generic.Inserted */
167
+ .codehilite .go { color: #888888 } /* Generic.Output */
168
+ .codehilite .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */
169
+ .codehilite .gs { font-weight: bold } /* Generic.Strong */
170
+ .codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
171
+ .codehilite .gt { color: #0044dd } /* Generic.Traceback */
172
+ .codehilite .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
173
+ .codehilite .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
174
+ .codehilite .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
175
+ .codehilite .kp { color: #003388; font-weight: bold } /* Keyword.Pseudo */
176
+ .codehilite .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
177
+ .codehilite .kt { color: #333399; font-weight: bold } /* Keyword.Type */
178
+ .codehilite .m { color: #6600ee; font-weight: bold } /* Literal.Number */
179
+ .codehilite .n { color: #933 } /* name? */
180
+ .codehilite .s { } /* Literal.String */
181
+ .codehilite .na { color: #0000cc } /* Name.Attribute */
182
+ .codehilite .nb { color: #007020 } /* Name.Builtin */
183
+ .codehilite .nc { color: #bb0066; font-weight: bold } /* Name.Class */
184
+ .codehilite .no { color: #003366; font-weight: bold } /* Name.Constant */
185
+ .codehilite .nd { color: #555555; font-weight: bold } /* Name.Decorator */
186
+ .codehilite .ni { color: #880000; font-weight: bold } /* Name.Entity */
187
+ .codehilite .ne { color: #ff0000; font-weight: bold } /* Name.Exception */
188
+ .codehilite .nf { color: #0066bb; font-weight: bold } /* Name.Function */
189
+ .codehilite .nl { color: #997700; font-weight: bold } /* Name.Label */
190
+ .codehilite .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */
191
+ .codehilite .nt { color: #007700 } /* Name.Tag */
192
+ .codehilite .nv { color: #996633 } /* Name.Variable */
193
+ .codehilite .ow { color: #000000; font-weight: bold } /* Operator.Word */
194
+ .codehilite .w { color: #bbbbbb } /* Text.Whitespace */
195
+ .codehilite .mf { color: #6600ee; font-weight: bold } /* Literal.Number.Float */
196
+ .codehilite .mh { color: #005588; font-weight: bold } /* Literal.Number.Hex */
197
+ .codehilite .mi { color: #0000dd; font-weight: bold } /* Literal.Number.Integer */
198
+ .codehilite .mo { color: #4400ee; font-weight: bold } /* Literal.Number.Oct */
199
+ .codehilite .sb { } /* Literal.String.Backtick */
200
+ .codehilite .sc { color: #0044dd } /* Literal.String.Char */
201
+ .codehilite .sd { color: #dd4422 } /* Literal.String.Doc */
202
+ .codehilite .s2 { } /* Literal.String.Double */
203
+ .codehilite .se { color: #666666; font-weight: bold; } /* Literal.String.Escape */
204
+ .codehilite .sh { } /* Literal.String.Heredoc */
205
+ .codehilite .si { } /* Literal.String.Interpol */
206
+ .codehilite .sx { color: #dd2200; } /* Literal.String.Other */
207
+ .codehilite .sr { color: #000000; } /* Literal.String.Regex */
208
+ .codehilite .s1 { } /* Literal.String.Single */
209
+ .codehilite .ss { color: #aa6600 } /* Literal.String.Symbol */
210
+ .codehilite .bp { color: #007020 } /* Name.Builtin.Pseudo */
211
+ .codehilite .vc { color: #336699 } /* Name.Variable.Class */
212
+ .codehilite .vg { color: #dd7700; font-weight: bold } /* Name.Variable.Global */
213
+ .codehilite .vi { color: #3333bb } /* Name.Variable.Instance */
214
+ .codehilite .il { color: #0000dd; font-weight: bold } /* Literal.Number.Integer.Long */
215
+ </style>
216
+ </head>
217
+ <body>
218
+ <div class="top">
219
+
220
+ <%= yield %></div>
221
+ </body>
222
+ </html>
223
+
224
+ @@ page
225
+ <%= erb :toc if (count = @list.size) > 1 %><%
226
+ @list.each_with_index do |file, i|
227
+ @file = file
228
+ @prev = @list[(i - 1) % count]
229
+ @next = @list[(i + 1) % count]
230
+ @curr = count > 1 ? "#{i + 1}) " : "" %>
231
+ <%= erb :file %><% end %>
232
+
233
+ @@ toc
234
+ <h4><a name="top">Index</a></h4>
235
+
236
+ <ul><% @list.each do |file| %>
237
+ <li><a href="#<%= file %>"><%= file %></a></li><% end %>
238
+ </ul>
239
+
240
+ @@ file
241
+ <%
242
+ begin
243
+ data = File.read(@file)
244
+ data.gsub!(/\t/, " ") # replace tab with 2 spaces (make configurable?)
245
+ data.sub!(/\A.*?(?=^[^#\s])/m, "") if $nuke # nuke top comments (license, etc.)
246
+ type = Rouge::Lexer.guess(filename:@file, source:data)
247
+ rescue ArgumentError => e
248
+ STDERR.puts "\n" unless $err; $err = true
249
+ STDERR.puts "Invalid byte encoding in '#{@file}'"
250
+ return "Invalid... something here?"
251
+ rescue => e
252
+ if (data =~ /\A[^\n]+ruby/)
253
+ type = Rouge::Lexer.guess(filename:"foo.rb")
254
+ else
255
+ return "<p>Unable to process <b>#{@file.inspect}</b> (#{$!})\n\n"
256
+ end
257
+ end
258
+ if type.tag == "plaintext" || (type.tag == "html" && @file !~ /\.html?$/i)
259
+ mime = `file -b --mime-type "#{@file}"`.chomp
260
+ type = (mime =~ %r!\b(?:octet|image|font)\b!) ? nil : Rouge::Lexer.guess(mimetype:mime)
261
+ end
262
+ %>
263
+
264
+ <h4>
265
+ <a name="<%= @file %>"><%= @curr %><%= @file %>
266
+ (<%= data.count("\n") + 1 %> lines)
267
+ <%= "[" + type.tag + "]" if type %>
268
+ <%= File.mtime(@file).strftime("on %Y-%m-%d at %-I:%M %P") %>
269
+ </a>
270
+ <span style="float: right">
271
+ <a class="jump" href="#<%= @prev %>">&nbsp;&nbsp;&nbsp;&nbsp;&darr;&nbsp;&nbsp;&nbsp;&nbsp;</a>
272
+ <a class="jump" href="#<%= @next %>">&nbsp;&nbsp;&nbsp;&nbsp;&UpArrow;&nbsp;&nbsp;&nbsp;&nbsp;</a>
273
+ <a class="jump" href="#top">&nbsp;&nbsp;&nbsp;&nbsp;⥣&nbsp;&nbsp;&nbsp;&nbsp;</a>
274
+ </span>
275
+ </h4>
276
+
277
+ <%=
278
+ if type
279
+ begin
280
+ html = $show.format(type.lex(data))
281
+ html[html.rindex("\n"), 1] = ""
282
+ html
283
+ rescue
284
+ type = Rouge::Lexer.guess(filename:"foo.txt")
285
+ "<p>Unable to parse, showing plaintext instead...</p>" +
286
+ $show.format(type.lex(data))
287
+ end
288
+ else
289
+ "<p>(binary file)</p>"
290
+ end
291
+ %>
data/synful.gemspec ADDED
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "synful"
5
+ s.version = "1.0.0"
6
+ s.author = "Steve Shreeve"
7
+ s.email = "steve.shreeve@gmail.com"
8
+ s.summary =
9
+ s.description = "Ruby utility that shows syntax highlighted code in your browser"
10
+ s.homepage = "https://github.com/shreeve/synful"
11
+ s.license = "MIT"
12
+ s.files = `git ls-files`.split("\n") - %w[.gitignore]
13
+ s.executables = `cd bin && git ls-files .`.split("\n")
14
+ s.add_runtime_dependency "rouge", "~> 4.0"
15
+ s.add_runtime_dependency "sinatra", "~> 3.0"
16
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: synful
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Steve Shreeve
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-02-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rouge
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sinatra
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ description: Ruby utility that shows syntax highlighted code in your browser
42
+ email: steve.shreeve@gmail.com
43
+ executables:
44
+ - synful
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - Gemfile
49
+ - LICENSE
50
+ - README.md
51
+ - bin/synful
52
+ - synful.gemspec
53
+ homepage: https://github.com/shreeve/synful
54
+ licenses:
55
+ - MIT
56
+ metadata: {}
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubygems_version: 3.4.6
73
+ signing_key:
74
+ specification_version: 4
75
+ summary: Ruby utility that shows syntax highlighted code in your browser
76
+ test_files: []