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 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
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ Style/StringLiteralsInInterpolation:
8
+ EnforcedStyle: double_quotes
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-05-13
4
+
5
+ - Initial release
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rubocop/rake_task"
5
+
6
+ RuboCop::RakeTask.new
7
+
8
+ task default: :rubocop
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faun
4
+ VERSION = "0.0.1"
5
+ end
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
@@ -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
+ }
@@ -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
@@ -0,0 +1,3 @@
1
+ body {
2
+ background-color: black;
3
+ }
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
+ }
@@ -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
+ }
@@ -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"
@@ -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")
@@ -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: []