faun 0.0.1
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 +7 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +5 -0
- data/README.md +40 -0
- data/Rakefile +8 -0
- data/lib/faun/version.rb +5 -0
- data/lib/faun.rb +261 -0
- data/public/content.css +168 -0
- data/public/details.css +41 -0
- data/public/index.css +3 -0
- data/public/index.html +21 -0
- data/public/main-layout.css +22 -0
- data/public/main-style.css +21 -0
- data/public/sidebar.css +97 -0
- data/public/thread.css +38 -0
- data/public/wipe.css +46 -0
- data/views/content.slim +82 -0
- data/views/details.slim +50 -0
- data/views/sidebar.slim +46 -0
- data/views/thread.slim +32 -0
- metadata +64 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a4ec8a65a517a94caad0d54cbff33f4ba100b2e2ce23f2c1e6f6a5c25dd19bf2
|
4
|
+
data.tar.gz: f868d4e0439f0182f9dfbac78b31afe2f2cea219ffb9c96f2bde82c9d48180a5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 257d74ef5b45a271a7b04deb7c8b44a9db6352a0afdebbd3c62e385f429f2e36e2b3e3c2bc3a8664435c09221f367a39c6dafa12f1c773cc14c73cbbd9e24344
|
7
|
+
data.tar.gz: b9d1a39ae0f72be88bb30871bb6e4dca2967925ece27c24ce2aa552d2fa81f431b32256230a01b8229fff6edb116b135c40bbf127c55f9dcbebaadd9b35d0868
|
data/.rubocop.yml
ADDED
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# Faun Forum
|
2
|
+
|
3
|
+
Faun is a minimalistic directory-based forum and asset catalog engine for small communities. It is
|
4
|
+
a self-hosting solution which can run locally or in any server environment without any hassle.
|
5
|
+
|
6
|
+
Faun works with local directory in a file system, avoiding complexity of database connections. Faun
|
7
|
+
directory has a special structure-by-convention, which allows the forum to be used with no software
|
8
|
+
at all: just with a file browser and any text editor supporting markdown and YAML.
|
9
|
+
|
10
|
+
The web app is made with pure HTML and CSS: no javascript. Thus, it can easily be browsed with Tor
|
11
|
+
and privacy-preserving browsers. It also uses no cookies and thus avoids spam of banners and other
|
12
|
+
sorts of confirmation dialogs.
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Install the gem and add to the application's Gemfile by executing:
|
17
|
+
|
18
|
+
$ bundle add faun
|
19
|
+
|
20
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
21
|
+
|
22
|
+
$ gem install faun
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
TODO: Write usage instructions here
|
27
|
+
|
28
|
+
## Development
|
29
|
+
|
30
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
31
|
+
You can also run `bin/faun` for an command-line tool and `bin/faund` to run a web server.
|
32
|
+
|
33
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new
|
34
|
+
version, update the version number in `version.rb`, and then run `bundle exec rake release`, which
|
35
|
+
will create a git tag for the version, push git commits and the created tag, and push the `.gem`
|
36
|
+
file to [rubygems.org](https://rubygems.org).
|
37
|
+
|
38
|
+
## Contributing
|
39
|
+
|
40
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/faun-forum/faun.
|
data/Rakefile
ADDED
data/lib/faun/version.rb
ADDED
data/lib/faun.rb
ADDED
@@ -0,0 +1,261 @@
|
|
1
|
+
require "async"
|
2
|
+
require "async/io"
|
3
|
+
require "async/io/generic"
|
4
|
+
require "async/io/protocol/line"
|
5
|
+
require "yaml"
|
6
|
+
|
7
|
+
require_relative "faun/version"
|
8
|
+
|
9
|
+
module Faun
|
10
|
+
class Error < StandardError; end
|
11
|
+
|
12
|
+
class Section
|
13
|
+
attr_reader :id, :name, :items
|
14
|
+
|
15
|
+
def initialize(id, name, path, type)
|
16
|
+
@id = id
|
17
|
+
@name = name
|
18
|
+
|
19
|
+
subs = {}
|
20
|
+
Dir.each_child(path) do |section|
|
21
|
+
dir = File.join(path, section)
|
22
|
+
next unless File.directory?(dir) and not section.start_with?('.')
|
23
|
+
|
24
|
+
name, _, id = section.rpartition(".@")
|
25
|
+
id = id.to_i
|
26
|
+
subs[id] = type.new(id, name, dir)
|
27
|
+
end
|
28
|
+
@items = subs.sort_by { |subid, _| subid }.to_h
|
29
|
+
end
|
30
|
+
|
31
|
+
def each(&block)
|
32
|
+
@items.each(&block)
|
33
|
+
end
|
34
|
+
|
35
|
+
def each_key(&block)
|
36
|
+
@items.each_key(&block)
|
37
|
+
end
|
38
|
+
|
39
|
+
def each_value(&block)
|
40
|
+
@items.each_value(&block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_json(*args)
|
44
|
+
{
|
45
|
+
:id => @id,
|
46
|
+
item_name => @items
|
47
|
+
}.to_json(*args)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class SectionWithMeta < Section
|
52
|
+
attr_reader :meta
|
53
|
+
|
54
|
+
def initialize(id, name, path, type)
|
55
|
+
super(id, name, path, type)
|
56
|
+
|
57
|
+
Async do
|
58
|
+
begin
|
59
|
+
File.open(File.join(path, "meta.yaml"), "r:UTF-8") do |file|
|
60
|
+
generic = Async::IO::Stream.new(file)
|
61
|
+
@meta = YAML.load(generic.read)
|
62
|
+
end
|
63
|
+
rescue
|
64
|
+
@meta = {}
|
65
|
+
end
|
66
|
+
end.wait
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Forum < SectionWithMeta
|
71
|
+
attr_reader :posts
|
72
|
+
|
73
|
+
def initialize(path)
|
74
|
+
super(0, nil, path, Topic)
|
75
|
+
|
76
|
+
@posts = @items.flat_map do |_, topic|
|
77
|
+
topic.posts.to_a
|
78
|
+
end.to_h
|
79
|
+
end
|
80
|
+
|
81
|
+
def author_name(nick)
|
82
|
+
@meta["authors"][nick] || nick
|
83
|
+
end
|
84
|
+
|
85
|
+
def topic(id)
|
86
|
+
@items[id]
|
87
|
+
end
|
88
|
+
|
89
|
+
def subtopic(id, subid)
|
90
|
+
@items[id].subtopic(subid)
|
91
|
+
end
|
92
|
+
|
93
|
+
def post(id)
|
94
|
+
@posts[id]
|
95
|
+
end
|
96
|
+
|
97
|
+
def item_name
|
98
|
+
"topics"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class Topic < SectionWithMeta
|
103
|
+
attr_reader :posts
|
104
|
+
|
105
|
+
def initialize(id, name, path)
|
106
|
+
super(id, name, path, Subtopic)
|
107
|
+
|
108
|
+
@posts = @items.flat_map do |_, sub|
|
109
|
+
sub.posts.to_a
|
110
|
+
end.to_h
|
111
|
+
end
|
112
|
+
|
113
|
+
def subtopic(subid)
|
114
|
+
@items[subid]
|
115
|
+
end
|
116
|
+
|
117
|
+
def item_name
|
118
|
+
"chapters"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class Subtopic < SectionWithMeta
|
123
|
+
def initialize(id, name, path)
|
124
|
+
super(id, name, path, Post)
|
125
|
+
end
|
126
|
+
|
127
|
+
def posts
|
128
|
+
@items
|
129
|
+
end
|
130
|
+
|
131
|
+
def item_name
|
132
|
+
"posts"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class Post < Section
|
137
|
+
attr_reader :id, :meta, :content
|
138
|
+
|
139
|
+
def initialize(id, name, path)
|
140
|
+
super(id, name, path, ForumThread)
|
141
|
+
Async do
|
142
|
+
File.open(File.join(path, "latest.md"), "r:UTF-8") do |file|
|
143
|
+
generic = Async::IO::Stream.new(file)
|
144
|
+
lines = Async::IO::Protocol::Line.new(generic).each_line
|
145
|
+
lines.next
|
146
|
+
meta = lines.take_while { |line| line.strip != "---" }.join("\n")
|
147
|
+
# lines.next while lines.peek.strip.empty?
|
148
|
+
@meta = YAML.load(meta)
|
149
|
+
@meta["written"] = DateTime.strptime(@meta["written"], "%Y-%m-%d %H:%M")
|
150
|
+
@content = generic.read.force_encoding("UTF-8")
|
151
|
+
end
|
152
|
+
end.wait
|
153
|
+
end
|
154
|
+
|
155
|
+
def title
|
156
|
+
@name
|
157
|
+
end
|
158
|
+
|
159
|
+
def author
|
160
|
+
@meta["author"]
|
161
|
+
end
|
162
|
+
|
163
|
+
def threads
|
164
|
+
@items
|
165
|
+
end
|
166
|
+
|
167
|
+
def item_name
|
168
|
+
"threads"
|
169
|
+
end
|
170
|
+
|
171
|
+
def to_json(*args)
|
172
|
+
json = @meta.clone
|
173
|
+
json.merge!(
|
174
|
+
id: @id,
|
175
|
+
content: @content,
|
176
|
+
threads: @items
|
177
|
+
)
|
178
|
+
json.to_json(*args)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class ForumThread < Section
|
183
|
+
def initialize(id, name, path)
|
184
|
+
@id = id
|
185
|
+
@name = name
|
186
|
+
|
187
|
+
comments = {}
|
188
|
+
Dir.each_child(path) do |filename|
|
189
|
+
file = File.join(path, filename)
|
190
|
+
next if File.directory?(file) or filename.start_with?('.') or not filename.end_with?('.md')
|
191
|
+
|
192
|
+
id, author, date_string = filename.scan(/^(\d{3})\.(\w+)\.(20\d\d-\d\d-\d\d\.\d\d-\d\d)\.md$/).first
|
193
|
+
id = id.to_i
|
194
|
+
datetime = DateTime.strptime(date_string, "%Y-%m-%d.%H-%M")
|
195
|
+
|
196
|
+
content = nil
|
197
|
+
Async do
|
198
|
+
begin
|
199
|
+
File.open(file, "r:UTF-8") do |file|
|
200
|
+
generic = Async::IO::Stream.new(file)
|
201
|
+
content = generic.read.force_encoding("UTF-8")
|
202
|
+
end
|
203
|
+
rescue
|
204
|
+
content = ""
|
205
|
+
end
|
206
|
+
end.wait
|
207
|
+
|
208
|
+
comments[id] = Comment.new(id, author, datetime, content)
|
209
|
+
end
|
210
|
+
@items = comments.sort_by { |id, _| id }.to_h
|
211
|
+
end
|
212
|
+
|
213
|
+
def title
|
214
|
+
@name
|
215
|
+
end
|
216
|
+
|
217
|
+
def comments
|
218
|
+
@items
|
219
|
+
end
|
220
|
+
|
221
|
+
def authors
|
222
|
+
@items.values.map(&:author).uniq
|
223
|
+
end
|
224
|
+
|
225
|
+
def item_name
|
226
|
+
"comments"
|
227
|
+
end
|
228
|
+
|
229
|
+
def to_json(*args)
|
230
|
+
{
|
231
|
+
:id => id,
|
232
|
+
:title => @name,
|
233
|
+
:comments => @items
|
234
|
+
}.to_json
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
class Comment
|
239
|
+
attr_reader :id, :author, :created, :content
|
240
|
+
|
241
|
+
def initialize(id, author, created, content)
|
242
|
+
@id = id
|
243
|
+
@author = author
|
244
|
+
@created = created
|
245
|
+
@content = content
|
246
|
+
end
|
247
|
+
|
248
|
+
def markdown_content
|
249
|
+
Kramdown::Document.new(@content).to_html
|
250
|
+
end
|
251
|
+
|
252
|
+
def to_json(*args)
|
253
|
+
{
|
254
|
+
:id => id,
|
255
|
+
:author => @author,
|
256
|
+
:created => @created,
|
257
|
+
:content => @content
|
258
|
+
}.to_json
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
data/public/content.css
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
body {
|
2
|
+
background-color: #1e1e1e;
|
3
|
+
color: white;
|
4
|
+
font-family: "Roboto Slab", sans-serif;
|
5
|
+
}
|
6
|
+
|
7
|
+
article {
|
8
|
+
line-height: 1.6;
|
9
|
+
margin-bottom: 6em;
|
10
|
+
}
|
11
|
+
|
12
|
+
article header h1 ~ p.author {
|
13
|
+
font-style: italic;
|
14
|
+
font-size: large;
|
15
|
+
}
|
16
|
+
|
17
|
+
article h1, article h2, article h3, article h4, article h5, article h6 {
|
18
|
+
font-family: "Alumni Sans", "Impact", sans-serif;
|
19
|
+
padding-top: 1em;
|
20
|
+
}
|
21
|
+
|
22
|
+
article h1 {
|
23
|
+
font-size: 500%;
|
24
|
+
line-height: 0.8;
|
25
|
+
}
|
26
|
+
|
27
|
+
article h2 {
|
28
|
+
font-size: xxx-large;
|
29
|
+
}
|
30
|
+
|
31
|
+
article h3 {
|
32
|
+
font-size: xx-large;
|
33
|
+
}
|
34
|
+
|
35
|
+
article h4 {
|
36
|
+
font-size: x-large;
|
37
|
+
}
|
38
|
+
|
39
|
+
article h6 {
|
40
|
+
font-size: large;
|
41
|
+
}
|
42
|
+
|
43
|
+
article p {
|
44
|
+
text-align: justify;
|
45
|
+
}
|
46
|
+
|
47
|
+
main :not(li) > p {
|
48
|
+
padding: 1em 0;
|
49
|
+
}
|
50
|
+
|
51
|
+
article header + p:first-of-type::first-letter {
|
52
|
+
float: left;
|
53
|
+
font-size: 500%;
|
54
|
+
font-weight: bold;
|
55
|
+
color: red;
|
56
|
+
padding-top: 5pt;
|
57
|
+
padding-right: 0.2rem;
|
58
|
+
}
|
59
|
+
|
60
|
+
article a, article a:hover, article a:visited, article a:active {
|
61
|
+
color: #ff8b8b;
|
62
|
+
}
|
63
|
+
|
64
|
+
article ul {
|
65
|
+
list-style-type: disc;
|
66
|
+
}
|
67
|
+
|
68
|
+
article ol {
|
69
|
+
list-style-type: decimal;
|
70
|
+
}
|
71
|
+
|
72
|
+
article ol, article ul {
|
73
|
+
padding-left: 1em;
|
74
|
+
margin-bottom: 1em;
|
75
|
+
}
|
76
|
+
|
77
|
+
article li {
|
78
|
+
margin-left: 1.33em;
|
79
|
+
padding-top: 0.25em;
|
80
|
+
padding-bottom: 0.25em;
|
81
|
+
text-align: justify;
|
82
|
+
}
|
83
|
+
|
84
|
+
article table {
|
85
|
+
margin: 1em -5em;
|
86
|
+
border-width: 1px 0;
|
87
|
+
border-color: #666;
|
88
|
+
border-style: solid;
|
89
|
+
}
|
90
|
+
|
91
|
+
article thead {
|
92
|
+
font-family: "Alumni Sans", sans-serif;
|
93
|
+
border-bottom: 1px dotted #666;
|
94
|
+
}
|
95
|
+
|
96
|
+
article th {
|
97
|
+
vertical-align: middle;
|
98
|
+
}
|
99
|
+
|
100
|
+
article thead tr:first-child th, article tr:first-child td {
|
101
|
+
padding-top: 1em;
|
102
|
+
}
|
103
|
+
|
104
|
+
article thead tr:last-child th, article tr:last-child td {
|
105
|
+
padding-bottom: 1em;
|
106
|
+
}
|
107
|
+
|
108
|
+
article td {
|
109
|
+
padding: 0.5em;
|
110
|
+
}
|
111
|
+
|
112
|
+
article tr:nth-child(even) {
|
113
|
+
background-color: rgba(0, 0, 0, 0.2);
|
114
|
+
}
|
115
|
+
|
116
|
+
/*
|
117
|
+
* Discussions
|
118
|
+
*/
|
119
|
+
|
120
|
+
#discussion {
|
121
|
+
background-color: #181818;
|
122
|
+
}
|
123
|
+
|
124
|
+
#discussion a, aside a:hover, #discussion a:visited, #discussion a:active {
|
125
|
+
color: white;
|
126
|
+
text-decoration: none;
|
127
|
+
}
|
128
|
+
|
129
|
+
#discussion li {
|
130
|
+
border-bottom: 1px dotted #666;
|
131
|
+
}
|
132
|
+
|
133
|
+
#discussion li > a:hover {
|
134
|
+
background-color: rgba(0, 0, 0, 0.2);
|
135
|
+
}
|
136
|
+
|
137
|
+
#discussion li > a:active, #discussion li > a.selected {
|
138
|
+
background-color: rgba(255, 0, 0, 0.2);
|
139
|
+
}
|
140
|
+
|
141
|
+
#discussion li > a {
|
142
|
+
font-family: "Alumni Sans", sans-serif;
|
143
|
+
font-size: larger;
|
144
|
+
}
|
145
|
+
|
146
|
+
#discussion li > a {
|
147
|
+
padding: 1rem;
|
148
|
+
|
149
|
+
display: flex;
|
150
|
+
justify-content: space-between;
|
151
|
+
align-items: center;
|
152
|
+
}
|
153
|
+
|
154
|
+
#discussion li > a span.count {
|
155
|
+
text-align: center;
|
156
|
+
font-family: sans-serif;
|
157
|
+
font-size: x-small;
|
158
|
+
font-weight: bold;
|
159
|
+
|
160
|
+
display: inline-block;
|
161
|
+
width: 13pt;
|
162
|
+
height: 13pt;
|
163
|
+
line-height: 13pt;
|
164
|
+
border-radius: 50%;
|
165
|
+
overflow: hidden;
|
166
|
+
background-color: #222;
|
167
|
+
color: #888;
|
168
|
+
}
|
data/public/details.css
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
body {
|
2
|
+
background-color: #222;
|
3
|
+
color: white;
|
4
|
+
}
|
5
|
+
|
6
|
+
a, a:hover, a:visited, a:active {
|
7
|
+
color: white;
|
8
|
+
text-decoration: none;
|
9
|
+
}
|
10
|
+
|
11
|
+
li > a {
|
12
|
+
padding: 1rem;
|
13
|
+
}
|
14
|
+
|
15
|
+
li > a:hover {
|
16
|
+
background-color: rgba(0, 0, 0, 0.2);
|
17
|
+
}
|
18
|
+
|
19
|
+
li > a:active, li > a.selected {
|
20
|
+
background-color: rgba(255, 0, 0, 0.2);
|
21
|
+
}
|
22
|
+
|
23
|
+
li > a > header {
|
24
|
+
font-family: "Alumni Sans", sans-serif;
|
25
|
+
font-size: larger;
|
26
|
+
}
|
27
|
+
|
28
|
+
li > a > div {
|
29
|
+
font-family: serif;
|
30
|
+
font-weight: lighter;
|
31
|
+
font-size: small;
|
32
|
+
color: #666;
|
33
|
+
}
|
34
|
+
|
35
|
+
li .author {
|
36
|
+
font-style: italic;
|
37
|
+
}
|
38
|
+
|
39
|
+
li > a > header + div {
|
40
|
+
margin-top: 1em;
|
41
|
+
}
|
data/public/index.css
ADDED
data/public/index.html
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<title>Forum</title>
|
7
|
+
<link rel="stylesheet" href="/wipe.css"/>
|
8
|
+
<link rel="stylesheet" href="/index.css"/>
|
9
|
+
</head>
|
10
|
+
<body style="margin: 0">
|
11
|
+
<div style="display: flex; height: 100%; width: 100%; position: absolute;">
|
12
|
+
|
13
|
+
<iframe style="width: 15rem; flex: 0" name="sidebar" src="/contents"></iframe>
|
14
|
+
|
15
|
+
<iframe style="width: 16rem; flex: 0" name="details" src="/posts"></iframe>
|
16
|
+
|
17
|
+
<iframe style="flex: 1" name="content" src="/post/1010"></iframe>
|
18
|
+
|
19
|
+
</div>
|
20
|
+
</body>
|
21
|
+
</html>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
nav {
|
2
|
+
position: fixed;
|
3
|
+
overflow: hidden;
|
4
|
+
top: 0;
|
5
|
+
left: 0;
|
6
|
+
width: 100%;
|
7
|
+
height: 32pt;
|
8
|
+
max-height: 32pt;
|
9
|
+
z-index: 1;
|
10
|
+
box-sizing: border-box;
|
11
|
+
|
12
|
+
display: flex;
|
13
|
+
align-items: center;
|
14
|
+
}
|
15
|
+
|
16
|
+
main {
|
17
|
+
padding-top: 32pt;
|
18
|
+
overflow-y: auto;
|
19
|
+
height: calc(100vh - 32pt);
|
20
|
+
position: relative;
|
21
|
+
z-index: 0;
|
22
|
+
}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
@import url('https://fonts.googleapis.com/css2?family=Alumni+Sans:wght@100..900&family=Roboto+Slab:wght@100..900&display=swap');
|
2
|
+
|
3
|
+
nav {
|
4
|
+
padding: 0 0.5rem;
|
5
|
+
background-color: #00000040;
|
6
|
+
backdrop-filter: blur(10px);
|
7
|
+
box-shadow: 0 1pt 2pt 0 rgba(0,0,0,0.5);
|
8
|
+
}
|
9
|
+
|
10
|
+
nav h1 {
|
11
|
+
font-family: "Alumni Sans", "Impact", sans-serif;
|
12
|
+
font-size: xxx-large;
|
13
|
+
text-transform: uppercase;
|
14
|
+
}
|
15
|
+
|
16
|
+
.empty {
|
17
|
+
color: #666;
|
18
|
+
font-style: italic;
|
19
|
+
text-align: center;
|
20
|
+
margin: 2em;
|
21
|
+
}
|
data/public/sidebar.css
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
/*
|
2
|
+
* General elements
|
3
|
+
*/
|
4
|
+
|
5
|
+
body {
|
6
|
+
/* background-color: #19164c; */
|
7
|
+
background-color: #2b2d30;
|
8
|
+
color: white;
|
9
|
+
}
|
10
|
+
|
11
|
+
a, a:hover, a:visited, a:active {
|
12
|
+
color: white;
|
13
|
+
text-decoration: none;
|
14
|
+
}
|
15
|
+
|
16
|
+
/*
|
17
|
+
* Structural elements
|
18
|
+
*/
|
19
|
+
|
20
|
+
main > header {
|
21
|
+
margin-top: 1rem !important;
|
22
|
+
width: calc(100% - 0.5rem);
|
23
|
+
}
|
24
|
+
|
25
|
+
main > header, li {
|
26
|
+
font-family: "Roboto Slab", sans-serif;
|
27
|
+
}
|
28
|
+
|
29
|
+
main section:last-child {
|
30
|
+
margin-bottom: 6rem;
|
31
|
+
}
|
32
|
+
|
33
|
+
section + section > h3 {
|
34
|
+
margin-top: 0.5rem;
|
35
|
+
}
|
36
|
+
|
37
|
+
h2, h3 > a, ol header, li > a {
|
38
|
+
padding: 0.4rem 0.5rem;
|
39
|
+
}
|
40
|
+
|
41
|
+
h2, h3, h4 {
|
42
|
+
font-family: "Alumni Sans", "Impact", sans-serif;
|
43
|
+
}
|
44
|
+
|
45
|
+
h2 {
|
46
|
+
color: #ff8b8b;
|
47
|
+
font-size: xxx-large;
|
48
|
+
}
|
49
|
+
|
50
|
+
h3 {
|
51
|
+
font-size: xx-large;
|
52
|
+
}
|
53
|
+
|
54
|
+
h4 {
|
55
|
+
color: #666;
|
56
|
+
font-size: x-large;
|
57
|
+
}
|
58
|
+
|
59
|
+
hr {
|
60
|
+
flex-grow: 1;
|
61
|
+
border: none;
|
62
|
+
border-top: 1px solid #666;
|
63
|
+
}
|
64
|
+
|
65
|
+
h4 + hr {
|
66
|
+
margin-left: 0.5em;
|
67
|
+
}
|
68
|
+
|
69
|
+
:not(h4) > hr {
|
70
|
+
margin-top: 0;
|
71
|
+
margin-bottom: 0;
|
72
|
+
}
|
73
|
+
|
74
|
+
ol {
|
75
|
+
margin-left: 1rem;
|
76
|
+
}
|
77
|
+
|
78
|
+
li > a:hover, h3 > a:hover {
|
79
|
+
background-color: rgba(0, 0, 0, 0.2);
|
80
|
+
}
|
81
|
+
|
82
|
+
li > a:active, h3 > a:active, li > a.selected, h3 > a.selected {
|
83
|
+
background-color: #1e1f22;
|
84
|
+
}
|
85
|
+
|
86
|
+
li > a, h3 > a {
|
87
|
+
display: flex;
|
88
|
+
justify-content: space-between;
|
89
|
+
align-items: center;
|
90
|
+
}
|
91
|
+
|
92
|
+
li span.count {
|
93
|
+
text-align: right;
|
94
|
+
font-family: sans-serif;
|
95
|
+
font-size: x-small;
|
96
|
+
color: #666;
|
97
|
+
}
|
data/public/thread.css
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
body {
|
2
|
+
background-color: #181818;
|
3
|
+
color: white;
|
4
|
+
}
|
5
|
+
|
6
|
+
section {
|
7
|
+
font-family: "Roboto Slab", sans-serif;
|
8
|
+
font-size: 85%;
|
9
|
+
line-height: 1.6;
|
10
|
+
}
|
11
|
+
|
12
|
+
section header {
|
13
|
+
font-family: serif;
|
14
|
+
font-weight: lighter;
|
15
|
+
font-size: small;
|
16
|
+
color: #888;
|
17
|
+
padding: 2pt 0.5rem 0 0.5rem;
|
18
|
+
|
19
|
+
opacity: 0.8;
|
20
|
+
border-top: 4pt solid #222;
|
21
|
+
background-size: 10px 100%;
|
22
|
+
}
|
23
|
+
|
24
|
+
.author {
|
25
|
+
font-style: italic;
|
26
|
+
color: #ff8b8b;
|
27
|
+
}
|
28
|
+
|
29
|
+
.date {
|
30
|
+
}
|
31
|
+
|
32
|
+
.comment {
|
33
|
+
padding: 0 0.5rem 1rem 0.5rem;
|
34
|
+
}
|
35
|
+
|
36
|
+
.comment p {
|
37
|
+
padding: 0.5em 0;
|
38
|
+
}
|
data/public/wipe.css
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
html, body, div, span, applet, object, iframe,
|
2
|
+
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
3
|
+
a, abbr, acronym, address, big, cite, code,
|
4
|
+
del, dfn, em, img, ins, kbd, q, s, samp,
|
5
|
+
small, strike, strong, sub, sup, tt, var,
|
6
|
+
b, u, i, center,
|
7
|
+
dl, dt, dd, ol, ul, li,
|
8
|
+
fieldset, form, label, legend,
|
9
|
+
table, caption, tbody, tfoot, thead, tr, th, td,
|
10
|
+
article, aside, canvas, details, embed,
|
11
|
+
figure, figcaption, footer, header, hgroup,
|
12
|
+
menu, nav, output, ruby, section, summary,
|
13
|
+
time, mark, audio, video {
|
14
|
+
margin: 0;
|
15
|
+
padding: 0;
|
16
|
+
border: 0;
|
17
|
+
vertical-align: baseline;
|
18
|
+
}
|
19
|
+
|
20
|
+
/* HTML5 display-role reset for older browsers */
|
21
|
+
article, aside, details, figcaption, figure,
|
22
|
+
footer, header, hgroup, menu, nav, section {
|
23
|
+
display: block;
|
24
|
+
}
|
25
|
+
|
26
|
+
body {
|
27
|
+
line-height: 1;
|
28
|
+
}
|
29
|
+
|
30
|
+
ol, ul {
|
31
|
+
list-style: none;
|
32
|
+
}
|
33
|
+
|
34
|
+
blockquote, q {
|
35
|
+
quotes: none;
|
36
|
+
}
|
37
|
+
|
38
|
+
blockquote:before, blockquote:after,
|
39
|
+
q:before, q:after {
|
40
|
+
content: none;
|
41
|
+
}
|
42
|
+
|
43
|
+
table {
|
44
|
+
border-collapse: collapse;
|
45
|
+
border-spacing: 0;
|
46
|
+
}
|
data/views/content.slim
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
doctype html
|
2
|
+
html
|
3
|
+
head
|
4
|
+
title Post Content
|
5
|
+
link rel="stylesheet" href="/wipe.css"
|
6
|
+
link rel="stylesheet" href="/main-layout.css"
|
7
|
+
link rel="stylesheet" href="/main-style.css"
|
8
|
+
link rel="stylesheet" href="/content.css"
|
9
|
+
|
10
|
+
css:
|
11
|
+
body {
|
12
|
+
display: flex;
|
13
|
+
height: 100%;
|
14
|
+
width: 100%;
|
15
|
+
position: absolute;
|
16
|
+
}
|
17
|
+
|
18
|
+
nav {
|
19
|
+
right: 35rem;
|
20
|
+
width: auto;
|
21
|
+
}
|
22
|
+
|
23
|
+
main {
|
24
|
+
flex: 1;
|
25
|
+
}
|
26
|
+
|
27
|
+
aside {
|
28
|
+
flex: 0 0 13rem;
|
29
|
+
width: 13rem;
|
30
|
+
}
|
31
|
+
|
32
|
+
iframe {
|
33
|
+
flex: 0 0 22rem;
|
34
|
+
width: 22rem;
|
35
|
+
}
|
36
|
+
|
37
|
+
aside > nav {
|
38
|
+
left: auto;
|
39
|
+
right: 22rem;
|
40
|
+
width: 13rem;
|
41
|
+
}
|
42
|
+
|
43
|
+
aside > section {
|
44
|
+
padding-top: 32pt;
|
45
|
+
overflow-y: auto;
|
46
|
+
height: calc(100vh - 32pt);
|
47
|
+
position: relative;
|
48
|
+
z-index: 0;
|
49
|
+
}
|
50
|
+
|
51
|
+
article {
|
52
|
+
min-width: 20em;
|
53
|
+
max-width: 55em;
|
54
|
+
margin-left: auto;
|
55
|
+
margin-right: auto;
|
56
|
+
padding-left: 2em;
|
57
|
+
padding-right: 2em;
|
58
|
+
}
|
59
|
+
|
60
|
+
body
|
61
|
+
main
|
62
|
+
nav: h1 Article
|
63
|
+
article
|
64
|
+
header
|
65
|
+
h1 =post.title
|
66
|
+
p.author #{@forum.author_name(post.author)}, #{post.meta["written"].strftime("%d %b %Y, %I:%M %p")}
|
67
|
+
|
68
|
+
==content
|
69
|
+
|
70
|
+
aside
|
71
|
+
nav: h1 Discussions
|
72
|
+
section#discussion
|
73
|
+
- if post.threads.empty?
|
74
|
+
.empty No discussions yet
|
75
|
+
|
76
|
+
ul
|
77
|
+
- post.threads.each do |tid, thread|
|
78
|
+
li: a[href="/post/#{post.id}/thread/#{tid}" target="comments"]
|
79
|
+
=thread.title
|
80
|
+
span.count =thread.comments.count
|
81
|
+
|
82
|
+
iframe name="comments" src="/post/#{post.id}/thread"
|
data/views/details.slim
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
doctype html
|
2
|
+
html
|
3
|
+
head
|
4
|
+
title Post List
|
5
|
+
link rel="stylesheet" href="/wipe.css"
|
6
|
+
link rel="stylesheet" href="/main-layout.css"
|
7
|
+
link rel="stylesheet" href="/main-style.css"
|
8
|
+
link rel="stylesheet" href="/details.css"
|
9
|
+
|
10
|
+
css:
|
11
|
+
li {
|
12
|
+
display: flex;
|
13
|
+
flex-direction: column;
|
14
|
+
box-sizing: border-box;
|
15
|
+
}
|
16
|
+
|
17
|
+
li > a {
|
18
|
+
display: block;
|
19
|
+
}
|
20
|
+
|
21
|
+
li, li > a > header, li > a > div {
|
22
|
+
width: 100%;
|
23
|
+
}
|
24
|
+
|
25
|
+
li > a > div {
|
26
|
+
display: flex;
|
27
|
+
justify-content: space-between;
|
28
|
+
}
|
29
|
+
|
30
|
+
li > a .date {
|
31
|
+
text-align: right;
|
32
|
+
}
|
33
|
+
|
34
|
+
|
35
|
+
body
|
36
|
+
nav
|
37
|
+
h1 Publications
|
38
|
+
|
39
|
+
main
|
40
|
+
- if posts.empty?
|
41
|
+
.empty No posts here yet
|
42
|
+
|
43
|
+
ul
|
44
|
+
- posts.each do |id, post|
|
45
|
+
li
|
46
|
+
a href="/post/#{id}" target="content"
|
47
|
+
header =post.title
|
48
|
+
div
|
49
|
+
span.author =@forum.author_name(post.author)
|
50
|
+
span.date =post.meta["written"].strftime("%d %b %Y")
|
data/views/sidebar.slim
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
doctype html
|
2
|
+
html
|
3
|
+
head
|
4
|
+
title Forum Contents
|
5
|
+
|
6
|
+
link rel="stylesheet" href="/wipe.css"
|
7
|
+
link rel="stylesheet" href="/main-layout.css"
|
8
|
+
link rel="stylesheet" href="/main-style.css"
|
9
|
+
link rel="stylesheet" href="/sidebar.css"
|
10
|
+
|
11
|
+
css:
|
12
|
+
header {
|
13
|
+
display: flex;
|
14
|
+
align-items: center;
|
15
|
+
}
|
16
|
+
|
17
|
+
h2, h4 {
|
18
|
+
flex-grow: 0;
|
19
|
+
flex-shrink: 0;
|
20
|
+
}
|
21
|
+
|
22
|
+
|
23
|
+
body
|
24
|
+
nav
|
25
|
+
h1 Contents
|
26
|
+
|
27
|
+
main
|
28
|
+
- @forum.each do |id, topic|
|
29
|
+
- if (part = (@forum.meta['parts'] || {})[id])
|
30
|
+
header title=part['details']
|
31
|
+
h2 =part['name']
|
32
|
+
hr
|
33
|
+
section
|
34
|
+
h3(title=topic.meta['details']): a[href="/posts/#{id}" target="details"] =topic.name
|
35
|
+
ol
|
36
|
+
- topic.each do |subid, sub|
|
37
|
+
- if (subpart = (topic.meta['parts'] || {})[subid])
|
38
|
+
header title=subpart['details']
|
39
|
+
- if subpart['name']
|
40
|
+
h4 =subpart['name']
|
41
|
+
hr
|
42
|
+
li(title=sub.meta['details'])
|
43
|
+
a[href="/posts/#{id}/#{subid}" target="details"]
|
44
|
+
=sub.name
|
45
|
+
- unless sub.posts.empty?
|
46
|
+
span.count =sub.posts.count
|
data/views/thread.slim
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
doctype html
|
2
|
+
html
|
3
|
+
head
|
4
|
+
title Post Content
|
5
|
+
link rel="stylesheet" href="/wipe.css"
|
6
|
+
link rel="stylesheet" href="/main-layout.css"
|
7
|
+
link rel="stylesheet" href="/main-style.css"
|
8
|
+
link rel="stylesheet" href="/thread.css"
|
9
|
+
|
10
|
+
css:
|
11
|
+
li header {
|
12
|
+
display: flex;
|
13
|
+
justify-content: space-between;
|
14
|
+
}
|
15
|
+
|
16
|
+
li header .date {
|
17
|
+
text-align: right;
|
18
|
+
}
|
19
|
+
|
20
|
+
body
|
21
|
+
nav: h1 Comments
|
22
|
+
main
|
23
|
+
- if thread.comments.empty?
|
24
|
+
.empty No comments yet
|
25
|
+
|
26
|
+
ol
|
27
|
+
- thread.comments.each do |cid, comment|
|
28
|
+
li: section
|
29
|
+
header
|
30
|
+
span.author =@forum.author_name(comment.author)
|
31
|
+
span.date =comment.created.strftime("%d %b %Y, %I:%M %p")
|
32
|
+
.comment ==comment.markdown_content
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: faun
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dr Maxim Orlovsky
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-05-17 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
- dr@orlovsky.ch
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".rubocop.yml"
|
21
|
+
- CHANGELOG.md
|
22
|
+
- README.md
|
23
|
+
- Rakefile
|
24
|
+
- lib/faun.rb
|
25
|
+
- lib/faun/version.rb
|
26
|
+
- public/content.css
|
27
|
+
- public/details.css
|
28
|
+
- public/index.css
|
29
|
+
- public/index.html
|
30
|
+
- public/main-layout.css
|
31
|
+
- public/main-style.css
|
32
|
+
- public/sidebar.css
|
33
|
+
- public/thread.css
|
34
|
+
- public/wipe.css
|
35
|
+
- views/content.slim
|
36
|
+
- views/details.slim
|
37
|
+
- views/sidebar.slim
|
38
|
+
- views/thread.slim
|
39
|
+
homepage: https://github.com/faun-forum/faun
|
40
|
+
licenses: []
|
41
|
+
metadata:
|
42
|
+
homepage_uri: https://github.com/faun-forum/faun
|
43
|
+
source_code_uri: https://github.com/faun-forum/faun
|
44
|
+
changelog_uri: https://github.com/faun-forum/faun/CHANGELOG.md
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 3.0.0
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubygems_version: 3.3.15
|
61
|
+
signing_key:
|
62
|
+
specification_version: 4
|
63
|
+
summary: Self-hosted directory-based forum and asset catalog for small communities
|
64
|
+
test_files: []
|