markdownr 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/lib/markdown_server/app.rb +175 -0
- data/lib/markdown_server/version.rb +1 -1
- data/views/directory.erb +7 -1
- data/views/layout.erb +158 -0
- data/views/markdown.erb +8 -1
- data/views/raw.erb +8 -1
- data/views/search.erb +47 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb86e2f05b5c71f9365fba3fdcf497f1c53439e7b395d13efb674eb36947f3dc
|
|
4
|
+
data.tar.gz: 3c2d3da0044862746e47d76c386712b8ab8a0f6c1a7c60514e511953e2af5542
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1aa636326b55bbff250fa04e13f5befadda9791fbabdf175c7c774886d8df793ee4e1ecc93d59a3bc01428eaa26e2c104ab6b831dd0e1323666e970fd64be634
|
|
7
|
+
data.tar.gz: 1ad0d23592aa3874b519cfa3afbfc310bc0fd12c90c223a54bf4a530e1e7ca7da77d1c00a548210fde5a043ffad2f52460f40a2989cb30b2f900db666ad3fac1
|
data/lib/markdown_server/app.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "json"
|
|
|
7
7
|
require "uri"
|
|
8
8
|
require "cgi"
|
|
9
9
|
require "pathname"
|
|
10
|
+
require "set"
|
|
10
11
|
|
|
11
12
|
module MarkdownServer
|
|
12
13
|
class App < Sinatra::Base
|
|
@@ -192,6 +193,150 @@ module MarkdownServer
|
|
|
192
193
|
return settings.custom_title if settings.respond_to?(:custom_title) && settings.custom_title
|
|
193
194
|
File.basename(root_dir).gsub(/[-_]/, " ").gsub(/\b\w/, &:upcase)
|
|
194
195
|
end
|
|
196
|
+
|
|
197
|
+
def search_form_path(relative_path)
|
|
198
|
+
"/search/" + relative_path.split("/").map { |p| encode_path_component(p) }.join("/")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def parent_dir_path(relative_path)
|
|
202
|
+
parts = relative_path.split("/")
|
|
203
|
+
parts.length > 1 ? parts[0..-2].join("/") : ""
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
BINARY_EXTENSIONS = %w[
|
|
207
|
+
.png .jpg .jpeg .gif .bmp .ico .svg .webp
|
|
208
|
+
.pdf .epub .mobi
|
|
209
|
+
.zip .gz .tar .bz2 .7z .rar
|
|
210
|
+
.exe .dll .so .dylib .o
|
|
211
|
+
.mp3 .mp4 .avi .mov .wav .flac .ogg
|
|
212
|
+
.woff .woff2 .ttf .eot .otf
|
|
213
|
+
.pyc .class .beam
|
|
214
|
+
.sqlite .db
|
|
215
|
+
].freeze
|
|
216
|
+
|
|
217
|
+
MAX_SEARCH_FILES = 100
|
|
218
|
+
MAX_FILE_READ_BYTES = 512_000 # 500KB
|
|
219
|
+
CONTEXT_LINES = 2 # lines before/after match to send
|
|
220
|
+
MAX_LINE_DISPLAY = 200 # chars before truncating a line
|
|
221
|
+
|
|
222
|
+
def search_files(dir_path, regexes)
|
|
223
|
+
results = []
|
|
224
|
+
base = File.realpath(root_dir)
|
|
225
|
+
|
|
226
|
+
catch(:search_limit) do
|
|
227
|
+
walk_directory(dir_path) do |file_path|
|
|
228
|
+
throw :search_limit if results.length >= MAX_SEARCH_FILES
|
|
229
|
+
|
|
230
|
+
content = File.binread(file_path, MAX_FILE_READ_BYTES) rescue next
|
|
231
|
+
content.force_encoding("utf-8")
|
|
232
|
+
next unless content.valid_encoding?
|
|
233
|
+
|
|
234
|
+
# All regexes must match somewhere in the file
|
|
235
|
+
next unless regexes.all? { |re| re.match?(content) }
|
|
236
|
+
|
|
237
|
+
relative = file_path.sub("#{base}/", "")
|
|
238
|
+
lines = content.lines
|
|
239
|
+
matches = collect_matching_lines(lines, regexes)
|
|
240
|
+
|
|
241
|
+
results << { path: relative, matches: matches }
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
results
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def walk_directory(dir_path, &block)
|
|
249
|
+
Dir.entries(dir_path).sort.each do |entry|
|
|
250
|
+
next if entry.start_with?(".") || EXCLUDED.include?(entry)
|
|
251
|
+
full = File.join(dir_path, entry)
|
|
252
|
+
|
|
253
|
+
if File.directory?(full)
|
|
254
|
+
walk_directory(full, &block)
|
|
255
|
+
elsif File.file?(full)
|
|
256
|
+
ext = File.extname(entry).downcase
|
|
257
|
+
next if BINARY_EXTENSIONS.include?(ext)
|
|
258
|
+
block.call(full)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def collect_matching_lines(lines, regexes)
|
|
264
|
+
match_indices = Set.new
|
|
265
|
+
lines.each_with_index do |line, i|
|
|
266
|
+
if regexes.any? { |re| re.match?(line) }
|
|
267
|
+
match_indices << i
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Build context groups
|
|
272
|
+
groups = []
|
|
273
|
+
sorted = match_indices.sort
|
|
274
|
+
|
|
275
|
+
sorted.each do |idx|
|
|
276
|
+
range_start = [idx - CONTEXT_LINES, 0].max
|
|
277
|
+
range_end = [idx + CONTEXT_LINES, lines.length - 1].min
|
|
278
|
+
|
|
279
|
+
if groups.last && range_start <= groups.last[:end] + 1
|
|
280
|
+
groups.last[:end] = range_end
|
|
281
|
+
else
|
|
282
|
+
groups << { start: range_start, end: range_end }
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
groups.map do |g|
|
|
287
|
+
context_lines = (g[:start]..g[:end]).map do |i|
|
|
288
|
+
distance = match_indices.include?(i) ? 0 : match_indices.map { |m| (m - i).abs }.min
|
|
289
|
+
{ number: i + 1, text: lines[i].to_s.chomp, distance: distance }
|
|
290
|
+
end
|
|
291
|
+
{ lines: context_lines }
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def highlight_search_line(text, regexes, is_match)
|
|
296
|
+
# Build a combined regex with non-greedy quantifiers for shorter highlights
|
|
297
|
+
combined = Regexp.union(regexes.map { |r|
|
|
298
|
+
Regexp.new(r.source.gsub(/(?<!\\)([*+}])(?!\?)/, '\1?'), r.options)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
# Truncate long lines, centering around the first match
|
|
302
|
+
prefix_trunc = false
|
|
303
|
+
suffix_trunc = false
|
|
304
|
+
if text.length > MAX_LINE_DISPLAY
|
|
305
|
+
if is_match && (m = combined.match(text))
|
|
306
|
+
center = m.begin(0) + m[0].length / 2
|
|
307
|
+
half = MAX_LINE_DISPLAY / 2
|
|
308
|
+
start = [[center - half, 0].max, [text.length - MAX_LINE_DISPLAY, 0].max].min
|
|
309
|
+
else
|
|
310
|
+
start = 0
|
|
311
|
+
end
|
|
312
|
+
prefix_trunc = start > 0
|
|
313
|
+
suffix_trunc = (start + MAX_LINE_DISPLAY) < text.length
|
|
314
|
+
text = text[start, MAX_LINE_DISPLAY]
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
html = ""
|
|
318
|
+
html << '<span class="truncated">...</span>' if prefix_trunc
|
|
319
|
+
if is_match
|
|
320
|
+
pieces = text.split(combined)
|
|
321
|
+
matches = text.scan(combined)
|
|
322
|
+
pieces.each_with_index do |piece, i|
|
|
323
|
+
html << h(piece)
|
|
324
|
+
html << %(<span class="highlight-match">#{h(matches[i])}</span>) if matches[i]
|
|
325
|
+
end
|
|
326
|
+
else
|
|
327
|
+
html << h(text)
|
|
328
|
+
end
|
|
329
|
+
html << '<span class="truncated">...</span>' if suffix_trunc
|
|
330
|
+
html
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def compile_regexes(query)
|
|
334
|
+
words = query.split(/\s+/).reject(&:empty?)
|
|
335
|
+
return nil if words.empty?
|
|
336
|
+
words.map { |w| Regexp.new(w, Regexp::IGNORECASE) }
|
|
337
|
+
rescue RegexpError => e
|
|
338
|
+
raise RegexpError, e.message
|
|
339
|
+
end
|
|
195
340
|
end
|
|
196
341
|
|
|
197
342
|
# Routes
|
|
@@ -225,6 +370,36 @@ module MarkdownServer
|
|
|
225
370
|
send_file real_path, disposition: "attachment"
|
|
226
371
|
end
|
|
227
372
|
|
|
373
|
+
get "/search/?*" do
|
|
374
|
+
requested = params["splat"].first.to_s.chomp("/")
|
|
375
|
+
@query = params[:q].to_s.strip
|
|
376
|
+
|
|
377
|
+
if requested.empty?
|
|
378
|
+
search_dir = File.realpath(root_dir)
|
|
379
|
+
else
|
|
380
|
+
search_dir = safe_path(requested)
|
|
381
|
+
halt 404 unless File.directory?(search_dir)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
@path = requested
|
|
385
|
+
@crumbs = breadcrumbs(requested)
|
|
386
|
+
@title = requested.empty? ? dir_title : File.basename(requested)
|
|
387
|
+
@results = []
|
|
388
|
+
@regexes = nil
|
|
389
|
+
@error = nil
|
|
390
|
+
|
|
391
|
+
unless @query.empty?
|
|
392
|
+
begin
|
|
393
|
+
@regexes = compile_regexes(@query)
|
|
394
|
+
@results = search_files(search_dir, @regexes) if @regexes
|
|
395
|
+
rescue RegexpError => e
|
|
396
|
+
@error = "Invalid regex: #{e.message}"
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
erb :search
|
|
401
|
+
end
|
|
402
|
+
|
|
228
403
|
private
|
|
229
404
|
|
|
230
405
|
def render_directory(real_path, relative_path)
|
data/views/directory.erb
CHANGED
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
})();
|
|
16
16
|
</script>
|
|
17
17
|
|
|
18
|
-
<
|
|
18
|
+
<div class="title-bar">
|
|
19
|
+
<h1 class="page-title"><%= h(@title) %></h1>
|
|
20
|
+
<form class="search-form" action="<%= search_form_path(@path) %>" method="get">
|
|
21
|
+
<input type="text" name="q" placeholder="Search files..." value="<%= h(params[:q].to_s) %>">
|
|
22
|
+
<button type="submit">Search</button>
|
|
23
|
+
</form>
|
|
24
|
+
</div>
|
|
19
25
|
|
|
20
26
|
<div class="dir-header">
|
|
21
27
|
<div class="dir-count">
|
data/views/layout.erb
CHANGED
|
@@ -416,12 +416,170 @@
|
|
|
416
416
|
.toc-mobile .toc-h3 { padding-left: 0.8rem; }
|
|
417
417
|
.toc-mobile .toc-h4 { padding-left: 1.4rem; }
|
|
418
418
|
|
|
419
|
+
/* Title bar with search */
|
|
420
|
+
.title-bar {
|
|
421
|
+
display: flex;
|
|
422
|
+
align-items: baseline;
|
|
423
|
+
gap: 1rem;
|
|
424
|
+
margin: 0 0 1.2rem;
|
|
425
|
+
border-bottom: 2px solid #d4b96a;
|
|
426
|
+
padding-bottom: 0.5rem;
|
|
427
|
+
}
|
|
428
|
+
.title-bar h1.page-title {
|
|
429
|
+
border-bottom: none;
|
|
430
|
+
padding-bottom: 0;
|
|
431
|
+
margin: 0;
|
|
432
|
+
flex: 1;
|
|
433
|
+
min-width: 0;
|
|
434
|
+
}
|
|
435
|
+
.search-form {
|
|
436
|
+
display: flex;
|
|
437
|
+
flex-shrink: 0;
|
|
438
|
+
}
|
|
439
|
+
.search-form input[type="text"] {
|
|
440
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
441
|
+
font-size: 0.82rem;
|
|
442
|
+
padding: 0.3rem 0.6rem;
|
|
443
|
+
border: 1px solid #d4b96a;
|
|
444
|
+
border-radius: 4px 0 0 4px;
|
|
445
|
+
background: #fdfcf9;
|
|
446
|
+
color: #2c2c2c;
|
|
447
|
+
width: 180px;
|
|
448
|
+
outline: none;
|
|
449
|
+
transition: border-color 0.15s;
|
|
450
|
+
}
|
|
451
|
+
.search-form input[type="text"]:focus {
|
|
452
|
+
border-color: #8b6914;
|
|
453
|
+
}
|
|
454
|
+
.search-form input[type="text"]::placeholder {
|
|
455
|
+
color: #bbb;
|
|
456
|
+
}
|
|
457
|
+
.search-form button {
|
|
458
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
459
|
+
font-size: 0.82rem;
|
|
460
|
+
padding: 0.3rem 0.6rem;
|
|
461
|
+
border: 1px solid #d4b96a;
|
|
462
|
+
border-left: none;
|
|
463
|
+
border-radius: 0 4px 4px 0;
|
|
464
|
+
background: #f5f0e4;
|
|
465
|
+
color: #8b6914;
|
|
466
|
+
cursor: pointer;
|
|
467
|
+
transition: background 0.15s;
|
|
468
|
+
}
|
|
469
|
+
.search-form button:hover {
|
|
470
|
+
background: #e8dfc8;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* Search results */
|
|
474
|
+
.search-summary {
|
|
475
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
476
|
+
font-size: 0.85rem;
|
|
477
|
+
color: #888;
|
|
478
|
+
margin-bottom: 1.2rem;
|
|
479
|
+
}
|
|
480
|
+
.search-error {
|
|
481
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
482
|
+
font-size: 0.9rem;
|
|
483
|
+
color: #c44;
|
|
484
|
+
background: #fdf0f0;
|
|
485
|
+
border: 1px solid #f0c0c0;
|
|
486
|
+
border-radius: 6px;
|
|
487
|
+
padding: 0.8rem 1rem;
|
|
488
|
+
margin-bottom: 1.2rem;
|
|
489
|
+
}
|
|
490
|
+
.search-result {
|
|
491
|
+
margin-bottom: 1.5rem;
|
|
492
|
+
}
|
|
493
|
+
.search-result-path {
|
|
494
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
495
|
+
font-size: 0.9rem;
|
|
496
|
+
margin-bottom: 0.4rem;
|
|
497
|
+
}
|
|
498
|
+
.search-result-path a {
|
|
499
|
+
color: #8b6914;
|
|
500
|
+
text-decoration: none;
|
|
501
|
+
}
|
|
502
|
+
.search-result-path a:hover {
|
|
503
|
+
text-decoration: underline;
|
|
504
|
+
}
|
|
505
|
+
.search-result-path .icon {
|
|
506
|
+
margin-right: 0.3rem;
|
|
507
|
+
}
|
|
508
|
+
.search-context {
|
|
509
|
+
background: #2d2d2d;
|
|
510
|
+
color: #f0f0f0;
|
|
511
|
+
border-radius: 6px;
|
|
512
|
+
overflow: hidden;
|
|
513
|
+
font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
514
|
+
font-size: 0.8rem;
|
|
515
|
+
line-height: 1.5;
|
|
516
|
+
}
|
|
517
|
+
.search-context-group {
|
|
518
|
+
padding: 0.4rem 0;
|
|
519
|
+
}
|
|
520
|
+
.search-context-group + .search-context-group {
|
|
521
|
+
border-top: 1px dashed #555;
|
|
522
|
+
}
|
|
523
|
+
.search-line {
|
|
524
|
+
display: flex;
|
|
525
|
+
padding: 0 0.8rem;
|
|
526
|
+
max-width: 100%;
|
|
527
|
+
}
|
|
528
|
+
.search-line-num {
|
|
529
|
+
color: #75715e;
|
|
530
|
+
text-align: right;
|
|
531
|
+
min-width: 3.5em;
|
|
532
|
+
padding-right: 1em;
|
|
533
|
+
flex-shrink: 0;
|
|
534
|
+
user-select: none;
|
|
535
|
+
}
|
|
536
|
+
.search-line-text {
|
|
537
|
+
flex: 1;
|
|
538
|
+
min-width: 0;
|
|
539
|
+
white-space: pre;
|
|
540
|
+
overflow: hidden;
|
|
541
|
+
text-overflow: ellipsis;
|
|
542
|
+
}
|
|
543
|
+
.search-line.match-line {
|
|
544
|
+
background: rgba(212, 185, 106, 0.15);
|
|
545
|
+
}
|
|
546
|
+
.search-line .highlight-match {
|
|
547
|
+
background: #b8860b;
|
|
548
|
+
color: #fff;
|
|
549
|
+
font-weight: 600;
|
|
550
|
+
padding: 0.05em 0.2em;
|
|
551
|
+
border-radius: 2px;
|
|
552
|
+
}
|
|
553
|
+
.search-line .truncated {
|
|
554
|
+
color: #75715e;
|
|
555
|
+
font-style: italic;
|
|
556
|
+
}
|
|
557
|
+
.search-line.ctx-2,
|
|
558
|
+
.search-line.ctx-3,
|
|
559
|
+
.search-line.ctx-4 {
|
|
560
|
+
display: none;
|
|
561
|
+
}
|
|
562
|
+
.search-no-results {
|
|
563
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
564
|
+
font-size: 0.95rem;
|
|
565
|
+
color: #888;
|
|
566
|
+
text-align: center;
|
|
567
|
+
padding: 2rem 0;
|
|
568
|
+
}
|
|
569
|
+
|
|
419
570
|
/* Responsive */
|
|
420
571
|
@media (max-width: 768px) {
|
|
421
572
|
.container { padding: 1rem; }
|
|
422
573
|
.container.has-toc { max-width: 900px; }
|
|
423
574
|
h1.page-title { font-size: 1.3rem; }
|
|
424
575
|
|
|
576
|
+
.title-bar {
|
|
577
|
+
flex-wrap: wrap;
|
|
578
|
+
}
|
|
579
|
+
.search-form input[type="text"] {
|
|
580
|
+
width: 140px;
|
|
581
|
+
}
|
|
582
|
+
|
|
425
583
|
.page-with-toc { display: block; }
|
|
426
584
|
.toc-sidebar { display: none; }
|
|
427
585
|
.toc-mobile { display: block; }
|
data/views/markdown.erb
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
<% _parent = parent_dir_path(@crumbs.map { |c| c[:name] }.drop(1).join("/")) %>
|
|
2
|
+
<div class="title-bar">
|
|
3
|
+
<h1 class="page-title"><%= h(@title) %></h1>
|
|
4
|
+
<form class="search-form" action="<%= search_form_path(_parent) %>" method="get">
|
|
5
|
+
<input type="text" name="q" placeholder="Search files..." value="">
|
|
6
|
+
<button type="submit">Search</button>
|
|
7
|
+
</form>
|
|
8
|
+
</div>
|
|
2
9
|
|
|
3
10
|
<div class="toolbar">
|
|
4
11
|
<a href="<%= @download_href %>">Download</a>
|
data/views/raw.erb
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
<% _parent = parent_dir_path(@crumbs.map { |c| c[:name] }.drop(1).join("/")) %>
|
|
2
|
+
<div class="title-bar">
|
|
3
|
+
<h1 class="page-title"><%= h(@title) %></h1>
|
|
4
|
+
<form class="search-form" action="<%= search_form_path(_parent) %>" method="get">
|
|
5
|
+
<input type="text" name="q" placeholder="Search files..." value="">
|
|
6
|
+
<button type="submit">Search</button>
|
|
7
|
+
</form>
|
|
8
|
+
</div>
|
|
2
9
|
|
|
3
10
|
<div class="toolbar">
|
|
4
11
|
<a href="<%= @download_href %>">Download</a>
|
data/views/search.erb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<div class="title-bar">
|
|
2
|
+
<h1 class="page-title">Search: <%= h(@title) %></h1>
|
|
3
|
+
<form class="search-form" action="<%= search_form_path(@path) %>" method="get">
|
|
4
|
+
<input type="text" name="q" placeholder="Search files..." value="<%= h(@query) %>">
|
|
5
|
+
<button type="submit">Search</button>
|
|
6
|
+
</form>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<% if @error %>
|
|
10
|
+
<div class="search-error"><%= h(@error) %></div>
|
|
11
|
+
<% elsif !@query.empty? %>
|
|
12
|
+
<div class="search-summary">
|
|
13
|
+
<% if @results.empty? %>
|
|
14
|
+
No results for "<%= h(@query) %>"
|
|
15
|
+
<% else %>
|
|
16
|
+
<%= @results.length %><%= @results.length >= 100 ? "+" : "" %> file<%= @results.length == 1 ? "" : "s" %> matching "<%= h(@query) %>"
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
19
|
+
<% end %>
|
|
20
|
+
|
|
21
|
+
<% if @results.empty? && @error.nil? && !@query.empty? %>
|
|
22
|
+
<div class="search-no-results">No matching files found.</div>
|
|
23
|
+
<% end %>
|
|
24
|
+
|
|
25
|
+
<% @results.each do |result| %>
|
|
26
|
+
<div class="search-result">
|
|
27
|
+
<div class="search-result-path">
|
|
28
|
+
<span class="icon"><%= icon_for(File.basename(result[:path]), false) %></span>
|
|
29
|
+
<a href="/browse/<%= result[:path].split("/").map { |p| encode_path_component(p) }.join("/") %>"><%= h(result[:path]) %></a>
|
|
30
|
+
</div>
|
|
31
|
+
<% unless result[:matches].empty? %>
|
|
32
|
+
<div class="search-context">
|
|
33
|
+
<% result[:matches].each do |group| %>
|
|
34
|
+
<div class="search-context-group">
|
|
35
|
+
<% group[:lines].each do |line| %>
|
|
36
|
+
<% css_class = line[:distance] == 0 ? "search-line match-line" : "search-line ctx-#{line[:distance]}" %>
|
|
37
|
+
<div class="<%= css_class %>">
|
|
38
|
+
<span class="search-line-num"><%= line[:number] %></span>
|
|
39
|
+
<span class="search-line-text"><%= highlight_search_line(line[:text], @regexes, line[:distance] == 0) %></span>
|
|
40
|
+
</div>
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
</div>
|
|
45
|
+
<% end %>
|
|
46
|
+
</div>
|
|
47
|
+
<% end %>
|
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Dunn
|
|
@@ -108,6 +108,7 @@ files:
|
|
|
108
108
|
- views/layout.erb
|
|
109
109
|
- views/markdown.erb
|
|
110
110
|
- views/raw.erb
|
|
111
|
+
- views/search.erb
|
|
111
112
|
homepage: https://github.com/brianmd/markdown-server
|
|
112
113
|
licenses:
|
|
113
114
|
- MIT
|