servel 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0bbb0f367b145394675e204c7a80df6418b6bbb2
4
- data.tar.gz: 6070307eafbfe804dd06539c736a95c0991c93b3
3
+ metadata.gz: 6b04f7ed035910d2308b11a3ca5cde78e3e5464f
4
+ data.tar.gz: aa266ba4e42980678226a9aad763ef23fef31f67
5
5
  SHA512:
6
- metadata.gz: 865cdba7e39632c9b242d6d7961f2a17940a9b509a158e9d120f9f15fe26c1a0986cd9bce0e6f3e40880d528aa96a50e36de67ae7f6d2619a4f1f20145809caf
7
- data.tar.gz: 07d88d0349d4e20e2eb42cad1003b91679a276c37b9105015c6b792b76b01688b8b75842c8626921d3a717465ba5bdb7451d49749b7e3a6243c38c505fc67f3d
6
+ metadata.gz: 61c466680fffedef13a2a2e9d996379a7c1d98d356b0d8eb044e358e89100bcce1baa1105a2d0c2acd5f463fa087e5b85a29e091552c26f0342ad365bda404e9
7
+ data.tar.gz: 4fa87e8bcdd2b192517f32d21272da99002e242b29a2e01fcb7f6b05aa59b333b44ba5d3940786722156d03a4e47f82f5652617b7f202c08b808bd99332f35f3
@@ -1,27 +1,26 @@
1
- require 'rack'
2
- require 'rack/handler/puma'
3
- require 'haml'
4
- require 'naturalsorter'
5
- require 'active_support/all'
6
-
7
- require 'json'
8
- require 'pathname'
9
- require 'delegate'
10
-
11
- module Servel
12
- def self.build_app(path_map)
13
- url_map = path_map.map { |root, url_root| [url_root, Servel::App.new(root)] }.to_h
14
- url_map["/"] = Servel::HomeApp.new(path_map.values) unless url_map.keys.include?("/")
15
-
16
- Rack::URLMap.new(url_map)
17
- end
18
- end
19
-
20
- require "servel/version"
21
- require "servel/path"
22
- require "servel/path_builder"
23
- require "servel/haml_context"
24
- require "servel/locals"
25
- require "servel/app"
26
- require "servel/home_app"
27
- require "servel/cli"
1
+ require 'rack'
2
+ require 'rack/handler/puma'
3
+ require 'haml'
4
+ require 'naturalsorter'
5
+ require 'active_support/all'
6
+
7
+ require 'thread'
8
+ require 'pathname'
9
+
10
+ module Servel
11
+ def self.build_app(path_map)
12
+ url_map = path_map.map { |root, url_root| [url_root, Servel::App.new(root)] }.to_h
13
+ url_map["/"] = Servel::HomeApp.new(path_map.values) unless url_map.keys.include?("/")
14
+
15
+ Rack::URLMap.new(url_map)
16
+ end
17
+ end
18
+
19
+ require "servel/version"
20
+ require "servel/entry"
21
+ require "servel/entry_factory"
22
+ require "servel/haml_context"
23
+ require "servel/index"
24
+ require "servel/app"
25
+ require "servel/home_app"
26
+ require "servel/cli"
@@ -18,7 +18,7 @@ class Servel::App
18
18
 
19
19
  return [404, {}, []] unless fs_path.exist?
20
20
 
21
- index(Servel::Locals.new(url_root: url_root, url_path: url_path, fs_path: fs_path))
21
+ Servel::Index.new(url_root: url_root, url_path: url_path, fs_path: fs_path).render
22
22
  end
23
23
 
24
24
  def redirect(location)
@@ -30,11 +30,4 @@ class Servel::App
30
30
  raise unless Rack::Utils.valid_path?(url_path)
31
31
  Rack::Utils.clean_path_info(url_path)
32
32
  end
33
-
34
- def index(locals)
35
- @haml_context ||= Servel::HamlContext.new
36
- body = @haml_context.render('index.haml', locals.resolve)
37
-
38
- [200, {}, [body]]
39
- end
40
33
  end
@@ -0,0 +1,18 @@
1
+ class Servel::Entry
2
+ attr_reader :type, :media_type, :listing_classes, :icon, :href, :name, :size, :mtime
3
+
4
+ def initialize(type:, media_type: nil, listing_classes:, icon:, href:, name:, size: nil, mtime: nil)
5
+ @type = type
6
+ @media_type = media_type
7
+ @listing_classes = listing_classes
8
+ @icon = icon
9
+ @href = href
10
+ @name = name
11
+ @size = size
12
+ @mtime = mtime
13
+ end
14
+
15
+ def media?
16
+ !@media_type.nil?
17
+ end
18
+ end
@@ -1,14 +1,39 @@
1
- class Servel::PathBuilder
1
+ class Servel::EntryFactory
2
2
  IMAGE_EXTS = %w(.jpg .jpeg .png .gif)
3
3
  VIDEO_EXTS = %w(.webm .mp4 .mkv)
4
4
  AUDIO_EXTS = %w(.mp3 .m4a .wav)
5
+ TEXT_EXTS = %w(.txt)
6
+
7
+ def self.top(href)
8
+ Servel::Entry.new(
9
+ type: "Dir",
10
+ listing_classes: "top directory",
11
+ icon: "🔝",
12
+ href: href,
13
+ name: "Top Directory"
14
+ )
15
+ end
16
+
17
+ def self.parent(href)
18
+ Servel::Entry.new(
19
+ type: "Dir",
20
+ listing_classes: "parent directory",
21
+ icon: "⬆️",
22
+ href: href,
23
+ name: "Parent Directory"
24
+ )
25
+ end
26
+
27
+ def self.for(path)
28
+ new(path).entry
29
+ end
5
30
 
6
31
  def initialize(path)
7
32
  @path = Pathname.new(path)
8
33
  end
9
34
 
10
- def build
11
- Servel::Path.new(
35
+ def entry
36
+ Servel::Entry.new(
12
37
  type: type,
13
38
  media_type: media_type,
14
39
  listing_classes: listing_classes,
@@ -40,6 +65,8 @@ class Servel::PathBuilder
40
65
  :video
41
66
  when *AUDIO_EXTS
42
67
  :audio
68
+ when *TEXT_EXTS
69
+ :text
43
70
  else
44
71
  nil
45
72
  end
@@ -2,10 +2,14 @@ class Servel::HamlContext
2
2
  include ActiveSupport::NumberHelper
3
3
 
4
4
  ENGINE_OPTIONS = { remove_whitespace: true, escape_html: true, ugly: true }
5
+ LOCK = Mutex.new
5
6
 
6
- def initialize()
7
+ def self.render(template, locals)
8
+ [200, {}, [new.render(template, locals)]]
9
+ end
10
+
11
+ def initialize
7
12
  @build_path = Pathname.new(__FILE__).dirname.realpath + 'templates'
8
- @haml_engine_cache = {}
9
13
  end
10
14
 
11
15
  def render(template, locals = {})
@@ -21,9 +25,12 @@ class Servel::HamlContext
21
25
  end
22
26
 
23
27
  def haml_engine(path)
24
- unless @haml_engine_cache.key?(path)
25
- @haml_engine_cache[path] = Haml::Engine.new(include(path), ENGINE_OPTIONS.merge(filename: path))
28
+ LOCK.synchronize do
29
+ @@haml_engine_cache ||= {}
30
+ unless @@haml_engine_cache.key?(path)
31
+ @@haml_engine_cache[path] = Haml::Engine.new(include(path), ENGINE_OPTIONS.merge(filename: path))
32
+ end
33
+ @@haml_engine_cache[path]
26
34
  end
27
- @haml_engine_cache[path]
28
35
  end
29
36
  end
@@ -1,12 +1,12 @@
1
1
  class Servel::HomeApp
2
+ FAVICON_PATH = "/favicon.ico"
3
+
2
4
  def initialize(roots)
3
5
  @roots = roots
4
6
  end
5
7
 
6
8
  def call(env)
7
- @haml_context ||= Servel::HamlContext.new
8
- body = @haml_context.render('home.haml', { roots: @roots })
9
-
10
- [200, {}, [body]]
9
+ return [404, {}, []] if env["PATH_INFO"] == FAVICON_PATH
10
+ Servel::HamlContext.render('home.haml', { roots: @roots })
11
11
  end
12
12
  end
@@ -1,41 +1,41 @@
1
- class Servel::Locals
2
- def initialize(url_root:, url_path:, fs_path:)
3
- @url_root = url_root
4
- @url_path = url_path
5
- @fs_path = fs_path
6
- end
7
-
8
- def resolve
9
- {
10
- url_root: @url_root,
11
- url_path: @url_path,
12
- directories: directories,
13
- files: files
14
- }
15
- end
16
-
17
- def directories
18
- list = @fs_path.children.select { |child| child.directory? }
19
- list = sort_paths(list).map { |path| build(path) }
20
-
21
- unless @url_path == "/"
22
- list.unshift(Servel::Path.parent("../"))
23
- list.unshift(Servel::Path.top(@url_root == "" ? "/" : @url_root))
24
- end
25
-
26
- list
27
- end
28
-
29
- def build(path)
30
- Servel::PathBuilder.new(path).build
31
- end
32
-
33
- def files
34
- list = @fs_path.children.select { |child| child.file? }
35
- sort_paths(list).map { |path| build(path) }
36
- end
37
-
38
- def sort_paths(paths)
39
- Naturalsorter::Sorter.sort(paths.map(&:to_s), true).map { |path| Pathname.new(path) }
40
- end
1
+ class Servel::Index
2
+ def initialize(url_root:, url_path:, fs_path:)
3
+ @url_root = url_root
4
+ @url_path = url_path
5
+ @fs_path = fs_path
6
+ end
7
+
8
+ def render
9
+ Servel::HamlContext.render('index.haml', locals)
10
+ end
11
+
12
+ def locals
13
+ {
14
+ url_root: @url_root,
15
+ url_path: @url_path,
16
+ directories: directories,
17
+ files: files
18
+ }
19
+ end
20
+
21
+ def directories
22
+ list = @fs_path.children.select { |child| child.directory? }
23
+ list = sort_paths(list).map { |path| Servel::EntryFactory.for(path) }
24
+
25
+ unless @url_path == "/"
26
+ list.unshift(Servel::EntryFactory.parent("../"))
27
+ list.unshift(Servel::EntryFactory.top(@url_root == "" ? "/" : @url_root))
28
+ end
29
+
30
+ list
31
+ end
32
+
33
+ def files
34
+ list = @fs_path.children.select { |child| child.file? }
35
+ sort_paths(list).map { |path| Servel::EntryFactory.for(path) }
36
+ end
37
+
38
+ def sort_paths(paths)
39
+ Naturalsorter::Sorter.sort(paths.map(&:to_s), true).map { |path| Pathname.new(path) }
40
+ end
41
41
  end
@@ -1,6 +1,9 @@
1
1
  %img#image
2
2
  %video#video{autoplay: true, controls: true}
3
3
  %audio#audio{autoplay: true, controls: true}
4
+ #text
5
+ %a#text-anchor{href: '#'}
6
+ #text-content
4
7
  #page-back.paginator ◀
5
8
  #page-next.paginator ▶
6
9
  #page-back-10.paginator ◀◀
@@ -0,0 +1,39 @@
1
+ #text-content {
2
+ max-width: 1000px;
3
+ margin: 0 auto 0 auto;
4
+ padding: 30px;
5
+ }
6
+
7
+ #text-content p {
8
+ margin-top: 0;
9
+ margin-bottom: 1.2em;
10
+ }
11
+
12
+ #text-content h1, #text-content h2, #text-content h3, #text-content h4, #text-content h5, #text-content h6 {
13
+ margin-top: 0;
14
+ margin-bottom: 0.6em;
15
+ }
16
+
17
+ #text-content h1 {
18
+ font-size: 36px;
19
+ }
20
+
21
+ #text-content h2 {
22
+ font-size: 26px;
23
+ }
24
+
25
+ #text-content h3 {
26
+ font-size: 22px;
27
+ }
28
+
29
+ #text-content h4 {
30
+ font-size: 18px;
31
+ }
32
+
33
+ #text-content h5 {
34
+ font-size: 16px;
35
+ }
36
+
37
+ #text-content h6 {
38
+ font-size: 16px;
39
+ }
@@ -3,7 +3,7 @@
3
3
  transform: translateZ(0px);
4
4
  }
5
5
 
6
- #image, #video, #audio {
6
+ #image, #video, #audio, #text {
7
7
  display: none;
8
8
  max-width: 100%;
9
9
  margin: 0 auto 0 auto;
@@ -13,8 +13,14 @@
13
13
  width: 100%;
14
14
  }
15
15
 
16
+ #text {
17
+ overflow: auto;
18
+ color: #000000;
19
+ background-color: #ffffff;
20
+ }
21
+
16
22
  @media (min-width: 992px) {
17
- #image, #video, #audio {
23
+ #image, #video, #audio, #text {
18
24
  max-height: 100%;
19
25
  }
20
26
  }
@@ -39,6 +45,14 @@
39
45
  display: block;
40
46
  }
41
47
 
48
+ #gallery.text {
49
+ padding: 0 100px;
50
+ }
51
+
52
+ #gallery.text #text {
53
+ display: block;
54
+ }
55
+
42
56
  .paginator {
43
57
  position: fixed;
44
58
  width: 100px;
@@ -5,9 +5,9 @@
5
5
  %title Browse Listings
6
6
  %meta{name: 'viewport', content: 'width=device-width, height=device-height, user-scalable=no'}
7
7
  :css
8
- #{include('normalize.css')}
9
- #{include('common.css')}
10
- #{include('home.css')}
8
+ #{include('css/normalize.css')}
9
+ #{include('css/common.css')}
10
+ #{include('css/home.css')}
11
11
  %body
12
12
  #home
13
13
  %h1 Browse Listings
@@ -5,14 +5,16 @@
5
5
  %title Listing of #{url_root}#{url_path}
6
6
  %meta{name: 'viewport', content: 'width=device-width, height=device-height, user-scalable=no'}
7
7
  :css
8
- #{include('normalize.css')}
9
- #{include('common.css')}
10
- #{include('index.css')}
11
- #{include('listing.css')}
12
- #{include('gallery.css')}
8
+ #{include('css/normalize.css')}
9
+ #{include('css/common.css')}
10
+ #{include('css/index.css')}
11
+ #{include('css/listing.css')}
12
+ #{include('css/gallery.css')}
13
+ #{include('css/gallery-text.css')}
13
14
 
14
15
  :javascript
15
- #{include('gallery.js')}
16
+ #{include('js/ume.js')}
17
+ #{include('js/gallery.js')}
16
18
  %body
17
19
  - if files.any? { |f| f.media? }
18
20
  #gallery!= partial('gallery')
@@ -1,3 +1,5 @@
1
+ var $;
2
+
1
3
  var Gallery = (function() {
2
4
  var urls = [];
3
5
  var types = [];
@@ -13,15 +15,33 @@ var Gallery = (function() {
13
15
  }
14
16
  }
15
17
 
18
+ function renderText(url) {
19
+ var http = new XMLHttpRequest();
20
+ http.open("GET", url, true);
21
+ http.onload = function() {
22
+ $("#text-content").innerHTML = ume(http.responseText);
23
+ $("#text").scrollTop = 0;
24
+ $("#text-anchor").focus();
25
+ }
26
+ http.send();
27
+ }
28
+
16
29
  function render() {
17
30
  var url = urls[currentIndex];
18
31
  var type = types[currentIndex];
19
32
 
20
- var galleryElement = document.querySelector("#gallery");
21
- galleryElement.classList.remove("image", "video", "audio");
22
- galleryElement.classList.add(type);
33
+ var $gallery = $("#gallery");
34
+ $gallery.classList.remove("image", "video", "audio", "text");
35
+ $gallery.classList.add(type);
23
36
 
24
- document.getElementById(type).src = url;
37
+ if(type == "text") {
38
+ renderText(url);
39
+ }
40
+ else {
41
+ var $element = $("#" + type);
42
+ $element.src = url;
43
+ $element.focus();
44
+ }
25
45
 
26
46
  window.scrollTo(0, 0);
27
47
 
@@ -128,7 +148,8 @@ var Gallery = (function() {
128
148
  })();
129
149
 
130
150
  function initGallery() {
131
- if(!document.querySelector("#gallery")) return;
151
+ $ = document.querySelector.bind(document);
152
+ if(!$("#gallery")) return;
132
153
  Gallery.init();
133
154
  }
134
155
 
@@ -0,0 +1,63 @@
1
+ var ume = (function() {
2
+ function normalise(text) {
3
+ return text.replace(/\s+/g, " ").trim();
4
+ }
5
+
6
+ function escapeEntities(text) {
7
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
8
+ }
9
+
10
+ const BOLD_ITALIC_REGEX = /(?:(\W|^)(\*)|(\*)(\W|$)|(\W|^)(_)|(_)(\W|$))/;
11
+
12
+ function parseInlines(text) {
13
+ const parts = text.split(BOLD_ITALIC_REGEX);
14
+
15
+ if(parts.filter(function(part) { return part == "*"; }).length % 2 != 0) return text;
16
+ if(parts.filter(function(part) { return part == "_"; }).length % 2 != 0) return text;
17
+
18
+ var inBold = false;
19
+ var inItalic = false;
20
+ var html = [];
21
+
22
+ for(const part of parts) {
23
+ if(part == "*") {
24
+ inBold = !inBold;
25
+ html.push(inBold ? "<b>" : "</b>"); //Note that in_bold has been inverted, so this is inverted as well
26
+ }
27
+ else if(part == "_") {
28
+ inItalic = !inItalic;
29
+ html.push(inItalic ? "<i>" : "</i>"); //Note that in_italic has been inverted, so this is inverted as well
30
+ }
31
+ else {
32
+ html.push(part);
33
+ }
34
+ }
35
+
36
+ return html.join("");
37
+ }
38
+
39
+ function parseHeading(text) {
40
+ const parts = text.match(/^(#+) (.*)$/);
41
+ return "<h" + parts[1].length + ">" + parseInlines(parts[2]) + "</h" + parts[1].length + ">";
42
+ }
43
+
44
+ function parseParagraph(text) {
45
+ return "<p>" + parseInlines(text) + "</p>";
46
+ }
47
+
48
+ return function(text) {
49
+ const nodes = text.split(/(\r?\n){2,}/g);
50
+ var html = [];
51
+
52
+ for(const node of nodes) {
53
+ const cleanedNode = escapeEntities(normalise(node));
54
+
55
+ if(cleanedNode.startsWith("#")) html.push(parseHeading(cleanedNode));
56
+ else html.push(parseParagraph(cleanedNode));
57
+ }
58
+
59
+ return html.join("");
60
+ }
61
+ })();
62
+
63
+ if(typeof module === "object" && typeof module.exports === "object") module.exports = ume;
@@ -1,3 +1,3 @@
1
1
  module Servel
2
- VERSION = "0.9.0"
2
+ VERSION = "0.10.0"
3
3
  end
@@ -1,31 +1,31 @@
1
-
2
- lib = File.expand_path("../lib", __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "servel/version"
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "servel"
8
- spec.version = Servel::VERSION
9
- spec.authors = ["Brenton \"B-Train\" Fletcher"]
10
- spec.email = ["i@bloople.net"]
11
-
12
- spec.summary = %q{Serves files and directories over HTTP.}
13
- spec.description = %q{Serves files and directories over HTTP.}
14
- spec.homepage = "http://bloople.net/"
15
- spec.license = "MIT"
16
-
17
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
- f.match(%r{^(test|spec|features)/})
19
- end
20
- spec.bindir = "bin"
21
- spec.executables = "servel"
22
- spec.require_paths = ["lib"]
23
-
24
- spec.add_development_dependency "bundler", "~> 1.16"
25
- spec.add_development_dependency "rake", "~> 12.0"
26
- spec.add_dependency "rack", "~> 2.0"
27
- spec.add_dependency "puma"
28
- spec.add_dependency "naturalsorter"
29
- spec.add_dependency "haml", "~> 4"
30
- spec.add_dependency "activesupport"
31
- end
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "servel/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "servel"
8
+ spec.version = Servel::VERSION
9
+ spec.authors = ["Brenton \"B-Train\" Fletcher"]
10
+ spec.email = ["i@bloople.net"]
11
+
12
+ spec.summary = %q{Serves files and directories over HTTP.}
13
+ spec.description = %q{Serves files and directories over HTTP.}
14
+ spec.homepage = "http://bloople.net/"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "bin"
21
+ spec.executables = "servel"
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.16"
25
+ spec.add_development_dependency "rake", "~> 12.0"
26
+ spec.add_dependency "rack", "~> 2.0"
27
+ spec.add_dependency "puma"
28
+ spec.add_dependency "naturalsorter"
29
+ spec.add_dependency "haml", "~> 4"
30
+ spec.add_dependency "activesupport"
31
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: servel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brenton "B-Train" Fletcher
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-12 00:00:00.000000000 Z
11
+ date: 2018-03-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -127,22 +127,24 @@ files:
127
127
  - lib/servel.rb
128
128
  - lib/servel/app.rb
129
129
  - lib/servel/cli.rb
130
+ - lib/servel/entry.rb
131
+ - lib/servel/entry_factory.rb
130
132
  - lib/servel/haml_context.rb
131
133
  - lib/servel/home_app.rb
132
- - lib/servel/locals.rb
133
- - lib/servel/path.rb
134
- - lib/servel/path_builder.rb
134
+ - lib/servel/index.rb
135
135
  - lib/servel/templates/_gallery.haml
136
136
  - lib/servel/templates/_listing.haml
137
- - lib/servel/templates/common.css
138
- - lib/servel/templates/gallery.css
139
- - lib/servel/templates/gallery.js
140
- - lib/servel/templates/home.css
137
+ - lib/servel/templates/css/common.css
138
+ - lib/servel/templates/css/gallery-text.css
139
+ - lib/servel/templates/css/gallery.css
140
+ - lib/servel/templates/css/home.css
141
+ - lib/servel/templates/css/index.css
142
+ - lib/servel/templates/css/listing.css
143
+ - lib/servel/templates/css/normalize.css
141
144
  - lib/servel/templates/home.haml
142
- - lib/servel/templates/index.css
143
145
  - lib/servel/templates/index.haml
144
- - lib/servel/templates/listing.css
145
- - lib/servel/templates/normalize.css
146
+ - lib/servel/templates/js/gallery.js
147
+ - lib/servel/templates/js/ume.js
146
148
  - lib/servel/version.rb
147
149
  - servel.gemspec
148
150
  homepage: http://bloople.net/
@@ -1,38 +0,0 @@
1
- class Servel::Path
2
- attr_reader :type, :media_type, :listing_classes, :icon, :href, :name, :size, :mtime
3
-
4
- def initialize(type:, media_type: nil, listing_classes:, icon:, href:, name:, size: nil, mtime: nil)
5
- @type = type
6
- @media_type = media_type
7
- @listing_classes = listing_classes
8
- @icon = icon
9
- @href = href
10
- @name = name
11
- @size = size
12
- @mtime = mtime
13
- end
14
-
15
- def media?
16
- !@media_type.nil?
17
- end
18
-
19
- def self.top(href)
20
- Servel::Path.new(
21
- type: "Dir",
22
- listing_classes: "top directory",
23
- icon: "🔝",
24
- href: href,
25
- name: "Top Directory"
26
- )
27
- end
28
-
29
- def self.parent(href)
30
- Servel::Path.new(
31
- type: "Dir",
32
- listing_classes: "parent directory",
33
- icon: "⬆️",
34
- href: href,
35
- name: "Parent Directory"
36
- )
37
- end
38
- end