Zettelstore Contrib

Check-in [e1c9a3a7f6]
Login

Check-in [e1c9a3a7f6]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Zettel Social code has been moved to its own repository.
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: e1c9a3a7f6251e986faed7026a50b15449296264e4118e66a82dc2f36f311f35
User & Date: stern 2024-04-15 13:39:05
Context
2024-04-16
15:11
Presenter: update sx dependency ... (check-in: cbd7c11751 user: stern tags: trunk)
2024-04-15
13:39
Zettel Social code has been moved to its own repository. ... (check-in: e1c9a3a7f6 user: stern tags: trunk)
10:05
Social: adapt to sx changes ... (check-in: 2cffaa25ba user: stern tags: trunk)
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Deleted social/LICENSE.txt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
Copyright (c) 2024-present Detlef Stern

                          Licensed under the EUPL

Zettel Social is licensed under the European Union Public License, version
1.2 or later (EUPL v. 1.2). The license is available in the official
languages of the EU. The English version is included here. Please see
https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official
translations of the other languages.


-------------------------------------------------------------------------------


EUROPEAN UNION PUBLIC LICENCE v. 1.2
EUPL © the European Union 2007, 2016

This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
below) which is provided under the terms of this Licence. Any use of the Work,
other than as authorised under this Licence is prohibited (to the extent such
use is covered by a right of the copyright holder of the Work).

The Work is provided under the terms of this Licence when the Licensor (as
defined below) has placed the following notice immediately following the
copyright notice for the Work:

                          Licensed under the EUPL

or has expressed by any other means his willingness to license under the EUPL.

1. Definitions

In this Licence, the following terms have the following meaning:

— ‘The Licence’: this Licence.
— ‘The Original Work’: the work or software distributed or communicated by the
  Licensor under this Licence, available as Source Code and also as Executable
  Code as the case may be.
— ‘Derivative Works’: the works or software that could be created by the
  Licensee, based upon the Original Work or modifications thereof. This Licence
  does not define the extent of modification or dependence on the Original Work
  required in order to classify a work as a Derivative Work; this extent is
  determined by copyright law applicable in the country mentioned in Article
  15.
— ‘The Work’: the Original Work or its Derivative Works.
— ‘The Source Code’: the human-readable form of the Work which is the most
  convenient for people to study and modify.
— ‘The Executable Code’: any code which has generally been compiled and which
  is meant to be interpreted by a computer as a program.
— ‘The Licensor’: the natural or legal person that distributes or communicates
  the Work under the Licence.
— ‘Contributor(s)’: any natural or legal person who modifies the Work under the
  Licence, or otherwise contributes to the creation of a Derivative Work.
— ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
  the Work under the terms of the Licence.
— ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
  renting, distributing, communicating, transmitting, or otherwise making
  available, online or offline, copies of the Work or providing access to its
  essential functionalities at the disposal of any other natural or legal
  person.

2. Scope of the rights granted by the Licence

The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
sublicensable licence to do the following, for the duration of copyright vested
in the Original Work:

— use the Work in any circumstance and for all usage,
— reproduce the Work,
— modify the Work, and make Derivative Works based upon the Work,
— communicate to the public, including the right to make available or display
  the Work or copies thereof to the public and perform publicly, as the case
  may be, the Work,
— distribute the Work or copies thereof,
— lend and rent the Work or copies thereof,
— sublicense rights in the Work or copies thereof.

Those rights can be exercised on any media, supports and formats, whether now
known or later invented, as far as the applicable law permits so.

In the countries where moral rights apply, the Licensor waives his right to
exercise his moral right to the extent allowed by law in order to make
effective the licence of the economic rights here above listed.

The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
any patents held by the Licensor, to the extent necessary to make use of the
rights granted on the Work under this Licence.

3. Communication of the Source Code

The Licensor may provide the Work either in its Source Code form, or as
Executable Code. If the Work is provided as Executable Code, the Licensor
provides in addition a machine-readable copy of the Source Code of the Work
along with each copy of the Work that the Licensor distributes or indicates, in
a notice following the copyright notice attached to the Work, a repository
where the Source Code is easily and freely accessible for as long as the
Licensor continues to distribute or communicate the Work.

4. Limitations on copyright

Nothing in this Licence is intended to deprive the Licensee of the benefits
from any exception or limitation to the exclusive rights of the rights owners
in the Work, of the exhaustion of those rights or of other applicable
limitations thereto.

5. Obligations of the Licensee

The grant of the rights mentioned above is subject to some restrictions and
obligations imposed on the Licensee. Those obligations are the following:

Attribution right: The Licensee shall keep intact all copyright, patent or
trademarks notices and all notices that refer to the Licence and to the
disclaimer of warranties. The Licensee must include a copy of such notices and
a copy of the Licence with every copy of the Work he/she distributes or
communicates. The Licensee must cause any Derivative Work to carry prominent
notices stating that the Work has been modified and the date of modification.

Copyleft clause: If the Licensee distributes or communicates copies of the
Original Works or Derivative Works, this Distribution or Communication will be
done under the terms of this Licence or of a later version of this Licence
unless the Original Work is expressly distributed only under this version of
the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
(becoming Licensor) cannot offer or impose any additional terms or conditions
on the Work or Derivative Work that alter or restrict the terms of the Licence.

Compatibility clause: If the Licensee Distributes or Communicates Derivative
Works or copies thereof based upon both the Work and another work licensed
under a Compatible Licence, this Distribution or Communication can be done
under the terms of this Compatible Licence. For the sake of this clause,
‘Compatible Licence’ refers to the licences listed in the appendix attached to
this Licence. Should the Licensee's obligations under the Compatible Licence
conflict with his/her obligations under this Licence, the obligations of the
Compatible Licence shall prevail.

Provision of Source Code: When distributing or communicating copies of the
Work, the Licensee will provide a machine-readable copy of the Source Code or
indicate a repository where this Source will be easily and freely available for
as long as the Licensee continues to distribute or communicate the Work.

Legal Protection: This Licence does not grant permission to use the trade
names, trademarks, service marks, or names of the Licensor, except as required
for reasonable and customary use in describing the origin of the Work and
reproducing the content of the copyright notice.

6. Chain of Authorship

The original Licensor warrants that the copyright in the Original Work granted
hereunder is owned by him/her or licensed to him/her and that he/she has the
power and authority to grant the Licence.

Each Contributor warrants that the copyright in the modifications he/she brings
to the Work are owned by him/her or licensed to him/her and that he/she has the
power and authority to grant the Licence.

Each time You accept the Licence, the original Licensor and subsequent
Contributors grant You a licence to their contributions to the Work, under the
terms of this Licence.

7. Disclaimer of Warranty

The Work is a work in progress, which is continuously improved by numerous
Contributors. It is not a finished work and may therefore contain defects or
‘bugs’ inherent to this type of development.

For the above reason, the Work is provided under the Licence on an ‘as is’
basis and without warranties of any kind concerning the Work, including without
limitation merchantability, fitness for a particular purpose, absence of
defects or errors, accuracy, non-infringement of intellectual property rights
other than copyright as stated in Article 6 of this Licence.

This disclaimer of warranty is an essential part of the Licence and a condition
for the grant of any rights to the Work.

8. Disclaimer of Liability

Except in the cases of wilful misconduct or damages directly caused to natural
persons, the Licensor will in no event be liable for any direct or indirect,
material or moral, damages of any kind, arising out of the Licence or of the
use of the Work, including without limitation, damages for loss of goodwill,
work stoppage, computer failure or malfunction, loss of data or any commercial
damage, even if the Licensor has been advised of the possibility of such
damage. However, the Licensor will be liable under statutory product liability
laws as far such laws apply to the Work.

9. Additional agreements

While distributing the Work, You may choose to conclude an additional
agreement, defining obligations or services consistent with this Licence.
However, if accepting obligations, You may act only on your own behalf and on
your sole responsibility, not on behalf of the original Licensor or any other
Contributor, and only if You agree to indemnify, defend, and hold each
Contributor harmless for any liability incurred by, or claims asserted against
such Contributor by the fact You have accepted any warranty or additional
liability.

10. Acceptance of the Licence

The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
placed under the bottom of a window displaying the text of this Licence or by
affirming consent in any other similar way, in accordance with the rules of
applicable law. Clicking on that icon indicates your clear and irrevocable
acceptance of this Licence and all of its terms and conditions.

Similarly, you irrevocably accept this Licence and all of its terms and
conditions by exercising any rights granted to You by Article 2 of this
Licence, such as the use of the Work, the creation by You of a Derivative Work
or the Distribution or Communication by You of the Work or copies thereof.

11. Information to the public

In case of any Distribution or Communication of the Work by means of electronic
communication by You (for example, by offering to download the Work from
a remote location) the distribution channel or media (for example, a website)
must at least provide to the public the information requested by the applicable
law regarding the Licensor, the Licence and the way it may be accessible,
concluded, stored and reproduced by the Licensee.

12. Termination of the Licence

The Licence and the rights granted hereunder will terminate automatically upon
any breach by the Licensee of the terms of the Licence.

Such a termination will not terminate the licences of any person who has
received the Work from the Licensee under the Licence, provided such persons
remain in full compliance with the Licence.

13. Miscellaneous

Without prejudice of Article 9 above, the Licence represents the complete
agreement between the Parties as to the Work.

If any provision of the Licence is invalid or unenforceable under applicable
law, this will not affect the validity or enforceability of the Licence as
a whole. Such provision will be construed or reformed so as necessary to make
it valid and enforceable.

The European Commission may publish other linguistic versions or new versions
of this Licence or updated versions of the Appendix, so far this is required
and reasonable, without reducing the scope of the rights granted by the
Licence. New versions of the Licence will be published with a unique version
number.

All linguistic versions of this Licence, approved by the European Commission,
have identical value. Parties can take advantage of the linguistic version of
their choice.

14. Jurisdiction

Without prejudice to specific agreement between parties,

— any litigation resulting from the interpretation of this License, arising
  between the European Union institutions, bodies, offices or agencies, as
  a Licensor, and any Licensee, will be subject to the jurisdiction of the
  Court of Justice of the European Union, as laid down in article 272 of the
  Treaty on the Functioning of the European Union,
— any litigation arising between other parties and resulting from the
  interpretation of this License, will be subject to the exclusive jurisdiction
  of the competent court where the Licensor resides or conducts its primary
  business.

15. Applicable Law

Without prejudice to specific agreement between parties,

— this Licence shall be governed by the law of the European Union Member State
  where the Licensor has his seat, resides or has his registered office,
— this licence shall be governed by Belgian law if the Licensor has no seat,
  residence or registered office inside a European Union Member State.


                                  Appendix


‘Compatible Licences’ according to Article 5 EUPL are:

— GNU General Public License (GPL) v. 2, v. 3
— GNU Affero General Public License (AGPL) v. 3
— Open Software License (OSL) v. 2.1, v. 3.0
— Eclipse Public License (EPL) v. 1.0
— CeCILL v. 2.0, v. 2.1
— Mozilla Public Licence (MPL) v. 2
— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
  works other than software
— European Union Public Licence (EUPL) v. 1.1, v. 1.2
— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
  Reciprocity (LiLiQ-R+)

The European Commission may update this Appendix to later versions of the above
licences without producing a new version of the EUPL, as long as they provide
the rights granted in Article 2 of this Licence and protect the covered Source
Code from exclusive appropriation.

All other changes or additions to this Appendix require the production of a new
EUPL version.
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































































































































































































































































































































































































































































































































































































Deleted social/Makefile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
##-----------------------------------------------------------------------------
## Copyright (c) 2024-present Detlef Stern
##
## This file is part of Zettel Social
##
## Zettel Social is licensed under the latest version of the EUPL (European
## Union Public License). Please see file LICENSE.txt for your rights and
## obligations under this license.
##
## SPDX-License-Identifier: EUPL-1.2
## SPDX-FileCopyrightText: 2024-present Detlef Stern
##-----------------------------------------------------------------------------

.PHONY: linux

linux:
	GOOS=linux GOARCH=amd64 go build -tags osusergo,netgo -trimpath -ldflags "-w" .
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


































Changes to social/README.md.

1
2
3
4
5
6
7
8
9
*Zettel Social* is a service that allows an individual to interact with
others. In its initial incarnation it just publishes static HTML content,
but this will change. In the future, it might process content created by a
[Zettelstore](https://zettelstore.de). It might automatically produce several
RSS stream, handle ActivityPub requests, manage IndieWeb interactions, and so
on.

Zettel Social is at first the personal interaction platform of Detlef Stern. If
you find it useful, be happy about it.
<
<
<
<
<
<
|
<
<






1








*Zettel Social* is moved to its own repository <https://t73f.de/r/social>.


Deleted social/config/config.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

// Package config handles application configuration.
package config

import (
	"flag"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxreader"
	"zettelstore.de/contrib/social/site"
)

// Config stores all relevant configuration data.
type Config struct {
	WebPort      uint
	DocumentRoot string
	TemplateRoot string
	DataRoot     string
	Debug        bool
	Repositories RepositoryMap
	RejectUA     *regexp.Regexp
	ActionUA     []UAAction
	Site         *site.Site

	logger *slog.Logger
}

// MakeLogger creates a sub-logger for the given subsystem.
func (cfg *Config) MakeLogger(system string) *slog.Logger {
	return cfg.logger.With("system", system)
}

// RepositoryMap maps repository names to repository data.
type RepositoryMap map[string]*Repository

// Repository stores all details about a single source code repository.
type Repository struct {
	Name        *sx.Symbol
	Description string
	Type        *sx.Symbol
	RemoteURL   string
	NeedVanity  bool
}

// UAAction stores the regexp match and the resulting values to produce a HTTP response.
type UAAction struct {
	Regexp *regexp.Regexp
	Status int
}

// Command line flags
var (
	sConfig = flag.String("c", "", "name of configuration file")
	uPort   = flag.Uint("port", defaultPort, "http port")
	sRoot   = flag.String("doc-root", "", "path of document root")
	bDebug  = flag.Bool("debug", false, "enable debug mode")
)

const (
	defaultPort     = 23125
	defaultUAStatus = 429
)

// Initialize configuration values.
func (cfg *Config) Initialize(logger *slog.Logger) error {
	if !flag.Parsed() {
		flag.Parse()
	}
	cfg.WebPort = *uPort
	cfg.DocumentRoot = *sRoot
	cfg.TemplateRoot = ".template"
	cfg.DataRoot = ".data"
	cfg.Debug = *bDebug
	cfg.logger = logger

	if err := cfg.read(); err != nil {
		return err
	}
	if port := *uPort; port > 0 && port != defaultPort {
		cfg.WebPort = *uPort
	}
	if *bDebug {
		cfg.Debug = true
	}
	return nil
}

func (cfg *Config) read() error {
	if sConfig == nil || *sConfig == "" {
		return nil
	}
	file, err := os.Open(*sConfig)
	if err != nil {
		return err
	}
	defer file.Close()
	rdr := sxreader.MakeReader(file)
	objs, err := rdr.ReadAll()
	if err != nil {
		return err
	}
	for _, obj := range objs {
		if sx.IsNil(obj) {
			continue
		}
		lst, isPair := sx.GetPair(obj)
		if !isPair {
			continue
		}
		if sym, isSymbol := sx.GetSymbol(lst.Car()); isSymbol {
			if fn, found := cmdMap[sym.GetValue()]; found {
				if errFn := fn(cfg, sym, lst.Tail()); errFn != nil {
					return errFn
				}
			} else {
				cfg.logger.Warn("Unknown config", "entry", sym)
			}
			continue
		}
	}
	return nil
}

var cmdMap = map[string]func(*Config, *sx.Symbol, *sx.Pair) error{
	"DEBUG": parseDebug,
	"PORT":  parsePort,
	"DOCUMENT-ROOT": func(cfg *Config, sym *sx.Symbol, args *sx.Pair) error {
		return parseSetFilePath(&cfg.DocumentRoot, sym, args)
	},
	"TEMPLATE-ROOT": func(cfg *Config, sym *sx.Symbol, args *sx.Pair) error {
		return parseSetFilePath(&cfg.TemplateRoot, sym, args)
	},
	"DATA-ROOT": func(cfg *Config, sym *sx.Symbol, args *sx.Pair) error {
		return parseSetFilePath(&cfg.DataRoot, sym, args)
	},
	"SITE-LAYOUT": parseSiteLayout,
	"REPOS":       parseRepositories,
	"REJECT-UA":   parseRejectUA,
}

func parseDebug(cfg *Config, _ *sx.Symbol, args *sx.Pair) error {
	debug := true
	if args != nil {
		debug = sx.IsTrue(args.Car())
	}
	cfg.Debug = debug
	return nil
}

func parsePort(cfg *Config, sym *sx.Symbol, args *sx.Pair) error {
	val := args.Car()
	if iVal, isInt64 := val.(sx.Int64); isInt64 {
		if iVal > 0 {
			cfg.WebPort = uint(iVal)
			return nil
		}
		return fmt.Errorf("%v value <= 0: %d", sym, iVal)
	}
	return fmt.Errorf("%v is not Int64: %T/%v", sym, val, val)
}

func parseSetFilePath(target *string, sym *sx.Symbol, args *sx.Pair) error {
	s, err := parseString(sym, args)
	if err != nil {
		return err
	}
	*target = filepath.Clean(s)
	return nil
}

func parseString(obj sx.Object, args *sx.Pair) (string, error) {
	if sx.IsNil(args) {
		return "", fmt.Errorf("missing string value for %v", obj.GoString())
	}
	val := args.Car()
	if sVal, isString := sx.GetString(val); isString {
		return sVal.GetValue(), nil
	}
	return "", fmt.Errorf("expected string value in %v, but got: %T/%v", obj.GoString(), val, val)
}

func parseSiteLayout(cfg *Config, sym *sx.Symbol, args *sx.Pair) error {
	name, err := parseString(sym, args)
	if err != nil {
		return err
	}
	curr := args.Tail()
	path, err := parseString(sym, curr)
	if err != nil {
		return err
	}
	curr = curr.Tail()
	dummy := site.CreateRootNode("")
	if err = parseNodeAttributes(dummy, curr); err != nil {
		return err
	}
	curr = curr.Tail()
	rootTitle, err := parseString(sym, curr)
	if err != nil {
		return err
	}
	curr = curr.Tail()
	root := site.CreateRootNode(rootTitle).SetLanguage(dummy.Language())
	if err = parseNodeAttributes(root, curr); err != nil {
		return err
	}
	curr = curr.Tail()

	st, err := site.CreateSite(name, path, root)
	if err != nil {
		return err
	}
	if err = parseNodeChildren(sym, root, curr.Tail()); err != nil {
		return err
	}
	st = st.SetLanguage(dummy.Language())
	cfg.Site = st
	return nil
}
func parseNodeChildren(sym *sx.Symbol, parent *site.Node, args *sx.Pair) error {
	for curr := args; curr != nil; curr = curr.Tail() {
		if err := parseNode(sym, parent, curr); err != nil {
			return err
		}
	}
	return nil
}
func parseNode(sym *sx.Symbol, parent *site.Node, args *sx.Pair) error {
	car := args.Car()
	lst, isPair := sx.GetPair(car)
	if !isPair {
		return fmt.Errorf("node list expected in %v, but got: %T/%v", sym.GetValue(), car, car)
	}
	title, err := parseString(lst, lst)
	if err != nil {
		return err
	}
	curr := lst.Tail()
	path, err := parseString(lst, curr)
	if err != nil {
		return err
	}
	node, err := parent.CreateNode(title, path)
	if err != nil {
		return err
	}
	curr = curr.Tail()
	if !sx.IsNil(curr) {
		if err = parseNodeAttributes(node, curr); err != nil {
			return err
		}
		curr = curr.Tail()
	}
	return parseNodeChildren(sym, node, curr)
}
func parseNodeAttributes(node *site.Node, args *sx.Pair) error {
	attrsObj := args.Car()
	attrs, isAttrsPair := sx.GetPair(attrsObj)
	if !isAttrsPair {
		return fmt.Errorf("attribute list for node path %q expected, but got: %T/%v", node.Path(), attrsObj, attrsObj)
	}
	for curr := attrs; curr != nil; curr = curr.Tail() {
		attrObj := curr.Car()
		if attrObj.IsNil() {
			continue
		}
		attr, isPair := sx.GetPair(attrObj)
		if !isPair {
			return fmt.Errorf("attribute for node %q must be a list, but is: %T/%v", node.Path(), attrObj, attrObj)
		}
		keyObj := attr.Car()
		sym, isSymbol := sx.GetSymbol(keyObj)
		if !isSymbol {
			return fmt.Errorf("attribute key of node %q must be a symbol, but is: %T/%v", node.Path(), keyObj, keyObj)
		}
		val := attr.Cdr()
		if !sx.IsNil(val) {
			if next, isList := sx.GetPair(val); isList {
				val = next.Car()
			}
		}

		if sym.IsEqual(sx.MakeSymbol("invisible")) {
			node = node.SetInvisible()
		} else if sym.IsEqual(sx.MakeSymbol("language")) {
			sVal, isString := sx.GetString(val)
			if !isString || sVal.GetValue() == "" {
				return fmt.Errorf("language value for node %q must be a non-empty string, but is: %T/%v", node.Path(), val, val)
			}
			node = node.SetLanguage(sVal.GetValue())
		} else if sx.IsNil(val) {
			node.SetProperty(sym.GetValue(), "")
		} else {
			sVal, isString := sx.GetString(val)
			if !isString {
				return fmt.Errorf("attribute %q for node %q must be a string, but is: %T/%v", sym.GetValue(), node.Path(), val, val)
			}
			node.SetProperty(sym.GetValue(), sVal.GetValue())
		}
	}
	return nil
}

func parseRepositories(cfg *Config, sym *sx.Symbol, args *sx.Pair) error {
	for node := args; node != nil; node = node.Tail() {
		obj := node.Car()
		if sx.IsNil(obj) {
			continue
		}
		pair, isPair := sx.GetPair(obj)
		if !isPair {
			return fmt.Errorf("repository info list expected for %s, got: %T/%v", sym.GetValue(), obj, obj)
		}
		vec := pair.AsVector()
		if len(vec) != 4 && len(vec) != 5 {
			return fmt.Errorf("repository info list must be of length 4 or 5, but is: %d (%v)", len(vec), pair)
		}
		nameSym, isSymbol := sx.GetSymbol(vec[0])
		if !isSymbol {
			return fmt.Errorf("name component ist not a symbol, but: %T/%v", vec[0], vec[0])
		}
		name := nameSym.GetValue()
		if len(cfg.Repositories) > 0 {
			if _, found := cfg.Repositories[name]; found {
				return fmt.Errorf("repository %q already defined", name)
			}
		}
		descr, isString := sx.GetString(vec[1])
		if !isString {
			return fmt.Errorf("description component ist not a string, but: %T/%v", vec[1], vec[1])
		}
		repoTypeSym, isSymbol := sx.GetSymbol(vec[2])
		if !isSymbol {
			return fmt.Errorf("repository type component ist not a symbol, but: %T/%v", vec[2], vec[2])
		}
		remoteURL, isString := sx.GetString(vec[3])
		if !isString {
			return fmt.Errorf("remote URL component ist not a string, but: %T/%v", vec[3], vec[3])
		}
		var needVanity bool
		if len(vec) > 4 {
			needVanity = sx.IsTrue(vec[4])
		}
		repo := Repository{
			Name:        nameSym,
			Description: descr.GetValue(),
			Type:        repoTypeSym,
			RemoteURL:   remoteURL.GetValue(),
			NeedVanity:  needVanity,
		}
		if cfg.Repositories == nil {
			cfg.Repositories = RepositoryMap{name: &repo}
		} else {
			cfg.Repositories[name] = &repo
		}
	}
	return nil
}

func parseRejectUA(cfg *Config, _ *sx.Symbol, args *sx.Pair) error {
	var uaAction []UAAction
	for node := args; node != nil; node = node.Tail() {
		obj := node.Car()
		if sx.IsNil(obj) {
			continue
		}
		if sVal, isString := sx.GetString(obj); isString {
			re, err := regexp.Compile(sVal.GetValue())
			if err != nil {
				return err
			}
			uaAction = append(uaAction, UAAction{re, defaultUAStatus})
			continue
		}
		if pair, isPair := sx.GetPair(obj); isPair {
			first := pair.Car()
			if sVal, isString := sx.GetString(first); isString {
				re, err := regexp.Compile(sVal.GetValue())
				if err != nil {
					return err
				}
				status := defaultUAStatus

				pair = pair.Tail()
				second := pair.Car()
				if iVal, isInt64 := second.(sx.Int64); isInt64 && 100 <= iVal && iVal <= 999 {
					status = int(iVal)
				}
				uaAction = append(uaAction, UAAction{re, status})
				continue
			}
		}
	}
	if len(uaAction) == 0 {
		cfg.RejectUA = nil
		cfg.ActionUA = nil
		return nil
	}

	var expr strings.Builder
	for i, action := range uaAction {
		if i > 0 {
			expr.WriteByte('|')
		}
		expr.WriteString(action.Regexp.String())
	}
	rex, err := regexp.Compile(expr.String())
	if err != nil {
		return err
	}
	cfg.RejectUA = rex
	cfg.ActionUA = uaAction
	return nil
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
































































































































































































































































































































































































































































































































































































































































































































































































































































































Deleted social/go.mod.

1
2
3
4
5
module zettelstore.de/contrib/social

go 1.22

require t73f.de/r/sx v0.0.0-20240415085856-baa8c519ff55
<
<
<
<
<










Deleted social/go.sum.

1
2
t73f.de/r/sx v0.0.0-20240415085856-baa8c519ff55 h1:Ni91K4BVhalkV52zkW/QFIv6uBSnolqYYyZy09gzeLQ=
t73f.de/r/sx v0.0.0-20240415085856-baa8c519ff55/go.mod h1:G9pD1j2R6y9ZkPBb81mSnmwaAvTOg7r6jKp/OF7WeFA=
<
<




Deleted social/kernel/kernel.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

// Package kernel coordinates the different services.
package kernel

import (
	"log/slog"
	"os"
	"os/signal"
	"sync"
	"syscall"

	"zettelstore.de/contrib/social/config"
	"zettelstore.de/contrib/social/web/server"
)

// Kernel is the central server for the whole application.
// It consists of serveral services, which are implemented as servers as well.
type Kernel struct {
	logger     *slog.Logger
	wg         sync.WaitGroup
	interrupt  chan os.Signal
	cfg        *config.Config
	webService *server.Server
}

// NewKernel creates a new application server
func NewKernel(cfg *config.Config, h *server.Handler) *Kernel {
	k := Kernel{
		logger:     cfg.MakeLogger("kernel"),
		interrupt:  make(chan os.Signal, 5),
		cfg:        cfg,
		webService: server.CreateWebServer(cfg, h),
	}
	return &k
}

// Start the application server.
func (k *Kernel) Start() error {
	if err := k.webService.Start(); err != nil {
		return err
	}
	k.wg.Add(1)
	signal.Notify(k.interrupt, os.Interrupt, syscall.SIGTERM)
	go func() {
		// Wait for interrupt.
		sig := <-k.interrupt
		if strSig := sig.String(); strSig != "" {
			k.logger.Info("Shut down", "signal", strSig)
		}
		k.doShutdown()
		k.wg.Done()
	}()
	return nil
}

func (k *Kernel) doShutdown() {
	_ = k.webService.Stop()
}

// WaitForShutdown waits until a shutdown event is detected.
func (k *Kernel) WaitForShutdown() {
	k.wg.Wait()
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






















































































































































Deleted social/repository/repository.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

// Package repository stores application specific data.
package repository

import (
	"context"
	"io"
	"os"
	"path/filepath"
	"slices"
	"sync"

	"zettelstore.de/contrib/social/config"
)

// UACollector collects user agent data.
type UACollector struct {
	statusFn func(string) int
	mx       sync.Mutex
	uaSet    map[string]int
}

// MakeUACollector builds a new collector of user agent data.
func MakeUACollector(statusFn func(string) int) *UACollector {
	return &UACollector{
		statusFn: statusFn,
		uaSet:    map[string]int{},
	}
}

// Add an user agent and return if it is an allowed one.
func (uac *UACollector) AddUserAgent(_ context.Context, ua string) int {
	status := uac.statusFn(ua)
	uac.mx.Lock()
	if len(uac.uaSet) < 2048 {
		uac.uaSet[ua] = status
	}
	uac.mx.Unlock()
	return status
}

// GetAll collected user agent data, separated into allowed and unallowed ones.
func (uac *UACollector) GetAllUserAgents(context.Context) ([]string, []string) {
	uac.mx.Lock()
	resultTrue := make([]string, 0, len(uac.uaSet))
	resultFalse := make([]string, 0, len(uac.uaSet))
	for ua, status := range uac.uaSet {
		if status == 0 {
			resultTrue = append(resultTrue, ua)
		} else {
			resultFalse = append(resultFalse, ua)
		}
	}
	uac.mx.Unlock()
	slices.Sort(resultTrue)
	slices.Sort(resultFalse)
	return resultTrue, resultFalse
}

// FileReader fetches OPML data from a file.
type FileReader struct {
	dataRoot string
	opmlName string
}

// NewFileReader creates a new FileReader.
func NewFileReader(dataRoot string) *FileReader {
	return &FileReader{
		dataRoot: dataRoot,
		opmlName: filepath.Join(dataRoot, "feeds.opml"),
	}
}

// GetSxHTML returns the content of a SxHTML page file.
func (fr *FileReader) GetSxHTML(basename string) ([]byte, error) {
	return getFileContent(filepath.Join(fr.dataRoot, basename) + ".sxhtml")
}

// GetOPML returns the OPML data.
func (fr *FileReader) GetOPML() ([]byte, error) {
	return getFileContent(fr.opmlName)
}

func getFileContent(filename string) ([]byte, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	data, err := io.ReadAll(f)
	_ = f.Close()
	return data, err
}

// Repositories stores all source code repositories.
type Repositories struct {
	repos config.RepositoryMap
}

// NewRepositories creates a new repository repository ;)
func NewRepositories(cfgRepos config.RepositoryMap) *Repositories {
	repos := make(config.RepositoryMap, len(cfgRepos))
	for _, cfgRepo := range cfgRepos {
		repoData := *cfgRepo // make a copy
		repos[repoData.Name.GetValue()] = &repoData
	}
	return &Repositories{repos}
}

// GetAllRepositories returns an unsorted list of repositories.
func (rs *Repositories) GetAllRepositories() []*config.Repository {
	result := make([]*config.Repository, 0, len(rs.repos))
	for _, repo := range rs.repos {
		newRepo := *repo // make a copy
		result = append(result, &newRepo)
	}
	return result
}

// GetRepository returns the repository with the given name, or nil.
func (rs *Repositories) GetRepository(name string) *config.Repository {
	if repo, found := rs.repos[name]; found {
		return repo
	}
	return nil
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































































































































































































































































Deleted social/site/site.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

// Package site manages all information about the website and its subordinate nodes.
package site

import (
	"fmt"
	"strings"
)

// DefaultLanguage is the language value used as a default.
const DefaultLanguage = "en"

// Site manages the website as a whole.
type Site struct {
	name     string
	basepath string
	language string
	root     *Node
	nodes    map[string]*Node
}

// CreateSite creates a web site model with the given site name, base path, and
// a root node. Path must have a trailing forward slash.
func CreateSite(name, path string, root *Node) (*Site, error) {
	if path == "" {
		return nil, fmt.Errorf("site path must not be empty")
	}
	if path[len(path)-1] != '/' {
		return nil, fmt.Errorf("site path must end with '/', but got %q", path)
	}
	site := &Site{
		name:     name,
		basepath: path,
		language: DefaultLanguage,
		root:     root,
	}
	root.site = site
	return site, nil
}

// Name returns the name of the site.
func (st *Site) Name() string { return st.name }

// BasePath return the base path of the site.
func (st *Site) BasePath() string { return st.basepath }

// Root returns the root node of the site.
func (st *Site) Root() *Node { return st.root }

// SetLanguage sets the default language of the site.
func (st *Site) SetLanguage(lang string) *Site { st.language = lang; return st }

// Language returns the language code of the site.
func (st *Site) Language() string { return st.language }

// Path returns the absolute path of the given node.
func (st *Site) Path(n *Node) string { return st.basepath + n.Path() }

// BestNode returns the node that matches the given path at best. If an
// absolute path (starting with '/') is given, a nil result indicates
func (st *Site) BestNode(path string) *Node {
	if path == "" {
		return st.root
	}
	relpath := path
	if path[0] == '/' {
		relpath = path[1:]
	}
	return st.root.BestNode(relpath)
}

// GetNode returns the node with the given ID, or nil.
func (st *Site) GetNode(id string) *Node {
	if nodes := st.nodes; len(nodes) > 0 {
		if n, found := nodes[id]; found {
			return n
		}
	}
	return nil
}

// Node contains all data about a node within a site, identified by the path of
// its parent and its own path.
type Node struct {
	site       *Site
	parent     *Node
	children   []*Node
	title      string
	nodepath   string
	properties map[string]string
	language   string
	visible    bool
}

// CreateRootNode creates a root node to be given as a argument to [CreateSite].
func CreateRootNode(title string) *Node {
	return &Node{
		site:       nil,
		parent:     nil,
		children:   nil,
		title:      title,
		nodepath:   "",
		properties: nil,
		language:   DefaultLanguage,
		visible:    true,
	}
}

// CreateNode creates a child node with the given node title and node path. The
// nodepath must not be the empty string. The rune '/' is only allowed at the
// end of the node path. The node path must be unique within the parent.
func (n *Node) CreateNode(title, nodepath string) (*Node, error) {
	if nodepath == "" {
		return nil, fmt.Errorf("path of parent %q must not be empty", n.title)
	}
	if pos := strings.IndexRune(nodepath, '/'); pos >= 0 && pos < len(nodepath)-1 {
		return nil, fmt.Errorf("path %q contains '/'", nodepath)
	}
	for _, child := range n.children {
		if nodepath == child.nodepath {
			return nil, fmt.Errorf("path %q already used in %q", nodepath, n.nodepath)
		}
	}
	node := &Node{
		site:       n.site,
		parent:     n,
		title:      title,
		nodepath:   nodepath,
		properties: nil,
		language:   n.language,
		visible:    n.visible,
	}
	n.children = append(n.children, node)
	return node, nil
}

// Node property names
const (
	PropertyID = "id"
)

// SetProperty sets the given property key with the given value.
func (n *Node) SetProperty(key, val string) error {
	switch key {
	case PropertyID:
		if len(n.site.nodes) > 0 {
			if _, found := n.site.nodes[val]; found {
				return fmt.Errorf("node ID %q already given to another node", val)
			}
			n.site.nodes[val] = n
			return nil
		}
		n.site.nodes = map[string]*Node{val: n}
		return nil
	}
	if n.properties == nil {
		n.properties = map[string]string{key: val}
		return nil
	}
	n.properties[key] = val
	return nil
}

// GetProperty returns the property value of the given key, plus an indication,
// whether there was such a key/value.
func (n *Node) GetProperty(key string) (string, bool) {
	if props := n.properties; props != nil {
		val, found := props[key]
		return val, found
	}
	return "", false
}

// SetLanguage sets the language attribute of the node.
func (n *Node) SetLanguage(lang string) *Node {
	n.language = lang
	return n
}

// Language returns the language of this node.
func (n *Node) Language() string { return n.language }

// SetInvisible makes the node and its children invisible.
func (n *Node) SetInvisible() *Node {
	n.visible = false
	for _, child := range n.children {
		child.SetInvisible()
	}
	return n
}

// IsVisible reports wheter the node should be visible.
func (n *Node) IsVisible() bool { return n.visible }

// Parent returns the parent node.
func (n *Node) Parent() *Node { return n.parent }

// Ancestors returns all ancestor nodes, including the current node.
func (n *Node) Ancestors() (result []*Node) {
	for curr := n; curr != nil; curr = curr.parent {
		result = append(result, curr)
	}
	return result
}

// Title returns the node title.
func (n *Node) Title() string { return n.title }

// Children returns the ordered list of children nodes.
func (n *Node) Children() []*Node { return n.children }

// NodePath returns the local path of the node.
func (n *Node) NodePath() string { return n.nodepath }

// Path returns the full relative path of the node.
func (n *Node) Path() string {
	if parent := n.parent; parent != nil {
		return parent.Path() + n.nodepath
	}
	return n.nodepath
}

// BestNode returns the node that matches the given relative path the best.
// It never returns nil.
func (n *Node) BestNode(relpath string) *Node {
	for _, child := range n.children {
		childpath := child.nodepath
		if len(relpath) < len(childpath) {
			continue
		}
		if relpath == childpath {
			return child
		}
		if childpath[len(childpath)-1] == '/' && relpath[0:len(childpath)] == childpath {
			return child.BestNode(relpath[len(childpath):])
		}
	}
	return n
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






















































































































































































































































































































































































































































































































Deleted social/site/site_test.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

// Package site manages all information about the website and its subordinate nodes.
package site_test

import (
	"testing"

	"zettelstore.de/contrib/social/site"
)

func TestBestNode(t *testing.T) {
	t.Parallel()
	root := site.CreateRootNode("root")
	child1, err := root.CreateNode("child1", "child1/")
	if err != nil {
		panic(err)
	}
	grandchild1, err := child1.CreateNode("grand1", "grand")
	if err != nil {
		panic(err)
	}
	child2, err := root.CreateNode("child2", "child2")
	if err != nil {
		panic(err)
	}
	st, err := site.CreateSite("SITE", "/", root)
	if err != nil {
		panic(err)
	}

	node := st.BestNode("")
	if node != root {
		t.Error(node)
	}
	node = st.BestNode("/")
	if node != root {
		t.Error(node)
	}
	node = st.BestNode("child1")
	if node != root {
		t.Error(node)
	}
	node = st.BestNode("child1/")
	if node != child1 {
		t.Error(node)
	}
	node = st.BestNode("child1/grand")
	if node != grandchild1 {
		t.Error(node)
	}
	node = st.BestNode("child2")
	if node != child2 {
		t.Error(node)
	}
	node = st.BestNode("child2/")
	if node != root {
		t.Error(node)
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































































































































Deleted social/social.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

// Package main is the starting point for the zettel social service.
package main

import (
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"os"

	"t73f.de/r/sx/sxeval"
	"zettelstore.de/contrib/social/config"
	"zettelstore.de/contrib/social/kernel"
	"zettelstore.de/contrib/social/repository"
	"zettelstore.de/contrib/social/site"
	"zettelstore.de/contrib/social/usecase"
	"zettelstore.de/contrib/social/web/server"
	"zettelstore.de/contrib/social/web/wui"
)

func main() {
	var cfg config.Config
	logger := slog.Default()
	if err := cfg.Initialize(logger); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	if cfg.Debug {
		slog.SetLogLoggerLevel(slog.LevelDebug)
	}
	logger.Debug("Configuration", "port", cfg.WebPort, "docroot", cfg.DocumentRoot)

	uaColl := repository.MakeUACollector(createUAStatusFunc(&cfg))
	fr := repository.NewFileReader(cfg.DataRoot)
	rs := repository.NewRepositories(cfg.Repositories)
	h := server.NewHandler(cfg.MakeLogger("HTTP"), usecase.NewAddUserAgent(uaColl))
	if err := setupRouting(h, uaColl, fr, rs, &cfg); err != nil {
		fmt.Fprintln(os.Stderr, err)
		var execErr sxeval.ExecuteError
		if errors.As(err, &execErr) {
			execErr.PrintStack(os.Stderr, "", nil, "")
		}
		os.Exit(1)
	}
	k := kernel.NewKernel(&cfg, h)
	if err := k.Start(); err != nil {
		logger.Error("kernel", "error", err)
	}
	k.WaitForShutdown()
}

func createUAStatusFunc(cfg *config.Config) func(string) int {
	re := cfg.RejectUA
	uaAction := cfg.ActionUA
	if len(uaAction) == 0 {
		return func(string) int { return 0 }
	}
	return func(ua string) int {
		if re.MatchString(ua) {
			for _, action := range uaAction {
				if action.Regexp.MatchString(ua) {
					return action.Status
				}
			}
			return 500
		}
		return 0
	}
}

func setupRouting(
	h *server.Handler,
	uaColl *repository.UACollector,
	fr *repository.FileReader,
	rs *repository.Repositories,
	cfg *config.Config,
) error {
	webui, err := wui.NewWebUI(cfg.MakeLogger("WebUI"), cfg.TemplateRoot, cfg.Site)
	if err != nil {
		return err
	}

	userAgentsHandler := webui.MakeGetAllUAHandler(usecase.NewGetAllUserAgents(uaColl))
	var handlerMap = map[string]http.HandlerFunc{
		"blogroll":    webui.MakeBlogrollHandler(usecase.NewGetBlogroll(fr)),
		"header":      webui.MakeHeaderHandler(),
		"html":        webui.MakeGetPageHandler(usecase.NewGetPage(fr)),
		"repos":       webui.MakeGetAllRepositoriesHandler(usecase.NewGetAllRepositories(rs)),
		"test":        webui.MakeTestHandler(),
		"user-agents": userAgentsHandler,
		"vanity":      webui.MakeVanityURLHandler(usecase.NewGetRepository(rs)),
	}

	h.HandleFunc("GET /", webui.MakeDocumentHandler(cfg.DocumentRoot))
	if site := cfg.Site; site != nil {
		registerHandler(h, handlerMap, "/", site.Root())
	} else {
		h.HandleFunc("GET /.ua/{$}", userAgentsHandler)
	}
	return nil
}

func registerHandler(h *server.Handler, hd map[string]http.HandlerFunc, basepath string, n *site.Node) {
	path := basepath + n.NodePath()
	if handlerType, hasType := n.GetProperty("handler"); hasType {
		if handler, found := hd[handlerType]; found {
			h.HandleFunc("GET "+path, handler)
		} else {
			slog.Error("unknown handler", "type", handlerType)
		}
	}

	for _, child := range n.Children() {
		registerHandler(h, hd, path, child)
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


































































































































































































































































Deleted social/usecase/blogroll.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package usecase

import (
	"encoding/xml"
	"sort"
	"strings"
)

// BlogInfo stores relevant data about a blog.
type BlogInfo struct {
	Title string
	URL   string
}

// GetBlogrollPort is the port of this use case.
type GetBlogrollPort interface {
	GetOPML() ([]byte, error)
}

// GetBlogroll is the use case itself.
type GetBlogroll struct {
	port GetBlogrollPort
}

// NewGetBlogroll creates a new use case
func NewGetBlogroll(port GetBlogrollPort) GetBlogroll {
	return GetBlogroll{port: port}
}

// Run the use case.
func (gbr *GetBlogroll) Run() ([]BlogInfo, error) {
	data, err := gbr.port.GetOPML()
	if err != nil {
		return nil, err
	}
	var doc opmlDoc
	err = xml.Unmarshal(data, &doc)
	if err != nil {
		return nil, err
	}
	var list []BlogInfo
	for _, outline := range doc.Outlines {
		list = collectLinks(list, outline)
	}
	sort.Slice(list, func(i, j int) bool { return strings.ToLower(list[i].Title) < strings.ToLower(list[j].Title) })
	return list, nil
}

func collectLinks(list []BlogInfo, o opmlOutline) []BlogInfo {
	if siteURL := o.GetSiteURL(); siteURL != "" {
		if title := o.GetTitle(); title != "" && !strings.HasSuffix(title, "*") {
			list = append(list, BlogInfo{Title: o.GetTitle(), URL: siteURL})
		}
	}
	for _, outline := range o.Outlines {
		list = collectLinks(list, outline)
	}
	return list
}

// Specs: http://opml.org/spec2.opml
type opmlDoc struct {
	Outlines opmlOutlineSlice `xml:"body>outline"`
}

type opmlOutline struct {
	Title    string           `xml:"title,attr,omitempty"`
	Text     string           `xml:"text,attr"`
	FeedURL  string           `xml:"xmlUrl,attr,omitempty"`
	SiteURL  string           `xml:"htmlUrl,attr,omitempty"`
	Outlines opmlOutlineSlice `xml:"outline,omitempty"`
}

func (o *opmlOutline) GetTitle() string {
	if o.Title != "" {
		return o.Title
	}
	if o.Text != "" {
		return o.Text
	}
	return o.GetSiteURL()
}

func (o *opmlOutline) GetSiteURL() string {
	if o.SiteURL != "" {
		return o.SiteURL
	}
	return o.FeedURL
}

type opmlOutlineSlice []opmlOutline
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
















































































































































































































Deleted social/usecase/page.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package usecase

// GetPagePort is the port for the page use case.
type GetPagePort interface {
	GetSxHTML(basename string) ([]byte, error)
}

// GetPage is the use case for delivering page content.
type GetPage struct {
	port GetPagePort
}

// NewGetPage creates a new get-page use case.
func NewGetPage(port GetPagePort) GetPage {
	return GetPage{port: port}
}

// RunSxHTML returns a SxHTML page content.
func (gp GetPage) RunSxHTML(basename string) ([]byte, error) {
	return gp.port.GetSxHTML(basename)
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




































































Deleted social/usecase/repo.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package usecase

import (
	"slices"
	"strings"

	"zettelstore.de/contrib/social/config"
)

// Repository stores use case specific information about source code repositories.
type Repository struct {
	Name        string
	Description string
	Kind        string
	RemoteURL   string
	NeedVanity  bool
}

// GetAllRepositoriesPort contains all needed repository functions for the use case.
type GetAllRepositoriesPort interface {
	GetAllRepositories() []*config.Repository
}

// GetAllRepositories is the use case itself.
type GetAllRepositories struct {
	port GetAllRepositoriesPort
}

// NewGetAllRepositories creates a new use case.
func NewGetAllRepositories(port GetAllRepositoriesPort) GetAllRepositories {
	return GetAllRepositories{port: port}
}

// Run the use case.
func (gr GetAllRepositories) Run() []Repository {
	cfgRepos := gr.port.GetAllRepositories()
	result := make([]Repository, 0, len(cfgRepos))
	for _, cfgRepo := range cfgRepos {
		result = append(result, makeRepository(cfgRepo))
	}
	slices.SortFunc(result, func(a, b Repository) int {
		return strings.Compare(a.Name, b.Name)
	})
	return result
}

// GetRepositoryPort defines acces to the repository function to fetch a source code repository.
type GetRepositoryPort interface {
	GetRepository(string) *config.Repository
}

// GetRepository is the use case to retrieve a specific reposity.
type GetRepository struct {
	port GetRepositoryPort
}

// NewGetRepository creates a new use case.
func NewGetRepository(port GetRepositoryPort) GetRepository {
	return GetRepository{port: port}
}

// Run the use case.
func (uc GetRepository) Run(name string) (Repository, bool) {
	rRepo := uc.port.GetRepository(name)
	if rRepo == nil {
		return Repository{}, false
	}
	return makeRepository(rRepo), true
}

func makeRepository(cfgRepo *config.Repository) Repository {
	return Repository{
		Name:        cfgRepo.Name.GetValue(),
		Description: cfgRepo.Description,
		Kind:        cfgRepo.Type.GetValue(),
		RemoteURL:   cfgRepo.RemoteURL,
		NeedVanity:  cfgRepo.NeedVanity,
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
























































































































































































Deleted social/usecase/usecase.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

// Package usecase contains all use cases for this application.
package usecase
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






























Deleted social/usecase/useragent.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package usecase

import "context"

// -- USECASE AddUserAgent

// AddUserAgentPort is the port interface
type AddUserAgentPort interface {
	AddUserAgent(context.Context, string) int
}

// AddUserAgent is the use case itself
type AddUserAgent struct {
	port AddUserAgentPort
}

// NewAddUserAgent creates a new use case.
func NewAddUserAgent(port AddUserAgentPort) AddUserAgent {
	return AddUserAgent{port}
}

// Run executes the use case.
func (uc AddUserAgent) Run(ctx context.Context, uas []string) int {
	if len(uas) == 0 {
		return uc.port.AddUserAgent(ctx, "")
	}
	for _, ua := range uas {
		if status := uc.port.AddUserAgent(ctx, ua); status != 0 {
			return status
		}
	}
	return 0
}

// --- USECASE GetAllUserAgents

// GetAllUserAgentsPort is the interface used by this use case
type GetAllUserAgentsPort interface {
	GetAllUserAgents(context.Context) ([]string, []string)
}

// GetAllUserAgents is the data for this use case.
type GetAllUserAgents struct {
	port GetAllUserAgentsPort
}

// NewGetAllUserAgents creates a new use case of this type.
func NewGetAllUserAgents(port GetAllUserAgentsPort) GetAllUserAgents {
	return GetAllUserAgents{port}
}

// Run executes the use case
func (uc GetAllUserAgents) Run(ctx context.Context) ([]string, []string) {
	return uc.port.GetAllUserAgents(ctx)
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<








































































































































Deleted social/web/server/handler.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

// Package server handles all aspects of the HTTP web server.
package server

import (
	"fmt"
	"log/slog"
	"net/http"

	"zettelstore.de/contrib/social/usecase"
)

// Handler is the base handler of the HTTP web service.
type Handler struct {
	mux    *http.ServeMux
	addUA  usecase.AddUserAgent
	logger *slog.Logger
}

// NewHandler creates a new top-level handler to be used in the web service.
func NewHandler(logger *slog.Logger, ucAddUA usecase.AddUserAgent) *Handler {
	h := Handler{
		mux:    http.NewServeMux(),
		addUA:  ucAddUA,
		logger: logger,
	}
	return &h
}

// ServeHTTP serves the HTTP traffic for this server.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	status := h.addUA.Run(ctx, r.Header.Values("User-Agent"))
	if status == 0 {
		arw := appResponseWriter{w: w}
		h.mux.ServeHTTP(&arw, r)
		h.logger.DebugContext(ctx, "Serve", "status", arw.code, "method", r.Method, "length", arw.length, "url", r.URL)
	} else {
		Error(w, status)
		h.logger.DebugContext(ctx, "Serve", "status", status, "method", r.Method, "url", r.URL)
	}
}

// HandleFunc registers the handler function for the given pattern.
func (h *Handler) HandleFunc(pattern string, handler http.HandlerFunc) {
	h.mux.HandleFunc(pattern, handler)
}

// ------

// Error writes a standard error message.
func Error(w http.ResponseWriter, code int) {
	text := http.StatusText(code)
	if text == "" {
		text = fmt.Sprintf("Unknown HTTP status code %d", code)
	}
	http.Error(w, text, code)
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<












































































































































Deleted social/web/server/server.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

// Package server handles all aspects of the HTTP web server.
package server

import (
	"context"
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"time"

	"zettelstore.de/contrib/social/config"
)

// Server timeout values
const (
	shutdownTimeout = 5 * time.Second
	readTimeout     = 5 * time.Second
	writeTimeout    = 10 * time.Second
	idleTimeout     = 120 * time.Second
)

// Server encapsulates the HTTP web server
type Server struct {
	http.Server

	logger *slog.Logger
}

// CreateWebServer creates a new HTTP web server.
func CreateWebServer(cfg *config.Config, h *Handler) *Server {
	addr := fmt.Sprintf(":%v", cfg.WebPort)
	s := Server{
		http.Server{
			Addr:         addr,
			Handler:      h,
			ReadTimeout:  readTimeout,
			WriteTimeout: writeTimeout,
			IdleTimeout:  idleTimeout,
		},
		cfg.MakeLogger("Web"),
	}
	if cfg.Debug {
		s.ReadTimeout = 0
		s.WriteTimeout = 0
		s.IdleTimeout = 0
	}
	return &s
}

// Start the HTTP web server.
func (s *Server) Start() error {
	s.logger.Info("Start", "listen", s.Addr)
	ln, err := net.Listen("tcp", s.Addr)
	if err != nil {
		return err
	}
	go func() { _ = s.Serve(ln) }()
	return nil
}

// Stop the HTTP web server.
func (s *Server) Stop() error {
	ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
	defer cancel()
	return s.Shutdown(ctx)
}

type appResponseWriter struct {
	w      http.ResponseWriter
	code   int
	length int
}

func (arw *appResponseWriter) Header() http.Header { return arw.w.Header() }

func (arw *appResponseWriter) Write(data []byte) (int, error) {
	length, err := arw.w.Write(data)
	arw.length += length
	return length, err
}
func (arw *appResponseWriter) WriteHeader(code int) {
	header := arw.Header()
	if _, found := header["Server"]; !found {
		header.Add("Server", "Zettel Social")
	}
	arw.code = code
	arw.w.WriteHeader(code)
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<












































































































































































































Deleted social/web/urlbuilder.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package web

import (
	"net/url"
	"strings"
)

// URLBuilder helps to build URLs
type URLBuilder struct {
	prefix   string
	path     []string
	fragment string
	query    []urlQuery
}
type urlQuery struct{ key, val string }

// NewURLBuilder creates a new URL builder with the given prefix.
func NewURLBuilder(prefix string) *URLBuilder {
	if pl := len(prefix); pl > 0 && prefix[pl-1] == '/' {
		prefix = prefix[0 : pl-1]
	}
	return &URLBuilder{prefix: prefix}
}

// AddPath adds a new path element.
func (ub *URLBuilder) AddPath(p string) *URLBuilder {
	for len(p) > 0 && p[0] == '/' {
		p = p[1:]
	}
	if p != "" {
		ub.path = append(ub.path, p)
	}
	return ub
}

// SetFragment stores the fragment
func (ub *URLBuilder) SetFragment(frag string) *URLBuilder {
	ub.fragment = frag
	return ub
}

// AddQuery adds a new key/value query parameter
func (ub *URLBuilder) AddQuery(key, value string) *URLBuilder {
	ub.query = append(ub.query, urlQuery{key, value})
	return ub
}

// String constructs a string representation of the URL.
func (ub *URLBuilder) String() string {
	var sb strings.Builder

	if prefix := ub.prefix; prefix == "" && len(ub.path) == 0 {
		sb.WriteByte('/')
	} else {
		sb.WriteString(ub.prefix)
		for _, p := range ub.path {
			sb.WriteByte('/')
			sb.WriteString(url.PathEscape(p))
		}
	}
	if ub.fragment != "" {
		sb.WriteByte('#')
		sb.WriteString(ub.fragment)
	}
	for i, q := range ub.query {
		if i == 0 {
			sb.WriteByte('?')
		} else {
			sb.WriteByte('&')
		}
		sb.WriteString(q.key)
		if val := q.val; val != "" {
			sb.WriteByte('=')
			sb.WriteString(url.QueryEscape(val))
		}
	}
	return sb.String()
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






















































































































































































Deleted social/web/wui/bindings.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package wui

import (
	"slices"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxbuiltins"
	"t73f.de/r/sx/sxeval"
	"t73f.de/r/sx/sxhtml"
	"zettelstore.de/contrib/social/site"
)

func (wui *WebUI) createRootBinding() (*sxeval.Binding, error) {
	root := sxeval.MakeRootBinding(len(specials) + len(builtins))
	for _, syntax := range specials {
		if err := root.BindSpecial(syntax); err != nil {
			return nil, err
		}
	}
	for _, b := range builtins {
		if err := root.BindBuiltin(b); err != nil {
			return nil, err
		}
	}
	if err := wui.bindExtra(root); err != nil {
		return nil, err
	}
	return root, nil
}

var (
	specials = []*sxeval.Special{
		&sxbuiltins.QuoteS, &sxbuiltins.QuasiquoteS, // quote, quasiquote
		&sxbuiltins.UnquoteS, &sxbuiltins.UnquoteSplicingS, // unquote, unquote-splicing
		&sxbuiltins.DefunS, &sxbuiltins.DefDynS, // defun, defdyn
		&sxbuiltins.LetS,      // let
		&sxbuiltins.IfS,       // if
		&sxbuiltins.DefMacroS, // defmacro
		&sxbuiltins.BeginS,    // begin
	}
	builtins = []*sxeval.Builtin{
		&sxbuiltins.Equal,                // =
		&sxbuiltins.NullP,                // null?
		&sxbuiltins.Car, &sxbuiltins.Cdr, // car, cdr
		&sxbuiltins.Caar, &sxbuiltins.Cadr, // caar, cadr
		&sxbuiltins.Cdar,          // cdar
		&sxbuiltins.Cadar,         // cadar
		&sxbuiltins.LengthGreater, // length>
		&sxbuiltins.List,          // list
		&sxbuiltins.Nth,           // nth
		&sxbuiltins.Map,           // map
		&sxbuiltins.BoundP,        // bound?
	}
)

func (wui *WebUI) bindExtra(root *sxeval.Binding) error {
	err := root.BindBuiltin(&sxeval.Builtin{
		Name:     "make-url",
		MinArity: 0,
		MaxArity: -1,
		TestPure: sxeval.AssertPure,
		Fn0: func(_ *sxeval.Environment) (sx.Object, error) {
			return sx.MakeString(wui.NewURLBuilder().String()), nil
		},
		Fn1: func(_ *sxeval.Environment, arg sx.Object) (sx.Object, error) {
			ub := wui.NewURLBuilder()
			s, err := sxbuiltins.GetString(arg, 0)
			if err != nil {
				return nil, err
			}
			ub = ub.AddPath(s.GetValue())
			return sx.MakeString(ub.String()), nil
		},
		Fn2: func(_ *sxeval.Environment, arg0, arg1 sx.Object) (sx.Object, error) {
			ub := wui.NewURLBuilder()
			s, err := sxbuiltins.GetString(arg0, 0)
			if err != nil {
				return nil, err
			}
			ub = ub.AddPath(s.GetValue())
			s, err = sxbuiltins.GetString(arg1, 1)
			if err != nil {
				return nil, err
			}
			ub = ub.AddPath(s.GetValue())
			return sx.MakeString(ub.String()), nil
		},
		Fn: func(_ *sxeval.Environment, args sx.Vector) (sx.Object, error) {
			ub := wui.NewURLBuilder()
			for i := 0; i < len(args); i++ {
				sVal, err := sxbuiltins.GetString(args[i], i)
				if err != nil {
					return nil, err
				}
				ub = ub.AddPath(sVal.GetValue())
			}
			return sx.MakeString(ub.String()), nil
		},
	})
	if err != nil {
		return err
	}
	err = root.BindBuiltin(&sxeval.Builtin{
		Name:     "nav-list",
		MinArity: 1,
		MaxArity: 1,
		TestPure: sxeval.AssertPure,
		Fn1: func(_ *sxeval.Environment, arg sx.Object) (sx.Object, error) {
			sPath, errString := sxbuiltins.GetString(arg, 0)
			if errString != nil {
				return nil, errString
			}
			site := wui.site
			if site == nil {
				return sx.Nil(), nil
			}
			node := site.BestNode(sPath.GetValue())
			topLevel := buildNavList(site, node)
			return topLevel, nil
		},
	})
	return err
}

func buildNavList(st *site.Site, node *site.Node) *sx.Pair {
	if node.Parent() == nil && node.IsVisible() {
		// node is root node
		var lb sx.ListBuilder
		lb.Add(symUL)
		lb.Add(makeNavItem(st, node, node))
		for _, child := range node.Children() {
			if child.IsVisible() {
				lb.Add(makeNavItem(st, child, nil))
			}
		}
		return lb.List()
	}
	ancestors := node.Ancestors()
	slices.Reverse(ancestors)
	for i, n := range ancestors {
		if !n.IsVisible() {
			ancestors = ancestors[0:i]
			break
		}
	}
	if len(ancestors) == 0 {
		return nil
	}
	root := ancestors[0]
	var lb sx.ListBuilder
	lb.Add(symUL)
	lb.Add(makeNavItem(st, root, nil))
	buildNavLevel(st, &lb, ancestors[1:], root.Children())
	return lb.List()
}

func buildNavLevel(st *site.Site, lb *sx.ListBuilder, ancestors, children []*site.Node) {
	var root *site.Node
	if len(ancestors) > 0 {
		root = ancestors[0]
	}
	for _, child := range children {
		if !child.IsVisible() {
			continue
		}
		lb.Add(makeNavItem(st, child, root))
		if child != root {
			continue
		}
		if grandchildren := root.Children(); len(grandchildren) > 0 {
			var sub sx.ListBuilder
			sub.Add(symUL)
			if len(ancestors) > 1 {
				buildNavLevel(st, &sub, ancestors[1:], grandchildren)
			} else {
				for _, grand := range grandchildren {
					if grand.IsVisible() {
						sub.Add(makeNavItem(st, grand, nil))
					}
				}
			}
			lb.Add(sx.MakeList(
				symLI, sx.MakeList(sxhtml.SymAttr, sx.Cons(symClass, sx.MakeString("sub-menu"))), sub.List(),
			))
		}
	}
}

func makeNavItem(st *site.Site, node, active *site.Node) *sx.Pair {
	var lb sx.ListBuilder
	lb.Add(symLI)
	if node == active {
		lb.Add(sx.MakeList(sxhtml.SymAttr, sx.Cons(symClass, sx.MakeString("active"))))
	}
	lb.Add(makeSimpleLink(sx.MakeString(st.Path(node)), sx.MakeString(node.Title())))
	return lb.List()
}

var (
	symA     = sx.MakeSymbol("a")
	symClass = sx.MakeSymbol("class")
	symDD    = sx.MakeSymbol("dd")
	symDL    = sx.MakeSymbol("dl")
	symDT    = sx.MakeSymbol("dt")
	symHref  = sx.MakeSymbol("href")
	symLI    = sx.MakeSymbol("li")
	symP     = sx.MakeSymbol("p")
	symUL    = sx.MakeSymbol("ul")

	symHTMLPage = sx.MakeSymbol("html-page")
)

func makeSimpleLink(href, text sx.String) *sx.Pair {
	return sx.MakeList(
		symA,
		sx.MakeList(sxhtml.SymAttr, sx.Cons(symHref, href)),
		text)
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































































































































































































































































































































































































































































Deleted social/web/wui/blogroll.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package wui

import (
	"net/http"

	"t73f.de/r/sx"
	"zettelstore.de/contrib/social/usecase"
)

func (wui *WebUI) MakeBlogrollHandler(ucBlogroll usecase.GetBlogroll) http.HandlerFunc {
	symBlogroll := sx.MakeSymbol("blogroll")
	return func(w http.ResponseWriter, r *http.Request) {
		bloginfo, err := ucBlogroll.Run()
		if err != nil {
			wui.handleError(w, "Opml", err)
			return
		}
		var lb sx.ListBuilder
		for _, sl := range bloginfo {
			lb.Add(sx.Cons(sx.MakeString(sl.Title), sx.MakeString(sl.URL)))
		}

		rdat := wui.makeRenderData("blogroll", r)
		rdat.bindObject("BLOGROLL", lb.List())
		wui.renderTemplate(w, symBlogroll, rdat)
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
















































































Deleted social/web/wui/document.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package wui

import "net/http"

// MakeDocumentHandler creates a handler to serve static documents from a
// given root directory.
func (*WebUI) MakeDocumentHandler(root string) http.HandlerFunc {
	h := http.FileServer(http.Dir(root))
	return func(w http.ResponseWriter, r *http.Request) {
		h.ServeHTTP(w, r)
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































Deleted social/web/wui/header.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package wui

import (
	"net/http"
	"slices"

	"t73f.de/r/sx"
)

// MakeHeaderHandler returns a HTTP handler that shows all HTTP header.
func (wui *WebUI) MakeHeaderHandler() http.HandlerFunc {
	symHTTPHeader := sx.MakeSymbol("http-header")
	return func(w http.ResponseWriter, r *http.Request) {
		keys := make([]string, 0, len(r.Header))
		for key := range r.Header {
			keys = append(keys, key)
		}
		slices.Sort(keys)

		var headerList sx.ListBuilder
		headerList.Add(symDL)
		for _, key := range keys {
			headerList.Add(sx.MakeList(symDT, sx.MakeString(key)))
			for _, val := range r.Header[key] {
				headerList.Add(sx.MakeList(symDD, sx.MakeString(val)))
			}
		}

		rdat := wui.makeRenderData("header", r)
		rdat.bindObject("HEADER-DL", headerList.List())
		wui.renderTemplate(w, symHTTPHeader, rdat)
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




























































































Deleted social/web/wui/page.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package wui

import (
	"bytes"
	"net/http"
	"path"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxreader"
	"zettelstore.de/contrib/social/usecase"
)

// MakeGetPageHandler creates a new HTTP handler to show the content of a
// SxHTML file.
func (wui *WebUI) MakeGetPageHandler(ucGetPage usecase.GetPage) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		pagePath := r.PathValue("pagepath")
		if pagePath == "" {
			pagePath = path.Base(r.URL.Path)
		}
		content, err := ucGetPage.RunSxHTML(pagePath)
		if err != nil {
			wui.handleError(w, "Page", err)
			return
		}

		rdr := sxreader.MakeReader(bytes.NewReader(content))
		objs, err := rdr.ReadAll()
		if err != nil {
			wui.handleError(w, "Page", err)
			return
		}

		rdat := wui.makeRenderData("page", r)
		rdat.bindObject("HTML-CONTENT", sx.MakeList(objs...))
		wui.renderTemplate(w, symHTMLPage, rdat)
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






































































































Deleted social/web/wui/repos.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package wui

import (
	"net/http"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxhtml"
	"zettelstore.de/contrib/social/usecase"
	"zettelstore.de/contrib/social/web/server"
)

func (wui *WebUI) MakeGetAllRepositoriesHandler(uc usecase.GetAllRepositories) http.HandlerFunc {
	symRepos := sx.MakeSymbol("repo-list")
	var vanityPath string
	if site := wui.site; site != nil {
		if vanityNode := site.GetNode("go-vanity"); vanityNode != nil {
			vanityPath = strings.TrimSuffix(vanityNode.Path(), "/")
		}
	}
	return func(w http.ResponseWriter, r *http.Request) {
		repos := uc.Run()

		var lb sx.ListBuilder
		for _, repo := range repos {
			var repoVanity string
			if repo.NeedVanity && vanityPath != "" {
				ub := wui.NewURLBuilder().AddPath(vanityPath).AddPath(repo.Name)
				repoVanity = ub.String()
			}
			vec := sx.Vector{
				sx.MakeString(repo.Name),
				sx.MakeString(repoVanity),
				sx.MakeString(repo.Description),
				sx.MakeString(repo.RemoteURL),
			}
			lb.Add(vec)
		}
		rdat := wui.makeRenderData("repos", r)
		rdat.bindObject("REPOS", lb.List())
		wui.renderTemplate(w, symRepos, rdat)
	}
}

func (wui *WebUI) MakeVanityURLHandler(uc usecase.GetRepository) http.HandlerFunc {
	symVanity := sx.MakeSymbol("vanity")
	return func(w http.ResponseWriter, r *http.Request) {
		repoName := r.PathValue("repo")
		if repoName == "" {
			server.Error(w, http.StatusNotFound)
			return
		}
		repo, found := uc.Run(repoName)
		if !found {
			server.Error(w, http.StatusNotFound)
			return
		}
		site := wui.site
		if !repo.NeedVanity || site == nil {
			http.Redirect(w, r, repo.RemoteURL, http.StatusFound)
			return
		}

		node := site.BestNode(r.URL.Path)
		fullName := site.Name() + site.BasePath() + node.Path() + repo.Name

		rdat := wui.makeRenderData("vanity", r)
		rdat.bindString("NAME", repo.Name)
		rdat.bindString("FULL-NAME", fullName)
		q := r.URL.Query()
		if val := q.Get("go-get"); val == "1" {
			importContent := fullName + " " + repo.Kind + " " + repo.RemoteURL
			vanityMeta := sx.MakeList(
				sx.MakeSymbol("meta"),
				sx.MakeList(
					sxhtml.SymAttr,
					sx.Cons(sx.MakeSymbol("name"), sx.MakeString("go-import")),
					sx.Cons(sx.MakeSymbol("content"), sx.MakeString(importContent)),
				),
			)
			rdat.bindObject("META", sx.Cons(vanityMeta, sx.Nil()))
		}
		rdat.bindString("DESCRIPTION", repo.Description)
		rdat.bindString("REMOTE-URL", repo.RemoteURL)
		rdat.bindString("VCS", repo.Kind)
		wui.renderTemplate(w, symVanity, rdat)
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<










































































































































































































Deleted social/web/wui/sxc/000_prelude.sxc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2024-present Detlef Stern
;;;
;;; This file is part of Zettel Social.
;;;
;;; Zettel Social is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2024-present Detlef Stern
;;;----------------------------------------------------------------------------

;;; This file contains general definitions to work with Sx.
;;; It assumes bindings to NIL and T

;; not macro
(defmacro not (x) `(if ,x NIL T))

;; not= macro, to negate an equivalence
(defmacro not= args `(not (= ,@args)))

;; let* macro
;;
;; (let* (BINDING ...) EXPR ...), where SYMBOL may occur in later bindings.
(defmacro let* (bindings . body)
    (if (null? bindings)
        `(begin ,@body)
        `(let ((,(caar bindings) ,(cadar bindings)))
               (let* ,(cdr bindings) ,@body))))

;; cond macro
;;
;; (cond ((COND EXPR) ...))
(defmacro cond clauses
    (if (null? clauses)
        ()
        (let* ((clause (car clauses))
               (the-cond (car clause)))
              (if (= the-cond T)
                  (cadr clause)
                  `(if ,the-cond
                       ,(cadr clause)
                       (cond ,@(cdr clauses)))))))

;; and macro
;;
;; (and EXPR ...)
(defmacro and args
    (cond ((null? args)       T)
          ((null? (cdr args)) (car args))
          (T                  `(if ,(car args) (and ,@(cdr args))))))


;; or macro
;;
;; (or EXPR ...)
(defmacro or args
    (cond ((null? args)       NIL)
          ((null? (cdr args)) (car args))
          (T                  `(if ,(car args) T (or ,@(cdr args))))))

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




























































































































Deleted social/web/wui/sxc/010_template.sxc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2024-present Detlef Stern
;;;
;;; This file is part of Zettel Social.
;;;
;;; Zettel Social is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2024-present Detlef Stern
;;;----------------------------------------------------------------------------

;;; This file contains definitions that are needed by the templates.

;; define-template macro
;;
;; Defines a template to be used later.
;;
(defmacro define-template (name template)
    `(defdyn ,name () ,template))

;; render-template macro
;;
;; Renders the template object.
;;
(defmacro render-template (obj)
    `(,obj))
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
























































Deleted social/web/wui/sxc/020_layout.sxc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2024-present Detlef Stern
;;;
;;; This file is part of Zettel Social.
;;;
;;; Zettel Social is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2024-present Detlef Stern
;;;----------------------------------------------------------------------------

;;; Base layout

(define-template layout
`(@@@@
(html (@ (lang ,SITE-LANGUAGE))
(head
  (meta (@ (charset "utf-8")))
  (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0")))
  (meta (@ (name "generator") (content "Zettel Social")))
  (meta (@ (name "format-detection") (content "telephone=no")))
  ,@META
(title ,TITLE)
)
(body
  (header
    (h1 ,TITLE)
  )
  (main
    (div ,@CONTENT)
  )
)))
)

;;; Template to emit raw SxHTML content.

(define-template html-page HTML-CONTENT)
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































































Deleted social/web/wui/sxc/030_social.sxc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2024-present Detlef Stern
;;;
;;; This file is part of Zettel Social.
;;;
;;; Zettel Social is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2024-present Detlef Stern
;;;----------------------------------------------------------------------------

;;; SXC code for social nodes.

;;; Blogroll

(define-template blogroll
`((h1 ,TITLE)
  (ul ,@(map pair->link BLOGROLL))
 )
)

(defun pair->link (p)
    `(li (a (@ (href ,(cdr p))) ,(car p)))
)

;;; Node "Repo-List

(define-template repo-list
`((h1, TITLE)
  (table
    (thead
      (tr (th "Name") (th "Description") (th "Source")))
    (tbody ,@(map repo-entry REPOS)
    ))
)
)

(defun repo-entry (r)
  (let ((name (nth r 0))
        (vanity (nth r 1))
        (url (nth r 3)))
    `(tr (td ,(if vanity `(a (@ (href ,vanity)) ,name) name))
         (td ,(nth r 2))
         (td (a (@ (href ,url)) ,url)))
))

;;; Node "Vanity"

(define-template vanity
`((h1 ,NAME)
  (p ,DESCRIPTION)
  (dl
    (dt "Update go.mod:")
    (dd (code "go get " ,FULL-NAME))
    (dt "Import statement:")
    (dd (code "import \"" ,FULL-NAME "\""))
    (dt "Source:")
    (dd (a (@ (href ,REMOTE-URL)) ,REMOTE-URL))
    (dt "Version control:")
    (dd (code ,VCS))
    (dd "(make sure to install it on your computer)")
  )
)
)

;;; Node "User Agents"

(defun string->item (s) `(li ,s))

(define-template user-agents
`(
  (h1 ,TITLE)
  (ul ,@(map string->item ALLOWED-AGENTS))
  ,@(if (length> BLOCKED-AGENTS 0)
        `((h2 "Blocked")
          (ul ,@(map string->item BLOCKED-AGENTS))))
  (a (@ (href "?plain")) "plain")
 )
)

;;; Node "HTTP Header"

(define-template http-header `((h1 ,TITLE) ,HEADER-DL))
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<










































































































































































Deleted social/web/wui/template.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package wui

import (
	"bytes"
	"errors"
	"fmt"
	"net/http"
	"strconv"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxeval"
	"t73f.de/r/sx/sxhtml"
)

const contentName = "CONTENT"

func (wui *WebUI) renderTemplate(w http.ResponseWriter, templateSym *sx.Symbol, rdat *renderData) {
	wui.renderTemplateStatus(w, http.StatusOK, templateSym, rdat)
}
func (wui *WebUI) renderTemplateStatus(w http.ResponseWriter, code int, templateSym *sx.Symbol, rdat *renderData) {
	if err := wui.internRenderTemplateStatus(w, code, templateSym, rdat); err != nil {
		wui.handleError(w, "Render", err)
	}
}

func (wui *WebUI) internRenderTemplateStatus(w http.ResponseWriter, code int, templateSym *sx.Symbol, rdat *renderData) error {
	if err := rdat.err; err != nil {
		return err
	}
	h := w.Header()
	rdat.calcETag()
	wui.logger.Debug("Render", "If-None-Match", rdat.reqETag, "Etag", rdat.etag)
	for _, etag := range rdat.reqETag {
		if rdat.etag == etag {
			h.Set("Etag", rdat.etag)
			w.WriteHeader(http.StatusNotModified)
			return nil
		}
	}
	binding := rdat.bind
	wui.logger.Debug("Render", "binding", binding.Bindings())
	env := sxeval.MakeExecutionEnvironment(binding)
	if _, templateBound := env.Resolve(templateSym); templateBound {
		obj, err := env.Eval(sx.MakeList(sx.MakeSymbol("render-template"), templateSym))
		if err != nil {
			return err
		}
		wui.logger.Debug("Render", "content", obj)
		rdat.bindObject(contentName, obj)

	} else if obj, contentBound := env.Resolve(sx.MakeSymbol(contentName)); contentBound && !sx.IsNil(obj) {
		if _, isList := sx.GetPair(obj); !isList {
			obj = sx.MakeList(symP, obj)
		}
		obj = sx.Cons(obj, sx.Nil())
		wui.logger.Debug("Render", "obj", obj)
		rdat.bindObject(contentName, obj)

	} else if templateSym != nil {
		rdat.bindObject(
			contentName,
			sx.MakeList(
				symP,
				sx.MakeString("Template "),
				sx.MakeString(templateSym.GetValue()),
				sx.MakeString(" not found."),
			))
	} else {
		rdat.bindObject(contentName, sx.MakeList(symP, sx.MakeString("No template given.")))
	}
	obj, err := env.Eval(sx.MakeList(sx.MakeSymbol("render-template"), sx.MakeSymbol(nameLayout)))
	if err != nil {
		return err
	}
	wui.logger.Debug("Render", "sxhtml", obj)
	gen := sxhtml.NewGenerator().SetNewline()
	var sb bytes.Buffer
	_, err = gen.WriteHTML(&sb, obj)
	if err != nil {
		return err
	}
	content := sb.Bytes()
	setResponseHeader(h, "text/html; charset=utf-8", len(content), rdat.etag)
	w.WriteHeader(code)
	if _, err = w.Write(content); err != nil {
		wui.logger.Error("Unable to write HTML", "error", err)
	}
	return nil
}

func (wui *WebUI) MakeTestHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		rdat := wui.makeRenderData("test", r)
		rdat.bindObject("CONTENT", sx.MakeList(symP, sx.MakeString(fmt.Sprintf("Some content, url is: %q", r.URL))))
		wui.renderTemplate(w, nil, rdat)
	}
}

func (wui *WebUI) handleError(w http.ResponseWriter, subsystem string, err error) {
	wui.logger.Error(subsystem, "error", err)
	var execErr sxeval.ExecuteError
	if errors.As(err, &execErr) {
		var buf bytes.Buffer
		fmt.Fprintf(&buf, "Error: %v\n\n", err)
		execErr.PrintStack(&buf, "", wui.logger, subsystem)

		content := buf.Bytes()
		setResponseHeader(w.Header(), "text/plain; charset=utf-8", len(content), "")
		w.WriteHeader(http.StatusInternalServerError)
		_, _ = w.Write(content)
		return
	}
	http.Error(w, err.Error(), http.StatusInternalServerError)
}

func setResponseHeader(h http.Header, contentType string, contentLength int, etag string) {
	h.Set("Content-Type", contentType)
	h.Set("Content-Length", strconv.Itoa(contentLength))
	h.Set("X-Content-Type-Options", "nosniff")
	if etag != "" {
		h.Set("Etag", etag)
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
















































































































































































































































































Deleted social/web/wui/useragent.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package wui

import (
	"bytes"
	"fmt"
	"net/http"

	"t73f.de/r/sx"
	"zettelstore.de/contrib/social/usecase"
	"zettelstore.de/contrib/social/web/server"
)

// MakeGetAllUAHandler creates a new HTTP handler to display the list of found
// user agents.
func (wui *WebUI) MakeGetAllUAHandler(ucAllUA usecase.GetAllUserAgents) http.HandlerFunc {
	symUserAgents := sx.MakeSymbol("user-agents")
	return func(w http.ResponseWriter, r *http.Request) {
		uasT, uasF := ucAllUA.Run(r.Context())

		q := r.URL.Query()
		if len(q) == 0 {
			rdat := wui.makeRenderData("user-agent", r)
			rdat.bindObject("ALLOWED-AGENTS", stringsTosxList(uasT))
			rdat.bindObject("BLOCKED-AGENTS", stringsTosxList(uasF))
			wui.renderTemplate(w, symUserAgents, rdat)
			return
		}

		if q.Has("plain") {
			var buf bytes.Buffer
			for _, ua := range uasT {
				fmt.Fprintln(&buf, ua)
			}
			if len(uasF) > 0 && len(uasT) > 0 {
				fmt.Fprintln(&buf, "---")
			}
			for _, ua := range uasF {
				fmt.Fprintln(&buf, ua)
			}
			content := buf.Bytes()

			h := w.Header()
			etag := etagFromBytes(content)
			for _, reqEtag := range r.Header.Values("If-None-Match") {
				if etag == reqEtag {
					h.Set("Etag", etag)
					w.WriteHeader(http.StatusNotModified)
					return
				}
			}
			setResponseHeader(h, "text/plain; charset=utf-8", len(content), etag)
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write(content)
			return
		}

		server.Error(w, http.StatusBadRequest)
	}
}

func stringsTosxList(sl []string) *sx.Pair {
	var lb sx.ListBuilder
	for _, s := range sl {
		lb.Add(sx.MakeString(s))
	}
	return lb.List()
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
































































































































































Deleted social/web/wui/wui.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettel Social
//
// Zettel Social is licensed under the latest version of the EUPL (European
// Union Public License). Please see file LICENSE.txt for your rights and
// obligations under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

// Package wui adapts use cases with http web handlers.
package wui

import (
	"bytes"
	"crypto/sha256"
	"embed"
	"encoding/base64"
	"io"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxeval"
	"t73f.de/r/sx/sxreader"
	"zettelstore.de/contrib/social/site"
	"zettelstore.de/contrib/social/web"
)

// WebUI stores data relevant to the web user interface adapter.
type WebUI struct {
	logger      *slog.Logger
	baseBinding *sxeval.Binding
	site        *site.Site
}

// NewWebUI creates a new adapter for the web user interface.
func NewWebUI(logger *slog.Logger, templateRoot string, st *site.Site) (*WebUI, error) {
	wui := WebUI{
		logger: logger,
		site:   st,
	}
	rootBinding, err := wui.createRootBinding()
	if err != nil {
		return nil, err
	}
	_ = rootBinding.Bind(sx.MakeSymbol("NIL"), sx.Nil())
	_ = rootBinding.Bind(sx.MakeSymbol("T"), sx.MakeSymbol("T"))
	rootBinding.Freeze()

	codeBinding := rootBinding.MakeChildBinding("code", 128)
	env := sxeval.MakeExecutionEnvironment(codeBinding)
	if err = wui.evalCode(env); err != nil {
		return nil, err
	}
	if err = wui.compileAllTemplates(env, templateRoot); err != nil {
		return nil, err
	}
	codeBinding.Freeze()
	wui.baseBinding = codeBinding
	return &wui, nil
}

// NewURLBuilder creates a new URL builder for this web user interface.
func (wui *WebUI) NewURLBuilder() *web.URLBuilder {
	if st := wui.site; st != nil {
		return web.NewURLBuilder(st.BasePath())
	}
	return web.NewURLBuilder("")
}

func (wui *WebUI) makeRenderData(name string, r *http.Request) *renderData {
	rdat := renderData{
		reqETag: r.Header.Values("If-None-Match"),
		err:     nil,
		bind:    wui.baseBinding.MakeChildBinding(name, 128),
		etag:    "",
	}
	rdat.bindObject("META", sx.Nil())
	urlPath := r.URL.Path
	rdat.bindString("URL-PATH", urlPath)

	if st := wui.site; st != nil {
		rdat.bindString("SITE-LANGUAGE", st.Language())
		rdat.bindString("SITE-NAME", st.Name())
		node := st.BestNode(urlPath)
		rdat.bindString("TITLE", node.Title())
		rdat.bindString("LANGUAGE", node.Language())
	} else {
		rdat.bindString("SITE-LANGUAGE", site.DefaultLanguage)
		rdat.bindString("SITE-NAME", "Site without a name")
		rdat.bindString("TITLE", "Welcome")
		rdat.bindString("LANGUAGE", site.DefaultLanguage)
	}
	return &rdat
}

type renderData struct {
	reqETag []string
	err     error
	bind    *sxeval.Binding
	etag    string
}

func (rdat *renderData) bindObject(key string, obj sx.Object) {
	if rdat.err == nil {
		rdat.err = rdat.bind.Bind(sx.MakeSymbol(key), obj)
	}
}
func (rdat *renderData) bindString(key, val string) {
	if rdat.err == nil {
		rdat.err = rdat.bind.Bind(sx.MakeSymbol(key), sx.MakeString(val))
	}
}

func (rdat *renderData) calcETag() {
	var buf bytes.Buffer
	for _, sym := range rdat.bind.Symbols() {
		val, found := rdat.bind.Lookup(sym)
		if !found {
			continue
		}
		buf.WriteString(sym.GetValue())
		buf.WriteString(val.GoString())
	}
	rdat.etag = etagFromBytes(buf.Bytes())
}

func etagFromBytes(content []byte) string {
	h := sha256.Sum256(content)
	return "\"zs-" + base64.RawStdEncoding.EncodeToString(h[:]) + "\""
}

//go:embed sxc/*.sxc
var fsSxc embed.FS

func (wui *WebUI) evalCode(env *sxeval.Environment) error {
	entries, errFS := fsSxc.ReadDir("sxc")
	if errFS != nil {
		return errFS
	}
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}
		filename := filepath.Join("sxc", entry.Name())
		wui.logger.Debug("Read", "filename", filename)
		content, err := fsSxc.ReadFile(filename)
		if err != nil {
			return err
		}
		rdr := sxreader.MakeReader(bytes.NewReader(content))
		if err := wui.evalReader(env, rdr); err != nil {
			return err
		}
	}
	return nil
}

func (wui *WebUI) evalReader(env *sxeval.Environment, rdr *sxreader.Reader) error {
	for {
		obj, err := rdr.Read()
		if err != nil {
			if err == io.EOF {
				break
			}
			return err
		}
		obj, err = env.Eval(obj)
		if err != nil {
			return err
		}
		wui.logger.Debug("Eval", "result", obj)
	}
	return nil
}

// Template names
const (
	nameLayout = "layout"
)

// compileAllTemplates compiles (parses, reworks) all needed templates.
func (wui *WebUI) compileAllTemplates(env *sxeval.Environment, dir string) error {
	for _, name := range []string{nameLayout} {
		if err := wui.evalTemplate(env, dir, name); err != nil {
			return err
		}
	}
	return nil
}

func (wui *WebUI) evalTemplate(env *sxeval.Environment, dir, name string) error {
	filename := filepath.Join(dir, name+".sxc")
	f, err := os.Open(filename)
	if err != nil {
		return nil
	}
	defer f.Close()
	rdr := sxreader.MakeReader(f)
	return wui.evalReader(env, rdr)
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






























































































































































































































































































































































































































Changes to www/index.wiki.

1
2
3
4

5
6
7
<title>Home</title>

This repository contains contributed software for
[https://zettelstore.de|Zettelstore]. Some of the software may be experimental.


  *  [/dir?ci=tip&name=presenter|Zettel Presenter]: create a web-based presentation from some zettel.
  *  [/dir?ci=tip&name=social|Zettel Social]: a personal service for social interactions.




>


<
1
2
3
4
5
6
7

<title>Home</title>

This repository contains contributed software for
[https://zettelstore.de|Zettelstore]. Some of the software may be experimental.
Some may move into its own repository, if it proved its usefulness.

  *  [/dir?ci=tip&name=presenter|Zettel Presenter]: create a web-based presentation from some zettel.