Zettelstore

Check-in Differences
Login

Check-in Differences

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

Difference From v0.22.0 To trunk

2025-11-06
13:52
Update dependencies ... (Leaf check-in: a3b149d584 user: stern tags: trunk)
2025-10-27
17:13
Parser: remove unneeded text encoder in Markdown parser ... (check-in: 6bb0627d73 user: stern tags: trunk)
2025-07-08
13:41
Fix manual w.r.t. key types ... (check-in: c997eeffe6 user: stern tags: trunk)
2025-07-07
08:54
Version 0.22.0 ... (check-in: 5336da12eb user: stern tags: trunk, release, v0.22.0)
07:31
Update dependencies ... (check-in: 46803d88ec user: stern tags: trunk)

Deleted .github/dependabot.yml.
1
2
3
4
5
6
7
8
9
10
11
12












-
-
-
-
-
-
-
-
-
-
-
-
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
  - package-ecosystem: "gomod" # See documentation for possible values
    directory: "/" # Location of package manifests
    schedule:
      interval: "daily"
    rebase-strategy: "disabled"
Changes to VERSION.
1


1
-
+
0.22.0
0.23.0-dev
Changes to cmd/cmd_file.go.
43
44
45
46
47
48
49

50
51
52
53
54
55
56
57

58
59
60
61
62
63
64
65
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

58

59
60
61
62
63
64
65







+







-
+
-







		zettel.Zettel{
			Meta:    m,
			Content: zettel.NewContent(inp.Src[inp.Pos:]),
		},
		string(m.GetDefault(meta.KeySyntax, meta.DefaultSyntax)),
		nil,
	)
	parser.Clean(z.Blocks)
	encdr := encoder.Create(
		api.Encoder(enc),
		&encoder.CreateParameter{Lang: string(m.GetDefault(meta.KeyLang, meta.ValueLangEN))})
	if encdr == nil {
		fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc)
		return 2, nil
	}
	_, err = encdr.WriteZettel(os.Stdout, z)
	if err = encdr.WriteZettel(os.Stdout, z); err != nil {
	if err != nil {
		return 2, err
	}
	fmt.Println()

	return 0, nil
}

Changes to cmd/cmd_run.go.
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
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








+
+


-
-
-
+
+
+

-
-
-
-
-
+
+
+
+
+

-
-
-
-
-
-
+
+
+
+
+
+



-
-
-
-
-
-
-
+
+
+
+
+
+
+

-
-
-
+
+
+








	webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager))
	if assetDir := kern.GetConfig(kernel.WebService, kernel.WebAssetDir).(string); assetDir != "" {
		const assetPrefix = "/assets/"
		webSrv.Handle(assetPrefix, http.StripPrefix(assetPrefix, http.FileServer(http.Dir(assetDir))))
		webSrv.Handle("/favicon.ico", wui.MakeFaviconHandler(assetDir))
	}

	const isAPI = true

	// Web user interface
	if !authManager.IsReadonly() {
		webSrv.AddListRoute('c', server.MethodGet, wui.MakeGetZettelFromListHandler(&ucQuery, &ucEvaluate, ucListRoles, ucListSyntax))
		webSrv.AddListRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCreateZettelHandler(
		webSrv.AddListRoute(!isAPI, 'c', server.MethodGet, wui.MakeGetZettelFromListHandler(&ucQuery, &ucEvaluate, ucListRoles, ucListSyntax))
		webSrv.AddListRoute(!isAPI, 'c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute(!isAPI, 'c', server.MethodGet, wui.MakeGetCreateZettelHandler(
			ucGetZettel, &ucCreateZettel, ucListRoles, ucListSyntax))
		webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel, ucGetAllZettel))
		webSrv.AddZettelRoute('d', server.MethodPost, wui.MakePostDeleteZettelHandler(&ucDelete))
		webSrv.AddZettelRoute('e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel, ucListRoles, ucListSyntax))
		webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(&ucUpdate))
		webSrv.AddZettelRoute(!isAPI, 'c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute(!isAPI, 'd', server.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel, ucGetAllZettel))
		webSrv.AddZettelRoute(!isAPI, 'd', server.MethodPost, wui.MakePostDeleteZettelHandler(&ucDelete))
		webSrv.AddZettelRoute(!isAPI, 'e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel, ucListRoles, ucListSyntax))
		webSrv.AddZettelRoute(!isAPI, 'e', server.MethodPost, wui.MakeEditSetZettelHandler(&ucUpdate))
	}
	webSrv.AddListRoute('g', server.MethodGet, wui.MakeGetGoActionHandler(&ucRefresh))
	webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex))
	webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler(&ucEvaluate, ucGetZettel))
	webSrv.AddListRoute('i', server.MethodGet, wui.MakeGetLoginOutHandler())
	webSrv.AddListRoute('i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate))
	webSrv.AddZettelRoute('i', server.MethodGet, wui.MakeGetInfoHandler(
	webSrv.AddListRoute(!isAPI, 'g', server.MethodGet, wui.MakeGetGoActionHandler(&ucRefresh))
	webSrv.AddListRoute(!isAPI, 'h', server.MethodGet, wui.MakeListHTMLMetaHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex))
	webSrv.AddZettelRoute(!isAPI, 'h', server.MethodGet, wui.MakeGetHTMLZettelHandler(&ucEvaluate, ucGetZettel))
	webSrv.AddListRoute(!isAPI, 'i', server.MethodGet, wui.MakeGetLoginOutHandler())
	webSrv.AddListRoute(!isAPI, 'i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate))
	webSrv.AddZettelRoute(!isAPI, 'i', server.MethodGet, wui.MakeGetInfoHandler(
		ucParseZettel, ucGetReferences, &ucEvaluate, ucGetZettel, ucGetAllZettel, &ucQuery))

	// API
	webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate))
	webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler())
	webSrv.AddZettelRoute('r', server.MethodGet, a.MakeGetReferencesHandler(ucParseZettel, ucGetReferences))
	webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion))
	webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh))
	webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex))
	webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel, ucParseZettel, ucEvaluate))
	webSrv.AddListRoute(isAPI, 'a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate))
	webSrv.AddListRoute(isAPI, 'a', server.MethodPut, a.MakeRenewAuthHandler())
	webSrv.AddZettelRoute(isAPI, 'r', server.MethodGet, a.MakeGetReferencesHandler(ucParseZettel, ucGetReferences))
	webSrv.AddListRoute(isAPI, 'x', server.MethodGet, a.MakeGetDataHandler(ucVersion))
	webSrv.AddListRoute(isAPI, 'x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh))
	webSrv.AddListRoute(isAPI, 'z', server.MethodGet, a.MakeQueryHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex))
	webSrv.AddZettelRoute(isAPI, 'z', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel, ucParseZettel, ucEvaluate))
	if !authManager.IsReadonly() {
		webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate))
		webSrv.AddZettelRoute('z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete))
		webSrv.AddListRoute(isAPI, 'z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute(isAPI, 'z', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate))
		webSrv.AddZettelRoute(isAPI, 'z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete))
	}

	if authManager.WithAuth() {
		webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager))
	}
}

Changes to docs/manual/00001004010000.zettel.
1
2
3
4
5
6
7

8
9
10
11
12
13
14
1
2
3
4
5
6

7
8
9
10
11
12
13
14






-
+







id: 00001004010000
title: Zettelstore startup configuration
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250627155145
modified: 20250828135622

The configuration file, specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options.
These cannot be stored in a [[configuration zettel|00001004020000]] because they are needed before Zettelstore can start or because of security reasons.
For example, Zettelstore needs to know in advance on which network address it must listen or where zettel are stored.
An attacker that is able to change the owner can do anything.
Therefore, only the owner of the computer on which Zettelstore runs can change this information.

47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
47
48
49
50
51
52
53

54
55
56
57
58
59
60







-







  During startup, __X__ is incremented, starting with one, until no key is found.
  This allows you to configure than one box.

  If no ''box-uri-1'' key is given, the overall effect will be the same as if only ''box-uri-1'' was specified with the value ""dir://.zettel"".
  In this case, even a key ''box-uri-2'' will be ignored.
; [!debug-mode|''debug-mode'']
: If set to [[true|00001006030500]], allows to debug the Zettelstore software (mostly used by Zettelstore developers).
  Disables any timeout values of the internal web server and does not send some security-related data.
  Sets [[''log-level''|#log-level]] to ""debug"".
  Enables [[''runtime-profiling''|#runtime-profiling]].

  Do not enable it for a production server.

  Default: ""false""
; [!default-dir-box-type|''default-dir-box-type'']
Changes to docs/manual/00001004051000.zettel.
1
2
3
4
5
6
7

8
9
10
11
12
13
14
1
2
3
4
5
6

7
8
9
10
11
12
13
14






-
+







id: 00001004051000
title: The ''run'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250627160733
modified: 20250828135353

=== ``zettelstore run``
This starts the web service.

```
zettelstore run [-a PORT] [-c CONFIGFILE] [-d DIR] [-debug] [-p PORT] [-r] [-v]
```
22
23
24
25
26
27
28
29
30


31
32
33
34
35
36
37
38
39
40
22
23
24
25
26
27
28


29
30

31

32
33
34
35
36
37
38







-
-
+
+
-

-








  Default: tries to read the following files in the ""current directory"": ''zettelstore.cfg'', ''zsconfig.txt'', ''zscfg.txt'', ''_zscfg'', and ''.zscfg''. 
; [!d|''-d DIR'']
: Specifies ''DIR'' as the directory that contains all zettel.

  Default is ''./zettel'' (''.\\zettel'' on Windows), where ''.'' denotes the ""current directory"".
; [!debug|''-debug'']
: Allows better debugging of the internal web server by disabling any timeout values.
  You should specify this only as a developer.
: Allows debugging of the internal web server.
  Same as setting [[''debug-mode''|00001004010000#debug-mode]] to ""true"".
  Especially do not enable it for a production server.

  [[https://blog.cloudflare.com/exposing-go-on-the-internet/#timeouts]] contains a good explanation for the usefulness of sensitive timeout values.
; [!p|''-p PORT'']
: Specifies the integer value ''PORT'' as the TCP port, where the Zettelstore web server listens for requests.

  Default: 23123.

  Zettelstore listens only on ''127.0.0.1'', e.g. only requests from the current computer will be processed.
  If you want to listen on network card to process requests from other computer, please use [[''listen-addr''|00001004010000#listen-addr]] of the configuration file as described below.
Changes to docs/manual/00001006030000.zettel.
1
2
3
4
5
6
7

8
9
10
11
12
13
14
15


16
17
18
19
20
21
22
1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24






-
+








+
+







id: 00001006030000
title: Supported Key Types
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250115172354
modified: 20250708154024

All [[supported metadata keys|00001006020000]] conform to a type.

User-defined metadata keys conform also to a type, based on the suffix of the key.

|=Suffix|Type
| ''-date'' | [[Timestamp|00001006034500]]
| ''-number'' | [[Number|00001006033000]]
| ''-ref''  | [[Identifier|00001006032000]]
| ''-refs''  | [[IdentifierSet|00001006032500]]
| ''-role'' | [[Word|00001006035500]]
| ''-time'' | [[Timestamp|00001006034500]]
| ''-url'' | [[URL|00001006035000]]
| ''-zettel''  | [[Identifier|00001006032000]]
| ''-zid''  | [[Identifier|00001006032000]]
| ''-zids''  | [[IdentifierSet|00001006032500]]
| any other suffix | [[EString|00001006031500]]
Changes to docs/manual/00001010000000.zettel.
1
2
3
4
5
6
7

8
9
10
11
12
13
14
1
2
3
4
5
6

7
8
9
10
11
12
13
14






-
+







id: 00001010000000
title: Security
role: manual
tags: #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102212014
modified: 20250828121450

Your zettel may contain sensitive content.
You probably want to ensure that only authorized persons can read and/or modify them.
Zettelstore ensures this in various ways.

=== Local first
The Zettelstore is designed to run on your local computer.
61
62
63
64
65
66
67
68

61
62
63
64
65
66
67

68







-
+
But you can put a server in front of it, which is able to handle encryption.
Most generic web server software allow this.

To enforce encryption, [[authenticated sessions|00001010040700]] are marked as secure by default.
If you still want to access the Zettelstore remotely without encryption, you must change the startup configuration.
Otherwise, authentication will not work.

* [[Use a server for encryption|00001010090100]]
* [[Use a server, also for encryption|00001010090100]]
Changes to docs/manual/00001010090100.zettel.
1
2

3
4
5
6
7

8
9
10
11
12
13
14
1

2
3
4
5
6

7
8
9
10
11
12
13
14

-
+




-
+







id: 00001010090100
title: External server to encrypt message transport
title: External server, also to encrypt message transport
role: manual
tags: #configuration #encryption #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250701125905
modified: 20250828135632

Since Zettelstore does not encrypt the messages it exchanges with its clients, you may need some additional software to enable encryption.

=== Public-key encryption
To enable encryption, you probably use some kind of encryption keys.
In most cases, you need to deploy a ""public-key encryption"" process, where your side publishes a public encryption key that only works with a corresponding private decryption key.
Technically, this is not trivial.
65
66
67
68
69
70
71













65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84







+
+
+
+
+
+
+
+
+
+
+
+
+
}
```
This will forward requests with the prefix ""/manual/"" to the running Zettelstore.
All other requests will be handled by Caddy itself.

In this case you must specify the [[startup configuration key ''url-prefix''|00001004010000#url-prefix]] with the value ""/manual/"".
This is to allow Zettelstore to ignore the prefix while reading web requests and to give the correct URLs with the given prefix when sending a web response.

=== Cross-origin protection

Zettelstore protects you against [[Cross-Site Request Forgery (CSRF)|https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF]].
The protection will work only if the external server forwards certain HTTP request headers to Zettelstore:

* [[''Sec-Fetch-Site''|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Site]] is read first and should contain the values ""same-origin"" or ""none"".
* [[''Origin''|https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin]] is read next, if ''Sec-Fetch-Site'' is either missing or contains other values.
  In this case ''Origin'' should contain the same host name as read via header [[''Host''|https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Host]].

Otherwise, an cross-origin request will be detected.

To debug the configuration of the external server, Zettelstore will log the received header values, if Zettelstore is run with the [[''-debug''|00001004051000#debug]] parameter.
Changes to docs/manual/00001012053800.zettel.
1
2
3
4
5
6
7

8
9
10
11
12
13
14
1
2
3
4
5
6

7
8
9
10
11
12
13
14






-
+







id: 00001012053800
title: API: Retrieve references of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20250415154139
modified: 20250416181257
modified: 20251013185705

The [[endpoint|00001012920000]] to retrieve references of a specific zettel is ''/r/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

A zettel may contain references to external material, in the form of an URI.
External material may be referenced via Zettelmarkup [[links|00001007040310]], [[inline embedding|00001007040320]], or a [[block transclusion|00001007031100]].
It may also be referenced within the [[metadata|00001006000000]] of a zettel, when a key of [[type|00001006030000]] [[URL|00001006035000]] is given.

37
38
39
40
41
42
43
44

45
46
47
48
49
50
51
37
38
39
40
41
42
43

44
45
46
47
48
49
50
51







-
+







https://commonmark.org/
https://xkcd.com/927/
https://github.github.com/gfm/
https://spec.commonmark.org/0.31.2/
https://github.com/yuin/goldmark
```

These examples should make clean, that no duplicates are removed and no sorting takes place.
These examples illustrate that the data remains unsorted and that duplicates are preserved.
The client must handle this, e.g.:
```sh
# curl 'http://127.0.0.1:23123/r/00001008010500' | sort -u
https://commonmark.org/
https://github.com/yuin/goldmark
https://github.github.com/gfm/
https://spec.commonmark.org/0.31.2/
Changes to docs/manual/00001012920525.zettel.
1
2
3
4
5
6
7

8
9
10
11
12
13
14
1
2
3
4
5
6

7
8
9
10
11
12
13
14






-
+







id: 00001012920525
title: SHTML Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230316181044
modified: 20250102180003
modified: 20250806192348

A zettel representation that is a [[s-expression|00001012930000]], syntactically similar to the [[Sz encoding|00001012920516]], but denotes [[HTML|00001012920510]] semantics.
It is derived from a XML encoding in s-expressions, called [[SXML|https://en.wikipedia.org/wiki/SXML]].

It is (relatively) easy to parse and contains everything to transform it into real HTML.
In contrast to HTML, SHTML is easier to parse and to manipulate.
For example, take a look at the SHTML encoding of this page, which is available via the ""Info"" sub-page of this zettel: 
28
29
30
31
32
33
34
35
36


28
29
30
31
32
33
34


35
36







-
-
+
+
A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms.
Before the last element of a list of at least two elements, a full stop character (""''.''"", U+002E) signal a pair as the last two elements.
This allows a more space economic storage of data.

An HTML tag like ``< a href="link">Text</a>`` is encoded in SHTML with a list, where the first element is a symbol named a the tag.
The second element is an optional encoding of the tag's attributes.
Further elements are either other tag encodings or a string.
The above tag is encoded as ``(a (@ (href . "link")) "Text")``.
Also possible is to encode the attribute without pairs: ``(a (@ (href "link")) "Text")`` (note the missing full stop character).
The above tag is encoded as ``(a ((href . "link")) "Text")``.
Also possible is to encode the attribute without pairs: ``(a ((href "link")) "Text")`` (note the missing full stop character).
Changes to docs/manual/00001012931000.zettel.
1
2
3
4
5
6
7

8
9
10
11
12
13
14
1
2
3
4
5
6

7
8
9
10
11
12
13
14






-
+







id: 00001012931000
title: Encoding of Sz
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403153903
modified: 20250102213403
modified: 20251021190931

Zettel in a [[Sz encoding|00001012920516]] are represented as a [[symbolic expression|00001012930000]].
To process these symbolic expressions, you need to know, how a specific part of a zettel is represented by a symbolic expression.

Basically, each part of a zettel is represented as a list, often a nested list.
The first element of that list is always a unique symbol, which denotes that part.
The meaning / semantic of all other elements depend on that symbol.
64
65
66
67
68
69
70

71
72

73
74
75
76
77
78

79

80










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







+

-
+





-
+

+
-
+
+
+
+
+
+
+
+
+
+

Either, there are no attributes.
These are specified by the empty list ''()''.
Or there are attributes.
In this case, the first element of the list must be the symbol ''quote'': ''(quote'' ''('' A,,1,, A,,2,, &hellip; A,,n,, '')'''')''.

=== Other
==== ''UNKNOWN''
A list with ''UNKNOWN'' as its first element signals an internal error during transforming a zettel into the Sz encoding.
It may be ignored, or it may produce an error.
It may be ignored or may result in an error.

:::syntax
__Unknown__ **=** ''(UNKNOWN'' Object &hellip; '')''.
:::

The list may only contain the symbol ''UNKNOWN'', or additionally an unlimited amount of other objects.
The list may only contain the symbol ''UNKNOWN'', or, in addition, an unlimited amount of other objects.

==== ''**xyz:NOT-FOUND**''
Similar, any symbol with the pattern ''**xyz:NOT-FOUND**'', where ''xyz'' is any string, signals an internal error.
Any symbol with the pattern ''**xyz:NOT-FOUND**'', where ""''xyz''"" is any string, signals an internal error.

==== ''*SPLICE-NODES*''
A list with ''*SPLICE-NODES*'' as its first element may occur during internal processing.
It signals that some processing routine intends to return more than one object instead of just one [[__BlockElement__|00001012931400]] or one [[__InlineElement__|00001012931600]].
The elements of such a list should be ""spliced"" into the parent list.

==== NIL (or ''()'')
Analogous to a splice node, which signal that more than one values is about to be returned, a ""NIL"" value (denoted as ""''()''"") signals that no object should be returned.
Therefore, a [[__BlockElement__|00001012931400]] or an [[__InlineElement__|00001012931600]] may be ""NIL"".
Changes to docs/manual/00001012931400.zettel.
1
2
3
4
5
6
7

8
9
10
11
12
13
14
1
2
3
4
5
6

7
8
9
10
11
12
13
14






-
+







id: 00001012931400
title: Encoding of Sz Block Elements
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161803
modified: 20250317143910
modified: 20251007144738

=== ''PARA''
:::syntax
__Paragraph__ **=** ''(PARA'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A paragraph is just a list of inline elements.

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
27
28
29
30
31
32
33

34
35

36
37

38
39
40





41
42
43
44
45
46
47







-
+

-
+

-
+


-
-
-
-
-







:::

=== ''ORDERED'', ''UNORDERED'', ''QUOTATION''
These three symbols are specifying different kinds of lists / enumerations: an ordered list, an unordered list, and a quotation list.
Their structure is the same.

:::syntax
__OrderedList__ **=** ''(ORDERED'' [[__Attributes__|00001012931000#attribute]] __ListElement__ &hellip; '')''.
__OrderedList__ **=** ''(ORDERED'' [[__Attributes__|00001012931000#attribute]] [[__Block__|00001012931000#block]] &hellip; '')''.

__UnorderedList__ **=** ''(UNORDERED'' [[__Attributes__|00001012931000#attribute]] __ListElement__ &hellip; '')''.
__UnorderedList__ **=** ''(UNORDERED'' [[__Attributes__|00001012931000#attribute]] [[__Block__|00001012931000#block]] &hellip; '')''.

__QuotationList__ **=** ''(QUOTATION'' [[__Attributes__|00001012931000#attribute]] __ListElement__ &hellip; '')''.
__QuotationList__ **=** ''(QUOTATION'' [[__Attributes__|00001012931000#attribute]] [[__Block__|00001012931000#block]] &hellip; '')''.
:::

:::syntax
__ListElement__ **=** [[__Block__|00001012931000#block]] **|** [[__Inline__|00001012931000#inline]].
:::
A list element is either a block or an inline.
If it is a block, it may contain a nested list.
=== ''DESCRIPTION''
:::syntax
__Description__ **=** ''(DESCRIPTION'' [[__Attributes__|00001012931000#attribute]] __DescriptionTerm__ __DescriptionValues__ __DescriptionTerm__ __DescriptionValues__ &hellip; '')''.
:::
A description is a sequence of one or more terms and values.

:::syntax
147
148
149
150
151
152
153
154

155
156
157
158
159
160

161
162
163
164
165
166
167
168
169
170
142
143
144
145
146
147
148

149
150
151
152
153
154

155
156
157
158
159
160
161
162
163
164
165







-
+





-
+










:::syntax
__ZettelVerbatim__ **=** ''(VERBATIM-ZETTEL'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as (nested) zettel content.

=== ''BLOB''
:::syntax
__BLOB__ **=** ''(BLOB'' [[__Attributes__|00001012931000#attribute]] ''('' [[__InlineElement__|00001012931600]] &hellip; '')'' String,,1,, String,,2,, '')''.
__BLOB__ **=** ''(BLOB'' [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A BLOB contains an image in block mode.
The inline elements states some description.
The first string contains the syntax of the image.
The second string contains the actual image.
If the syntax is ""SVG"", then the second string contains the SVG code.
If the syntax is ""''svg''"", then the second string contains the SVG code.
Otherwise the (binary) image data is encoded with base64.

=== ''TRANSCLUDE''
:::syntax
__Transclude__ **=** ''(TRANSCLUDE'' [[__Attributes__|00001012931000#attribute]] [[__Reference__|00001012931900]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A transclude list only occurs for a parsed zettel, but not for a evaluated zettel.
Evaluating a zettel also means that all transclusions are resolved.

__Reference__ denotes the zettel to be transcluded.
Changes to docs/manual/00001012931600.zettel.
1
2
3
4
5
6
7

8
9
10
11
12
13
14
1
2
3
4
5
6

7
8
9
10
11
12
13
14






-
+







id: 00001012931600
title: Encoding of Sz Inline Elements
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161845
modified: 20250313130428
modified: 20251007144653

=== ''TEXT''
:::syntax
__Text__ **=** ''(TEXT'' String '')''.
:::
Specifies the string as some text content, including white space characters.

45
46
47
48
49
50
51
52

53
54
55
56
57

58
59
60
61
62
63
64
45
46
47
48
49
50
51

52
53
54
55
56

57
58
59
60
61
62
63
64







-
+




-
+







If the string value is empty, it is an inline transclusion.
Otherwise it contains the syntax of an image.

The __InlineElement__ at the end of the list is interpreted as describing text.

=== ''EMBED-BLOB''
:::syntax
__EmbedBLOB__ **=** ''(EMBED-BLOB'' [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, '')''.
__EmbedBLOB__ **=** ''(EMBED-BLOB'' [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
If used if some processed image has to be embedded inside some inline material.
The first string specifies the syntax of the image content.
The second string contains the image content.
If the syntax is ""SVG"", the image content is not encoded further.
If the syntax is ""''svg''"", the image content is not encoded further.
Otherwise a base64 encoding is used.

=== ''CITE''
:::syntax
__CiteBLOB__ **=** ''(CITE'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The string contains the citation key.
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
137
138
139
140
141
142
143





144
145
146
147
148
149
150
151
152
153
154
155
156
157












-
-
-
-
-














-
-
-
-
-
The string contains text that should be treated as executable code.

:::syntax
__CommentLiteral__ **=** ''(LITERAL-COMMENT'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as an internal comment not to be interpreted further.

:::syntax
__HTMLLiteral__ **=** ''(LITERAL-HTML'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as HTML code.

:::syntax
__InputLiteral__ **=** ''(LITERAL-INPUT'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as input entered by a user.

:::syntax
__MathLiteral__ **=** ''(LITERAL-MATH'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as special code to be interpreted as mathematical formulas.

:::syntax
__OutputLiteral__ **=** ''(LITERAL-OUTPUT'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as computer output to be read by a user.

:::syntax
__ZettelLiteral__ **=** ''(LITERAL-ZETTEL'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as (nested) zettel content.
Changes to docs/manual/00001018000000.zettel.
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
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






-
+




















+
+
+
+







id: 00001018000000
title: Troubleshooting
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20211027105921
modified: 20250627130445
modified: 20250828140447

This page lists some problems and their solutions that may occur when using your Zettelstore.

=== Installation
* **Problem:** When you double-click on the Zettelstore executable icon, macOS complains that Zettelstore is an application from an unknown developer.
  Therefore, it will not start Zettelstore.
** **Solution:** Press the ''Ctrl'' key while opening the context menu of the Zettelstore executable with a right-click.
   A dialog is then opened where you can acknowledge that you understand the possible risks when you start Zettelstore.
   This dialog is only presented once for a given Zettelstore executable.
* **Problem:** When you double-click on the Zettelstore executable icon, Windows complains that Zettelstore is an application from an unknown developer.
** **Solution:** Windows displays a dialog where you can acknowledge possible risks and allow to start Zettelstore.

=== Authentication
* **Problem:** [[Authentication is enabled|00001010040100]] for a local running Zettelstore and there is a valid [[user zettel|00001010040200]] for the owner.
  But entering user name and password at the [[web user interface|00001014000000]] seems to be ignored, while entering a wrong password will result in an error message.
** **Explanation:** A local running Zettelstore typically means, that you are accessing the Zettelstore using a URL with schema ''http://'', and not ''https://'', for example ''http://localhost:23123/''.
   The difference between these two is the missing encryption of user name / password and for the answer of the Zettelstore if you use the ''http://'' schema.
   To be secure by default, the Zettelstore will not work in an insecure environment.
** **Solution 1:** If you are sure that your communication medium is safe, even if you use the ''http:/\/'' schema (for example, you are running the Zettelstore on the same computer you are working on, or if the Zettelstore is running on a computer in your protected local network), then you could add the entry ''insecure-cookie: true'' in your [[startup configuration|00001004010000#insecure-cookie]] file.
** **Solution 2:** If you are not sure about the security of your communication medium (for example, if unknown persons might use your local network), then you should run an [[external server|00001010090100]] in front of your Zettelstore to enable the use of the ''https://'' schema.
* **Problem:** [[Authentication is enabled|00001010040100]] for a Zettelstore running behind an external server (e.g. for encryption)
  Entering user name and password produces an error message, stating that a ""cross-origin request"" was detected.
** **Explanation:** The external web server dos not transfer needed HTTP header data to your Zettelstore.
** **Solution:** Try to run Zettelstore in debug mode, as described in [[Cross-origin protection|00001010090100#cross-origin-protection]].

=== Working with Zettel Files
* **Problem:** When you delete a zettel file by removing it from the ""disk"", e.g. by dropping it into the trash folder, by dragging into another folder, or by removing it from the command line, Zettelstore sometimes does not detect the change.
  If you access the zettel via Zettelstore, an error is reported.
** **Explanation:** Sometimes, the operating system does not tell Zettelstore about the removed zettel.
   This occurs mostly under MacOS.
** **Solution 1:** If you are running Zettelstore in [[""simple-mode""|00001004051100]] or if you have enabled [[''expert-mode''|00001004020000#expert-mode]], you are allowed to refresh the internal data by selecting ""Refresh"" in the Web User Interface (you can find in the menu ""Lists"").
Changes to go.mod.
1
2
3

4
5
6
7
8
9
10
11
12
13
14
15









16
17
18
19
20


21
1
2

3
4
5
6









7
8
9
10
11
12
13
14
15
16
17
18


19
20
21


-
+



-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+



-
-
+
+

module zettelstore.de/z

go 1.24
go 1.25

require (
	github.com/fsnotify/fsnotify v1.9.0
	github.com/yuin/goldmark v1.7.12
	golang.org/x/crypto v0.39.0
	golang.org/x/term v0.32.0
	t73f.de/r/sx v0.0.0-20250707071435-95b82f7d24bb
	t73f.de/r/sxwebs v0.0.0-20250707071704-c44197610ee4
	t73f.de/r/webs v0.0.0-20250707071548-227f3e99db55
	t73f.de/r/zero v0.0.0-20250703105709-bb38976d4455
	t73f.de/r/zsc v0.0.0-20250707072124-be388711ad2a
	t73f.de/r/zsx v0.0.0-20250707071920-5e29047e4db7
	github.com/yuin/goldmark v1.7.13
	golang.org/x/crypto v0.43.0
	golang.org/x/term v0.36.0
	t73f.de/r/sx v0.0.0-20251106134512-57bd2ba0e8e3
	t73f.de/r/sxwebs v0.0.0-20251106134640-a792e6dfefd8
	t73f.de/r/webs v0.0.0-20251106132628-d89a2b2c2373
	t73f.de/r/zero v0.0.0-20251106132433-8bb93fc3269f
	t73f.de/r/zsc v0.0.0-20251106134919-31f6048e2c2e
	t73f.de/r/zsx v0.0.0-20251106134735-30b13dc3ce8a
)

require (
	golang.org/x/sys v0.33.0 // indirect
	golang.org/x/text v0.26.0 // indirect
	golang.org/x/sys v0.37.0 // indirect
	golang.org/x/text v0.30.0 // indirect
)
Changes to go.sum.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24






















1
2






















3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24


-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
t73f.de/r/sx v0.0.0-20250707071435-95b82f7d24bb h1:cYvTOpaJinh/EPB7i8nx7PtT7hniuSP+NZr74P9U+fE=
t73f.de/r/sx v0.0.0-20250707071435-95b82f7d24bb/go.mod h1:uglbFdRHlcpQVVyCNh4Fd7jbKo8alGBCjRp0aZv8IIg=
t73f.de/r/sxwebs v0.0.0-20250707071704-c44197610ee4 h1:WbT8qQAQjqx1S0syci6yWi+YiNQFFYVBkOSfSYIRid4=
t73f.de/r/sxwebs v0.0.0-20250707071704-c44197610ee4/go.mod h1:zSel+qtBHV9NglxDHlFFPwvaetlZh4H0pLyd7OkpkpQ=
t73f.de/r/webs v0.0.0-20250707071548-227f3e99db55 h1:gl4XpbrzrtAFDY+4V9bixLTUruhzEcwvfKiZi1NqJY4=
t73f.de/r/webs v0.0.0-20250707071548-227f3e99db55/go.mod h1:b8/5E5Pe6WSWqh+T+sxLO5ZLiGVkuL5tgh86kx2OAIg=
t73f.de/r/zero v0.0.0-20250703105709-bb38976d4455 h1:TFRPPexX2WrwuF03hC+Be2ONx2bPzMMBlNDn0rk88eI=
t73f.de/r/zero v0.0.0-20250703105709-bb38976d4455/go.mod h1:Ovx7CYsjz45BNuIEMGZfqA7NdQxERydJqUGnOBoQaXQ=
t73f.de/r/zsc v0.0.0-20250707072124-be388711ad2a h1:p12BTQ8TdKZy6GcCRgVINKSeoMz0wBPcNG7ssYvLNrc=
t73f.de/r/zsc v0.0.0-20250707072124-be388711ad2a/go.mod h1:JnkeoahGBxNK0gDcTnAIfDP2rAP33BtnAU1/rCEewC8=
t73f.de/r/zsx v0.0.0-20250707071920-5e29047e4db7 h1:ERxpb1Hqln+NXoZDK6sqjmX3BzeoLO+O64f4bK0B6dk=
t73f.de/r/zsx v0.0.0-20250707071920-5e29047e4db7/go.mod h1:64/AjQ1GnEBoBhXI1D0bDMGDj7JCbtZUTT3WoA7kS0s=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
t73f.de/r/sx v0.0.0-20251106134512-57bd2ba0e8e3 h1:8EQK+viyafJrYKprKFjxv8KO80XLjH2Av5gaI24298g=
t73f.de/r/sx v0.0.0-20251106134512-57bd2ba0e8e3/go.mod h1:4hTEMWfmRB4ocR4ir0WuVoV+q0ed58pwGZwsznSXt1Q=
t73f.de/r/sxwebs v0.0.0-20251106134640-a792e6dfefd8 h1:4I4hAwUuMb4bDNqttF1ZXGbZRnTTBvgHeWZOdELUZw8=
t73f.de/r/sxwebs v0.0.0-20251106134640-a792e6dfefd8/go.mod h1:0rjepPI8mKx3vIe3+IVN3qJVr25uma2A6ozUDpnqbwY=
t73f.de/r/webs v0.0.0-20251106132628-d89a2b2c2373 h1:puhtswOd5A9+RzeTBRfuSsvhs0Nsn9e3PHM7zM4Yges=
t73f.de/r/webs v0.0.0-20251106132628-d89a2b2c2373/go.mod h1:n6yEgRO4XVZYi8F5ilHWgv7oFzVsRDZrXorCdXxdVqk=
t73f.de/r/zero v0.0.0-20251106132433-8bb93fc3269f h1:v334GYGrruo8wr9Wh9R/KahJr8gMX/Nbysg/wpgtAC8=
t73f.de/r/zero v0.0.0-20251106132433-8bb93fc3269f/go.mod h1:6TIoFD0Qn7oEE4GYUzA1cQzwrvhGAADYsm930FK6Yz0=
t73f.de/r/zsc v0.0.0-20251106134919-31f6048e2c2e h1:JEDbKtuem7NdzueTqPiJ7zhJBT1sue9c7+9Rsf4Oy50=
t73f.de/r/zsc v0.0.0-20251106134919-31f6048e2c2e/go.mod h1:WST5do8U/LEf6bprTRnvpLqNnW9qS7Am7qCKnFe4Br4=
t73f.de/r/zsx v0.0.0-20251106134735-30b13dc3ce8a h1:hPo18TPk3Wlmh34Vuj0DVGJN13TiZ8Mule18EvwYs1Y=
t73f.de/r/zsx v0.0.0-20251106134735-30b13dc3ce8a/go.mod h1:sQaYZqc37hSYmHENHrBqIiazhuoZc4Q4dpP9Wc6AM/I=
Changes to internal/ast/ast.go.
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
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









-
+
-

+


+
+



-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+


+
-
-
+
+
-
-
-
+
-
-
+
-
-
-
-
+
-
-
+
-
-
-
-
+
-
-
-
+
+
+
-
-
-
-

-
-
-

-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-
+
-
-
-
+
+
+
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
+
+
+
+
+
-
-
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package ast provides the abstract syntax tree for parsed zettel content.
package ast

import (
	"net/url"
	"fmt"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"

	"zettelstore.de/z/internal/zettel"
)

// ZettelNode is the root node of the abstract syntax tree.
// Zettel is the root node of the abstract syntax tree.
// It is *not* part of the visitor pattern.
type ZettelNode struct {
	Meta      *meta.Meta     // Original metadata
	Content   zettel.Content // Original content
	Zid       id.Zid         // Zettel identification.
	InhMeta   *meta.Meta     // Metadata of the zettel, with inherited values.
	BlocksAST BlockSlice     // Zettel abstract syntax tree is a sequence of block nodes.
	Syntax    string         // Syntax / parser that produced the Ast
type Zettel struct {
	Meta    *meta.Meta     // Original metadata
	Content zettel.Content // Original content
	Zid     id.Zid         // Zettel identification.
	InhMeta *meta.Meta     // Metadata of the zettel, with inherited values.
	Blocks  *sx.Pair       // Syntax tree, encodes as an sx.Object.
	Syntax  string         // Syntax / parser that produced the Ast
}

var mapMetaTypeS = map[*meta.DescriptionType]*sx.Symbol{
// Node is the interface, all nodes must implement.
type Node interface {
	meta.TypeCredential: sz.SymTypeCredential,
	meta.TypeEmpty:      sz.SymTypeEmpty,
	WalkChildren(v Visitor)
}

	meta.TypeID:         sz.SymTypeID,
// BlockNode is the interface that all block nodes must implement.
type BlockNode interface {
	meta.TypeIDSet:      sz.SymTypeIDSet,
	Node
	blockNode()
}

	meta.TypeNumber:     sz.SymTypeNumber,
// ItemNode is a node that can occur as a list item.
type ItemNode interface {
	meta.TypeString:     sz.SymTypeString,
	BlockNode
	itemNode()
}

	meta.TypeTagSet:     sz.SymTypeTagSet,
// ItemSlice is a slice of ItemNodes.
type ItemSlice []ItemNode

	meta.TypeTimestamp:  sz.SymTypeTimestamp,
	meta.TypeURL:        sz.SymTypeURL,
	meta.TypeWord:       sz.SymTypeWord,
// DescriptionNode is a node that contains just textual description.
type DescriptionNode interface {
	ItemNode
	descriptionNode()
}

// DescriptionSlice is a slice of DescriptionNodes.
type DescriptionSlice []DescriptionNode

// InlineNode is the interface that all inline nodes must implement.
type InlineNode interface {
	Node
	inlineNode()
}

// GetMetaSz transforms the given metadata into a sz list.
func GetMetaSz(m *meta.Meta) *sx.Pair {
	var lb sx.ListBuilder
	lb.Add(sz.SymMeta)
	for key, val := range m.Computed() {
		ty := m.Type(key)
		symType := mapGetS(mapMetaTypeS, ty)
		var obj sx.Object
		if ty.IsSet {
			var setObjs sx.ListBuilder
			for _, val := range val.AsSlice() {
				setObjs.Add(sx.MakeString(val))
			}
			obj = setObjs.List()
// Reference is a reference to external or internal material.
type Reference struct {
		} else {
	URL   *url.URL
	Value string
			obj = sx.MakeString(string(val))
	State RefState
}

		}
		lb.Add(sx.Nil().Cons(obj).Cons(sx.MakeSymbol(key)).Cons(symType))
	}
// RefState indicates the state of the reference.
type RefState int

	return lb.List()
}
// Constants for RefState
const (
	RefStateInvalid  RefState = iota // Invalid Reference
	RefStateZettel                   // Reference to an internal zettel
	RefStateSelf                     // Reference to same zettel with a fragment
	RefStateFound                    // Reference to an existing internal zettel, URL is ajusted
	RefStateBroken                   // Reference to a non-existing internal zettel
	RefStateHosted                   // Reference to local hosted non-Zettel, without URL change
	RefStateBased                    // Reference to local non-Zettel, to be prefixed
	RefStateQuery                    // Reference to a zettel query
	RefStateExternal                 // Reference to external material
)


func mapGetS[T comparable](m map[T]*sx.Symbol, k T) *sx.Symbol {
// ParseSpacedText returns an inline slice that consists just of test and space node.
// No Zettelmarkup parsing is done. It is typically used to transform the zettel
// description into an inline slice.
func ParseSpacedText(s string) InlineSlice {
	return InlineSlice{&TextNode{Text: NormalizedSpacedText(s)}}
}

	if result, found := m[k]; found {
		return result
	}
	return sx.MakeSymbol(fmt.Sprintf("**%v:NOT-FOUND**", k))
}
// NormalizedSpacedText returns the given string, but normalize multiple spaces to one space.
func NormalizedSpacedText(s string) string { return strings.Join(strings.Fields(s), " ") }
Deleted internal/ast/block.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















































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore 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: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package ast

import "t73f.de/r/zsx"

// Definition of Block nodes.

// BlockSlice is a slice of BlockNodes.
type BlockSlice []BlockNode

func (*BlockSlice) blockNode() { /* Just a marker */ }

// WalkChildren walks down to the descriptions.
func (bs *BlockSlice) WalkChildren(v Visitor) {
	if bs != nil {
		for _, bn := range *bs {
			Walk(v, bn)
		}
	}
}

// FirstParagraphInlines returns the inline list of the first paragraph that
// contains a inline list.
func (bs BlockSlice) FirstParagraphInlines() InlineSlice {
	for _, bn := range bs {
		pn, ok := bn.(*ParaNode)
		if !ok {
			continue
		}
		if inl := pn.Inlines; len(inl) > 0 {
			return inl
		}
	}
	return nil
}

//--------------------------------------------------------------------------

// ParaNode contains just a sequence of inline elements.
// Another name is "paragraph".
type ParaNode struct {
	Inlines InlineSlice
}

func (*ParaNode) blockNode()       { /* Just a marker */ }
func (*ParaNode) itemNode()        { /* Just a marker */ }
func (*ParaNode) descriptionNode() { /* Just a marker */ }

// CreateParaNode creates a parameter block from inline nodes.
func CreateParaNode(nodes ...InlineNode) *ParaNode { return &ParaNode{Inlines: nodes} }

// WalkChildren walks down the inline elements.
func (pn *ParaNode) WalkChildren(v Visitor) { Walk(v, &pn.Inlines) }

//--------------------------------------------------------------------------

// VerbatimNode contains uninterpreted text
type VerbatimNode struct {
	Kind    VerbatimKind
	Attrs   zsx.Attributes
	Content []byte
}

// VerbatimKind specifies the format that is applied to code inline nodes.
type VerbatimKind int

// Constants for VerbatimCode
const (
	_               VerbatimKind = iota
	VerbatimZettel               // Zettel content
	VerbatimCode                 // Program code
	VerbatimEval                 // Code to be externally interpreted. Syntax is stored in default attribute.
	VerbatimComment              // Block comment
	VerbatimHTML                 // Block HTML, e.g. for Markdown
	VerbatimMath                 // Block math mode
)

func (*VerbatimNode) blockNode() { /* Just a marker */ }
func (*VerbatimNode) itemNode()  { /* Just a marker */ }

// WalkChildren does nothing.
func (*VerbatimNode) WalkChildren(Visitor) { /* No children*/ }

//--------------------------------------------------------------------------

// RegionNode encapsulates a region of block nodes.
type RegionNode struct {
	Kind    RegionKind
	Attrs   zsx.Attributes
	Blocks  BlockSlice
	Inlines InlineSlice // Optional text at the end of the region
}

// RegionKind specifies the actual region type.
type RegionKind int

// Values for RegionCode
const (
	_           RegionKind = iota
	RegionSpan             // Just a span of blocks
	RegionQuote            // A longer quotation
	RegionVerse            // Line breaks matter
)

func (*RegionNode) blockNode() { /* Just a marker */ }
func (*RegionNode) itemNode()  { /* Just a marker */ }

// WalkChildren walks down the blocks and the text.
func (rn *RegionNode) WalkChildren(v Visitor) {
	Walk(v, &rn.Blocks)
	Walk(v, &rn.Inlines)
}

//--------------------------------------------------------------------------

// HeadingNode stores the heading text and level.
type HeadingNode struct {
	Level    int
	Attrs    zsx.Attributes
	Slug     string      // Heading text, normalized
	Fragment string      // Heading text, suitable to be used as an unique URL fragment
	Inlines  InlineSlice // Heading text, possibly formatted
}

func (*HeadingNode) blockNode() { /* Just a marker */ }
func (*HeadingNode) itemNode()  { /* Just a marker */ }

// WalkChildren walks the heading text.
func (hn *HeadingNode) WalkChildren(v Visitor) { Walk(v, &hn.Inlines) }

//--------------------------------------------------------------------------

// HRuleNode specifies a horizontal rule.
type HRuleNode struct {
	Attrs zsx.Attributes
}

func (*HRuleNode) blockNode() { /* Just a marker */ }
func (*HRuleNode) itemNode()  { /* Just a marker */ }

// WalkChildren does nothing.
func (*HRuleNode) WalkChildren(Visitor) { /* No children*/ }

//--------------------------------------------------------------------------

// NestedListNode specifies a nestable list, either ordered or unordered.
type NestedListNode struct {
	Kind  NestedListKind
	Attrs zsx.Attributes
	Items []ItemSlice
}

// NestedListKind specifies the actual list type.
type NestedListKind uint8

// Values for ListCode
const (
	_                   NestedListKind = iota
	NestedListOrdered                  // Ordered list.
	NestedListUnordered                // Unordered list.
	NestedListQuote                    // Quote list.
)

func (*NestedListNode) blockNode() { /* Just a marker */ }
func (*NestedListNode) itemNode()  { /* Just a marker */ }

// WalkChildren walks down the items.
func (ln *NestedListNode) WalkChildren(v Visitor) {
	if items := ln.Items; items != nil {
		for _, item := range items {
			WalkItemSlice(v, item)
		}
	}
}

//--------------------------------------------------------------------------

// DescriptionListNode specifies a description list.
type DescriptionListNode struct {
	Attrs        zsx.Attributes
	Descriptions []Description
}

// Description is one element of a description list.
type Description struct {
	Term         InlineSlice
	Descriptions []DescriptionSlice
}

func (*DescriptionListNode) blockNode() { /* Just a marker */ }

// WalkChildren walks down to the descriptions.
func (dn *DescriptionListNode) WalkChildren(v Visitor) {
	if descrs := dn.Descriptions; descrs != nil {
		for i, desc := range descrs {
			if len(desc.Term) > 0 {
				Walk(v, &descrs[i].Term) // Otherwise, changes in desc.Term will not go back into AST
			}
			if dss := desc.Descriptions; dss != nil {
				for _, dns := range dss {
					WalkDescriptionSlice(v, dns)
				}
			}
		}
	}
}

//--------------------------------------------------------------------------

// TableNode specifies a full table
type TableNode struct {
	Header TableRow    // The header row
	Align  []Alignment // Default column alignment
	Rows   []TableRow  // The slice of cell rows
}

// TableCell contains the data for one table cell
type TableCell struct {
	Align   Alignment   // Cell alignment
	Inlines InlineSlice // Cell content
}

// TableRow is a slice of cells.
type TableRow []*TableCell

// Alignment specifies text alignment.
// Currently only for tables.
type Alignment int

// Constants for Alignment.
const (
	_            Alignment = iota
	AlignDefault           // Default alignment, inherited
	AlignLeft              // Left alignment
	AlignCenter            // Center the content
	AlignRight             // Right alignment
)

func (*TableNode) blockNode() { /* Just a marker */ }

// WalkChildren walks down to the cells.
func (tn *TableNode) WalkChildren(v Visitor) {
	if header := tn.Header; header != nil {
		for i := range header {
			Walk(v, header[i]) // Otherwise changes will not go back
		}
	}
	if rows := tn.Rows; rows != nil {
		for _, row := range rows {
			for i := range row {
				Walk(v, &row[i].Inlines) // Otherwise changes will not go back
			}
		}
	}
}

// WalkChildren walks the list of inline elements.
func (cell *TableCell) WalkChildren(v Visitor) {
	Walk(v, &cell.Inlines) // Otherwise changes will not go back
}

//--------------------------------------------------------------------------

// TranscludeNode specifies block content from other zettel to embedded in
// current zettel
type TranscludeNode struct {
	Attrs   zsx.Attributes
	Ref     *Reference
	Inlines InlineSlice // Optional text.
}

func (*TranscludeNode) blockNode() { /* Just a marker */ }

// WalkChildren walks the associated text.
func (tn *TranscludeNode) WalkChildren(v Visitor) { Walk(v, &tn.Inlines) }

//--------------------------------------------------------------------------

// BLOBNode contains just binary data that must be interpreted according to
// a syntax.
type BLOBNode struct {
	Attrs       zsx.Attributes
	Description InlineSlice
	Syntax      string
	Blob        []byte
}

func (*BLOBNode) blockNode() { /* Just a marker */ }

// WalkChildren does nothing.
func (*BLOBNode) WalkChildren(Visitor) { /* No children*/ }
Deleted internal/ast/inline.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


















































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore 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: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package ast

import "t73f.de/r/zsx"

// Definitions of inline nodes.

// InlineSlice is a list of BlockNodes.
type InlineSlice []InlineNode

func (*InlineSlice) inlineNode() { /* Just a marker */ }

// WalkChildren walks down to the list.
func (is *InlineSlice) WalkChildren(v Visitor) {
	if is != nil {
		for _, in := range *is {
			Walk(v, in)
		}
	}
}

// --------------------------------------------------------------------------

// TextNode just contains some text.
type TextNode struct {
	Text string // The text itself.
}

func (*TextNode) inlineNode() { /* Just a marker */ }

// WalkChildren does nothing.
func (*TextNode) WalkChildren(Visitor) { /* No children*/ }

// --------------------------------------------------------------------------

// BreakNode signals a new line that must / should be interpreted as a new line break.
type BreakNode struct {
	Hard bool // Hard line break?
}

func (*BreakNode) inlineNode() { /* Just a marker */ }

// WalkChildren does nothing.
func (*BreakNode) WalkChildren(Visitor) { /* No children*/ }

// --------------------------------------------------------------------------

// LinkNode contains the specified link.
type LinkNode struct {
	Attrs   zsx.Attributes // Optional attributes
	Ref     *Reference
	Inlines InlineSlice // The text associated with the link.
}

func (*LinkNode) inlineNode() { /* Just a marker */ }

// WalkChildren walks to the link text.
func (ln *LinkNode) WalkChildren(v Visitor) {
	if len(ln.Inlines) > 0 {
		Walk(v, &ln.Inlines)
	}
}

// --------------------------------------------------------------------------

// EmbedRefNode contains the specified embedded reference material.
type EmbedRefNode struct {
	Attrs   zsx.Attributes // Optional attributes
	Ref     *Reference     // The reference to be embedded.
	Syntax  string         // Syntax of referenced material, if known
	Inlines InlineSlice    // Optional text associated with the image.
}

func (*EmbedRefNode) inlineNode() { /* Just a marker */ }

// WalkChildren walks to the text that describes the embedded material.
func (en *EmbedRefNode) WalkChildren(v Visitor) { Walk(v, &en.Inlines) }

// --------------------------------------------------------------------------

// EmbedBLOBNode contains the specified embedded BLOB material.
type EmbedBLOBNode struct {
	Attrs   zsx.Attributes // Optional attributes
	Syntax  string         // Syntax of Blob
	Blob    []byte         // BLOB data itself.
	Inlines InlineSlice    // Optional text associated with the image.
}

func (*EmbedBLOBNode) inlineNode() { /* Just a marker */ }

// WalkChildren walks to the text that describes the embedded material.
func (en *EmbedBLOBNode) WalkChildren(v Visitor) { Walk(v, &en.Inlines) }

// --------------------------------------------------------------------------

// CiteNode contains the specified citation.
type CiteNode struct {
	Attrs   zsx.Attributes // Optional attributes
	Key     string         // The citation key
	Inlines InlineSlice    // Optional text associated with the citation.
}

func (*CiteNode) inlineNode() { /* Just a marker */ }

// WalkChildren walks to the cite text.
func (cn *CiteNode) WalkChildren(v Visitor) { Walk(v, &cn.Inlines) }

// --------------------------------------------------------------------------

// MarkNode contains the specified merked position.
// It is a BlockNode too, because although it is typically parsed during inline
// mode, it is moved into block mode afterwards.
type MarkNode struct {
	Mark     string      // The mark text itself
	Slug     string      // Slugified form of Mark
	Fragment string      // Unique form of Slug
	Inlines  InlineSlice // Marked inline content
}

func (*MarkNode) inlineNode() { /* Just a marker */ }

// WalkChildren does nothing.
func (mn *MarkNode) WalkChildren(v Visitor) {
	if len(mn.Inlines) > 0 {
		Walk(v, &mn.Inlines)
	}
}

// --------------------------------------------------------------------------

// FootnoteNode contains the specified footnote.
type FootnoteNode struct {
	Attrs   zsx.Attributes // Optional attributes
	Inlines InlineSlice    // The footnote text.
}

func (*FootnoteNode) inlineNode() { /* Just a marker */ }

// WalkChildren walks to the footnote text.
func (fn *FootnoteNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) }

// --------------------------------------------------------------------------

// FormatNode specifies some inline formatting.
type FormatNode struct {
	Kind    FormatKind
	Attrs   zsx.Attributes // Optional attributes.
	Inlines InlineSlice
}

// FormatKind specifies the format that is applied to the inline nodes.
type FormatKind int

// Constants for FormatCode
const (
	_            FormatKind = iota
	FormatEmph              // Emphasized text
	FormatStrong            // Strongly emphasized text
	FormatInsert            // Inserted text
	FormatDelete            // Deleted text
	FormatSuper             // Superscripted text
	FormatSub               // SubscriptedText
	FormatQuote             // Quoted text
	FormatMark              // Marked text
	FormatSpan              // Generic inline container
)

func (*FormatNode) inlineNode() { /* Just a marker */ }

// WalkChildren walks to the formatted text.
func (fn *FormatNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) }

// --------------------------------------------------------------------------

// LiteralNode specifies some uninterpreted text.
type LiteralNode struct {
	Kind    LiteralKind
	Attrs   zsx.Attributes // Optional attributes.
	Content []byte
}

// LiteralKind specifies the format that is applied to code inline nodes.
type LiteralKind int

// Constants for LiteralCode
const (
	_              LiteralKind = iota
	LiteralCode                // Inline program code
	LiteralInput               // Computer input, e.g. Keyboard strokes
	LiteralOutput              // Computer output
	LiteralComment             // Inline comment
	LiteralMath                // Inline math mode
)

func (*LiteralNode) inlineNode() { /* Just a marker */ }

// WalkChildren does nothing.
func (*LiteralNode) WalkChildren(Visitor) { /* No children*/ }
Deleted internal/ast/ref.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













































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore 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: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package ast

import (
	"net/url"
	"strings"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
)

// QueryPrefix is the prefix that denotes a query expression.
const QueryPrefix = api.QueryPrefix

// ParseReference parses a string and returns a reference.
func ParseReference(s string) *Reference {
	if invalidReference(s) {
		return &Reference{URL: nil, Value: s, State: RefStateInvalid}
	}
	if strings.HasPrefix(s, QueryPrefix) {
		return &Reference{URL: nil, Value: s[len(QueryPrefix):], State: RefStateQuery}
	}
	if state, ok := localState(s); ok {
		if state == RefStateBased {
			s = s[1:]
		}
		u, err := url.Parse(s)
		if err == nil {
			return &Reference{URL: u, Value: s, State: state}
		}
	}
	u, err := url.Parse(s)
	if err != nil {
		return &Reference{URL: nil, Value: s, State: RefStateInvalid}
	}
	if !externalURL(u) {
		if _, err = id.Parse(u.Path); err == nil {
			return &Reference{URL: u, Value: s, State: RefStateZettel}
		}
		if u.Path == "" && u.Fragment != "" {
			return &Reference{URL: u, Value: s, State: RefStateSelf}
		}
	}
	return &Reference{URL: u, Value: s, State: RefStateExternal}
}

func invalidReference(s string) bool { return s == "" || s == "00000000000000" }
func externalURL(u *url.URL) bool {
	return u.Scheme != "" || u.Opaque != "" || u.Host != "" || u.User != nil
}

func localState(path string) (RefState, bool) {
	if len(path) > 0 && path[0] == '/' {
		if len(path) > 1 && path[1] == '/' {
			return RefStateBased, true
		}
		return RefStateHosted, true
	}
	if len(path) > 1 && path[0] == '.' {
		if len(path) > 2 && path[1] == '.' && path[2] == '/' {
			return RefStateHosted, true
		}
		return RefStateHosted, path[1] == '/'
	}
	return RefStateInvalid, false
}

// String returns the string representation of a reference.
func (r Reference) String() string {
	if r.State == RefStateQuery {
		return QueryPrefix + r.Value
	}
	if r.URL != nil {
		return r.URL.String()
	}
	return r.Value
}

// IsValid returns true if reference is valid
func (r *Reference) IsValid() bool { return r.State != RefStateInvalid }

// IsZettel returns true if it is a referencen to a local zettel.
func (r *Reference) IsZettel() bool {
	switch r.State {
	case RefStateZettel, RefStateSelf, RefStateFound, RefStateBroken:
		return true
	}
	return false
}

// IsLocal returns true if reference is local
func (r *Reference) IsLocal() bool {
	return r.State == RefStateHosted || r.State == RefStateBased
}

// IsExternal returns true if it is a referencen to external material.
func (r *Reference) IsExternal() bool { return r.State == RefStateExternal }
Deleted internal/ast/ref_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
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


































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore 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: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package ast_test

import (
	"testing"

	"zettelstore.de/z/internal/ast"
)

func TestParseReference(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		link string
		err  bool
		exp  string
	}{
		{"", true, ""},
		{"123", false, "123"},
		{",://", true, ""},
	}

	for i, tc := range testcases {
		got := ast.ParseReference(tc.link)
		if got.IsValid() == tc.err {
			t.Errorf(
				"TC=%d, expected parse error of %q: %v, but got %q", i, tc.link, tc.err, got)
		}
		if got.IsValid() && got.String() != tc.exp {
			t.Errorf("TC=%d, Reference of %q is %q, but got %q", i, tc.link, tc.exp, got)
		}
	}
}

func TestReferenceIsZettelMaterial(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		link       string
		isZettel   bool
		isExternal bool
		isLocal    bool
	}{
		{"", false, false, false},
		{"00000000000000", false, false, false},
		{"http://zettelstore.de/z/ast", false, true, false},
		{"12345678901234", true, false, false},
		{"12345678901234#local", true, false, false},
		{"http://12345678901234", false, true, false},
		{"http://zettelstore.de/z/12345678901234", false, true, false},
		{"http://zettelstore.de/12345678901234", false, true, false},
		{"/12345678901234", false, false, true},
		{"//12345678901234", false, false, true},
		{"./12345678901234", false, false, true},
		{"../12345678901234", false, false, true},
		{".../12345678901234", false, true, false},
	}

	for i, tc := range testcases {
		ref := ast.ParseReference(tc.link)
		isZettel := ref.IsZettel()
		if isZettel != tc.isZettel {
			t.Errorf(
				"TC=%d, Reference %q isZettel=%v expected, but got %v",
				i,
				tc.link,
				tc.isZettel,
				isZettel)
		}
		isLocal := ref.IsLocal()
		if isLocal != tc.isLocal {
			t.Errorf(
				"TC=%d, Reference %q isLocal=%v expected, but got %v",
				i,
				tc.link,
				tc.isLocal, isLocal)
		}
		isExternal := ref.IsExternal()
		if isExternal != tc.isExternal {
			t.Errorf(
				"TC=%d, Reference %q isExternal=%v expected, but got %v",
				i,
				tc.link,
				tc.isExternal,
				isExternal)
		}
	}
}
Deleted internal/ast/sztrans/sztrans.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
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651











































































































































































































































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2025-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore 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: 2025-present Detlef Stern
//-----------------------------------------------------------------------------

// Package sztrans allows to transform a sz representation of text into an
// abstract syntax tree.
package sztrans

import (
	"fmt"
	"log"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
)

type transformer struct{}

// GetBlockSlice returns the sz representations as a AST BlockSlice
func GetBlockSlice(pair *sx.Pair) (ast.BlockSlice, error) {
	if pair == nil {
		return nil, nil
	}
	var t transformer
	if obj := zsx.Walk(&t, pair, nil); !obj.IsNil() {
		if sxn, isNode := obj.(sxNode); isNode {
			if bs, ok := sxn.node.(*ast.BlockSlice); ok {
				return *bs, nil
			}
			return nil, fmt.Errorf("no BlockSlice AST: %T/%v for %v", sxn.node, sxn.node, pair)
		}
		return nil, fmt.Errorf("no AST for %v: %v", pair, obj)
	}
	return nil, fmt.Errorf("error walking %v", pair)
}

func (t *transformer) VisitBefore(pair *sx.Pair, _ *sx.Pair) (sx.Object, bool) {
	if sym, isSymbol := sx.GetSymbol(pair.Car()); isSymbol {
		switch sym {
		case zsx.SymText:
			if p := pair.Tail(); p != nil {
				if s, isString := sx.GetString(p.Car()); isString {
					return sxNode{&ast.TextNode{Text: s.GetValue()}}, true
				}
			}
		case zsx.SymSoft:
			return sxNode{&ast.BreakNode{Hard: false}}, true
		case zsx.SymHard:
			return sxNode{&ast.BreakNode{Hard: true}}, true
		case zsx.SymLiteralCode:
			return handleLiteral(ast.LiteralCode, pair.Tail())
		case zsx.SymLiteralComment:
			return handleLiteral(ast.LiteralComment, pair.Tail())
		case zsx.SymLiteralInput:
			return handleLiteral(ast.LiteralInput, pair.Tail())
		case zsx.SymLiteralMath:
			return handleLiteral(ast.LiteralMath, pair.Tail())
		case zsx.SymLiteralOutput:
			return handleLiteral(ast.LiteralOutput, pair.Tail())
		case zsx.SymThematic:
			return sxNode{&ast.HRuleNode{Attrs: zsx.GetAttributes(pair.Tail().Head())}}, true
		case zsx.SymVerbatimComment:
			return handleVerbatim(ast.VerbatimComment, pair.Tail())
		case zsx.SymVerbatimEval:
			return handleVerbatim(ast.VerbatimEval, pair.Tail())
		case zsx.SymVerbatimHTML:
			return handleVerbatim(ast.VerbatimHTML, pair.Tail())
		case zsx.SymVerbatimMath:
			return handleVerbatim(ast.VerbatimMath, pair.Tail())
		case zsx.SymVerbatimCode:
			return handleVerbatim(ast.VerbatimCode, pair.Tail())
		case zsx.SymVerbatimZettel:
			return handleVerbatim(ast.VerbatimZettel, pair.Tail())
		}
	}
	return sx.Nil(), false
}

func handleLiteral(kind ast.LiteralKind, rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
		attrs := zsx.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			if s, isString := sx.GetString(curr.Car()); isString {
				return sxNode{&ast.LiteralNode{
					Kind:    kind,
					Attrs:   attrs,
					Content: []byte(s.GetValue())}}, true
			}
		}
	}
	return nil, false
}

func handleVerbatim(kind ast.VerbatimKind, rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
		attrs := zsx.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			if s, isString := sx.GetString(curr.Car()); isString {
				return sxNode{&ast.VerbatimNode{
					Kind:    kind,
					Attrs:   attrs,
					Content: []byte(s.GetValue()),
				}}, true
			}
		}
	}
	return nil, false
}

func (t *transformer) VisitAfter(pair *sx.Pair, _ *sx.Pair) sx.Object {
	if sym, isSymbol := sx.GetSymbol(pair.Car()); isSymbol {
		switch sym {
		case zsx.SymBlock:
			bns := collectBlocks(pair.Tail())
			return sxNode{&bns}
		case zsx.SymPara:
			return sxNode{&ast.ParaNode{Inlines: collectInlines(pair.Tail())}}
		case zsx.SymHeading:
			return handleHeading(pair.Tail())
		case zsx.SymListOrdered:
			return handleList(ast.NestedListOrdered, pair.Tail())
		case zsx.SymListUnordered:
			return handleList(ast.NestedListUnordered, pair.Tail())
		case zsx.SymListQuote:
			return handleList(ast.NestedListQuote, pair.Tail())
		case zsx.SymDescription:
			return handleDescription(pair.Tail())
		case zsx.SymTable:
			return handleTable(pair.Tail())
		case zsx.SymCell:
			return handleCell(pair.Tail())
		case zsx.SymRegionBlock:
			return handleRegion(ast.RegionSpan, pair.Tail())
		case zsx.SymRegionQuote:
			return handleRegion(ast.RegionQuote, pair.Tail())
		case zsx.SymRegionVerse:
			return handleRegion(ast.RegionVerse, pair.Tail())
		case zsx.SymTransclude:
			return handleTransclude(pair.Tail())
		case zsx.SymBLOB:
			return handleBLOB(pair.Tail())

		case zsx.SymLink:
			return handleLink(pair.Tail())
		case zsx.SymEmbed:
			return handleEmbed(pair.Tail())
		case zsx.SymEmbedBLOB:
			return handleEmbedBLOB(pair.Tail())
		case zsx.SymCite:
			return handleCite(pair.Tail())
		case zsx.SymMark:
			return handleMark(pair.Tail())
		case zsx.SymEndnote:
			return handleEndnote(pair.Tail())
		case zsx.SymFormatDelete:
			return handleFormat(ast.FormatDelete, pair.Tail())
		case zsx.SymFormatEmph:
			return handleFormat(ast.FormatEmph, pair.Tail())
		case zsx.SymFormatInsert:
			return handleFormat(ast.FormatInsert, pair.Tail())
		case zsx.SymFormatMark:
			return handleFormat(ast.FormatMark, pair.Tail())
		case zsx.SymFormatQuote:
			return handleFormat(ast.FormatQuote, pair.Tail())
		case zsx.SymFormatSpan:
			return handleFormat(ast.FormatSpan, pair.Tail())
		case zsx.SymFormatSub:
			return handleFormat(ast.FormatSub, pair.Tail())
		case zsx.SymFormatSuper:
			return handleFormat(ast.FormatSuper, pair.Tail())
		case zsx.SymFormatStrong:
			return handleFormat(ast.FormatStrong, pair.Tail())
		}
		log.Println("MISS", pair)
	}
	return pair
}

func collectBlocks(lst *sx.Pair) (result ast.BlockSlice) {
	for val := range lst.Values() {
		if sxn, isNode := val.(sxNode); isNode {
			if bn, isInline := sxn.node.(ast.BlockNode); isInline {
				result = append(result, bn)
			}
		}
	}
	return result
}

func collectInlines(lst *sx.Pair) (result ast.InlineSlice) {
	for val := range lst.Values() {
		if sxn, isNode := val.(sxNode); isNode {
			if in, isInline := sxn.node.(ast.InlineNode); isInline {
				result = append(result, in)
			}
		}
	}
	return result
}

func handleHeading(rest *sx.Pair) sx.Object {
	if rest != nil {
		if num, isNumber := rest.Car().(sx.Int64); isNumber && num > 0 && num < 6 {
			if curr := rest.Tail(); curr != nil {
				attrs := zsx.GetAttributes(curr.Head())
				if curr = curr.Tail(); curr != nil {
					if sSlug, isSlug := sx.GetString(curr.Car()); isSlug {
						if curr = curr.Tail(); curr != nil {
							if sUniq, isUniq := sx.GetString(curr.Car()); isUniq {
								return sxNode{&ast.HeadingNode{
									Level:    int(num),
									Attrs:    attrs,
									Slug:     sSlug.GetValue(),
									Fragment: sUniq.GetValue(),
									Inlines:  collectInlines(curr.Tail()),
								}}
							}
						}
					}
				}
			}
		}
	}
	log.Println("HEAD", rest)
	return rest
}

func handleList(kind ast.NestedListKind, rest *sx.Pair) sx.Object {
	if rest != nil {
		attrs := zsx.GetAttributes(rest.Head())
		return sxNode{&ast.NestedListNode{
			Kind:  kind,
			Items: collectItemSlices(rest.Tail()),
			Attrs: attrs}}
	}
	log.Println("LIST", kind, rest)
	return rest
}

func collectItemSlices(lst *sx.Pair) (result []ast.ItemSlice) {
	for val := range lst.Values() {
		if sxn, isNode := val.(sxNode); isNode {
			if bns, isBlockSlice := sxn.node.(*ast.BlockSlice); isBlockSlice {
				items := make(ast.ItemSlice, len(*bns))
				for i, bn := range *bns {
					if it, ok := bn.(ast.ItemNode); ok {
						items[i] = it
					}
				}
				result = append(result, items)
			}
			if ins, isInline := sxn.node.(*ast.InlineSlice); isInline {
				items := make(ast.ItemSlice, len(*ins))
				for i, bn := range *ins {
					if it, ok := bn.(ast.ItemNode); ok {
						items[i] = it
					}
				}
				result = append(result, items)
			}
		}
	}
	return result
}

func handleDescription(rest *sx.Pair) sx.Object {
	if rest != nil {
		attrs := zsx.GetAttributes(rest.Head())
		var descs []ast.Description
		for curr := rest.Tail(); curr != nil; {
			term := collectInlines(curr.Head())
			curr = curr.Tail()
			if curr == nil {
				descr := ast.Description{Term: term, Descriptions: nil}
				descs = append(descs, descr)
				break
			}

			car := curr.Car()
			if sx.IsNil(car) {
				descs = append(descs, ast.Description{Term: term, Descriptions: nil})
				curr = curr.Tail()
				continue
			}

			sxn, isNode := car.(sxNode)
			if !isNode {
				descs = nil
				break
			}
			blocks, isBlocks := sxn.node.(*ast.BlockSlice)
			if !isBlocks {
				descs = nil
				break
			}

			descSlice := make([]ast.DescriptionSlice, 0, len(*blocks))
			for _, bn := range *blocks {
				bns, isBns := bn.(*ast.BlockSlice)
				if !isBns {
					continue
				}
				ds := make(ast.DescriptionSlice, 0, len(*bns))
				for _, b := range *bns {
					if defNode, isDef := b.(ast.DescriptionNode); isDef {
						ds = append(ds, defNode)
					}
				}
				descSlice = append(descSlice, ds)
			}

			descr := ast.Description{Term: term, Descriptions: descSlice}
			descs = append(descs, descr)

			curr = curr.Tail()
		}
		if len(descs) > 0 {
			return sxNode{&ast.DescriptionListNode{
				Attrs:        attrs,
				Descriptions: descs,
			}}
		}
	}
	log.Println("DESC", rest)
	return rest
}

func handleTable(rest *sx.Pair) sx.Object {
	if rest != nil {
		header := collectRow(rest.Head())
		cols := len(header)

		var rows []ast.TableRow
		for curr := range rest.Tail().Pairs() {
			row := collectRow(curr.Head())
			rows = append(rows, row)
			cols = max(cols, len(row))
		}
		align := make([]ast.Alignment, cols)
		for i := range cols {
			align[i] = ast.AlignDefault
		}

		return sxNode{&ast.TableNode{
			Header: header,
			Align:  align,
			Rows:   rows,
		}}
	}
	log.Println("TABL", rest)
	return rest
}

func collectRow(lst *sx.Pair) (row ast.TableRow) {
	for curr := range lst.Values() {
		if sxn, isNode := curr.(sxNode); isNode {
			if cell, isCell := sxn.node.(*ast.TableCell); isCell {
				row = append(row, cell)
			}
		}
	}
	return row
}

func handleCell(rest *sx.Pair) sx.Object {
	if rest != nil {
		align := ast.AlignDefault
		if alignPair := rest.Head().Assoc(zsx.SymAttrAlign); alignPair != nil {
			if alignValue := alignPair.Cdr(); zsx.AttrAlignCenter.IsEqual(alignValue) {
				align = ast.AlignCenter
			} else if zsx.AttrAlignLeft.IsEqual(alignValue) {
				align = ast.AlignLeft
			} else if zsx.AttrAlignRight.IsEqual(alignValue) {
				align = ast.AlignRight
			}
		}
		return sxNode{&ast.TableCell{
			Align:   align,
			Inlines: collectInlines(rest.Tail()),
		}}
	}
	log.Println("CELL", rest)
	return rest
}

func handleRegion(kind ast.RegionKind, rest *sx.Pair) sx.Object {
	if rest != nil {
		attrs := zsx.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			if blockList := curr.Head(); blockList != nil {
				return sxNode{&ast.RegionNode{
					Kind:    kind,
					Attrs:   attrs,
					Blocks:  collectBlocks(blockList),
					Inlines: collectInlines(curr.Tail()),
				}}
			}
		}
	}
	log.Println("REGI", rest)
	return rest
}

func handleTransclude(rest *sx.Pair) sx.Object {
	if rest != nil {
		attrs := zsx.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			ref := collectReference(curr.Head())
			return sxNode{&ast.TranscludeNode{
				Attrs:   attrs,
				Ref:     ref,
				Inlines: collectInlines(curr.Tail()),
			}}
		}
	}
	log.Println("TRAN", rest)
	return rest
}

func handleBLOB(rest *sx.Pair) sx.Object {
	if rest != nil {
		attrs := zsx.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			ins := collectInlines(curr.Head())
			if curr = curr.Tail(); curr != nil {
				if syntax, isString := sx.GetString(curr.Car()); isString {
					if curr = curr.Tail(); curr != nil {
						if blob, isBlob := sx.GetString(curr.Car()); isBlob {
							return sxNode{&ast.BLOBNode{
								Attrs:       attrs,
								Description: ins,
								Syntax:      syntax.GetValue(),
								Blob:        []byte(blob.GetValue()),
							}}

						}
					}
				}
			}
		}
	}
	log.Println("BLOB", rest)
	return rest
}

var mapRefState = map[*sx.Symbol]ast.RefState{
	zsx.SymRefStateInvalid:  ast.RefStateInvalid,
	sz.SymRefStateZettel:    ast.RefStateZettel,
	zsx.SymRefStateSelf:     ast.RefStateSelf,
	sz.SymRefStateFound:     ast.RefStateFound,
	sz.SymRefStateBroken:    ast.RefStateBroken,
	zsx.SymRefStateHosted:   ast.RefStateHosted,
	sz.SymRefStateBased:     ast.RefStateBased,
	sz.SymRefStateQuery:     ast.RefStateQuery,
	zsx.SymRefStateExternal: ast.RefStateExternal,
}

func handleLink(rest *sx.Pair) sx.Object {
	if rest != nil {
		attrs := zsx.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			if szref := curr.Head(); szref != nil {
				if stateSym, isSym := sx.GetSymbol(szref.Car()); isSym {
					refval, isString := sx.GetString(szref.Cdr())
					if !isString {
						refval, isString = sx.GetString(szref.Tail().Car())
					}
					if isString {
						ref := ast.ParseReference(refval.GetValue())
						ref.State = mapRefState[stateSym]
						ins := collectInlines(curr.Tail())
						return sxNode{&ast.LinkNode{
							Attrs:   attrs,
							Ref:     ref,
							Inlines: ins,
						}}
					}
				}
			}
		}
	}
	log.Println("LINK", rest)
	return rest
}

func handleEmbed(rest *sx.Pair) sx.Object {
	if rest != nil {
		attrs := zsx.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			if ref := collectReference(curr.Head()); ref != nil {
				if curr = curr.Tail(); curr != nil {
					if syntax, isString := sx.GetString(curr.Car()); isString {
						return sxNode{&ast.EmbedRefNode{
							Attrs:   attrs,
							Ref:     ref,
							Syntax:  syntax.GetValue(),
							Inlines: collectInlines(curr.Tail()),
						}}
					}
				}
			}
		}
	}
	log.Println("EMBE", rest)
	return rest
}

func handleEmbedBLOB(rest *sx.Pair) sx.Object {
	if rest != nil {
		attrs := zsx.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			if syntax, isSyntax := sx.GetString(curr.Car()); isSyntax {
				if curr = curr.Tail(); curr != nil {
					if content, isContent := sx.GetString(curr.Car()); isContent {
						return sxNode{&ast.EmbedBLOBNode{
							Attrs:   attrs,
							Syntax:  syntax.GetValue(),
							Blob:    []byte(content.GetValue()),
							Inlines: collectInlines(curr.Tail()),
						}}
					}
				}
			}
		}
	}
	log.Println("EMBL", rest)
	return rest
}

func collectReference(pair *sx.Pair) *ast.Reference {
	if pair != nil {
		if sym, isSymbol := sx.GetSymbol(pair.Car()); isSymbol {
			if next := pair.Tail(); next != nil {
				if sRef, isString := sx.GetString(next.Car()); isString {
					ref := ast.ParseReference(sRef.GetValue())
					switch sym {
					case zsx.SymRefStateInvalid:
						ref.State = ast.RefStateInvalid
					case sz.SymRefStateZettel:
						ref.State = ast.RefStateZettel
					case zsx.SymRefStateSelf:
						ref.State = ast.RefStateSelf
					case sz.SymRefStateFound:
						ref.State = ast.RefStateFound
					case sz.SymRefStateBroken:
						ref.State = ast.RefStateBroken
					case zsx.SymRefStateHosted:
						ref.State = ast.RefStateHosted
					case sz.SymRefStateBased:
						ref.State = ast.RefStateBased
					case sz.SymRefStateQuery:
						ref.State = ast.RefStateQuery
					case zsx.SymRefStateExternal:
						ref.State = ast.RefStateExternal
					}
					return ref
				}
			}
		}
	}
	return nil
}

func handleCite(rest *sx.Pair) sx.Object {
	if rest != nil {
		attrs := zsx.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			if sKey, isString := sx.GetString(curr.Car()); isString {
				return sxNode{&ast.CiteNode{
					Attrs:   attrs,
					Key:     sKey.GetValue(),
					Inlines: collectInlines(curr.Tail()),
				}}
			}
		}
	}
	log.Println("CITE", rest)
	return rest
}

func handleMark(rest *sx.Pair) sx.Object {
	if rest != nil {
		if sMark, isMarkS := sx.GetString(rest.Car()); isMarkS {
			if curr := rest.Tail(); curr != nil {
				if sSlug, isSlug := sx.GetString(curr.Car()); isSlug {
					if curr = curr.Tail(); curr != nil {
						if sUniq, isUniq := sx.GetString(curr.Car()); isUniq {
							return sxNode{&ast.MarkNode{
								Mark:     sMark.GetValue(),
								Slug:     sSlug.GetValue(),
								Fragment: sUniq.GetValue(),
								Inlines:  collectInlines(curr.Tail()),
							}}
						}
					}
				}
			}
		}
	}
	log.Println("MARK", rest)
	return rest
}

func handleEndnote(rest *sx.Pair) sx.Object {
	if rest != nil {
		attrs := zsx.GetAttributes(rest.Head())
		return sxNode{&ast.FootnoteNode{
			Attrs:   attrs,
			Inlines: collectInlines(rest.Tail()),
		}}
	}
	log.Println("ENDN", rest)
	return rest
}

func handleFormat(kind ast.FormatKind, rest *sx.Pair) sx.Object {
	if rest != nil {
		attrs := zsx.GetAttributes(rest.Head())
		return sxNode{&ast.FormatNode{
			Kind:    kind,
			Attrs:   attrs,
			Inlines: collectInlines(rest.Tail()),
		}}
	}
	log.Println("FORM", kind, rest)
	return rest
}

type sxNode struct {
	node ast.Node
}

func (sxNode) IsNil() bool        { return false }
func (sxNode) IsAtom() bool       { return true }
func (n sxNode) String() string   { return fmt.Sprintf("%T/%v", n.node, n.node) }
func (n sxNode) GoString() string { return n.String() }
func (n sxNode) IsEqual(other sx.Object) bool {
	return n.String() == other.String()
}
Deleted internal/ast/walk.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
















































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore 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: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package ast

// Visitor is a visitor for walking the AST.
type Visitor interface {
	Visit(node Node) Visitor
}

// Walk traverses the AST.
func Walk(v Visitor, node Node) {
	if v = v.Visit(node); v == nil {
		return
	}

	// Implementation note:
	// It is much faster to use interface dispatching than to use a switch statement.
	// On my "cpu: Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz", a switch statement
	// implementation tooks approx 940-980 ns/op. Interface dispatching is in the
	// range of 900-930 ns/op.
	node.WalkChildren(v)
	v.Visit(nil)
}

// WalkItemSlice traverses an item slice.
func WalkItemSlice(v Visitor, ins ItemSlice) {
	for _, in := range ins {
		Walk(v, in)
	}
}

// WalkDescriptionSlice traverses an item slice.
func WalkDescriptionSlice(v Visitor, dns DescriptionSlice) {
	for _, dn := range dns {
		Walk(v, dn)
	}
}
Deleted internal/ast/walk_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
72
73
74
75











































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore 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: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package ast_test

import (
	"testing"

	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
)

func BenchmarkWalk(b *testing.B) {
	root := ast.BlockSlice{
		&ast.HeadingNode{
			Inlines: ast.InlineSlice{&ast.TextNode{Text: "A Simple Heading"}},
		},
		&ast.ParaNode{
			Inlines: ast.InlineSlice{&ast.TextNode{Text: "This is the introduction."}},
		},
		&ast.NestedListNode{
			Kind: ast.NestedListUnordered,
			Items: []ast.ItemSlice{
				[]ast.ItemNode{
					&ast.ParaNode{
						Inlines: ast.InlineSlice{&ast.TextNode{Text: "Item 1"}},
					},
				},
				[]ast.ItemNode{
					&ast.ParaNode{
						Inlines: ast.InlineSlice{&ast.TextNode{Text: "Item 2"}},
					},
				},
			},
		},
		&ast.ParaNode{
			Inlines: ast.InlineSlice{&ast.TextNode{Text: "This is some intermediate text."}},
		},
		ast.CreateParaNode(
			&ast.FormatNode{
				Kind: ast.FormatEmph,
				Attrs: zsx.Attributes(map[string]string{
					"":      "class",
					"color": "green",
				}),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: "This is some emphasized text."}},
			},
			&ast.TextNode{Text: " "},
			&ast.LinkNode{
				Ref:     &ast.Reference{Value: "http://zettelstore.de"},
				Inlines: ast.InlineSlice{&ast.TextNode{Text: "URL text."}},
			},
		),
	}
	v := benchVisitor{}

	for b.Loop() {
		ast.Walk(&v, &root)
	}
}

type benchVisitor struct{}

func (bv *benchVisitor) Visit(ast.Node) ast.Visitor { return bv }
Changes to internal/box/constbox/base.sxn.
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
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







-
+

-
-
-
-
+
+
+
+

-
-
+
+




-
-
+
+

-
+

-
+

-
-
-
+
+
+



-
+

-
+

-
+


-
+

-
+



-
-
-
-
+
+
+
+

-
+



;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(@@@@
(html ,@(if lang `((@ (lang ,lang))))
(html ,@(if lang `(((lang ,lang))))
(head
  (meta (@ (charset "utf-8")))
  (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0")))
  (meta (@ (name "generator") (content "Zettelstore")))
  (meta (@ (name "format-detection") (content "telephone=no")))
  (meta ((charset "utf-8")))
  (meta ((name "viewport") (content "width=device-width, initial-scale=1.0")))
  (meta ((name "generator") (content "Zettelstore")))
  (meta ((name "format-detection") (content "telephone=no")))
  ,@META-HEADER
  (link (@ (rel "stylesheet") (href ,css-base-url)))
  (link (@ (rel "stylesheet") (href ,css-user-url)))
  (link ((rel "stylesheet") (href ,css-base-url)))
  (link ((rel "stylesheet") (href ,css-user-url)))
  ,@(ROLE-DEFAULT-meta (current-frame))
  ,@(let* ((frame (current-frame))(rem (resolve-symbol 'ROLE-EXTRA-meta frame))) (if (defined? rem) (rem frame)))
  (title ,title))
(body
  (nav (@ (class "zs-menu"))
    (a (@ (href ,home-url)) "Home")
  (nav ((class "zs-menu"))
    ,(wui-href home-url "Home")
    ,@(if with-auth
      `((div (@ (class "zs-dropdown"))
      `((div ((class "zs-dropdown"))
        (button "User")
        (nav (@ (class "zs-dropdown-content"))
        (nav ((class "zs-dropdown-content"))
          ,@(if user-is-valid
            `((a (@ (href ,user-zettel-url)) ,user-ident)
              (a (@ (href ,logout-url)) "Logout"))
            `((a (@ (href ,login-url)) "Login"))
            `(,(wui-href user-zettel-url user-ident)
              ,(wui-href logout-url "Logout"))
            `(,(wui-href login-url "Login"))
          )
      )))
    )
    (div (@ (class "zs-dropdown"))
    (div ((class "zs-dropdown"))
      (button "Lists")
      (nav (@ (class "zs-dropdown-content"))
      (nav ((class "zs-dropdown-content"))
        ,@list-urls
        ,@(if (symbol-bound? 'refresh-url) `((a (@ (href ,refresh-url)) "Refresh")))
        ,(if (symbol-bound? 'refresh-url) (wui-href refresh-url "Refresh"))
    ))
    ,@(if new-zettel-links
      `((div (@ (class "zs-dropdown"))
      `((div ((class "zs-dropdown"))
        (button "New")
        (nav (@ (class "zs-dropdown-content"))
        (nav ((class "zs-dropdown-content"))
          ,@(map wui-link new-zettel-links)
       )))
    )
    (search (form (@ (action ,search-url))
      (input (@ (type "search") (inputmode "search") (name ,query-key-query)
                (title "General search field, with same behaviour as search field in search result list")
                (placeholder "Search..") (dir "auto")))))
    (search (form ((action ,search-url))
      (input ((type "search") (inputmode "search") (name ,query-key-query)
              (title "General search field, with same behaviour as search field in search result list")
              (placeholder "Search..") (dir "auto")))))
  )
  (main (@ (class "content")) ,DETAIL)
  (main ((class "content")) ,DETAIL)
  ,@(if FOOTER `((footer (hr) ,@FOOTER)))
  ,@(if debug-mode '((div (b "WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!"))))
)))
Changes to internal/box/constbox/constbox.go.
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
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







-
+









-
+









-
+









-
+









-
+









-
+









-
+







		zettel.NewContent(contentDependencies)},
	id.ZidBaseTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Base HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230510155100",
			meta.KeyModified:   "20250623131400",
			meta.KeyModified:   "20250806182400",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentBaseSxn)},
	id.ZidLoginTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Login Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20240219145200",
			meta.KeyModified:   "20250806182600",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentLoginSxn)},
	id.ZidZettelTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Zettel HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230510155300",
			meta.KeyModified:   "20250626113800",
			meta.KeyModified:   "20250806182700",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentZettelSxn)},
	id.ZidInfoTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Info HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20250624160000",
			meta.KeyModified:   "20250806182500",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentInfoSxn)},
	id.ZidFormTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Form HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20250612180300",
			meta.KeyModified:   "20250806182500",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentFormSxn)},
	id.ZidDeleteTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Delete HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20200804111624",
			meta.KeyModified:   "20250612180200",
			meta.KeyModified:   "20250806182500",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentDeleteSxn)},
	id.ZidListTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore List Zettel HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230704122100",
			meta.KeyModified:   "20250612180200",
			meta.KeyModified:   "20250806182600",
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentListZettelSxn)},
	id.ZidErrorTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Error HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,
253
254
255
256
257
258
259
260

261
262
263
264
265
266
267
253
254
255
256
257
258
259

260
261
262
263
264
265
266
267







-
+







		zettel.NewContent(contentStartCodeSxn)},
	id.ZidSxnBase: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Sxn Base Code",
			meta.KeyRole:       meta.ValueRoleConfiguration,
			meta.KeySyntax:     meta.ValueSyntaxSxn,
			meta.KeyCreated:    "20230619132800",
			meta.KeyModified:   "20250624200200",
			meta.KeyModified:   "20250806182700",
			meta.KeyReadOnly:   meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentBaseCodeSxn)},
	id.ZidBaseCSS: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Base CSS",
Changes to internal/box/constbox/delete.sxn.
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
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







-
+





-
+






-
+






-
+

;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header (h1 "Delete Zettel " ,zid))
  (p "Do you really want to delete this zettel?")
  ,@(if shadowed-box
    `((div (@ (class "zs-info"))
    `((div ((class "zs-info"))
      (h2 "Information")
      (p "If you delete this zettel, the previously shadowed zettel from overlayed box " ,shadowed-box " becomes available.")
    ))
  )
  ,@(if incoming
    `((div (@ (class "zs-warning"))
    `((div ((class "zs-warning"))
      (h2 "Warning!")
      (p "If you delete this zettel, incoming references from the following zettel will become invalid.")
      (ul ,@(map wui-item-link incoming))
    ))
  )
  ,@(if (and (symbol-bound? 'useless) useless)
    `((div (@ (class "zs-warning"))
    `((div ((class "zs-warning"))
      (h2 "Warning!")
      (p "Deleting this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.")
      (ul ,@(map wui-item useless))
    ))
  )
  ,(wui-meta-desc metapairs)
  (form (@ (method "POST")) (input (@ (class "zs-primary") (type "submit") (value "Delete"))))
  (form ((method "POST")) (input ((class "zs-primary") (type "submit") (value "Delete"))))
)
Changes to internal/box/constbox/form.sxn.
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
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







-
+

-
-
+
+



-
-
+
+







-
-
+
+



-
-
+
+



-
-
+
+








-
-
+
+





-
-
-
+
+
+


;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header (h1 ,heading))
  (form (@ (action ,form-action-url) (method "POST") (enctype "multipart/form-data"))
  (form ((action ,form-action-url) (method "POST") (enctype "multipart/form-data"))
  (div
    (label (@ (for "zs-title")) "Title " (a (@ (title "Main heading of this zettel.")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-title") (name "title")
    (label ((for "zs-title")) "Title " (a ((title "Main heading of this zettel.")) (@H "&#9432;")))
    (input ((class "zs-input") (type "text") (id "zs-title") (name "title")
              (title "Title of this zettel")
              (placeholder "Title..") (value ,meta-title) (dir "auto") (autofocus))))
  (div
    (label (@ (for "zs-role")) "Role " (a (@ (title "One word, without spaces, to set the main role of this zettel.")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (pattern "\\w*") (id "zs-role") (name "role")
    (label ((for "zs-role")) "Role " (a ((title "One word, without spaces, to set the main role of this zettel.")) (@H "&#9432;")))
    (input ((class "zs-input") (type "text") (pattern "\\w*") (id "zs-role") (name "role")
              (title "One word, letters and digits, but no spaces, to set the main role of the zettel.")
              (placeholder "role..") (value ,meta-role) (dir "auto")
      ,@(if role-data '((list "zs-role-data")))
    ))
    ,@(wui-datalist "zs-role-data" role-data)
  )
  (div
    (label (@ (for "zs-tags")) "Tags " (a (@ (title "Tags must begin with an '#' sign. They are separated by spaces.")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-tags") (name "tags")
    (label ((for "zs-tags")) "Tags " (a ((title "Tags must begin with an '#' sign. They are separated by spaces.")) (@H "&#9432;")))
    (input ((class "zs-input") (type "text") (id "zs-tags") (name "tags")
              (title "Tags/keywords to categorize the zettel. Each tags is a word that begins with a '#' character; they are separated by spaces")
              (placeholder "#tag") (value ,meta-tags) (dir "auto"))))
  (div
    (label (@ (for "zs-meta")) "Metadata " (a (@ (title "Other metadata for this zettel. Each line contains a key/value pair, separated by a colon ':'.")) (@H "&#9432;")))
    (textarea (@ (class "zs-input") (id "zs-meta") (name "meta") (rows "4")
    (label ((for "zs-meta")) "Metadata " (a ((title "Other metadata for this zettel. Each line contains a key/value pair, separated by a colon ':'.")) (@H "&#9432;")))
    (textarea ((class "zs-input") (id "zs-meta") (name "meta") (rows "4")
                 (title "Additional metadata about the zettel")
                 (placeholder "metakey: metavalue") (dir "auto")) ,meta))
  (div
    (label (@ (for "zs-syntax")) "Syntax " (a (@ (title "Syntax of zettel content below, one word. Typically 'zmk' (for zettelmarkup).")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (pattern "\\w*") (id "zs-syntax") (name "syntax")
    (label ((for "zs-syntax")) "Syntax " (a ((title "Syntax of zettel content below, one word. Typically 'zmk' (for zettelmarkup).")) (@H "&#9432;")))
    (input ((class "zs-input") (type "text") (pattern "\\w*") (id "zs-syntax") (name "syntax")
              (title "Syntax/format of zettel content below, one word, letters and digits, no spaces.")
              (placeholder "syntax..") (value ,meta-syntax) (dir "auto")
      ,@(if syntax-data '((list "zs-syntax-data")))
    ))
    ,@(wui-datalist "zs-syntax-data" syntax-data)
  )
  ,@(if (symbol-bound? 'content)
    `((div
      (label (@ (for "zs-content")) "Content " (a (@ (title "Content for this zettel, according to above syntax.")) (@H "&#9432;")))
      (textarea (@ (class "zs-input zs-content") (id "zs-content") (name "content") (rows "20")
      (label ((for "zs-content")) "Content " (a ((title "Content for this zettel, according to above syntax.")) (@H "&#9432;")))
      (textarea ((class "zs-input zs-content") (id "zs-content") (name "content") (rows "20")
                   (title "Zettel content, according to the given syntax")
                   (placeholder "Zettel content..") (dir "auto")) ,content)
    ))
  )
  (div
    (input (@ (class "zs-primary") (type "submit") (value "Submit")))
    (input (@ (class "zs-secondary") (type "submit") (value "Save") (formaction "?save")))
    (input (@ (class "zs-upload") (type "file") (id "zs-file") (name "file")))
    (input ((class "zs-primary") (type "submit") (value "Submit")))
    (input ((class "zs-secondary") (type "submit") (value "Save") (formaction "?save")))
    (input ((class "zs-upload") (type "file") (id "zs-file") (name "file")))
  ))
)
Changes to internal/box/constbox/info.sxn.
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
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







-
-
-
+
+
+

-
-
-
+
+
+



-
-
+
+











-
-
+
+







;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header (h1 "Information for Zettel " ,zid)
    (p
      (a (@ (href ,web-url)) "Web")
      ,@(if (symbol-bound? 'edit-url) `((@H " &#183; ") (a (@ (href ,edit-url)) "Edit")))
      (@H " &#183; ") (a (@ (href ,context-full-url)) "Full Context")
      ,(wui-href web-url "Web")
      ,@(if (symbol-bound? 'edit-url) `((@H " &#183; ") ,(wui-href edit-url "Edit")))
      (@H " &#183; ") ,(wui-href context-full-url "Full Context")
      ,@(if (symbol-bound? 'thread-query-url)
            `((@H " &#183; [")  (a (@ (href ,thread-query-url)) "Thread")
              ,@(if (symbol-bound? 'folge-query-url) `((@H ", ") (a (@ (href ,folge-query-url)) "Folge")))
              ,@(if (symbol-bound? 'sequel-query-url) `((@H ", ") (a (@ (href ,sequel-query-url)) "Sequel")))
            `((@H " &#183; [")  ,(wui-href thread-query-url "Thread")
              ,@(if (symbol-bound? 'folge-query-url) `((@H ", ") ,(wui-href folge-query-url "Folge")))
              ,@(if (symbol-bound? 'sequel-query-url) `((@H ", ") ,(wui-href sequel-query-url "Sequel")))
              (@H "]")))
      ,@(ROLE-DEFAULT-actions (current-frame))
      ,@(let* ((frame (current-frame))(rea (resolve-symbol 'ROLE-EXTRA-actions frame))) (if (defined? rea) (rea frame)))
      ,@(if (symbol-bound? 'reindex-url) `((@H " &#183; ") (a (@ (href ,reindex-url)) "Reindex")))
      ,@(if (symbol-bound? 'delete-url) `((@H " &#183; ") (a (@ (href ,delete-url)) "Delete")))
      ,@(if (symbol-bound? 'reindex-url) `((@H " &#183; ") ,(wui-href reindex-url "Reindex")))
      ,@(if (symbol-bound? 'delete-url) `((@H " &#183; ") ,(wui-href delete-url "Delete")))
    )
  )
  (h2 "Interpreted Metadata")
  (table ,@(map wui-info-meta-table-row metadata))
  (h2 "References")
  ,@(if local-links `((h3 "Local")    (ul ,@(map wui-local-link local-links)))) 
  ,@(if query-links `((h3 "Queries")  (ul ,@(map wui-item-link query-links))))
  ,@(if ext-links   `((h3 "External") (ul ,@(map wui-item-popup-link ext-links))))
  (h3 "Unlinked")
  ,@unlinked-content
  (form
    (label (@ (for "phrase")) "Search Phrase")
    (input (@ (class "zs-input") (type "text") (id "phrase") (name ,query-key-phrase) (placeholder "Phrase..") (value ,phrase)))
    (label ((for "phrase")) "Search Phrase")
    (input ((class "zs-input") (type "text") (id "phrase") (name ,query-key-phrase) (placeholder "Phrase..") (value ,phrase)))
  )
  (h2 "Parts and encodings")
  ,(wui-enc-matrix enc-eval)
  (h3 "Parsed (not evaluated)")
  ,(wui-enc-matrix enc-parsed)
  ,@(if shadow-links
    `((h2 "Shadowed Boxes")
Changes to internal/box/constbox/listzettel.sxn.
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
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







-
-
+
+



-
+


-
+


-
+


-
+



-
+



-
+

-
+



-
-
-
+
+
+




;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header (h1 ,heading))
  (search (form (@ (action ,search-url))
    (input (@ (class "zs-input") (type "search") (inputmode "search") (name ,query-key-query)
  (search (form ((action ,search-url))
    (input ((class "zs-input") (type "search") (inputmode "search") (name ,query-key-query)
              (title "Contains the search that leads to the list below. You're allowed to modify it")
              (placeholder "Search..") (value ,query-value) (dir "auto")))))
  ,@(if (symbol-bound? 'tag-zettel)
     `((p (@ (class "zs-meta-zettel")) "Tag zettel: " ,@tag-zettel))
     `((p ((class "zs-meta-zettel")) "Tag zettel: " ,@tag-zettel))
    )
  ,@(if (symbol-bound? 'create-tag-zettel)
     `((p (@ (class "zs-meta-zettel")) "Create tag zettel: " ,@create-tag-zettel))
     `((p ((class "zs-meta-zettel")) "Create tag zettel: " ,@create-tag-zettel))
    )
  ,@(if (symbol-bound? 'role-zettel)
     `((p (@ (class "zs-meta-zettel")) "Role zettel: " ,@role-zettel))
     `((p ((class "zs-meta-zettel")) "Role zettel: " ,@role-zettel))
    )
  ,@(if (symbol-bound? 'create-role-zettel)
     `((p (@ (class "zs-meta-zettel")) "Create role zettel: " ,@create-role-zettel))
     `((p ((class "zs-meta-zettel")) "Create role zettel: " ,@create-role-zettel))
    )
  ,@content
  ,@endnotes
  (form (@ (action ,(if (symbol-bound? 'create-url) create-url)))
  (form ((action ,(if (symbol-bound? 'create-url) create-url)))
      ,(if (symbol-bound? 'data-url)
          `(@L "Other encodings"
               ,(if (> num-entries 3) `(@L " of these " ,num-entries " entries: ") ": ")
               (a (@ (href ,data-url)) "data")
               ,(wui-href data-url "data")
               ", "
               (a (@ (href ,plain-url)) "plain")
               ,(wui-href plain-url "plain")
           )
      )
      ,@(if (symbol-bound? 'create-url)
        `((input (@ (type "hidden") (name ,query-key-query) (value ,query-value)))
          (input (@ (type "hidden") (name ,query-key-seed) (value ,seed)))
          (input (@ (class "zs-primary") (type "submit") (value "Save As Zettel")))
        `((input ((type "hidden") (name ,query-key-query) (value ,query-value)))
          (input ((type "hidden") (name ,query-key-seed) (value ,seed)))
          (input ((class "zs-primary") (type "submit") (value "Save As Zettel")))
        )
      )
  )
)
Changes to internal/box/constbox/login.sxn.
9
10
11
12
13
14
15
16
17


18
19
20


21
22
23


24
25

26
27
9
10
11
12
13
14
15


16
17
18


19
20
21


22
23
24

25
26
27







-
-
+
+

-
-
+
+

-
-
+
+

-
+


;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header (h1 "Login"))
  ,@(if retry '((div (@ (class "zs-indication zs-error")) "Wrong user name / password. Try again.")))
  (form (@ (method "POST") (action ""))
  ,@(if retry '((div ((class "zs-indication zs-error")) "Wrong user name / password. Try again.")))
  (form ((method "POST") (action ""))
    (div
      (label (@ (for "username")) "User name:")
      (input (@ (class "zs-input") (type "text") (id "username") (name "username") (placeholder "Your user name..") (autofocus))))
      (label ((for "username")) "User name:")
      (input ((class "zs-input") (type "text") (id "username") (name "username") (placeholder "Your user name..") (autofocus))))
    (div
      (label (@ (for "password")) "Password:")
      (input (@ (class "zs-input") (type "password") (id "password") (name "password") (placeholder "Your password.."))))
      (label ((for "password")) "Password:")
      (input ((class "zs-input") (type "password") (id "password") (name "password") (placeholder "Your password.."))))
    (div
      (input (@ (class "zs-primary") (type "submit") (value "Login"))))
      (input ((class "zs-primary") (type "submit") (value "Login"))))
  )
)
Changes to internal/box/constbox/wuicode.sxn.
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
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







-
+

+
+
+

-
+


-
+












-
+


-
+





-
+








;; wui-list-item returns the argument as a HTML list item.
(defun wui-item (s) `(li ,s))

;; wui-info-meta-table-row takes a pair and translates it into a HTML table row
;; with two columns.
(defun wui-info-meta-table-row (p)
    `(tr (td (@ (class zs-info-meta-key)) ,(car p)) (td (@ (class zs-info-meta-value)) ,(cdr p))))
    `(tr (td ((class zs-info-meta-key)) ,(car p)) (td ((class zs-info-meta-value)) ,(cdr p))))

;; wui-href builds an HTML link
(defun wui-href (url text . attrs) `(a ((href ,url) ,@attrs) ,text))

;; wui-local-link translates a local link into HTML.
(defun wui-local-link (l) `(li (a (@ (href ,l )) ,l)))
(defun wui-local-link (l) `(li ,(wui-href l l)))

;; wui-link takes a link (title . url) and returns a HTML reference.
(defun wui-link (q) `(a (@ (href ,(cdr q))) ,(car q)))
(defun wui-link (q) (wui-href (cdr q) (car q)))

;; wui-item-link taks a pair (text . url) and returns a HTML link inside
;; a list item.
(defun wui-item-link (q) `(li ,(wui-link q)))

;; wui-tdata-link taks a pair (text . url) and returns a HTML link inside
;; a table data item.
(defun wui-tdata-link (q) `(td ,(wui-link q)))

;; wui-item-popup-link is like 'wui-item-link, but the HTML link will open
;; a new tab / window.
(defun wui-item-popup-link (e)
    `(li (a (@ (href ,e) (target "_blank") (rel "external noreferrer")) ,e)))
    `(li ,(wui-href e e '(target "_blank") '(rel "external noreferrer"))))

;; wui-option-value returns a value for an HTML option element.
(defun wui-option-value (v) `(option (@ (value ,v))))
(defun wui-option-value (v) `(option ((value ,v))))

;; wui-datalist returns a HTML datalist with the given HTML identifier and a
;; list of values.
(defun wui-datalist (id lst)
    (if lst
        `((datalist (@ (id ,id)) ,@(map wui-option-value lst)))))
        `((datalist ((id ,id)) ,@(map wui-option-value lst)))))

;; wui-pair-desc-item takes a pair '(term . text) and returns a list with
;; a HTML description term and a HTML description data. 
(defun wui-pair-desc-item (p) `((dt ,(car p)) (dd ,(cdr p))))

;; wui-meta-desc returns a HTML description list made from the list of pairs
;; given.
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
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







-
+














-
+











-
+

-
+

-
+







-
+









-
+







         (lambda (row) `(tr (th ,(car row)) ,@(map wui-tdata-link (cdr row))))
         matrix)))

;; wui-optional-link puts the text into a link, if symbol is defined. Otherwise just return the text
(defun wui-optional-link (text url-sym)
    (let ((url (resolve-symbol url-sym)))
         (if (defined? url)
             `(a (@ (href ,(resolve-symbol url-sym))) ,text)
             (wui-href url text)
             text)))

;; CSS-ROLE-map is a mapping (pair list, assoc list) of role names to zettel
;; identifier. It is used in the base template to update the metadata of the
;; HTML page to include some role specific CSS code.
;; Referenced in function "ROLE-DEFAULT-meta".
(defvar CSS-ROLE-map '())

;; ROLE-DEFAULT-meta returns some metadata for the base template. Any role
;; specific code should include the returned list of this function.
(defun ROLE-DEFAULT-meta (frame)
    `(,@(let* ((meta-role (resolve-symbol 'meta-role frame))
               (entry (assoc CSS-ROLE-map meta-role)))
              (if (pair? entry)
                  `((link (@ (rel "stylesheet") (href ,(zid-content-path (cdr entry))))))
                  `((link ((rel "stylesheet") (href ,(zid-content-path (cdr entry))))))
              )
        )
    )
)

;; ACTION-SEPARATOR defines a HTML value that separates actions links.
(defvar ACTION-SEPARATOR '(@H " &#183; "))

;; ROLE-DEFAULT-actions returns the default text for actions.
(defun ROLE-DEFAULT-actions (frame)
    `(,@(let ((copy-url (resolve-symbol 'copy-url frame)))
             (if (defined? copy-url) `((@H " &#183; ") (a (@ (href ,copy-url)) "Copy"))))
             (if (defined? copy-url) `((@H " &#183; ") ,(wui-href copy-url "Copy"))))
      ,@(let ((sequel-url (resolve-symbol 'sequel-url frame)))
             (if (defined? sequel-url) `((@H " &#183; ") (a (@ (href ,sequel-url)) "Sequel"))))
             (if (defined? sequel-url) `((@H " &#183; ") ,(wui-href sequel-url "Sequel"))))
      ,@(let ((folge-url (resolve-symbol 'folge-url frame)))
             (if (defined? folge-url) `((@H " &#183; ") (a (@ (href ,folge-url)) "Folge"))))
             (if (defined? folge-url) `((@H " &#183; ") ,(wui-href folge-url "Folge"))))
    )
)

;; ROLE-tag-actions returns an additional action "Zettel" for zettel with role "tag".
(defun ROLE-tag-actions (frame)
    `(,@(let ((title (resolve-symbol 'title frame)))
             (if (and (defined? title) title)
                 `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "tags:" title)))) "Zettel"))
                 `(,ACTION-SEPARATOR ,(wui-href (query->url (concat "tags:" title)) "Zettel"))
             )
        )
    )
)

;; ROLE-role-actions returns an additional action "Zettel" for zettel with role "role".
(defun ROLE-role-actions (frame)
    `(,@(let ((title (resolve-symbol 'title frame)))
             (if (and (defined? title) title)
                 `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "role:" title)))) "Zettel"))
                 `(,ACTION-SEPARATOR ,(wui-href (query->url (concat "role:" title)) "Zettel"))
             )
        )
    )
)

;; ROLE-DEFAULT-heading returns the default text for headings, below the
;; references of a zettel. In most cases it should be called from an
Changes to internal/box/constbox/zettel.sxn.
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
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







-
-
+
+

-
-
+
+

-
+


-
-
+
+














-
+



-
+


-
-
+
+



;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header
    (h1 ,heading)
    (div (@ (class "zs-meta"))
      ,@(if (symbol-bound? 'edit-url) `((a (@ (href ,edit-url)) "Edit") (@H " &#183; ")))
    (div ((class "zs-meta"))
      ,@(if (symbol-bound? 'edit-url) `(,(wui-href edit-url "Edit") (@H " &#183; ")))
      ,zid (@H " &#183; ")
      (a (@ (href ,info-url)) "Info") (@H " &#183; ")
      "(" ,@(if (symbol-bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role)))
      ,(wui-href info-url "Info") (@H " &#183; ")
      "(" ,@(if (symbol-bound? 'role-url) `(,(wui-href role-url meta-role)))
          ,@(if (and (symbol-bound? 'folge-role-url) (symbol-bound? 'meta-folge-role))
                `((@H " &rarr; ") (a (@ (href ,folge-role-url)) ,meta-folge-role)))
                `((@H " &rarr; ") ,(wui-href folge-role-url meta-folge-role)))
      ")"
      ,@(if tag-refs `((@H " &#183; ") ,@tag-refs))
      (@H " &#183; ") (a (@ (href ,context-url)) "Context")
      ,@(if (symbol-bound? 'thread-query-url) `((@H " &#183; ")  (a (@ (href ,thread-query-url)) "Thread")))
      (@H " &#183; ") ,(wui-href context-url "Context")
      ,@(if (symbol-bound? 'thread-query-url) `((@H " &#183; ") ,(wui-href thread-query-url "Thread")))
      ,@(ROLE-DEFAULT-actions (current-frame))
      ,@(let* ((frame (current-frame))(rea (resolve-symbol 'ROLE-EXTRA-actions frame))) (if (defined? rea) (rea frame)))
      ,@(if superior-refs `((br) "Superior: " ,superior-refs))
      ,@(if prequel-refs `((br) ,(wui-optional-link "Prequel" 'sequel-query-url) ": " ,prequel-refs))
      ,@(if precursor-refs `((br) ,(wui-optional-link "Precursor" 'folge-query-url) ": " ,precursor-refs))
      ,@(ROLE-DEFAULT-heading (current-frame))
      ,@(let* ((frame (current-frame))(reh (resolve-symbol 'ROLE-EXTRA-heading frame))) (if (defined? reh) (reh frame)))
    )
  )
  ,@content
  ,endnotes
  ,@(if (or folge-links sequel-links back-links subordinate-links)
    `((nav
      ,@(if folge-links
            `((details (@ (,folge-open))
            `((details ((,folge-open))
                       (summary ,(wui-optional-link "Folgezettel" 'folge-query-url))
                       (ul ,@(map wui-item-link folge-links)))))
      ,@(if sequel-links
            `((details (@ (,sequel-open))
            `((details ((,sequel-open))
                       (summary ,(wui-optional-link "Sequel" 'sequel-query-url))
                       (ul ,@(map wui-item-link sequel-links)))))
      ,@(if subordinate-links `((details (@ (,subordinate-open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links)))))
      ,@(if back-links `((details (@ (,back-open)) (summary "Incoming") (ul ,@(map wui-item-link back-links)))))
      ,@(if subordinate-links `((details ((,subordinate-open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links)))))
      ,@(if back-links `((details ((,back-open)) (summary "Incoming") (ul ,@(map wui-item-link back-links)))))
     ))
  )
)
Changes to internal/box/manager/collect.go.
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
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






















+



+
+

-















-
-
+
+


-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------

package manager

import (
	"strings"

	"t73f.de/r/sx"
	zerostrings "t73f.de/r/zero/strings"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/id/idset"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/box/manager/store"
)

type collectData struct {
	refs  *idset.Set
	words store.WordSet
	urls  store.WordSet
}

func (data *collectData) initialize() {
	data.refs = idset.New()
	data.words = store.NewWordSet()
	data.urls = store.NewWordSet()
}

func collectZettelIndexData(zn *ast.ZettelNode, data *collectData) {
	ast.Walk(data, &zn.BlocksAST)
func collectZettelIndexData(blocks *sx.Pair, data *collectData) {
	zsx.WalkIt(data, blocks, nil)
}

func (data *collectData) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.VerbatimNode:
		data.addText(string(n.Content))
	case *ast.TranscludeNode:
		data.addRef(n.Ref)
	case *ast.TextNode:
		data.addText(n.Text)
	case *ast.LinkNode:
		data.addRef(n.Ref)
	case *ast.EmbedRefNode:
		data.addRef(n.Ref)
	case *ast.CiteNode:
		data.addText(n.Key)
	case *ast.LiteralNode:
		data.addText(string(n.Content))
	}
	return data
func (data *collectData) VisitItBefore(node *sx.Pair, _ *sx.Pair) bool {
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		switch sym {
		case zsx.SymText:
			data.addText(zsx.GetText(node))
		case zsx.SymVerbatimCode, zsx.SymVerbatimComment, zsx.SymVerbatimEval,
			zsx.SymVerbatimHTML, zsx.SymVerbatimMath, zsx.SymVerbatimZettel:
			_, _, s := zsx.GetVerbatim(node)
			data.addText(s)
		case zsx.SymLiteralCode, zsx.SymLiteralComment, zsx.SymLiteralInput,
			zsx.SymLiteralMath, zsx.SymLiteralOutput:
			_, _, s := zsx.GetLiteral(node)
			data.addText(s)
		case zsx.SymLink:
			_, ref, _ := zsx.GetLink(node)
			data.addRef(ref)
		case zsx.SymEmbed:
			_, ref, _, _ := zsx.GetEmbed(node)
			data.addRef(ref)
		case zsx.SymTransclude:
			_, ref, _ := zsx.GetTransclusion(node)
			data.addRef(ref)
		case zsx.SymCite:
			_, key, _ := zsx.GetCite(node)
			data.addText(key)
		}
	}
	return false
}
func (data *collectData) VisitItAfter(*sx.Pair, *sx.Pair) {}

func (data *collectData) addRef(ref *sx.Pair) {
	sym, refValue := zsx.GetReference(ref)
	if zsx.SymRefStateExternal.IsEqual(sym) {
		data.urls.Add(strings.ToLower(refValue))
	} else if sz.SymRefStateZettel.IsEqual(sym) {
		if zid, err := id.Parse(refValue); err == nil {
			data.refs.Add(zid)
		}
	}
}

func (data *collectData) addText(s string) {
	for _, word := range zerostrings.NormalizeWords(s) {
		data.words.Add(word)
	}
}

func (data *collectData) addRef(ref *ast.Reference) {
	if ref == nil {
		return
	}
	if ref.IsExternal() {
		data.urls.Add(strings.ToLower(ref.Value))
	}
	if !ref.IsZettel() {
		return
	}
	if zid, err := id.Parse(ref.URL.Path); err == nil {
		data.refs.Add(zid)
	}
}
Changes to internal/box/manager/indexer.go.
148
149
150
151
152
153
154
155


156
157
158
159
160
161
162
148
149
150
151
152
153
154

155
156
157
158
159
160
161
162
163







-
+
+







	return true
}

func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel zettel.Zettel) {
	var cData collectData
	cData.initialize()
	if mustIndexZettel(zettel.Meta) {
		collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData)
		zn := parser.ParseZettel(ctx, zettel, "", mgr.rtConfig)
		collectZettelIndexData(zn.Blocks, &cData)
	}

	m := zettel.Meta
	zi := store.NewZettelIndex(m)
	mgr.idxCollectFromMeta(ctx, m, zi, &cData)
	mgr.idxProcessData(ctx, zi, &cData)
	toCheck := mgr.idxStore.UpdateReferences(ctx, zi)
Changes to internal/collect/collect.go.
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

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







-
+
+



-
+




-
-
-
+
+
+

-
+




-
+

-
+

-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+

+

// Package collect provides functions to collect items from a syntax tree.
package collect

import (
	"iter"

	"zettelstore.de/z/internal/ast"
	"t73f.de/r/sx"
	"t73f.de/r/zsx"
)

type refYielder struct {
	yield func(*ast.Reference) bool
	yield func(*sx.Pair) bool
	stop  bool
}

// ReferenceSeq returns an iterator of all references mentioned in the given
// zettel. This also includes references to images.
func ReferenceSeq(zn *ast.ZettelNode) iter.Seq[*ast.Reference] {
	return func(yield func(*ast.Reference) bool) {
// block slice. This also includes references to images.
func ReferenceSeq(block *sx.Pair) iter.Seq[*sx.Pair] {
	return func(yield func(*sx.Pair) bool) {
		yielder := refYielder{yield, false}
		ast.Walk(&yielder, &zn.BlocksAST)
		zsx.WalkIt(&yielder, block, nil)
	}
}

// Visit all node to collect data for the summary.
func (y *refYielder) Visit(node ast.Node) ast.Visitor {
func (y *refYielder) VisitItBefore(node *sx.Pair, _ *sx.Pair) bool {
	if y.stop {
		return nil
		return true
	}
	var stop bool
	switch n := node.(type) {
	case *ast.TranscludeNode:
		stop = !y.yield(n.Ref)
	case *ast.LinkNode:
		stop = !y.yield(n.Ref)
	case *ast.EmbedRefNode:
		stop = !y.yield(n.Ref)
	}
	if stop {
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		switch sym {
		case zsx.SymLink:
			_, ref, _ := zsx.GetLink(node)
			y.stop = !y.yield(ref)

		case zsx.SymEmbed:
			_, ref, _, _ := zsx.GetEmbed(node)
			y.stop = !y.yield(ref)

		case zsx.SymTransclude:
			_, ref, _ := zsx.GetTransclusion(node)
			y.stop = !y.yield(ref)
		}
		if y.stop {
		y.stop = true
		return nil
	}
	return y
			return true
		}
	}
	return false
}
func (*refYielder) VisitItAfter(*sx.Pair, *sx.Pair) {}
Changes to internal/collect/collect_test.go.
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
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







+
+
+
-
+



-
-
-
+
+
+
+







-
-
+




-
-
-
-
+
+
+
+




-
-
+
+




-
-
+
-
-
+




// Package collect_test provides some unit test for collectors.
package collect_test

import (
	"slices"
	"testing"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"
	"zettelstore.de/z/internal/ast"

	"zettelstore.de/z/internal/collect"
)

func parseRef(s string) *ast.Reference {
	r := ast.ParseReference(s)
	if !r.IsValid() {
func parseRef(s string) *sx.Pair {
	r := sz.ScanReference(s)
	sym, _ := zsx.GetReference(r)
	if zsx.SymRefStateInvalid.IsEqualSymbol(sym) {
		panic(s)
	}
	return r
}

func TestReferenceSeq(t *testing.T) {
	t.Parallel()
	zn := &ast.ZettelNode{}
	summary := slices.Collect(collect.ReferenceSeq(zn))
	summary := slices.Collect(collect.ReferenceSeq(nil))
	if len(summary) != 0 {
		t.Error("No references expected, but got:", summary)
	}

	intNode := &ast.LinkNode{Ref: parseRef("01234567890123")}
	para := ast.CreateParaNode(intNode, &ast.LinkNode{Ref: parseRef("https://zettelstore.de/z")})
	zn.BlocksAST = ast.BlockSlice{para}
	summary = slices.Collect(collect.ReferenceSeq(zn))
	intNode := zsx.MakeLink(nil, parseRef("01234567890123"), nil)
	para := zsx.MakePara(intNode, zsx.MakeLink(nil, parseRef("https://zettelstore.de/z"), nil))
	blocks := zsx.MakeBlock(para)
	summary = slices.Collect(collect.ReferenceSeq(blocks))
	if len(summary) != 2 {
		t.Error("2 refs expected, but got:", summary)
	}

	para.Inlines = append(para.Inlines, intNode)
	summary = slices.Collect(collect.ReferenceSeq(zn))
	para.LastPair().AppendBang(intNode)
	summary = slices.Collect(collect.ReferenceSeq(blocks))
	if cnt := len(summary); cnt != 3 {
		t.Error("Ref count does not work. Expected: 3, got", summary)
	}

	zn = &ast.ZettelNode{
		BlocksAST: ast.BlockSlice{ast.CreateParaNode(&ast.EmbedRefNode{Ref: parseRef("12345678901234")})},
	blocks = zsx.MakeBlock(zsx.MakePara(zsx.MakeEmbed(nil, parseRef("12345678901234"), "", nil)))
	}
	summary = slices.Collect(collect.ReferenceSeq(zn))
	summary = slices.Collect(collect.ReferenceSeq(blocks))
	if len(summary) != 1 {
		t.Error("Only one image ref expected, but got: ", summary)
	}
}
Changes to internal/collect/order.go.
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
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







-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+


-
-
-
-
+
+
+
+
+
+
+







-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
-
-
-
-
-
+
+
+
+




// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package collect provides functions to collect items from a syntax tree.
package collect

import "zettelstore.de/z/internal/ast"

// Order of internal links within the given zettel.
func Order(zn *ast.ZettelNode) (result []*ast.LinkNode) {
	for _, bn := range zn.BlocksAST {
		ln, ok := bn.(*ast.NestedListNode)
		if !ok {
import (
	"t73f.de/r/sx"
	"t73f.de/r/zsx"
)

// Order returns links in the items of the first list found in the given block node.
func Order(block *sx.Pair) *sx.Pair {
	var lb sx.ListBuilder
	blocks := zsx.GetBlock(block)
	for bn := range blocks.Pairs() {
		blk := bn.Head()
		if sym, isSymbol := sx.GetSymbol(blk.Car()); isSymbol {
			continue
		}
		switch ln.Kind {
		case ast.NestedListOrdered, ast.NestedListUnordered:
			for _, is := range ln.Items {
				if ln := firstItemZettelLink(is); ln != nil {
					result = append(result, ln)
				}
			}
		}
	}
	return result
			if zsx.SymListUnordered.IsEqualSymbol(sym) || zsx.SymListOrdered.IsEqualSymbol(sym) {
				_, _, items := zsx.GetList(blk)
				for item := range items.Pairs() {
					if ln := firstItemZettelLink(item.Head()); ln != nil {
						lb.Add(ln)
					}
				}
			}
		}
	}
	return lb.List()
}

func firstItemZettelLink(is ast.ItemSlice) *ast.LinkNode {
	for _, in := range is {
		if pn, ok := in.(*ast.ParaNode); ok {
			if ln := firstInlineZettelLink(pn.Inlines); ln != nil {
func firstItemZettelLink(item *sx.Pair) *sx.Pair {
	blocks := zsx.GetBlock(item)
	for bn := range blocks.Pairs() {
		blk := bn.Head()
		if sym, isSymbol := sx.GetSymbol(blk.Car()); isSymbol && zsx.SymPara.IsEqualSymbol(sym) {
			inlines := zsx.GetPara(blk)
			if ln := firstInlineZettelLink(inlines); ln != nil {
				return ln
			}
		}
	}
	return nil
}

func firstInlineZettelLink(is ast.InlineSlice) (result *ast.LinkNode) {
	for _, inl := range is {
		switch in := inl.(type) {
		case *ast.LinkNode:
			return in
func firstInlineZettelLink(inlines *sx.Pair) (result *sx.Pair) {
	for inode := range inlines.Pairs() {
		inl := inode.Head()
		if sym, isSymbol := sx.GetSymbol(inl.Car()); isSymbol {
			switch sym {
			case zsx.SymLink:
				return inl
		case *ast.EmbedRefNode:
			result = firstInlineZettelLink(in.Inlines)
		case *ast.EmbedBLOBNode:
			result = firstInlineZettelLink(in.Inlines)
		case *ast.CiteNode:
			result = firstInlineZettelLink(in.Inlines)
			case zsx.SymFormatDelete, zsx.SymFormatEmph, zsx.SymFormatInsert, zsx.SymFormatMark,
				zsx.SymFormatQuote, zsx.SymFormatSpan, zsx.SymFormatStrong, zsx.SymFormatSub,
				zsx.SymFormatSuper:
				_, _, finlines := zsx.GetFormat(inl)
				result = firstInlineZettelLink(finlines)
			case zsx.SymCite:
				_, _, cinlines := zsx.GetCite(inl)
				result = firstInlineZettelLink(cinlines)
			case zsx.SymEmbed:
				_, _, _, einlines := zsx.GetEmbed(inl)
				result = firstInlineZettelLink(einlines)
		case *ast.FootnoteNode:
			// Ignore references in footnotes
			continue
		case *ast.FormatNode:
			result = firstInlineZettelLink(in.Inlines)
			case zsx.SymEmbedBLOB:
				_, _, _, binlines := zsx.GetEmbedBLOBuncode(inl)
				result = firstInlineZettelLink(binlines)
		default:
			continue
		}
		if result != nil {
			return result
			}
			if result != nil {
				return result
			}
		}
	}
	return nil
}
Changes to internal/encoder/encoder.go.
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
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







+










-
+


-
+

-
-
+
+









-









-






-
+







// Package encoder provides a generic interface to encode the abstract syntax
// tree into some text form.
package encoder

import (
	"io"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"

	"zettelstore.de/z/internal/ast"
)

// Encoder is an interface that allows to encode different parts of a zettel.
type Encoder interface {
	// WriteZettel encodes a whole zettel and writes it to the Writer.
	WriteZettel(io.Writer, *ast.ZettelNode) (int, error)
	WriteZettel(io.Writer, *ast.Zettel) error

	// WriteMeta encodes just the metadata.
	WriteMeta(io.Writer, *meta.Meta) (int, error)
	WriteMeta(io.Writer, *meta.Meta) error

	// WiteBlocks encodes a block slice, i.e. the zettel content.
	WriteBlocks(io.Writer, *ast.BlockSlice) (int, error)
	// WriteSz encodes  SZ represented zettel content.
	WriteSz(io.Writer, *sx.Pair) error
}

// Create builds a new encoder with the given options.
func Create(enc api.EncodingEnum, params *CreateParameter) Encoder {
	switch enc {
	case api.EncoderHTML:
		// We need a new transformer every time, because tx.inVerse must be unique.
		// If we can refactor it out, the transformer can be created only once.
		return &htmlEncoder{
			tx:   NewSzTransformer(),
			th:   shtml.NewEvaluator(1),
			lang: params.Lang,
		}
	case api.EncoderMD:
		return &mdEncoder{lang: params.Lang}
	case api.EncoderSHTML:
		// We need a new transformer every time, because tx.inVerse must be unique.
		// If we can refactor it out, the transformer can be created only once.
		return &shtmlEncoder{
			tx:   NewSzTransformer(),
			th:   shtml.NewEvaluator(1),
			lang: params.Lang,
		}
	case api.EncoderSz:
		// We need a new transformer every time, because trans.inVerse must be unique.
		// If we can refactor it out, the transformer can be created only once.
		return &szEncoder{trans: NewSzTransformer()}
		return &szEncoder{}
	case api.EncoderText:
		return (*TextEncoder)(nil)
	case api.EncoderZmk:
		return (*zmkEncoder)(nil)
	}
	return nil
}
Changes to internal/encoder/encoder_blob_test.go.
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
16
17
18
19
20
21
22

23
24
25
26
27
28
29







-







import (
	"testing"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/parser"
)

type blobTestCase struct {
	descr  string
	syntax string
	blob   []byte
40
41
42
43
44
45
46
47
48


49
50
51
52
53
54
55
56
57
58
59
60
61


62
63
39
40
41
42
43
44
45


46
47
48
49
50
51
52
53
54
55
56
57
58


59
60
61
62







-
-
+
+











-
-
+
+


			0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x7e, 0x9b,
			0x55, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x62, 0x00, 0x00, 0x00,
			0x06, 0x00, 0x03, 0x36, 0x37, 0x7c, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
			0x42, 0x60, 0x82,
		},
		expect: expectMap{
			encoderHTML:  `<p><img alt="Minimal PNG" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="></p>`,
			encoderSz:    `(BLOCK (BLOB () ((TEXT "Minimal PNG")) "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="))`,
			encoderSHTML: `((p (img (@ (alt . "Minimal PNG") (src . "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==")))))`,
			encoderSz:    `(BLOCK (BLOB () "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==" (TEXT "Minimal PNG")))`,
			encoderSHTML: `((p (img ((alt . "Minimal PNG") (src . "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==")))))`,
			encoderText:  "",
			encoderZmk:   `%% Unable to display BLOB with description 'Minimal PNG' and syntax 'png'.`,
		},
	},
}

func TestBlob(t *testing.T) {
	m := meta.New(id.Invalid)
	for testNum, tc := range pngTestCases {
		m.Set(meta.KeyTitle, meta.Value(tc.descr))
		inp := input.NewInput(tc.blob)
		bs := parser.Parse(inp, m, tc.syntax, config.NoHTML)
		checkEncodings(t, testNum, bs, false, tc.descr, tc.expect, "???")
		node := parser.Parse(inp, m, tc.syntax, nil)
		checkEncodings(t, testNum, node, false, tc.descr, tc.expect, "???")
	}
}
Changes to internal/encoder/encoder_block_test.go.
8
9
10
11
12
13
14






15
16
17
18
19
20
21
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27







+
+
+
+
+
+







// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package encoder_test

import "t73f.de/r/zsc/domain/meta"

// func TestEncoderBlock(t *testing.T) {
// 	executeTestCases(t, tcsBlock)
// }

var tcsBlock = []zmkTestCase{
	{
		descr: "Empty Zettelmarkup should produce near nothing",
		zmk:   "",
		expect: expectMap{
			encoderHTML:  "",
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
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







-
+










-
+











-
+











-
+







	{
		descr: "Simple Heading",
		zmk:   `=== Top Job`,
		expect: expectMap{
			encoderHTML:  "<h2 id=\"top-job\">Top Job</h2>",
			encoderMD:    "# Top Job",
			encoderSz:    `(BLOCK (HEADING 1 () "top-job" "top-job" (TEXT "Top Job")))`,
			encoderSHTML: `((h2 (@ (id . "top-job")) "Top Job"))`,
			encoderSHTML: `((h2 ((id . "top-job")) "Top Job"))`,
			encoderText:  `Top Job`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple List",
		zmk:   "* A\n* B\n* C",
		expect: expectMap{
			encoderHTML:  "<ul><li>A</li><li>B</li><li>C</li></ul>",
			encoderMD:    "* A\n* B\n* C",
			encoderSz:    `(BLOCK (UNORDERED () (INLINE (TEXT "A")) (INLINE (TEXT "B")) (INLINE (TEXT "C"))))`,
			encoderSz:    `(BLOCK (UNORDERED () (BLOCK (PARA (TEXT "A"))) (BLOCK (PARA (TEXT "B"))) (BLOCK (PARA (TEXT "C")))))`,
			encoderSHTML: `((ul (li "A") (li "B") (li "C")))`,
			encoderText:  "A\nB\nC",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Nested List",
		zmk:   "* T1\n*# T2\n* T3\n** T4\n** T5\n* T6",
		expect: expectMap{
			encoderHTML:  `<ul><li><p>T1</p><ol><li>T2</li></ol></li><li><p>T3</p><ul><li>T4</li><li>T5</li></ul></li><li><p>T6</p></li></ul>`,
			encoderMD:    "* T1\n    1. T2\n* T3\n    * T4\n    * T5\n* T6",
			encoderSz:    `(BLOCK (UNORDERED () (BLOCK (PARA (TEXT "T1")) (ORDERED () (INLINE (TEXT "T2")))) (BLOCK (PARA (TEXT "T3")) (UNORDERED () (INLINE (TEXT "T4")) (INLINE (TEXT "T5")))) (BLOCK (PARA (TEXT "T6")))))`,
			encoderSz:    `(BLOCK (UNORDERED () (BLOCK (PARA (TEXT "T1")) (ORDERED () (BLOCK (PARA (TEXT "T2"))))) (BLOCK (PARA (TEXT "T3")) (UNORDERED () (BLOCK (PARA (TEXT "T4"))) (BLOCK (PARA (TEXT "T5"))))) (BLOCK (PARA (TEXT "T6")))))`,
			encoderSHTML: `((ul (li (p "T1") (ol (li "T2"))) (li (p "T3") (ul (li "T4") (li "T5"))) (li (p "T6"))))`,
			encoderText:  "T1\nT2\nT3\nT4\nT5\nT6",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Sequence of two lists",
		zmk:   "* Item1.1\n* Item1.2\n* Item1.3\n\n* Item2.1\n* Item2.2",
		expect: expectMap{
			encoderHTML:  "<ul><li>Item1.1</li><li>Item1.2</li><li>Item1.3</li><li>Item2.1</li><li>Item2.2</li></ul>",
			encoderMD:    "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2",
			encoderSz:    `(BLOCK (UNORDERED () (INLINE (TEXT "Item1.1")) (INLINE (TEXT "Item1.2")) (INLINE (TEXT "Item1.3")) (INLINE (TEXT "Item2.1")) (INLINE (TEXT "Item2.2"))))`,
			encoderSz:    `(BLOCK (UNORDERED () (BLOCK (PARA (TEXT "Item1.1"))) (BLOCK (PARA (TEXT "Item1.2"))) (BLOCK (PARA (TEXT "Item1.3"))) (BLOCK (PARA (TEXT "Item2.1"))) (BLOCK (PARA (TEXT "Item2.2")))))`,
			encoderSHTML: `((ul (li "Item1.1") (li "Item1.2") (li "Item1.3") (li "Item2.1") (li "Item2.2")))`,
			encoderText:  "Item1.1\nItem1.2\nItem1.3\nItem2.1\nItem2.2",
			encoderZmk:   "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2",
		},
	},
	{
		descr: "Simple horizontal rule",
125
126
127
128
129
130
131
132

133
134
135
136
137
138
139
131
132
133
134
135
136
137

138
139
140
141
142
143
144
145







-
+







	{
		descr: "Thematic break with attribute",
		zmk:   `---{lang="zmk"}`,
		expect: expectMap{
			encoderHTML:  `<hr lang="zmk">`,
			encoderMD:    "---",
			encoderSz:    `(BLOCK (THEMATIC (("lang" . "zmk"))))`,
			encoderSHTML: `((hr (@ (lang . "zmk"))))`,
			encoderSHTML: `((hr ((lang . "zmk"))))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "No list after paragraph",
		zmk:   "Text\n*abc",
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
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







-
+











-
+







	},
	{
		descr: "A list after paragraph",
		zmk:   "Text\n# abc",
		expect: expectMap{
			encoderHTML:  "<p>Text</p><ol><li>abc</li></ol>",
			encoderMD:    "Text\n\n1. abc",
			encoderSz:    `(BLOCK (PARA (TEXT "Text")) (ORDERED () (INLINE (TEXT "abc"))))`,
			encoderSz:    `(BLOCK (PARA (TEXT "Text")) (ORDERED () (BLOCK (PARA (TEXT "abc")))))`,
			encoderSHTML: `((p "Text") (ol (li "abc")))`,
			encoderText:  "Text\nabc",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple List Quote",
		zmk:   "> ToBeOrNotToBe",
		expect: expectMap{
			encoderHTML:  "<blockquote>ToBeOrNotToBe</blockquote>",
			encoderMD:    "> ToBeOrNotToBe",
			encoderSz:    `(BLOCK (QUOTATION () (INLINE (TEXT "ToBeOrNotToBe"))))`,
			encoderSz:    `(BLOCK (QUOTATION () (BLOCK (PARA (TEXT "ToBeOrNotToBe")))))`,
			encoderSHTML: `((blockquote (@L "ToBeOrNotToBe")))`,
			encoderText:  "ToBeOrNotToBe",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Quote Block",
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
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







-
+











-
+







	{
		descr: "Simple Verbatim Eval",
		zmk:   "~~~\nHello\nWorld\n~~~",
		expect: expectMap{
			encoderHTML:  "<pre><code class=\"zs-eval\">Hello\nWorld</code></pre>",
			encoderMD:    "",
			encoderSz:    `(BLOCK (VERBATIM-EVAL () "Hello\nWorld"))`,
			encoderSHTML: "((pre (code (@ (class . \"zs-eval\")) \"Hello\\nWorld\")))",
			encoderSHTML: "((pre (code ((class . \"zs-eval\")) \"Hello\\nWorld\")))",
			encoderText:  "Hello\nWorld",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Verbatim Math",
		zmk:   "$$$\nHello\n\\LaTeX\n$$$",
		expect: expectMap{
			encoderHTML:  "<pre><code class=\"zs-math\">Hello\n\\LaTeX</code></pre>",
			encoderMD:    "",
			encoderSz:    `(BLOCK (VERBATIM-MATH () "Hello\n\\LaTeX"))`,
			encoderSHTML: "((pre (code (@ (class . \"zs-math\")) \"Hello\\n\\\\LaTeX\")))",
			encoderSHTML: "((pre (code ((class . \"zs-math\")) \"Hello\\n\\\\LaTeX\")))",
			encoderText:  "Hello\n\\LaTeX",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Description List",
		zmk:   "; Zettel\n: Paper\n: Note\n; Zettelkasten\n: Slip box",
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
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
433
434
435
436
437
438











-
+














-
-
+
+
















-
+











-
+











-
+















+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+












-
-
-
-
	},
	{
		descr: "Simple Table",
		zmk:   "|c1|c2|c3\n|d1||d3",
		expect: expectMap{
			encoderHTML:  `<table><tbody><tr><td>c1</td><td>c2</td><td>c3</td></tr><tr><td>d1</td><td></td><td>d3</td></tr></tbody></table>`,
			encoderMD:    "",
			encoderSz:    `(BLOCK (TABLE () ((CELL () (TEXT "c1")) (CELL () (TEXT "c2")) (CELL () (TEXT "c3"))) ((CELL () (TEXT "d1")) (CELL ()) (CELL () (TEXT "d3")))))`,
			encoderSz:    `(BLOCK (TABLE () () ((CELL () (TEXT "c1")) (CELL () (TEXT "c2")) (CELL () (TEXT "c3"))) ((CELL () (TEXT "d1")) (CELL ()) (CELL () (TEXT "d3")))))`,
			encoderSHTML: `((table (tbody (tr (td "c1") (td "c2") (td "c3")) (tr (td "d1") (td) (td "d3")))))`,
			encoderText:  "c1 c2 c3\nd1  d3",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Table with alignment and comment",
		zmk: `|h1>|=h2|h3:|
|%--+---+---+
|<c1|c2|:c3|
|f1|f2|=f3`,
		expect: expectMap{
			encoderHTML:  `<table><thead><tr><th class="right">h1</th><th>h2</th><th class="center">h3</th></tr></thead><tbody><tr><td class="left">c1</td><td>c2</td><td class="center">c3</td></tr><tr><td class="right">f1</td><td>f2</td><td class="center">=f3</td></tr></tbody></table>`,
			encoderMD:    "",
			encoderSz:    `(BLOCK (TABLE ((CELL ((align . "right")) (TEXT "h1")) (CELL () (TEXT "h2")) (CELL ((align . "center")) (TEXT "h3"))) ((CELL ((align . "left")) (TEXT "c1")) (CELL () (TEXT "c2")) (CELL ((align . "center")) (TEXT "c3"))) ((CELL ((align . "right")) (TEXT "f1")) (CELL () (TEXT "f2")) (CELL ((align . "center")) (TEXT "=f3")))))`,
			encoderSHTML: `((table (thead (tr (th (@ (class . "right")) "h1") (th "h2") (th (@ (class . "center")) "h3"))) (tbody (tr (td (@ (class . "left")) "c1") (td "c2") (td (@ (class . "center")) "c3")) (tr (td (@ (class . "right")) "f1") (td "f2") (td (@ (class . "center")) "=f3")))))`,
			encoderSz:    `(BLOCK (TABLE () ((CELL ((align . "right")) (TEXT "h1")) (CELL () (TEXT "h2")) (CELL ((align . "center")) (TEXT "h3"))) ((CELL ((align . "left")) (TEXT "c1")) (CELL () (TEXT "c2")) (CELL ((align . "center")) (TEXT "c3"))) ((CELL ((align . "right")) (TEXT "f1")) (CELL () (TEXT "f2")) (CELL ((align . "center")) (TEXT "=f3")))))`,
			encoderSHTML: `((table (thead (tr (th ((class . "right")) "h1") (th "h2") (th ((class . "center")) "h3"))) (tbody (tr (td ((class . "left")) "c1") (td "c2") (td ((class . "center")) "c3")) (tr (td ((class . "right")) "f1") (td "f2") (td ((class . "center")) "=f3")))))`,
			encoderText:  "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3",
			encoderZmk: /*`|=h1>|=h2|=h3:
			|<c1|c2|c3
			|f1|f2|=f3`,*/
			`|=>h1|=h2|=:h3
|<c1|c2|:c3
|>f1|f2|:=f3`,
		},
	},
	{
		descr: "Simple Endnote",
		zmk:   `Text[^Endnote]`,
		expect: expectMap{
			encoderHTML:  "<p>Text<sup id=\"fnref:1\"><a class=\"zs-noteref\" href=\"#fn:1\" role=\"doc-noteref\">1</a></sup></p><ol class=\"zs-endnotes\"><li class=\"zs-endnote\" id=\"fn:1\" role=\"doc-endnote\" value=\"1\">Endnote <a class=\"zs-endnote-backref\" href=\"#fnref:1\" role=\"doc-backlink\">\u21a9\ufe0e</a></li></ol>",
			encoderMD:    "Text",
			encoderSz:    `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (TEXT "Endnote"))))`,
			encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))",
			encoderSHTML: "((p \"Text\" (sup ((id . \"fnref:1\")) (a ((class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))",
			encoderText:  "Text Endnote",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Nested Endnotes",
		zmk:   `Text[^Endnote[^Nested]]`,
		expect: expectMap{
			encoderHTML:  "<p>Text<sup id=\"fnref:1\"><a class=\"zs-noteref\" href=\"#fn:1\" role=\"doc-noteref\">1</a></sup></p><ol class=\"zs-endnotes\"><li class=\"zs-endnote\" id=\"fn:1\" role=\"doc-endnote\" value=\"1\">Endnote<sup id=\"fnref:2\"><a class=\"zs-noteref\" href=\"#fn:2\" role=\"doc-noteref\">2</a></sup> <a class=\"zs-endnote-backref\" href=\"#fnref:1\" role=\"doc-backlink\">\u21a9\ufe0e</a></li><li class=\"zs-endnote\" id=\"fn:2\" role=\"doc-endnote\" value=\"2\">Nested <a class=\"zs-endnote-backref\" href=\"#fnref:2\" role=\"doc-backlink\">\u21a9\ufe0e</a></li></ol>",
			encoderMD:    "Text",
			encoderSz:    `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (TEXT "Endnote") (ENDNOTE () (TEXT "Nested")))))`,
			encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))",
			encoderSHTML: "((p \"Text\" (sup ((id . \"fnref:1\")) (a ((class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))",
			encoderText:  "Text Endnote Nested",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Transclusion",
		zmk:   `{{{http://example.com/image}}}{width="100px"}`,
		expect: expectMap{
			encoderHTML:  `<p><img class="external" src="http://example.com/image" width="100px"></p>`,
			encoderMD:    "",
			encoderSz:    `(BLOCK (TRANSCLUDE (("width" . "100px")) (EXTERNAL "http://example.com/image")))`,
			encoderSHTML: `((p (img (@ (class . "external") (src . "http://example.com/image") (width . "100px")))))`,
			encoderSHTML: `((p (img ((class . "external") (src . "http://example.com/image") (width . "100px")))))`,
			encoderText:  "",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "A paragraph with a inline comment only should be empty in HTML",
		zmk:   `%% Comment`,
		expect: expectMap{
			// encoderHTML:  ``,
			encoderSz: `(BLOCK (PARA (LITERAL-COMMENT () "Comment")))`,
			// encoderSHTML: ``,
			encoderText: "",
			encoderZmk:  useZmk,
		},
	},
	{
		descr:  "Zettel with disallowed syntax HTML",
		zmk:    "<h1>Hello</h1>\nWorld\n",
		syntax: meta.ValueSyntaxHTML,
		expect: expectMap{
			encoderHTML:  "<pre><code class=\"language-html\">&lt;h1&gt;Hello&lt;/h1&gt;\nWorld</code></pre>",
			encoderSz:    `(BLOCK (VERBATIM-CODE (("" . "html")) "<h1>Hello</h1>\nWorld"))`,
			encoderSHTML: `((pre (code ((class . "language-html")) "<h1>Hello</h1>\nWorld")))`,
			encoderText:  "<h1>Hello</h1>\nWorld",
			encoderZmk:   "```{=\"html\"}\n<h1>Hello</h1>\nWorld\n```",
		},
	},
	{
		descr:     "Zettel with allowed syntax HTML",
		zmk:       "<h1>Hello</h1>\nWorld\n",
		syntax:    meta.ValueSyntaxHTML,
		allowHTML: true,
		expect: expectMap{
			encoderHTML:  "<h1>Hello</h1>\nWorld",
			encoderSz:    `(BLOCK (VERBATIM-HTML (("" . "html")) "<h1>Hello</h1>\nWorld"))`,
			encoderSHTML: `((@H "<h1>Hello</h1>\nWorld"))`,
			encoderText:  "<h1>Hello</h1>\nWorld",
			encoderZmk:   "@@@{=\"html\"}\n<h1>Hello</h1>\nWorld\n@@@",
		},
	},
	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderHTML:  ``,
			encoderSz:    `(BLOCK)`,
			encoderSHTML: `()`,
			encoderText:  "",
			encoderZmk:   useZmk,
		},
	},
}

// func TestEncoderBlock(t *testing.T) {
// 	executeTestCases(t, tcsBlock)
// }
Changes to internal/encoder/encoder_inline_test.go.
161
162
163
164
165
166
167
168

169
170
171
172
173
174
175
161
162
163
164
165
166
167

168
169
170
171
172
173
174
175







-
+







	{
		descr: "Quotes formatting (german)",
		zmk:   `""quotes""{lang=de}`,
		expect: expectMap{
			encoderHTML:  `<p><span lang="de">&bdquo;quotes&ldquo;</span></p>`,
			encoderMD:    "&bdquo;quotes&ldquo;",
			encoderSz:    `(BLOCK (PARA (FORMAT-QUOTE (("lang" . "de")) (TEXT "quotes"))))`,
			encoderSHTML: `((p (span (@ (lang . "de")) (@H "&bdquo;") "quotes" (@H "&ldquo;"))))`,
			encoderSHTML: `((p (span ((lang . "de")) (@H "&bdquo;") "quotes" (@H "&ldquo;"))))`,
			encoderText:  `quotes`,
			encoderZmk:   `""quotes""{lang="de"}`,
		},
	},
	{
		descr: "Empty quotes (default)",
		zmk:   `""""`,
185
186
187
188
189
190
191
192

193
194
195
196
197
198
199
185
186
187
188
189
190
191

192
193
194
195
196
197
198
199







-
+







	{
		descr: "Empty quotes (unknown)",
		zmk:   `""""{lang=unknown}`,
		expect: expectMap{
			encoderHTML:  `<p><span lang="unknown">&quot;&quot;</span></p>`,
			encoderMD:    "&quot;&quot;",
			encoderSz:    `(BLOCK (PARA (FORMAT-QUOTE (("lang" . "unknown")))))`,
			encoderSHTML: `((p (span (@ (lang . "unknown")) (@H "&quot;" "&quot;"))))`,
			encoderSHTML: `((p (span ((lang . "unknown")) (@H "&quot;" "&quot;"))))`,
			encoderText:  ``,
			encoderZmk:   `""""{lang="unknown"}`,
		},
	},
	{
		descr: "Nested quotes (default)",
		zmk:   `""say: ::""yes, ::""or?""::""::""`,
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
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







-
+











-
+




+
+
+
+
+
+
+
+
+
+
+
+







	{
		descr: "Math formatting",
		zmk:   `$$\TeX$$`,
		expect: expectMap{
			encoderHTML:  `<p><code class="zs-math">\TeX</code></p>`,
			encoderMD:    "\\TeX",
			encoderSz:    `(BLOCK (PARA (LITERAL-MATH () "\\TeX")))`,
			encoderSHTML: `((p (code (@ (class . "zs-math")) "\\TeX")))`,
			encoderSHTML: `((p (code ((class . "zs-math")) "\\TeX")))`,
			encoderText:  `\TeX`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Nested Span Quote formatting",
		zmk:   `::""abc""::{lang=fr}`,
		expect: expectMap{
			encoderHTML:  `<p><span lang="fr">&laquo;&nbsp;abc&nbsp;&raquo;</span></p>`,
			encoderMD:    "&laquo;&nbsp;abc&nbsp;&raquo;",
			encoderSz:    `(BLOCK (PARA (FORMAT-SPAN (("lang" . "fr")) (FORMAT-QUOTE () (TEXT "abc")))))`,
			encoderSHTML: `((p (span (@ (lang . "fr")) (@L (@H "&laquo;" "&nbsp;") "abc" (@H "&nbsp;" "&raquo;")))))`,
			encoderSHTML: `((p (span ((lang . "fr")) (@L (@H "&laquo;" "&nbsp;") "abc" (@H "&nbsp;" "&raquo;")))))`,
			encoderText:  `abc`,
			encoderZmk:   `::""abc""::{lang="fr"}`,
		},
	},
	{
		descr: "Nested Insert Quote formatting",
		zmk:   `>>""abc"">>{lang=fr}`,
		expect: expectMap{
			encoderHTML:  `<p><ins lang="fr">&laquo;&nbsp;abc&nbsp;&raquo;</ins></p>`,
			encoderMD:    "&laquo;&nbsp;abc&nbsp;&raquo;",
			encoderSz:    `(BLOCK (PARA (FORMAT-INSERT (("lang" . "fr")) (FORMAT-QUOTE () (TEXT "abc")))))`,
			encoderSHTML: `((p (ins ((lang . "fr")) (@L (@H "&laquo;" "&nbsp;") "abc" (@H "&nbsp;" "&raquo;")))))`,
			encoderText:  `abc`,
			encoderZmk:   `>>""abc"">>{lang="fr"}`,
		},
	},
	{
		descr: "Simple Citation",
		zmk:   `[@Stern18]`,
		expect: expectMap{
			encoderHTML:  `<p><span>Stern18</span></p>`, // TODO
			encoderMD:    "",
			encoderSz:    `(BLOCK (PARA (CITE () "Stern18")))`,
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
433
434
435
436
437
438
439
440
441
442
443

444
445
446
447
448
449
450
409
410
411
412
413
414
415

416
417
418
419
420
421
422
423
424
425
426
427
428
429
430

431
432
433
434
435
436
437
438
439
440
441
442

443
444
445
446
447
448
449
450
451
452
453
454

455
456
457
458
459
460
461
462







-
+














-
+











-
+











-
+







			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Comment after text and with -->",
		zmk:   `Text%%{-} comment --> end`,
		expect: expectMap{
			encoderHTML:  `<p>Text<!-- comment -&#45;> end --></p>`,
			encoderHTML:  `<p>Text<!-- comment --&gt; end --></p>`,
			encoderMD:    "Text",
			encoderSz:    `(BLOCK (PARA (TEXT "Text") (LITERAL-COMMENT (("-" . "")) "comment --> end")))`,
			encoderSHTML: `((p "Text" (@@ "comment --> end")))`,
			encoderText:  `Text`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple inline endnote",
		zmk:   `[^endnote]`,
		expect: expectMap{
			encoderHTML:  `<p><sup id="fnref:1"><a class="zs-noteref" href="#fn:1" role="doc-noteref">1</a></sup></p><ol class="zs-endnotes"><li class="zs-endnote" id="fn:1" role="doc-endnote" value="1">endnote <a class="zs-endnote-backref" href="#fnref:1" role="doc-backlink">↩︎</a></li></ol>`,
			encoderMD:    "",
			encoderSz:    `(BLOCK (PARA (ENDNOTE () (TEXT "endnote"))))`,
			encoderSHTML: `((p (sup (@ (id . "fnref:1")) (a (@ (class . "zs-noteref") (href . "#fn:1") (role . "doc-noteref")) "1"))))`,
			encoderSHTML: `((p (sup ((id . "fnref:1")) (a ((class . "zs-noteref") (href . "#fn:1") (role . "doc-noteref")) "1"))))`,
			encoderText:  `endnote`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple mark",
		zmk:   `[!mark]`,
		expect: expectMap{
			encoderHTML:  `<p><a id="mark"></a></p>`,
			encoderMD:    "",
			encoderSz:    `(BLOCK (PARA (MARK "mark" "mark" "mark")))`,
			encoderSHTML: `((p (a (@ (id . "mark")))))`,
			encoderSHTML: `((p (a ((id . "mark")))))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Mark with text",
		zmk:   `[!mark|with text]`,
		expect: expectMap{
			encoderHTML:  `<p><a id="mark">with text</a></p>`,
			encoderMD:    "with text",
			encoderSz:    `(BLOCK (PARA (MARK "mark" "mark" "mark" (TEXT "with text"))))`,
			encoderSHTML: `((p (a (@ (id . "mark")) "with text")))`,
			encoderSHTML: `((p (a ((id . "mark")) "with text")))`,
			encoderText:  `with text`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Invalid Link",
		zmk:   `[[link|00000000000000]]`,
472
473
474
475
476
477
478
479













480
481
482
483
484
485
486
487
488
489
490
491

492
493
494
495
496
497
498
499
500
501
502
503

504
505
506
507
508
509
510
511
512
513
514
515

516
517
518
519
520
521
522
523
524
525
526
527

528
529
530
531
532
533
534
535
536
537
538
539

540
541
542
543
544
545
546
547
548
549
550
551

552
553
554
555
556
557
558
559
560
561
562
563

564
565
566
567
568
569
570
571
572
573
574
575

576
577
578
579
580
581
582
583
584
585
586
587
588

589
590
591
592
593
594
595
596
597
598
599

600
601
602
603
604
605
606
607
608
609
610
611

612
613
614
615
616
617
618
619
620
621
622
623

624
625
626
627
628
629
630
631
632
633
634
635













636
637
638
639












640
641
642
643
644
645
646
647
648
649
650
651
652
484
485
486
487
488
489
490

491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514

515
516
517
518
519
520
521
522
523
524
525
526

527
528
529
530
531
532
533
534
535
536
537
538

539
540
541
542
543
544
545
546
547
548
549
550

551
552
553
554
555
556
557
558
559
560
561
562

563
564
565
566
567
568
569
570
571
572
573
574

575
576
577
578
579
580
581
582
583
584
585
586

587
588
589
590
591
592
593
594
595
596
597
598

599
600
601
602
603
604
605
606
607
608
609
610
611

612
613
614
615
616
617
618
619
620
621
622

623
624
625
626
627
628
629
630
631
632
633
634

635
636
637
638
639
640
641
642
643
644
645
646

647
648
649
650
651
652
653
654
655
656
657
658

659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700







-
+
+
+
+
+
+
+
+
+
+
+
+
+











-
+











-
+











-
+











-
+











-
+











-
+











-
+











-
+












-
+










-
+











-
+











-
+











-
+
+
+
+
+
+
+
+
+
+
+
+
+




+
+
+
+
+
+
+
+
+
+
+
+













	{
		descr: "Dummy Link",
		zmk:   `[[abc]]`,
		expect: expectMap{
			encoderHTML:  `<p><a href="abc">abc</a></p>`,
			encoderMD:    "[abc](abc)",
			encoderSz:    `(BLOCK (PARA (LINK () (HOSTED "abc"))))`,
			encoderSHTML: `((p (a (@ (href . "abc")) "abc")))`,
			encoderSHTML: `((p (a ((href . "abc")) "abc")))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Dummy Link with attribute",
		zmk:   `[[abc]]{a="b"}`,
		expect: expectMap{
			encoderHTML:  `<p><a a="b" href="abc">abc</a></p>`,
			encoderMD:    "[abc](abc)",
			encoderSz:    `(BLOCK (PARA (LINK (("a" . "b")) (HOSTED "abc"))))`,
			encoderSHTML: `((p (a ((a . "b") (href . "abc")) "abc")))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple URL",
		zmk:   `[[https://zettelstore.de]]`,
		expect: expectMap{
			encoderHTML:  `<p><a href="https://zettelstore.de" rel="external">https://zettelstore.de</a></p>`,
			encoderMD:    "<https://zettelstore.de>",
			encoderSz:    `(BLOCK (PARA (LINK () (EXTERNAL "https://zettelstore.de"))))`,
			encoderSHTML: `((p (a (@ (href . "https://zettelstore.de") (rel . "external")) "https://zettelstore.de")))`,
			encoderSHTML: `((p (a ((href . "https://zettelstore.de") (rel . "external")) "https://zettelstore.de")))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "URL with Text",
		zmk:   `[[Home|https://zettelstore.de]]`,
		expect: expectMap{
			encoderHTML:  `<p><a href="https://zettelstore.de" rel="external">Home</a></p>`,
			encoderMD:    "[Home](https://zettelstore.de)",
			encoderSz:    `(BLOCK (PARA (LINK () (EXTERNAL "https://zettelstore.de") (TEXT "Home"))))`,
			encoderSHTML: `((p (a (@ (href . "https://zettelstore.de") (rel . "external")) "Home")))`,
			encoderSHTML: `((p (a ((href . "https://zettelstore.de") (rel . "external")) "Home")))`,
			encoderText:  `Home`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Zettel ID",
		zmk:   `[[00000000000100]]`,
		expect: expectMap{
			encoderHTML:  `<p><a href="00000000000100">00000000000100</a></p>`,
			encoderMD:    "[00000000000100](00000000000100)",
			encoderSz:    `(BLOCK (PARA (LINK () (ZETTEL "00000000000100"))))`,
			encoderSHTML: `((p (a (@ (href . "00000000000100")) "00000000000100")))`,
			encoderSHTML: `((p (a ((href . "00000000000100")) "00000000000100")))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Zettel ID with Text",
		zmk:   `[[Config|00000000000100]]`,
		expect: expectMap{
			encoderHTML:  `<p><a href="00000000000100">Config</a></p>`,
			encoderMD:    "[Config](00000000000100)",
			encoderSz:    `(BLOCK (PARA (LINK () (ZETTEL "00000000000100") (TEXT "Config"))))`,
			encoderSHTML: `((p (a (@ (href . "00000000000100")) "Config")))`,
			encoderSHTML: `((p (a ((href . "00000000000100")) "Config")))`,
			encoderText:  `Config`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Zettel ID with fragment",
		zmk:   `[[00000000000100#frag]]`,
		expect: expectMap{
			encoderHTML:  `<p><a href="00000000000100#frag">00000000000100#frag</a></p>`,
			encoderMD:    "[00000000000100#frag](00000000000100#frag)",
			encoderSz:    `(BLOCK (PARA (LINK () (ZETTEL "00000000000100#frag"))))`,
			encoderSHTML: `((p (a (@ (href . "00000000000100#frag")) "00000000000100#frag")))`,
			encoderSHTML: `((p (a ((href . "00000000000100#frag")) "00000000000100#frag")))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Zettel ID with Text and fragment",
		zmk:   `[[Config|00000000000100#frag]]`,
		expect: expectMap{
			encoderHTML:  `<p><a href="00000000000100#frag">Config</a></p>`,
			encoderMD:    "[Config](00000000000100#frag)",
			encoderSz:    `(BLOCK (PARA (LINK () (ZETTEL "00000000000100#frag") (TEXT "Config"))))`,
			encoderSHTML: `((p (a (@ (href . "00000000000100#frag")) "Config")))`,
			encoderSHTML: `((p (a ((href . "00000000000100#frag")) "Config")))`,
			encoderText:  `Config`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Fragment link to self",
		zmk:   `[[#frag]]`,
		expect: expectMap{
			encoderHTML:  `<p><a href="#frag">#frag</a></p>`,
			encoderMD:    "[#frag](#frag)",
			encoderSz:    `(BLOCK (PARA (LINK () (SELF "#frag"))))`,
			encoderSHTML: `((p (a (@ (href . "#frag")) "#frag")))`,
			encoderSHTML: `((p (a ((href . "#frag")) "#frag")))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Hosted link",
		zmk:   `[[H|/hosted]]`,
		expect: expectMap{
			encoderHTML:  `<p><a href="/hosted">H</a></p>`,
			encoderMD:    "[H](/hosted)",
			encoderSz:    `(BLOCK (PARA (LINK () (HOSTED "/hosted") (TEXT "H"))))`,
			encoderSHTML: `((p (a (@ (href . "/hosted")) "H")))`,
			encoderSHTML: `((p (a ((href . "/hosted")) "H")))`,
			encoderText:  `H`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Based link",
		zmk:   `[[B|//based]]`,
		expect: expectMap{
			encoderHTML:  `<p><a href="/based">B</a></p>`,
			encoderMD:    "[B](/based)",
			encoderSz:    `(BLOCK (PARA (LINK () (BASED "/based") (TEXT "B"))))`,
			encoderText:  `B`,
			encoderSHTML: `((p (a (@ (href . "/based")) "B")))`,
			encoderSHTML: `((p (a ((href . "/based")) "B")))`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Relative link",
		zmk:   `[[R|../relative]]`,
		expect: expectMap{
			encoderHTML:  `<p><a href="../relative">R</a></p>`,
			encoderMD:    "[R](../relative)",
			encoderSz:    `(BLOCK (PARA (LINK () (HOSTED "../relative") (TEXT "R"))))`,
			encoderSHTML: `((p (a (@ (href . "../relative")) "R")))`,
			encoderSHTML: `((p (a ((href . "../relative")) "R")))`,
			encoderText:  `R`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Query link w/o text",
		zmk:   `[[query:title:syntax]]`,
		expect: expectMap{
			encoderHTML:  `<p><a href="?q=title%3Asyntax">title:syntax</a></p>`,
			encoderMD:    "",
			encoderSz:    `(BLOCK (PARA (LINK () (QUERY "title:syntax"))))`,
			encoderSHTML: `((p (a (@ (href . "?q=title%3Asyntax")) "title:syntax")))`,
			encoderSHTML: `((p (a ((href . "?q=title%3Asyntax")) "title:syntax")))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Query link with text",
		zmk:   `[[Q|query:title:syntax]]`,
		expect: expectMap{
			encoderHTML:  `<p><a href="?q=title%3Asyntax">Q</a></p>`,
			encoderMD:    "Q",
			encoderSz:    `(BLOCK (PARA (LINK () (QUERY "title:syntax") (TEXT "Q"))))`,
			encoderSHTML: `((p (a (@ (href . "?q=title%3Asyntax")) "Q")))`,
			encoderSHTML: `((p (a ((href . "?q=title%3Asyntax")) "Q")))`,
			encoderText:  `Q`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Dummy Embed",
		zmk:   `{{abc}}`,
		expect: expectMap{
			encoderHTML:  `<p><img src="abc"></p>`,
			encoderMD:    "![abc](abc)",
			encoderSz:    `(BLOCK (PARA (EMBED () (HOSTED "abc") "")))`,
			encoderSHTML: `((p (img (@ (src . "abc")))))`,
			encoderSHTML: `((p (img ((src . "abc")))))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Dummy Embed with attributes",
		zmk:   `{{abc}}{a="b"}`,
		expect: expectMap{
			encoderHTML:  `<p><img a="b" src="abc"></p>`,
			encoderMD:    "![abc](abc)",
			encoderSz:    `(BLOCK (PARA (EMBED (("a" . "b")) (HOSTED "abc") "")))`,
			encoderSHTML: `((p (img ((a . "b") (src . "abc")))))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Link and attributes with quotes",
		zmk:   `[[text|/url]]{title="TITL \"\""}`,
		expect: expectMap{
			encoderHTML:  `<p><a href="/url" title="TITL &quot;&quot;">text</a></p>`,
			encoderMD:    "[text](/url)", // better: [text](/url "TITL \"\"")
			encoderSz:    `(BLOCK (PARA (LINK (("title" . "TITL \"\"")) (HOSTED "/url") (TEXT "text"))))`,
			encoderSHTML: `((p (a ((href . "/url") (title . "TITL \"\"")) "text")))`,
			encoderText:  `text`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSz:    `(BLOCK)`,
			encoderSHTML: `()`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
}
Changes to internal/encoder/encoder_test.go.
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
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







-
+




-
-





-
-
-
-
+
+
+
+
+
+


















-
+





+
+
+
+
+
+
+
+
-
-
+
+
+
-



-
+


-
+







package encoder_test

import (
	"fmt"
	"strings"
	"testing"

	"t73f.de/r/sx/sxreader"
	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/encoder"
	"zettelstore.de/z/internal/parser"
)

type zmkTestCase struct {
	descr  string
	zmk    string
	inline bool
	expect expectMap
	descr     string
	zmk       string
	syntax    string
	allowHTML bool
	inline    bool
	expect    expectMap
}

type expectMap map[api.EncodingEnum]string

const useZmk = "\000"
const (
	encoderHTML  = api.EncoderHTML
	encoderMD    = api.EncoderMD
	encoderSz    = api.EncoderSz
	encoderSHTML = api.EncoderSHTML
	encoderText  = api.EncoderText
	encoderZmk   = api.EncoderZmk
)

func TestEncoder(t *testing.T) {
	for i := range tcsInline {
		tcsInline[i].inline = true
	}
	executeTestCases(t, append(tcsBlock, tcsInline...))
	executeTestCases(t, append(append([]zmkTestCase{}, tcsBlock...), tcsInline...))
}

func executeTestCases(t *testing.T, testCases []zmkTestCase) {
	for testNum, tc := range testCases {
		inp := input.NewInput([]byte(tc.zmk))
		syntax := tc.syntax
		if syntax == "" {
			syntax = meta.ValueSyntaxZmk
		}
		alst := sx.Nil()
		if tc.allowHTML {
			alst = alst.Cons(sx.Cons(parser.SymAllowHTML, nil))
		}
		bs := parser.Parse(inp, nil, meta.ValueSyntaxZmk, config.NoHTML)
		checkEncodings(t, testNum, bs, tc.inline, tc.descr, tc.expect, tc.zmk)
		node := parser.Parse(inp, nil, syntax, alst)
		parser.Clean(node)
		checkEncodings(t, testNum, node, tc.inline, tc.descr, tc.expect, tc.zmk)
		checkSz(t, testNum, bs, tc.inline, tc.descr)
	}
}

func checkEncodings(t *testing.T, testNum int, bs ast.BlockSlice, isInline bool, descr string, expected expectMap, zmkDefault string) {
func checkEncodings(t *testing.T, testNum int, node *sx.Pair, isInline bool, descr string, expected expectMap, zmkDefault string) {
	for enc, exp := range expected {
		encdr := encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN})
		got, err := encode(encdr, bs)
		got, err := encode(encdr, node)
		if err != nil {
			prefix := fmt.Sprintf("Test #%d", testNum)
			if d := descr; d != "" {
				prefix += "\nReason:   " + d
			}
			prefix += "\nMode:     " + mode(isInline)
			t.Errorf("%s\nEncoder:  %s\nError:    %v", prefix, enc, err)
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
95
96
97
98
99
100
101

























102
103

104
105
106
107
108
109
110
111
112
113







-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+

-
+









			}
			prefix += "\nMode:     " + mode(isInline)
			t.Errorf("%s\nEncoder:  %s\nExpected: %q\nGot:      %q", prefix, enc, exp, got)
		}
	}
}

func checkSz(t *testing.T, testNum int, bs ast.BlockSlice, isInline bool, descr string) {
	t.Helper()
	encdr := encoder.Create(encoderSz, nil)
	exp, err := encode(encdr, bs)
	if err != nil {
		t.Error(err)
		return
	}
	val, err := sxreader.MakeReader(strings.NewReader(exp)).Read()
	if err != nil {
		t.Error(err)
		return
	}
	got := val.String()
	if exp != got {
		prefix := fmt.Sprintf("Test #%d", testNum)
		if d := descr; d != "" {
			prefix += "\nReason:   " + d
		}
		prefix += "\nMode:     " + mode(isInline)
		t.Errorf("%s\n\nExpected: %q\nGot:      %q", prefix, exp, got)
	}
}

func encode(e encoder.Encoder, bs ast.BlockSlice) (string, error) {
func encode(e encoder.Encoder, node *sx.Pair) (string, error) {
	var sb strings.Builder
	_, err := e.WriteBlocks(&sb, &bs)
	err := e.WriteSz(&sb, node)
	return sb.String(), err
}

func mode(isInline bool) string {
	if isInline {
		return "inline"
	}
	return "block"
}
Changes to internal/encoder/htmlenc.go.
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
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







+
+






-






-
+

-
+

-
+


-
+



-
+
-
-
+

-
+



-
-
+

-
+






-
+




-
+







	"io"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
)

// htmlEncoder contains all data needed for encoding.
type htmlEncoder struct {
	tx      SzTransformer
	th      *shtml.Evaluator
	lang    string
	textEnc TextEncoder
}

// WriteZettel encodes a full zettel as HTML5.
func (he *htmlEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) {
func (he *htmlEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error {
	env := shtml.MakeEnvironment(he.lang)
	hm, err := he.th.Evaluate(he.tx.GetMeta(zn.InhMeta), &env)
	hm, err := he.th.Evaluate(ast.GetMetaSz(zn.InhMeta), &env)
	if err != nil {
		return 0, err
		return err
	}

	var isTitle ast.InlineSlice
	var szTitle *sx.Pair
	var htitle *sx.Pair
	plainTitle, hasTitle := zn.InhMeta.Get(meta.KeyTitle)
	if hasTitle {
		isTitle = ast.ParseSpacedText(string(plainTitle))
		szTitle = zsx.MakeInline((zsx.MakeText(sz.NormalizedSpacedText(string(plainTitle)))))
		xtitle := he.tx.GetSz(&isTitle)
		htitle, err = he.th.Evaluate(xtitle, &env)
		htitle, err = he.th.Evaluate(szTitle, &env)
		if err != nil {
			return 0, err
			return err
		}
	}

	xast := he.tx.GetSz(&zn.BlocksAST)
	hast, err := he.th.Evaluate(xast, &env)
	hast, err := he.th.Evaluate(zn.Blocks, &env)
	if err != nil {
		return 0, err
		return err
	}
	hen := shtml.Endnotes(&env)

	var head sx.ListBuilder
	head.AddN(
		shtml.SymHead,
		sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sx.MakeSymbol("charset"), sx.MakeString("utf-8"))).Cons(sxhtml.SymAttr)).Cons(shtml.SymMeta),
		sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sxhtml.MakeSymbol("charset"), sx.MakeString("utf-8")))).Cons(shtml.SymMeta),
	)
	head.ExtendBang(hm)
	var sb strings.Builder
	if hasTitle {
		_, _ = he.textEnc.WriteInlines(&sb, &isTitle)
		_ = he.textEnc.WriteSz(&sb, szTitle)
	} else {
		sb.Write(zn.Meta.Zid.Bytes())
	}
	head.Add(sx.MakeList(shtml.SymAttrTitle, sx.MakeString(sb.String())))

	var body sx.ListBuilder
	body.Add(shtml.SymBody)
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
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







-
+

-
+

-
+





-
-
+
+

-
+


-
+
-
-
+


-
+
-
-

-
+

	)

	gen := sxhtml.NewGenerator().SetNewline()
	return gen.WriteHTML(w, doc)
}

// WriteMeta encodes meta data as HTML5.
func (he *htmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
func (he *htmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) error {
	env := shtml.MakeEnvironment(he.lang)
	hm, err := he.th.Evaluate(he.tx.GetMeta(m), &env)
	hm, err := he.th.Evaluate(ast.GetMetaSz(m), &env)
	if err != nil {
		return 0, err
		return err
	}
	gen := sxhtml.NewGenerator().SetNewline()
	return gen.WriteListHTML(w, hm)
}

// WriteBlocks encodes a block slice.
func (he *htmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
// WriteSz encodes SZ represented zettel content.
func (he *htmlEncoder) WriteSz(w io.Writer, node *sx.Pair) error {
	env := shtml.MakeEnvironment(he.lang)
	hobj, err := he.th.Evaluate(he.tx.GetSz(bs), &env)
	hobj, err := he.th.Evaluate(node, &env)
	if err == nil {
		gen := sxhtml.NewGenerator()
		length, err2 := gen.WriteListHTML(w, hobj)
		if err = gen.WriteListHTML(w, hobj); err != nil {
		if err2 != nil {
			return length, err2
			return err
		}

		l, err2 := gen.WriteHTML(w, shtml.Endnotes(&env))
		return gen.WriteHTML(w, shtml.Endnotes(&env))
		length += l
		return length, err2
	}
	return 0, err
	return err
}
Changes to internal/encoder/mdenc.go.
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
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







+

+


+











-
+

-
+





-
-
+
+
-



-
-
-
+
+
+
-
-
+


-
-
-
-
-
-
+
-
-
+

-
-
+
+
-


-




-
+




-
+


+
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+



-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


+
+
-
-
-
+
+
+
+
+
+


-
-
-
-
+
-
-
-
-

-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+














-
+
-
-
-
+
-
-
-
+
-
-
-
-
+
-
-
-
-
-
-
-
-
+

-
-
+
+

-
+

-
+


-
-
+


-
-
-
-
-
-
+
+
+
+
+
+
+


-
+
-
-
-
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
+
-
-
-
-
+
+
+
+





-
+







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+









package encoder

// Encodes the abstract syntax tree back into Markdown.

import (
	"io"
	"net/url"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
)

// mdEncoder contains all data needed for encoding.
type mdEncoder struct {
	lang string
}

// WriteZettel writes the encoded zettel to the writer.
func (me *mdEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) {
func (me *mdEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error {
	v := newMDVisitor(w, me.lang)
	v.acceptMeta(zn.InhMeta)
	v.b.WriteMeta(zn.InhMeta)
	if zn.InhMeta.YamlSep {
		v.b.WriteString("---\n")
	} else {
		v.b.WriteLn()
	}
	ast.Walk(&v, &zn.BlocksAST)
	length, err := v.b.Flush()
	v.walk(zn.Blocks, nil)
	return v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as markdown.
func (me *mdEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
	v := newMDVisitor(w, me.lang)
	v.acceptMeta(m)
func (*mdEncoder) WriteMeta(w io.Writer, m *meta.Meta) error {
	ew := newEncWriter(w)
	ew.WriteMeta(m)
	length, err := v.b.Flush()
	return length, err
	return ew.Flush()
}

func (v *mdVisitor) acceptMeta(m *meta.Meta) {
	for key, val := range m.Computed() {
		v.b.WriteStrings(key, ": ", string(val), "\n")
	}
}

// WriteSz encodes SZ represented zettel content.
// WriteBlocks writes the content of a block slice to the writer.
func (me *mdEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
func (me *mdEncoder) WriteSz(w io.Writer, node *sx.Pair) error {
	v := newMDVisitor(w, me.lang)
	ast.Walk(&v, bs)
	length, err := v.b.Flush()
	zsx.WalkIt(&v, node, nil)
	return v.b.Flush()
	return length, err
}

// mdVisitor writes the abstract syntax tree to an EncWriter.
type mdVisitor struct {
	b            encWriter
	listInfo     []int
	listPrefix   string
	langStack    shtml.LangStack
	defLang      string
	quoteNesting uint
}

func newMDVisitor(w io.Writer, lang string) mdVisitor {
	return mdVisitor{b: newEncWriter(w), langStack: shtml.NewLangStack(lang)}
	return mdVisitor{b: newEncWriter(w), defLang: lang}
}

var symLang = sx.MakeSymbol("lang")
// pushAttribute adds the current attributes to the visitor.
func (v *mdVisitor) pushAttributes(a zsx.Attributes) {
	if value, ok := a.Get("lang"); ok {
		v.langStack.Push(value)

func (v *mdVisitor) getLanguage(alst *sx.Pair) string {
	if a := alst.Assoc(symLang); a != nil {
		if s, isString := sx.GetString(a.Cdr()); isString {
			return s.GetValue()
	} else {
		v.langStack.Dup()
	}
}

		}
	}
	return v.defLang
}
// popAttributes removes the current attributes from the visitor.
func (v *mdVisitor) popAttributes() { v.langStack.Pop() }

// getLanguage returns the current language,
func (v *mdVisitor) getLanguage() string { return v.langStack.Top() }

func (v *mdVisitor) getQuotes() (string, string, bool) {
	qi := shtml.GetQuoteInfo(v.getLanguage())
func (*mdVisitor) setLanguage(alst, attrs *sx.Pair) *sx.Pair {
	if a := attrs.Assoc(sx.MakeString("lang")); a != nil {
		val := a.Cdr()
		if p, isPair := sx.GetPair(val); isPair {
			val = p.Car()
		}
		return alst.Cons(sx.Cons(symLang, val))
	}
	return alst
}

func (v *mdVisitor) getQuotes(alst *sx.Pair) (string, string, bool) {
	qi := shtml.GetQuoteInfo(v.getLanguage(alst))
	leftQ, rightQ := qi.GetQuotes(v.quoteNesting)
	return leftQ, rightQ, qi.GetNBSp()
}

func (v *mdVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		v.visitBlockSlice(n)
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
	case *ast.RegionNode:
		v.visitRegion(n)
	case *ast.HeadingNode:
		v.visitHeading(n)
	case *ast.HRuleNode:
		v.b.WriteString("---")
	case *ast.NestedListNode:
		v.visitNestedList(n)
	case *ast.DescriptionListNode:
		return nil // Should write no content
	case *ast.TableNode:
		return nil // Should write no content
	case *ast.TextNode:
		v.b.WriteString(n.Text)
	case *ast.BreakNode:
		v.visitBreak(n)
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.EmbedRefNode:
		v.visitEmbedRef(n)
	case *ast.FootnoteNode:
func (v *mdVisitor) walk(node, alst *sx.Pair)    { zsx.WalkIt(v, node, alst) }
func (v *mdVisitor) walkList(lst, alst *sx.Pair) { zsx.WalkItList(v, lst, 0, alst) }
func (v *mdVisitor) VisitItBefore(node *sx.Pair, alst *sx.Pair) bool {
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		switch sym {
		case zsx.SymBlock:
			v.visitBlock(node, alst)

		case zsx.SymText:
			v.b.WriteString(zsx.GetText(node))
		case zsx.SymSoft:
			v.visitBreak(false)
		case zsx.SymHard:
			v.visitBreak(true)

		case zsx.SymLink:
			attrs, ref, inlines := zsx.GetLink(node)
			alst = v.setLanguage(alst, attrs)
			v.visitReference(ref, inlines, alst)
		case zsx.SymEmbed:
			attrs, ref, _, inlines := zsx.GetEmbed(node)
			alst = v.setLanguage(alst, attrs)
			_ = v.b.WriteByte('!')
			v.visitReference(ref, inlines, alst)

		case zsx.SymFormatEmph:
			v.visitFormat(node, alst, "*", "*")
		case zsx.SymFormatStrong:
			v.visitFormat(node, alst, "__", "__")
		case zsx.SymFormatQuote:
			v.visitQuote(node, alst)
		case zsx.SymFormatMark:
			v.visitFormat(node, alst, "<mark>", "</mark>")
		case zsx.SymFormatSpan, zsx.SymFormatDelete, zsx.SymFormatInsert, zsx.SymFormatSub, zsx.SymFormatSuper:
			v.visitFormat(node, alst, "", "")

		case zsx.SymLiteralCode, zsx.SymLiteralInput, zsx.SymLiteralOutput:
			_, _, content := zsx.GetLiteral(node)
			v.b.WriteStrings("`", content, "`")
		case zsx.SymLiteralMath:
			_, _, content := zsx.GetLiteral(node)
			v.b.WriteString(content)

		case zsx.SymHeading:
			level, attrs, text, _, _ := zsx.GetHeading(node)
			const headingSigns = "###### "
			v.b.WriteString(headingSigns[len(headingSigns)-level-1:])
			v.walkList(text, v.setLanguage(alst, attrs))

		case zsx.SymThematic:
			v.b.WriteString("---")

		case zsx.SymListOrdered:
			v.visitNestedList(node, alst, enumOrdered)
		case zsx.SymListUnordered:
			v.visitNestedList(node, alst, enumUnordered)
		case zsx.SymListQuote:
			if len(v.listInfo) == 0 {
				v.visitListQuote(node, alst)
			}

		case zsx.SymVerbatimCode:
			v.visitVerbatim(node)

		return nil // Should write no content
	case *ast.FormatNode:
		v.visitFormat(n)
	case *ast.LiteralNode:
		v.visitLiteral(n)
	default:
		return v
	}
	return nil
		case zsx.SymRegionQuote:
			v.visitRegion(node, alst)

		case zsx.SymRegionBlock, zsx.SymRegionVerse,
			zsx.SymVerbatimComment, zsx.SymVerbatimEval, zsx.SymVerbatimHTML, zsx.SymVerbatimMath, zsx.SymVerbatimZettel,
			zsx.SymDescription, zsx.SymTable, zsx.SymEndnote,
			zsx.SymLiteralComment:
			// Do nothing, ignore it.

		default:
			return false
		}
		return true
	}
	return false
}

func (v *mdVisitor) VisitItAfter(*sx.Pair, *sx.Pair) {}

func (v *mdVisitor) visitBlockSlice(bs *ast.BlockSlice) {
	for i, bn := range *bs {
		if i > 0 {
func (v *mdVisitor) visitBlock(node *sx.Pair, alst *sx.Pair) {
	first := true
	for bn := range node.Tail().Pairs() {
		if first {
			first = false
		} else {
			v.b.WriteString("\n\n")
		}
		ast.Walk(v, bn)
	}
}

		v.walk(bn.Head(), alst)
func (v *mdVisitor) visitVerbatim(vn *ast.VerbatimNode) {
	lc := len(vn.Content)
	if vn.Kind != ast.VerbatimCode || lc == 0 {
		return
	}
	v.writeSpaces(4)
	lcm1 := lc - 1
	for i := 0; i < lc; i++ {
		b := vn.Content[i]
		if b != '\n' && b != '\r' {
			_ = v.b.WriteByte(b)
			continue
		}
}
		j := i + 1
		for ; j < lc; j++ {
			c := vn.Content[j]
			if c != '\n' && c != '\r' {
				break
			}

		}
		if j >= lcm1 {
			break
		}
		v.b.WriteLn()
		v.writeSpaces(4)
		i = j - 1
	}
}

func (v *mdVisitor) visitRegion(rn *ast.RegionNode) {
	if rn.Kind != ast.RegionQuote {
		return
	}
	v.pushAttributes(rn.Attrs)
	defer v.popAttributes()

	first := true
	for _, bn := range rn.Blocks {
		pn, ok := bn.(*ast.ParaNode)
		if !ok {
			continue
		}
		if !first {
			v.b.WriteString("\n>\n")
		}
		first = false
		v.b.WriteString("> ")
		ast.Walk(v, &pn.Inlines)
	}
}

func (v *mdVisitor) visitHeading(hn *ast.HeadingNode) {
	v.pushAttributes(hn.Attrs)
	defer v.popAttributes()

	const headingSigns = "###### "
	v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-1:])
	ast.Walk(v, &hn.Inlines)
}

func (v *mdVisitor) visitNestedList(ln *ast.NestedListNode) {
	switch ln.Kind {
	case ast.NestedListOrdered:
		v.writeNestedList(ln, "1. ")
	case ast.NestedListUnordered:
		v.writeNestedList(ln, "* ")
	case ast.NestedListQuote:
		v.writeListQuote(ln)
	}
	v.listInfo = v.listInfo[:len(v.listInfo)-1]
}

func (v *mdVisitor) writeNestedList(ln *ast.NestedListNode, enum string) {
	v.listInfo = append(v.listInfo, len(enum))
	regIndent := 4*len(v.listInfo) - 4
	paraIndent := regIndent + len(enum)
	for i, item := range ln.Items {
		if i > 0 {
			v.b.WriteLn()
		}
		v.writeSpaces(regIndent)
		v.b.WriteString(enum)
		for j, in := range item {
			if j > 0 {
				v.b.WriteLn()
				if _, ok := in.(*ast.ParaNode); ok {
					v.writeSpaces(paraIndent)
				}
			}
			ast.Walk(v, in)
		}
	}
}

func (v *mdVisitor) writeListQuote(ln *ast.NestedListNode) {
	v.listInfo = append(v.listInfo, 0)
	if len(v.listInfo) > 1 {
		return
	}

	prefix := v.listPrefix
	v.listPrefix = "> "

	for i, item := range ln.Items {
		if i > 0 {
			v.b.WriteLn()
		}
		v.b.WriteString(v.listPrefix)
		for j, in := range item {
			if j > 0 {
				v.b.WriteLn()
				if _, ok := in.(*ast.ParaNode); ok {
					v.b.WriteString(v.listPrefix)
				}
			}
			ast.Walk(v, in)
		}
	}

	v.listPrefix = prefix
}

func (v *mdVisitor) visitBreak(bn *ast.BreakNode) {
	if bn.Hard {
func (v *mdVisitor) visitBreak(isHard bool) {
	if isHard {
		v.b.WriteString("\\\n")
	} else {
		v.b.WriteLn()
	}
	if l := len(v.listInfo); l > 0 {
		if v.listPrefix == "" {
			v.writeSpaces(4*l - 4 + v.listInfo[l-1])
		} else {
			v.writeSpaces(4*l - 4)
			v.b.WriteString(v.listPrefix)
		}
	}
}

func (v *mdVisitor) visitLink(ln *ast.LinkNode) {
func (v *mdVisitor) visitReference(ref, inlines, alst *sx.Pair) {
	v.pushAttributes(ln.Attrs)
	defer v.popAttributes()

	refState, val := zsx.GetReference(ref)
	v.writeReference(ln.Ref, ln.Inlines)
}

	if sz.SymRefStateQuery.IsEqualSymbol(refState) {
func (v *mdVisitor) visitEmbedRef(en *ast.EmbedRefNode) {
	v.pushAttributes(en.Attrs)
	defer v.popAttributes()

		v.walkList(inlines, alst)
	_ = v.b.WriteByte('!')
	v.writeReference(en.Ref, en.Inlines)
}

func (v *mdVisitor) writeReference(ref *ast.Reference, is ast.InlineSlice) {
	if ref.State == ast.RefStateQuery {
		ast.Walk(v, &is)
	} else if len(is) > 0 {
	} else if inlines != nil {
		_ = v.b.WriteByte('[')
		ast.Walk(v, &is)
		v.b.WriteStrings("](", ref.String())
		v.walkList(inlines, alst)
		v.b.WriteStrings("](", val)
		_ = v.b.WriteByte(')')
	} else if isAutoLinkable(ref) {
	} else if isAutoLinkable(refState, val) {
		_ = v.b.WriteByte('<')
		v.b.WriteString(ref.String())
		v.b.WriteString(val)
		_ = v.b.WriteByte('>')
	} else {
		s := ref.String()
		v.b.WriteStrings("[", s, "](", s, ")")
		v.b.WriteStrings("[", val, "](", val, ")")
	}
}

func isAutoLinkable(ref *ast.Reference) bool {
	if ref.State != ast.RefStateExternal || ref.URL == nil {
		return false
	}
	return ref.URL.Scheme != ""
func isAutoLinkable(refState *sx.Symbol, val string) bool {
	if zsx.SymRefStateExternal.IsEqualSymbol(refState) {
		if u, err := url.Parse(val); err == nil && u.Scheme != "" {
			return true
		}
	}
	return false
}

func (v *mdVisitor) visitFormat(fn *ast.FormatNode) {
func (v *mdVisitor) visitFormat(node, alst *sx.Pair, delim1, delim2 string) {
	v.pushAttributes(fn.Attrs)
	defer v.popAttributes()

	_, attrs, inlines := zsx.GetFormat(node)
	switch fn.Kind {
	case ast.FormatEmph:
		_ = v.b.WriteByte('*')
	alst = v.setLanguage(alst, attrs)
		ast.Walk(v, &fn.Inlines)
		_ = v.b.WriteByte('*')
	case ast.FormatStrong:
		v.b.WriteString("__")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("__")
	case ast.FormatQuote:
		v.writeQuote(fn)
	case ast.FormatMark:
		v.b.WriteString("<mark>")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("</mark>")
	v.b.WriteString(delim1)
	v.walkList(inlines, alst)
	v.b.WriteString(delim2)
	default:
		ast.Walk(v, &fn.Inlines)
	}
}
}

func (v *mdVisitor) writeQuote(fn *ast.FormatNode) {
	leftQ, rightQ, withNbsp := v.getQuotes()
func (v *mdVisitor) visitQuote(node, alst *sx.Pair) {
	_, attrs, inlines := zsx.GetFormat(node)
	alst = v.setLanguage(alst, attrs)
	leftQ, rightQ, withNbsp := v.getQuotes(alst)
	v.b.WriteString(leftQ)
	if withNbsp {
		v.b.WriteString("&nbsp;")
	}
	v.quoteNesting++
	ast.Walk(v, &fn.Inlines)
	v.walkList(inlines, alst)
	v.quoteNesting--
	if withNbsp {
		v.b.WriteString("&nbsp;")
	}
	v.b.WriteString(rightQ)
}

const enumOrdered = "1. "
const enumUnordered = "* "

func (v *mdVisitor) visitNestedList(node *sx.Pair, alst *sx.Pair, enum string) {
	v.listInfo = append(v.listInfo, len(enum))
	regIndent := 4*len(v.listInfo) - 4
	paraIndent := regIndent + len(enum)
	_, attrs, blocks := zsx.GetList(node)
	alst = v.setLanguage(alst, attrs)
	firstBlk := true
	for blk := range blocks.Pairs() {
		if firstBlk {
			firstBlk = false
		} else {
			v.b.WriteLn()
		}
		v.writeSpaces(regIndent)
		v.b.WriteString(enum)
		first := true
		for item := range blk.Head().Tail().Pairs() {
			in := item.Head()
			if first {
				first = false
			} else {
				v.b.WriteLn()
				if zsx.SymPara.IsEqual(in.Car()) {
					v.writeSpaces(paraIndent)
				}
			}
			v.walk(in, alst)
		}
	}
	v.listInfo = v.listInfo[:len(v.listInfo)-1]
}
func (v *mdVisitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {
	case ast.LiteralCode, ast.LiteralInput, ast.LiteralOutput:
		_ = v.b.WriteByte('`')
		_, _ = v.b.Write(ln.Content)
		_ = v.b.WriteByte('`')
	case ast.LiteralComment: // ignore everything
	default:
		_, _ = v.b.Write(ln.Content)
func (v *mdVisitor) visitListQuote(node *sx.Pair, alst *sx.Pair) {
	v.listInfo = []int{0}
	oldPrefix := v.listPrefix
	v.listPrefix = "> "

	_, attrs, blocks := zsx.GetList(node)
	alst = v.setLanguage(alst, attrs)
	firstBlk := true
	for blk := range blocks.Pairs() {
		if firstBlk {
			firstBlk = false
		} else {
			v.b.WriteLn()
		}
		v.b.WriteString(v.listPrefix)
		first := true
		for item := range blk.Head().Tail().Pairs() {
			in := item.Head()
			if first {
				first = false
			} else {
				v.b.WriteLn()
				if zsx.SymPara.IsEqual(in.Car()) {
					v.b.WriteString(v.listPrefix)
				}
			}
			v.walk(in, alst)
		}
	}
	v.listPrefix = oldPrefix
	v.listInfo = nil
}

func (v *mdVisitor) visitVerbatim(node *sx.Pair) {
	if _, _, content := zsx.GetVerbatim(node); content != "" {
		lc := len(content)
		v.writeSpaces(4)
		lcm1 := lc - 1
		for i := 0; i < lc; i++ {
			b := content[i]
			if b != '\n' && b != '\r' {
				_ = v.b.WriteByte(b)
				continue
			}
			j := i + 1
			for ; j < lc; j++ {
				c := content[j]
				if c != '\n' && c != '\r' {
					break
				}
			}
			if j >= lcm1 {
				break
			}
			v.b.WriteLn()
			v.writeSpaces(4)
			i = j - 1
		}
	}
}

func (v *mdVisitor) visitRegion(node *sx.Pair, alst *sx.Pair) {
	_, attrs, blocks, _ := zsx.GetRegion(node)
	alst = v.setLanguage(alst, attrs)

	first := true
	for n := range blocks.Pairs() {
		blk := n.Head()
		if zsx.SymPara.IsEqual(blk.Car()) {
			if first {
				first = false
			} else {
				v.b.WriteString("\n>\n")
			}
			v.b.WriteString("> ")
			v.walk(blk, alst)
		}
	}
}

func (v *mdVisitor) writeSpaces(n int) {
	for range n {
		v.b.WriteSpace()
	}
}
Changes to internal/encoder/shtmlenc.go.
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
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







-





-
+

-
+

-
+

-
+

-
+


+
-
+



-
+

-
+

-
+

-
+
+


-
-
+
+

-
+

-
+

+
-
+

	"t73f.de/r/zsc/shtml"

	"zettelstore.de/z/internal/ast"
)

// shtmlEncoder contains all data needed for encoding.
type shtmlEncoder struct {
	tx   SzTransformer
	th   *shtml.Evaluator
	lang string
}

// WriteZettel writes the encoded zettel to the writer.
func (enc *shtmlEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) {
func (enc *shtmlEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error {
	env := shtml.MakeEnvironment(enc.lang)
	metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(zn.InhMeta), &env)
	metaSHTML, err := enc.th.Evaluate(ast.GetMetaSz(zn.InhMeta), &env)
	if err != nil {
		return 0, err
		return err
	}
	contentSHTML, err := enc.th.Evaluate(enc.tx.GetSz(&zn.BlocksAST), &env)
	contentSHTML, err := enc.th.Evaluate(zn.Blocks, &env)
	if err != nil {
		return 0, err
		return err
	}
	result := sx.Cons(metaSHTML, contentSHTML)
	_, err = result.Print(w)
	return result.Print(w)
	return err
}

// WriteMeta encodes meta data as s-expression.
func (enc *shtmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
func (enc *shtmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) error {
	env := shtml.MakeEnvironment(enc.lang)
	metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(m), &env)
	metaSHTML, err := enc.th.Evaluate(ast.GetMetaSz(m), &env)
	if err != nil {
		return 0, err
		return err
	}
	return sx.Print(w, metaSHTML)
	_, err = sx.Print(w, metaSHTML)
	return err
}

// WriteBlocks writes a block slice to the writer
func (enc *shtmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
// WriteSz encodes SZ represented zettel content.
func (enc *shtmlEncoder) WriteSz(w io.Writer, node *sx.Pair) error {
	env := shtml.MakeEnvironment(enc.lang)
	hval, err := enc.th.Evaluate(enc.tx.GetSz(bs), &env)
	hval, err := enc.th.Evaluate(node, &env)
	if err != nil {
		return 0, err
		return err
	}
	_, err = hval.Print(w)
	return sx.Print(w, hval)
	return err
}
Changes to internal/encoder/szenc.go.
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
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







-
+
-
-
+
-

-
+
-
-
-
+
+
+



-
-
+
+
+


-
-
-
+
+
+
+

	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/meta"

	"zettelstore.de/z/internal/ast"
)

// szEncoder contains all data needed for encoding.
type szEncoder struct {
type szEncoder struct{}
	trans SzTransformer
}


// WriteZettel writes the encoded zettel to the writer.
func (enc *szEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) {
func (enc *szEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error {
	content := enc.trans.GetSz(&zn.BlocksAST)
	meta := enc.trans.GetMeta(zn.InhMeta)
	return sx.MakeList(meta, content).Print(w)
	meta := ast.GetMetaSz(zn.InhMeta)
	_, err := sx.MakeList(meta, zn.Blocks).Print(w)
	return err
}

// WriteMeta encodes meta data as s-expression.
func (enc *szEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
	return enc.trans.GetMeta(m).Print(w)
func (enc *szEncoder) WriteMeta(w io.Writer, m *meta.Meta) error {
	_, err := ast.GetMetaSz(m).Print(w)
	return err
}

// WriteBlocks writes a block slice to the writer
func (enc *szEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	return enc.trans.GetSz(bs).Print(w)
// WriteSz encodes SZ represented zettel content.
func (*szEncoder) WriteSz(w io.Writer, node *sx.Pair) error {
	_, err := node.Print(w)
	return err
}
Deleted internal/encoder/sztransform.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







































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore 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: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package encoder

import (
	"encoding/base64"
	"fmt"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
)

// NewSzTransformer returns a new transformer to create s-expressions from AST nodes.
func NewSzTransformer() SzTransformer {
	return SzTransformer{}
}

// SzTransformer contains all data needed to transform into a s-expression.
type SzTransformer struct {
	inVerse bool
}

// GetSz transforms the given node into a sx list.
func (t *SzTransformer) GetSz(node ast.Node) *sx.Pair {
	switch n := node.(type) {
	case *ast.BlockSlice:
		return zsx.MakeBlockList(t.getBlockList(n))
	case *ast.InlineSlice:
		return zsx.MakeInlineList(t.getInlineList(*n))
	case *ast.ParaNode:
		return zsx.MakeParaList(t.getInlineList(n.Inlines))
	case *ast.VerbatimNode:
		return zsx.MakeVerbatim(mapGetS(mapVerbatimKindS, n.Kind), getAttributes(n.Attrs), string(n.Content))
	case *ast.RegionNode:
		return t.getRegion(n)
	case *ast.HeadingNode:
		return zsx.MakeHeading(n.Level, getAttributes(n.Attrs), t.getInlineList(n.Inlines), n.Slug, n.Fragment)
	case *ast.HRuleNode:
		return zsx.MakeThematic(getAttributes(n.Attrs))
	case *ast.NestedListNode:
		return t.getNestedList(n)
	case *ast.DescriptionListNode:
		return t.getDescriptionList(n)
	case *ast.TableNode:
		return t.getTable(n)
	case *ast.TranscludeNode:
		return zsx.MakeTransclusion(getAttributes(n.Attrs), getReference(n.Ref), t.getInlineList(n.Inlines))
	case *ast.BLOBNode:
		return t.getBLOB(n)
	case *ast.TextNode:
		return zsx.MakeText(n.Text)
	case *ast.BreakNode:
		if n.Hard {
			return zsx.MakeHard()
		}
		return zsx.MakeSoft()
	case *ast.LinkNode:
		return t.getLink(n)
	case *ast.EmbedRefNode:
		return zsx.MakeEmbed(getAttributes(n.Attrs), getReference(n.Ref), n.Syntax, t.getInlineList(n.Inlines))
	case *ast.EmbedBLOBNode:
		return t.getEmbedBLOB(n)
	case *ast.CiteNode:
		return zsx.MakeCite(getAttributes(n.Attrs), n.Key, t.getInlineList(n.Inlines))
	case *ast.FootnoteNode:
		return zsx.MakeEndnote(getAttributes(n.Attrs), t.getInlineList(n.Inlines))
	case *ast.MarkNode:
		return zsx.MakeMark(n.Mark, n.Slug, n.Fragment, t.getInlineList(n.Inlines))
	case *ast.FormatNode:
		return zsx.MakeFormat(mapGetS(mapFormatKindS, n.Kind), getAttributes(n.Attrs), t.getInlineList(n.Inlines))
	case *ast.LiteralNode:
		return zsx.MakeLiteral(mapGetS(mapLiteralKindS, n.Kind), getAttributes(n.Attrs), string(n.Content))
	}
	return sx.MakeList(zsx.SymUnknown, sx.MakeString(fmt.Sprintf("%T %v", node, node)))
}

var mapVerbatimKindS = map[ast.VerbatimKind]*sx.Symbol{
	ast.VerbatimZettel:  zsx.SymVerbatimZettel,
	ast.VerbatimCode:    zsx.SymVerbatimCode,
	ast.VerbatimEval:    zsx.SymVerbatimEval,
	ast.VerbatimMath:    zsx.SymVerbatimMath,
	ast.VerbatimComment: zsx.SymVerbatimComment,
	ast.VerbatimHTML:    zsx.SymVerbatimHTML,
}

var mapFormatKindS = map[ast.FormatKind]*sx.Symbol{
	ast.FormatEmph:   zsx.SymFormatEmph,
	ast.FormatStrong: zsx.SymFormatStrong,
	ast.FormatDelete: zsx.SymFormatDelete,
	ast.FormatInsert: zsx.SymFormatInsert,
	ast.FormatSuper:  zsx.SymFormatSuper,
	ast.FormatSub:    zsx.SymFormatSub,
	ast.FormatQuote:  zsx.SymFormatQuote,
	ast.FormatMark:   zsx.SymFormatMark,
	ast.FormatSpan:   zsx.SymFormatSpan,
}

var mapLiteralKindS = map[ast.LiteralKind]*sx.Symbol{
	ast.LiteralCode:    zsx.SymLiteralCode,
	ast.LiteralInput:   zsx.SymLiteralInput,
	ast.LiteralOutput:  zsx.SymLiteralOutput,
	ast.LiteralComment: zsx.SymLiteralComment,
	ast.LiteralMath:    zsx.SymLiteralMath,
}

var mapRegionKindS = map[ast.RegionKind]*sx.Symbol{
	ast.RegionSpan:  zsx.SymRegionBlock,
	ast.RegionQuote: zsx.SymRegionQuote,
	ast.RegionVerse: zsx.SymRegionVerse,
}

func (t *SzTransformer) getRegion(rn *ast.RegionNode) *sx.Pair {
	saveInVerse := t.inVerse
	if rn.Kind == ast.RegionVerse {
		t.inVerse = true
	}
	symBlocks := t.getBlockList(&rn.Blocks)
	t.inVerse = saveInVerse
	return zsx.MakeRegion(
		mapGetS(mapRegionKindS, rn.Kind),
		getAttributes(rn.Attrs), symBlocks,
		t.getInlineList(rn.Inlines),
	)
}

var mapNestedListKindS = map[ast.NestedListKind]*sx.Symbol{
	ast.NestedListOrdered:   zsx.SymListOrdered,
	ast.NestedListUnordered: zsx.SymListUnordered,
	ast.NestedListQuote:     zsx.SymListQuote,
}

func (t *SzTransformer) getNestedList(ln *ast.NestedListNode) *sx.Pair {
	var items sx.ListBuilder
	isCompact := isCompactList(ln.Items)
	for _, item := range ln.Items {
		if isCompact && len(item) > 0 {
			paragraph := t.GetSz(item[0])
			items.Add(zsx.MakeInlineList(paragraph.Tail()))
			continue
		}
		var itemObjs sx.ListBuilder
		for _, in := range item {
			itemObjs.Add(t.GetSz(in))
		}
		if isCompact {
			items.Add(zsx.MakeInlineList(itemObjs.List()))
		} else {
			items.Add(zsx.MakeBlockList(itemObjs.List()))
		}
	}
	return zsx.MakeList(mapGetS(mapNestedListKindS, ln.Kind), getAttributes(ln.Attrs), items.List())
}
func isCompactList(itemSlice []ast.ItemSlice) bool {
	for _, items := range itemSlice {
		if len(items) > 1 {
			return false
		}
		if len(items) == 1 {
			if _, ok := items[0].(*ast.ParaNode); !ok {
				return false
			}
		}
	}
	return true
}

func (t *SzTransformer) getDescriptionList(dn *ast.DescriptionListNode) *sx.Pair {
	var dlObjs sx.ListBuilder
	for _, def := range dn.Descriptions {
		dlObjs.Add(t.getInlineList(def.Term))
		var descObjs sx.ListBuilder
		for _, b := range def.Descriptions {
			var dVal sx.ListBuilder
			for _, dn := range b {
				dVal.Add(t.GetSz(dn))
			}
			descObjs.Add(zsx.MakeBlockList(dVal.List()))
		}
		dlObjs.Add(zsx.MakeBlockList(descObjs.List()))
	}
	return dlObjs.List().Cons(getAttributes(dn.Attrs)).Cons(zsx.SymDescription)
}

func (t *SzTransformer) getTable(tn *ast.TableNode) *sx.Pair {
	var lb sx.ListBuilder
	lb.AddN(zsx.SymTable, t.getHeader(tn.Header))
	for _, row := range tn.Rows {
		lb.Add(t.getRow(row))
	}
	return lb.List()
}
func (t *SzTransformer) getHeader(header ast.TableRow) *sx.Pair {
	if len(header) == 0 {
		return nil
	}
	return t.getRow(header)
}
func (t *SzTransformer) getRow(row ast.TableRow) *sx.Pair {
	var lb sx.ListBuilder
	for _, cell := range row {
		lb.Add(t.getCell(cell))
	}
	return lb.List()
}

func (t *SzTransformer) getCell(cell *ast.TableCell) *sx.Pair {
	var attrs *sx.Pair
	switch cell.Align {
	case ast.AlignCenter:
		attrs = sx.Cons(sx.Cons(zsx.SymAttrAlign, zsx.AttrAlignCenter), nil)
	case ast.AlignLeft:
		attrs = sx.Cons(sx.Cons(zsx.SymAttrAlign, zsx.AttrAlignLeft), nil)
	case ast.AlignRight:
		attrs = sx.Cons(sx.Cons(zsx.SymAttrAlign, zsx.AttrAlignRight), nil)
	}
	return zsx.MakeCell(attrs, t.getInlineList(cell.Inlines))
}

func (t *SzTransformer) getBLOB(bn *ast.BLOBNode) *sx.Pair {
	var content string
	if bn.Syntax == meta.ValueSyntaxSVG {
		content = string(bn.Blob)
	} else {
		content = getBase64String(bn.Blob)
	}
	return zsx.MakeBLOB(getAttributes(bn.Attrs), t.getInlineList(bn.Description), bn.Syntax, content)
}

func (t *SzTransformer) getLink(ln *ast.LinkNode) *sx.Pair {
	return zsx.MakeLink(
		getAttributes(ln.Attrs),
		getReference(ln.Ref),
		t.getInlineList(ln.Inlines),
	)
}

func (t *SzTransformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sx.Pair {
	var content string
	if en.Syntax == meta.ValueSyntaxSVG {
		content = string(en.Blob)
	} else {
		content = getBase64String(en.Blob)
	}
	return zsx.MakeEmbedBLOB(getAttributes(en.Attrs), en.Syntax, content, t.getInlineList(en.Inlines))
}

func (t *SzTransformer) getBlockList(bs *ast.BlockSlice) *sx.Pair {
	var lb sx.ListBuilder
	for _, n := range *bs {
		lb.Add(t.GetSz(n))
	}
	return lb.List()
}
func (t *SzTransformer) getInlineList(is ast.InlineSlice) *sx.Pair {
	var lb sx.ListBuilder
	for _, n := range is {
		lb.Add(t.GetSz(n))
	}
	return lb.List()
}

func getAttributes(a zsx.Attributes) *sx.Pair {
	if a.IsEmpty() {
		return sx.Nil()
	}
	keys := a.Keys()
	var lb sx.ListBuilder
	for _, k := range keys {
		lb.Add(sx.Cons(sx.MakeString(k), sx.MakeString(a[k])))
	}
	return lb.List()
}

var mapRefStateS = map[ast.RefState]*sx.Symbol{
	ast.RefStateInvalid:  zsx.SymRefStateInvalid,
	ast.RefStateZettel:   sz.SymRefStateZettel,
	ast.RefStateSelf:     zsx.SymRefStateSelf,
	ast.RefStateFound:    sz.SymRefStateFound,
	ast.RefStateBroken:   sz.SymRefStateBroken,
	ast.RefStateHosted:   zsx.SymRefStateHosted,
	ast.RefStateBased:    sz.SymRefStateBased,
	ast.RefStateQuery:    sz.SymRefStateQuery,
	ast.RefStateExternal: zsx.SymRefStateExternal,
}

func getReference(ref *ast.Reference) *sx.Pair {
	return sx.MakeList(mapGetS(mapRefStateS, ref.State), sx.MakeString(ref.Value))
}

var mapMetaTypeS = map[*meta.DescriptionType]*sx.Symbol{
	meta.TypeCredential: sz.SymTypeCredential,
	meta.TypeEmpty:      sz.SymTypeEmpty,
	meta.TypeID:         sz.SymTypeID,
	meta.TypeIDSet:      sz.SymTypeIDSet,
	meta.TypeNumber:     sz.SymTypeNumber,
	meta.TypeString:     sz.SymTypeString,
	meta.TypeTagSet:     sz.SymTypeTagSet,
	meta.TypeTimestamp:  sz.SymTypeTimestamp,
	meta.TypeURL:        sz.SymTypeURL,
	meta.TypeWord:       sz.SymTypeWord,
}

// GetMeta transforms the given metadata into a sx list.
func (t *SzTransformer) GetMeta(m *meta.Meta) *sx.Pair {
	var lb sx.ListBuilder
	lb.Add(sz.SymMeta)
	for key, val := range m.Computed() {
		ty := m.Type(key)
		symType := mapGetS(mapMetaTypeS, ty)
		var obj sx.Object
		if ty.IsSet {
			var setObjs sx.ListBuilder
			for _, val := range val.AsSlice() {
				setObjs.Add(sx.MakeString(val))
			}
			obj = setObjs.List()
		} else {
			obj = sx.MakeString(string(val))
		}
		lb.Add(sx.Nil().Cons(obj).Cons(sx.MakeSymbol(key)).Cons(symType))
	}
	return lb.List()
}

func mapGetS[T comparable](m map[T]*sx.Symbol, k T) *sx.Symbol {
	if result, found := m[k]; found {
		return result
	}
	return sx.MakeSymbol(fmt.Sprintf("**%v:NOT-FOUND**", k))
}

func getBase64String(data []byte) string {
	var sb strings.Builder
	encoder := base64.NewEncoder(base64.StdEncoding, &sb)
	_, err := encoder.Write(data)
	if err == nil {
		err = encoder.Close()
	}
	if err == nil {
		return sb.String()
	}
	return ""
}
Changes to internal/encoder/textenc.go.
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

























































































































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







+

+









-
+

-
-
-
+
+
+
-



-
+









-
+
-











-
-
-
+
+
+
-
-
+

-
-
-
-
-
+
-
-
-
-
-
+
-


-
-
+
+
-
-
-




-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

// textenc encodes the abstract syntax tree into its text.

import (
	"io"
	"iter"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/ast"
)

// TextEncoder encodes just the text and ignores any formatting.
type TextEncoder struct{}

// WriteZettel writes metadata and content.
func (te *TextEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) {
func (te *TextEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error {
	v := newTextVisitor(w)
	_, _ = te.WriteMeta(&v.b, zn.InhMeta)
	v.visitBlockSlice(&zn.BlocksAST)
	length, err := v.b.Flush()
	_ = te.WriteMeta(&v.b, zn.InhMeta)
	v.walk(zn.Blocks, nil)
	return v.b.Flush()
	return length, err
}

// WriteMeta encodes metadata as text.
func (te *TextEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
func (te *TextEncoder) WriteMeta(w io.Writer, m *meta.Meta) error {
	buf := newEncWriter(w)
	for key, val := range m.Computed() {
		if meta.Type(key) == meta.TypeTagSet {
			writeTagSet(&buf, val.Elems())
		} else {
			buf.WriteString(string(val))
		}
		buf.WriteLn()
	}
	length, err := buf.Flush()
	return buf.Flush()
	return length, err
}

func writeTagSet(buf *encWriter, tags iter.Seq[meta.Value]) {
	first := true
	for tag := range tags {
		if !first {
			buf.WriteSpace()
		}
		first = false
		buf.WriteString(string(tag.CleanTag()))
	}

}

}

// WriteSz writes SZ encoded content to the writer.
// WriteBlocks writes the content of a block slice to the writer.
func (*TextEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
func (*TextEncoder) WriteSz(w io.Writer, node *sx.Pair) error {
	v := newTextVisitor(w)
	v.visitBlockSlice(bs)
	length, err := v.b.Flush()
	return length, err
}

	v.walk(node, nil)
// WriteInlines writes an inline slice to the writer
func (*TextEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newTextVisitor(w)
	ast.Walk(&v, is)
	length, err := v.b.Flush()
	return v.b.Flush()
	return length, err
}

// textVisitor writes the abstract syntax tree to an io.Writer.
type textVisitor struct {
// textVisitor writes the sx.Object-based AST to an io.Writer.
type textVisitor struct{ b encWriter }
	b         encWriter
	inlinePos int
}

func newTextVisitor(w io.Writer) textVisitor {
	return textVisitor{b: newEncWriter(w)}
}

func (v *textVisitor) walk(node, alst *sx.Pair)    { zsx.WalkIt(v, node, alst) }
func (v *textVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		v.visitBlockSlice(n)
		return nil
	case *ast.InlineSlice:
		v.visitInlineSlice(n)
		return nil
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
		return nil
	case *ast.RegionNode:
		v.visitBlockSlice(&n.Blocks)
		if len(n.Inlines) > 0 {
			v.b.WriteLn()
			ast.Walk(v, &n.Inlines)
		}
		return nil
	case *ast.NestedListNode:
		v.visitNestedList(n)
		return nil
	case *ast.DescriptionListNode:
		v.visitDescriptionList(n)
		return nil
	case *ast.TableNode:
		v.visitTable(n)
		return nil
	case *ast.TranscludeNode:
		ast.Walk(v, &n.Inlines)
	case *ast.BLOBNode:
		return nil
	case *ast.TextNode:
		v.visitText(n.Text)
		return nil
	case *ast.BreakNode:
		if n.Hard {
			v.b.WriteLn()
		} else {
			v.b.WriteSpace()
		}
		return nil
	case *ast.LinkNode:
		if len(n.Inlines) > 0 {
			ast.Walk(v, &n.Inlines)
		}
		return nil
	case *ast.MarkNode:
		if len(n.Inlines) > 0 {
			ast.Walk(v, &n.Inlines)
		}
		return nil
	case *ast.FootnoteNode:
		if v.inlinePos > 0 {
			v.b.WriteSpace()
		}
		// No 'return nil' to write text
	case *ast.LiteralNode:
		if n.Kind != ast.LiteralComment {
			_, _ = v.b.Write(n.Content)
		}
	}
	return v
}

func (v *textVisitor) walkList(lst, alst *sx.Pair) { zsx.WalkItList(v, lst, 0, alst) }
func (v *textVisitor) visitVerbatim(vn *ast.VerbatimNode) {
	if vn.Kind != ast.VerbatimComment {
		_, _ = v.b.Write(vn.Content)
	}
}

func (v *textVisitor) visitNestedList(ln *ast.NestedListNode) {
	for i, item := range ln.Items {
		v.writePosChar(i, '\n')
		for j, it := range item {
			v.writePosChar(j, '\n')
			ast.Walk(v, it)
		}
	}
}

func (v *textVisitor) visitDescriptionList(dl *ast.DescriptionListNode) {
	for i, descr := range dl.Descriptions {
		v.writePosChar(i, '\n')
		ast.Walk(v, &descr.Term)
		for _, b := range descr.Descriptions {
			v.b.WriteLn()
			for k, d := range b {
				v.writePosChar(k, '\n')
				ast.Walk(v, d)
			}
		}
	}
}

func (v *textVisitor) visitTable(tn *ast.TableNode) {
func (v *textVisitor) VisitItBefore(node *sx.Pair, alst *sx.Pair) bool {
	if len(tn.Header) > 0 {
		v.writeRow(tn.Header)
		v.b.WriteLn()
	}
	for i, row := range tn.Rows {
		v.writePosChar(i, '\n')
		v.writeRow(row)
	}
}

	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
func (v *textVisitor) writeRow(row ast.TableRow) {
	for i, cell := range row {
		v.writePosChar(i, ' ')
		ast.Walk(v, &cell.Inlines)
	}
}

		switch sym {
func (v *textVisitor) visitBlockSlice(bs *ast.BlockSlice) {
	for i, bn := range *bs {
		v.writePosChar(i, '\n')
		ast.Walk(v, bn)
	}
}

		case zsx.SymText:
func (v *textVisitor) visitInlineSlice(is *ast.InlineSlice) {
	for i, in := range *is {
		v.inlinePos = i
		ast.Walk(v, in)
	}
	v.inlinePos = 0
}

			s := zsx.GetText(node)
func (v *textVisitor) visitText(s string) {
	spaceFound := false
	for _, ch := range s {
		if input.IsSpace(ch) {
			if !spaceFound {
				v.b.WriteSpace()
				spaceFound = true
			}
			continue
		}
		spaceFound = false
		v.b.WriteString(string(ch))
	}
}
			spaceFound := false
			for _, ch := range s {
				if input.IsSpace(ch) {
					if !spaceFound {
						v.b.WriteSpace()
						spaceFound = true
					}
					continue
				}
				spaceFound = false
				v.b.WriteString(string(ch))
			}

		case zsx.SymHard:
			v.b.WriteLn()
		case zsx.SymSoft:
			_ = v.b.WriteByte(' ')

		case zsx.SymEndnote:
			if zsx.GetWalkPos(alst) > 0 {
				_ = v.b.WriteByte(' ')
			}
			return false
func (v *textVisitor) writePosChar(pos int, ch byte) {
	if pos > 0 {
		_ = v.b.WriteByte(ch)
	}
}

		case zsx.SymLiteralCode, zsx.SymLiteralInput, zsx.SymLiteralMath, zsx.SymLiteralOutput:
			if s, found := sx.GetString(node.Tail().Tail().Car()); found {
				v.b.WriteString(s.GetValue())
			}
		case zsx.SymLiteralComment:
			// Do nothing

		case zsx.SymBlock:
			blocks := zsx.GetBlock(node)
			first := true
			for n := range blocks.Pairs() {
				if first {
					first = false
				} else {
					v.b.WriteLn()
				}
				v.walk(n.Head(), alst)
			}
		case zsx.SymInline:
			inlines := zsx.GetInline(node)
			first := true
			for n := range inlines.Pairs() {
				if first {
					first = false
				} else {
					v.b.WriteLn()
				}
				v.walk(n.Head(), alst)
			}

		case zsx.SymListOrdered, zsx.SymListUnordered, zsx.SymListQuote:
			_, _, items := zsx.GetList(node)
			first := true
			for n := range items.Pairs() {
				if first {
					first = false
				} else {
					v.b.WriteLn()
				}
				v.walk(n.Head(), alst)
			}

		case zsx.SymTable:
			_, header, rowList := zsx.GetTable(node)
			firstRow := true
			for n := range rowList.Cons(header).Pairs() {
				row := n.Head()
				if row == nil {
					continue
				}
				if firstRow {
					firstRow = false
				} else {
					v.b.WriteLn()
				}
				firstCell := true
				for elem := range row.Pairs() {
					if firstCell {
						firstCell = false
					} else {
						_ = v.b.WriteByte(' ')
					}
					v.walk(elem.Head(), alst)
				}
			}

		case zsx.SymDescription:
			_, termsVals := zsx.GetDescription(node)
			first := true
			for n := termsVals; n != nil; n = n.Tail() {
				if first {
					first = false
				} else {
					v.b.WriteLn()
				}
				v.walkList(n.Head(), alst)
				n = n.Tail()
				if n == nil {
					break
				}
				dvals := n.Head()
				if zsx.SymBlock.IsEqual(dvals.Car()) {
					for val := range dvals.Tail().Pairs() {
						v.b.WriteLn()
						v.walk(val.Head(), alst)
					}
				}
			}

		case zsx.SymRegionBlock, zsx.SymRegionQuote, zsx.SymRegionVerse:
			_, _, content, inlines := zsx.GetRegion(node)
			first := true
			for n := range content.Pairs() {
				if first {
					first = false
				} else {
					v.b.WriteLn()
				}
				v.walk(n.Head(), alst)
			}
			if inlines != nil {
				v.b.WriteLn()
				v.walkList(inlines, alst)
			}

		case zsx.SymVerbatimCode, zsx.SymVerbatimEval, zsx.SymVerbatimHTML, zsx.SymVerbatimMath, zsx.SymVerbatimZettel:
			_, _, s := zsx.GetVerbatim(node)
			v.b.WriteString(s)

		case zsx.SymVerbatimComment, zsx.SymBLOB:
			// Do nothing

		default:
			return false
		}
		return true
	}
	return false
}
func (v *textVisitor) VisitItAfter(*sx.Pair, *sx.Pair) {}
Changes to internal/encoder/write.go.
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

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







-

+
+




-
-
+
+
-











-








-
-
+
-











-
-
+
+
-
+






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














+
+
+
+
+
+
+
+
+

-
+
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package encoder

import (
	"encoding/base64"
	"io"

	"t73f.de/r/zsc/domain/meta"
)

// encWriter is a specialized writer for encoding zettel.
type encWriter struct {
	w      io.Writer // The io.Writer to write to
	err    error     // Collect error
	w   io.Writer // The io.Writer to write to
	err error     // Collect error
	length int       // Collected length
}

// newEncWriter creates a new encWriter
func newEncWriter(w io.Writer) encWriter { return encWriter{w: w} }

// Write writes the content of p.
func (w *encWriter) Write(p []byte) (l int, err error) {
	if w.err != nil {
		return 0, w.err
	}
	l, w.err = w.w.Write(p)
	w.length += l
	return l, w.err
}

// WriteString writes the content of s.
func (w *encWriter) WriteString(s string) {
	if w.err != nil {
		return
	}
	var l int
	l, w.err = io.WriteString(w.w, s)
	_, w.err = io.WriteString(w.w, s)
	w.length += l
}

// WriteStrings writes the content of sl.
func (w *encWriter) WriteStrings(sl ...string) {
	for _, s := range sl {
		w.WriteString(s)
	}
}

// WriteByte writes the content of b.
func (w *encWriter) WriteByte(b byte) error {
	var l int
	l, w.err = w.Write([]byte{b})
	if w.err == nil {
		_, w.err = w.Write([]byte{b})
	w.length += l
	}
	return w.err
}

// WriteBytes writes the content of bs.
func (w *encWriter) WriteBytes(bs ...byte) { _, _ = w.Write(bs) }

// WriteBase64 writes the content of p, encoded with base64.
func (w *encWriter) WriteBase64(p []byte) {
	if w.err == nil {
		encoder := base64.NewEncoder(base64.StdEncoding, w.w)
		var l int
		l, w.err = encoder.Write(p)
		w.length += l
		err1 := encoder.Close()
		if w.err == nil {
			w.err = err1
		}
	}
}

// WriteLn writes a new line character.
func (w *encWriter) WriteLn() {
	if w.err == nil {
		w.err = w.WriteByte('\n')
	}
}

// WriteLn writes a space character.
func (w *encWriter) WriteSpace() {
	if w.err == nil {
		w.err = w.WriteByte(' ')
	}
}

// WriteMeta write the content of the meta data in a standard way:
//
//	key: val
func (w *encWriter) WriteMeta(m *meta.Meta) {
	for key, val := range m.Computed() {
		w.WriteStrings(key, ": ", string(val), "\n")
	}
}

// Flush returns the collected length and error.
func (w *encWriter) Flush() (int, error) { return w.length, w.err }
func (w *encWriter) Flush() error { return w.err }
Changes to internal/encoder/zmkenc.go.
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
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465

466
467
468
469
470
471
472
473
474
475
476
477
478

479
480
481
482
483
484


485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505







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
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448

449
450



451
452
453
454
455
456
457


458
459
460

461
462
463
464
465
466
467
468
469
470

471
472



473
474
475
476
477
478
479
480
481

482



































































































483




484
485
486
487
488
489
490
491

492
493
494
495
496
497

498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517



518
519
520
521
522
523
524







-



+


+









-
+

-
+





-
-
+
+
-



-
-
-
+
+
+
-
-
+


-
-
-
-
-
-
+
-
-
+

-
-
+
+
-


-

-
+
-
-
+
-
-




+
+
+
-
-
-
+
+
+
+
-
-
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
+
-
-
-
-
+
+
-
-
+
+
+
+
-
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+

-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
-

-
-
-
-
+
+
+

+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


+
+
-
+
-
-
-
+
+
+


-
+
-
-
-
+
+
+
-
+
+

-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-
+
+

-
-
-
+
+
+


-
-
-
+
+
+
-
-
-
+
+
-
-
-
-
+
+
+
+




+
+
-
-
+
+
+

-
+


+
+

-
+





-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-
-
-
+
+
+
+
+
+
+



+
-
-
-
+
+
+
+
+
+
+

+
-
-
+
+

+
+

-
+





-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
-
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
-
-
-
-
+
+
-
-
-
+
+
+
+
+
+

-
-
-
-
-
+
+
+
+
+
-
-
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
+



+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+






-
+

-
-
-
+
+
+




-
-
+
+

-
+









-
+

-
-
-
+
+







-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-








-
+





-
+
+


















-
-
-
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------

package encoder

// zmkenc encodes the abstract syntax tree back into Zettelmarkup.

import (
	"fmt"
	"io"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zero/set"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
)

// zmkEncoder contains all data needed for encoding.
type zmkEncoder struct{}

// WriteZettel writes the encoded zettel to the writer.
func (*zmkEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) {
func (ze *zmkEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error {
	v := newZmkVisitor(w)
	v.acceptMeta(zn.InhMeta)
	v.b.WriteMeta(zn.InhMeta)
	if zn.InhMeta.YamlSep {
		v.b.WriteString("---\n")
	} else {
		v.b.WriteLn()
	}
	ast.Walk(&v, &zn.BlocksAST)
	length, err := v.b.Flush()
	v.walk(zn.Blocks, nil)
	return v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as zmk.
func (*zmkEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
	v := newZmkVisitor(w)
	v.acceptMeta(m)
func (ze *zmkEncoder) WriteMeta(w io.Writer, m *meta.Meta) error {
	ew := newEncWriter(w)
	ew.WriteMeta(m)
	length, err := v.b.Flush()
	return length, err
	return ew.Flush()
}

func (v *zmkVisitor) acceptMeta(m *meta.Meta) {
	for key, val := range m.Computed() {
		v.b.WriteStrings(key, ": ", string(val), "\n")
	}
}

// WriteSz encodes SZ represented zettel content.
// WriteBlocks writes the content of a block slice to the writer.
func (*zmkEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
func (*zmkEncoder) WriteSz(w io.Writer, node *sx.Pair) error {
	v := newZmkVisitor(w)
	ast.Walk(&v, bs)
	length, err := v.b.Flush()
	zsx.WalkIt(&v, node, nil)
	return v.b.Flush()
	return length, err
}

// zmkVisitor writes the abstract syntax tree to an io.Writer.
type zmkVisitor struct {
	b         encWriter
	b      encWriter
	textEnc   TextEncoder
	prefix    []byte
	prefix []byte
	inVerse   bool
	inlinePos int
}

func newZmkVisitor(w io.Writer) zmkVisitor { return zmkVisitor{b: newEncWriter(w)} }

func (v *zmkVisitor) walk(node, alst *sx.Pair)    { zsx.WalkIt(v, node, alst) }
func (v *zmkVisitor) walkList(lst, alst *sx.Pair) { zsx.WalkItList(v, lst, 0, alst) }

func (v *zmkVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
func (v *zmkVisitor) VisitItBefore(node *sx.Pair, alst *sx.Pair) bool {
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		switch sym {
		case zsx.SymText:
		v.visitBlockSlice(n)
	case *ast.InlineSlice:
			v.writeText(zsx.GetText(node))
		case zsx.SymSoft:
		for i, in := range *n {
			v.inlinePos = i
			ast.Walk(v, in)
		}
			v.writeBreak(false)
		case zsx.SymHard:
			v.writeBreak(true)

		v.inlinePos = 0
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
	case *ast.RegionNode:
		v.visitRegion(n)
	case *ast.HeadingNode:
		v.visitHeading(n)
	case *ast.HRuleNode:
		case zsx.SymFormatEmph:
			v.visitFormat(node, alst, "__")
		case zsx.SymFormatStrong:
			v.visitFormat(node, alst, "**")
		case zsx.SymFormatInsert:
			v.visitFormat(node, alst, ">>")
		case zsx.SymFormatDelete:
		v.b.WriteString("---")
		v.visitAttributes(n.Attrs)
	case *ast.NestedListNode:
		v.visitNestedList(n)
	case *ast.DescriptionListNode:
		v.visitDescriptionList(n)
	case *ast.TableNode:
		v.visitTable(n)
	case *ast.TranscludeNode:
		v.b.WriteStrings("{{{", n.Ref.String(), "}}}") // FIXME n.Inlines
		v.visitAttributes(n.Attrs)
	case *ast.BLOBNode:
		v.visitBLOB(n)
	case *ast.TextNode:
		v.visitText(n)
	case *ast.BreakNode:
			v.visitFormat(node, alst, "~~")
		case zsx.SymFormatSuper:
			v.visitFormat(node, alst, "^^")
		case zsx.SymFormatSub:
			v.visitFormat(node, alst, ",,")
		case zsx.SymFormatQuote:
			v.visitFormat(node, alst, `""`)
		case zsx.SymFormatMark:
			v.visitFormat(node, alst, "##")
		case zsx.SymFormatSpan:
			v.visitFormat(node, alst, "::")

		case zsx.SymLiteralCode:
			_, attrs, content := zsx.GetLiteral(node)
			v.writeLiteral('`', attrs, content)
		case zsx.SymLiteralMath:
			_, attrs, content := zsx.GetLiteral(node)
			v.b.WriteStrings("$$", content, "$$")
			v.writeAttributes(attrs)
		case zsx.SymLiteralInput:
			_, attrs, content := zsx.GetLiteral(node)
			v.writeLiteral('\'', attrs, content)
		case zsx.SymLiteralOutput:
			_, attrs, content := zsx.GetLiteral(node)
			v.writeLiteral('=', attrs, content)
		case zsx.SymLiteralComment:
			_, attrs, content := zsx.GetLiteral(node)
			v.b.WriteString("%%")
			v.writeAttributes(attrs)
			v.b.WriteSpace()
			v.b.WriteString(content)

		v.visitBreak(n)
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.EmbedRefNode:
		v.visitEmbedRef(n)
	case *ast.EmbedBLOBNode:
		v.visitEmbedBLOB(n)
	case *ast.CiteNode:
		v.visitCite(n)
		case zsx.SymLink:
			v.visitLink(node, alst)
		case zsx.SymEmbed:
			v.visitEmbedRef(node, alst)
		case zsx.SymEndnote:
			v.visitEndnote(node, alst)
		case zsx.SymCite:
			v.visitCite(node, alst)
	case *ast.FootnoteNode:
		v.b.WriteString("[^")
		ast.Walk(v, &n.Inlines)
		_ = v.b.WriteByte(']')
		v.visitAttributes(n.Attrs)
	case *ast.MarkNode:
		v.visitMark(n)
		case zsx.SymMark:
			v.visitMark(node, alst)
	case *ast.FormatNode:
		v.visitFormat(n)
	case *ast.LiteralNode:
		v.visitLiteral(n)
	default:
		return v
	}

	return nil
}

func (v *zmkVisitor) visitBlockSlice(bs *ast.BlockSlice) {
		case zsx.SymBlock:
			v.visitBlock(node, alst)
	var lastWasParagraph bool
	for i, bn := range *bs {
		case zsx.SymHeading:
			v.visitHeading(node, alst)
		case zsx.SymThematic:
			attrs := zsx.GetThematic(node)
		if i > 0 {
			v.b.WriteLn()
			v.b.WriteString("---")
			if lastWasParagraph && !v.inVerse {
				if _, ok := bn.(*ast.ParaNode); ok {
					v.b.WriteLn()
				}
			}
			v.writeAttributes(attrs)

		case zsx.SymListOrdered:
			v.visitNestedList(node, alst, '#')
		case zsx.SymListQuote:
			v.visitNestedList(node, alst, '>')
		case zsx.SymListUnordered:
			v.visitNestedList(node, alst, '*')

		}
		ast.Walk(v, bn)
		_, lastWasParagraph = bn.(*ast.ParaNode)
	}
		case zsx.SymRegionBlock:
			v.visitRegion(node, alst, ":::")
		case zsx.SymRegionQuote:
			v.visitRegion(node, alst, "<<<")
		case zsx.SymRegionVerse:
			v.visitRegion(node, alst, "\"\"\"")

}
		case zsx.SymDescription:
			v.visitDescription(node, alst)
		case zsx.SymTable:
			v.visitTable(node, alst)
		case zsx.SymCell:
			v.visitCell(node, alst)

var mapVerbatimKind = map[ast.VerbatimKind]string{
	ast.VerbatimZettel:  "@@@",
	ast.VerbatimComment: "%%%",
	ast.VerbatimHTML:    "@@@", // Attribute is set to {="html"}
	ast.VerbatimCode:    "```",
	ast.VerbatimEval:    "~~~",
	ast.VerbatimMath:    "$$$",
}

		case zsx.SymVerbatimCode:
			v.visitVerbatim(node, "```")
		case zsx.SymVerbatimComment:
			v.visitVerbatim(node, "%%%")
		case zsx.SymVerbatimEval:
			v.visitVerbatim(node, "~~~")
		case zsx.SymVerbatimHTML:
			v.visitVerbatim(node, "@@@")
		case zsx.SymVerbatimMath:
			v.visitVerbatim(node, "$$$")
		case zsx.SymVerbatimZettel:
			v.visitVerbatim(node, "@@@")

		case zsx.SymBLOB:
			v.visitBLOB(node)
		case zsx.SymTransclude:
			v.visitTransclude(node, alst)
		default:
			return false
		}
func (v *zmkVisitor) visitVerbatim(vn *ast.VerbatimNode) {
	kind, ok := mapVerbatimKind[vn.Kind]
	if !ok {
		return true
		panic(fmt.Sprintf("Unknown verbatim kind %d", vn.Kind))
	}
	attrs := vn.Attrs
	if vn.Kind == ast.VerbatimHTML {
		attrs = syntaxToHTML(attrs)
	}
	return false
}
func (v *zmkVisitor) VisitItAfter(*sx.Pair, *sx.Pair) {}

func (v *zmkVisitor) visitFormat(node *sx.Pair, alst *sx.Pair, delim string) {
	// TODO: scan cn.Lines to find embedded kind[0]s at beginning
	v.b.WriteString(kind)
	v.visitAttributes(attrs)
	v.b.WriteLn()
	_, _ = v.b.Write(vn.Content)
	v.b.WriteLn()
	v.b.WriteString(kind)
	_, attrs, inlines := zsx.GetFormat(node)
	v.b.WriteString(delim)
	v.walkList(inlines, alst)
	v.b.WriteString(delim)
	v.writeAttributes(attrs)
}

func (v *zmkVisitor) writeLiteral(code byte, attrs *sx.Pair, content string) {
	v.b.WriteBytes(code, code)
	v.writeEscaped(content, code)
	v.b.WriteBytes(code, code)
	v.writeAttributes(attrs)
}

func (v *zmkVisitor) visitLink(node *sx.Pair, alst *sx.Pair) {
	attrs, ref, inlines := zsx.GetLink(node)
	v.b.WriteString("[[")
	if inlines != nil {
		v.walkList(inlines, alst)
		_ = v.b.WriteByte('|')
	}
	_ = sz.WriteReference(&v.b, ref)
	v.b.WriteString("]]")
	v.writeAttributes(attrs)
}

func (v *zmkVisitor) visitEmbedRef(node *sx.Pair, alst *sx.Pair) {
	attrs, ref, _, inlines := zsx.GetEmbed(node)
	v.b.WriteString("{{")
	if inlines != nil {
		v.walkList(inlines, alst)
		_ = v.b.WriteByte('|')
	}
	_ = sz.WriteReference(&v.b, ref)
	v.b.WriteString("}}")
	v.writeAttributes(attrs)
}

func (v *zmkVisitor) visitEndnote(node *sx.Pair, alst *sx.Pair) {
	attrs, inlines := zsx.GetEndnote(node)
var mapRegionKind = map[ast.RegionKind]string{
	v.b.WriteString("[^")
	ast.RegionSpan:  ":::",
	ast.RegionQuote: "<<<",
	ast.RegionVerse: "\"\"\"",
	v.walkList(inlines, alst)
	_ = v.b.WriteByte(']')
	v.writeAttributes(attrs)
}

func (v *zmkVisitor) visitRegion(rn *ast.RegionNode) {
func (v *zmkVisitor) visitCite(node *sx.Pair, alst *sx.Pair) {
	// Scan rn.Blocks for embedded regions to adjust length of regionCode
	kind, ok := mapRegionKind[rn.Kind]
	if !ok {
	attrs, key, inlines := zsx.GetCite(node)
	v.b.WriteStrings("[@", key)
	if inlines != nil {
		panic(fmt.Sprintf("Unknown region kind %d", rn.Kind))
		v.b.WriteSpace()
		v.walkList(inlines, alst)
	}
	v.b.WriteString(kind)
	v.visitAttributes(rn.Attrs)
	v.b.WriteLn()
	saveInVerse := v.inVerse
	_ = v.b.WriteByte(']')
	v.writeAttributes(attrs)
}

func (v *zmkVisitor) visitMark(node *sx.Pair, alst *sx.Pair) {
	mark, _, _, inlines := zsx.GetMark(node)
	v.b.WriteStrings("[!", mark)
	if inlines != nil {
	v.inVerse = rn.Kind == ast.RegionVerse
	ast.Walk(v, &rn.Blocks)
	v.inVerse = saveInVerse
	v.b.WriteLn()
	v.b.WriteString(kind)
	if len(rn.Inlines) > 0 {
		v.b.WriteSpace()
		ast.Walk(v, &rn.Inlines)
		_ = v.b.WriteByte('|')
		v.walkList(inlines, alst)
	}
	_ = v.b.WriteByte(']')
}

func (v *zmkVisitor) visitBlock(node *sx.Pair, alst *sx.Pair) {
	blocks := zsx.GetBlock(node)
	lastWasParagraph := false
	first := true
	for bn := range blocks.Pairs() {
		blk := bn.Head()
		if first {
			first = false
		} else {
			v.b.WriteLn()
			if lastWasParagraph && alst.Assoc(zsx.SymRegionVerse) == nil {
				if zsx.SymPara.IsEqual(blk.Car()) {
					v.b.WriteLn()
				}
			}
		}
		v.walk(blk, alst)
		lastWasParagraph = zsx.SymPara.IsEqual(blk.Car())
	}
}

func (v *zmkVisitor) visitHeading(hn *ast.HeadingNode) {
func (v *zmkVisitor) visitHeading(node *sx.Pair, alst *sx.Pair) {
	level, attrs, inlines, _, _ := zsx.GetHeading(node)
	const headingSigns = "========= "
	v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-3:])
	ast.Walk(v, &hn.Inlines)
	v.visitAttributes(hn.Attrs)
	v.b.WriteString(headingSigns[len(headingSigns)-level-3:])
	v.walkList(inlines, alst)
	v.writeAttributes(attrs)
}

var mapNestedListKind = map[ast.NestedListKind]byte{
	ast.NestedListOrdered:   '#',
	ast.NestedListUnordered: '*',
func (v *zmkVisitor) visitNestedList(node *sx.Pair, alst *sx.Pair, code byte) {
	_, _, items := zsx.GetList(node)
	v.prefix = append(v.prefix, code)
	ast.NestedListQuote:     '>',
}


	first := true
func (v *zmkVisitor) visitNestedList(ln *ast.NestedListNode) {
	v.prefix = append(v.prefix, mapNestedListKind[ln.Kind])
	for i, item := range ln.Items {
		if i > 0 {
	for itm := range items.Pairs() {
		if first {
			first = false
		} else {
			v.b.WriteLn()
		}
		_, _ = v.b.Write(v.prefix)
		v.b.WriteSpace()
		item := zsx.GetBlock(itm.Head())
		second := false
		for j, in := range item {
			if j > 0 {
		for inn := range item.Pairs() {
			inl := inn.Head()
			if second {
				v.b.WriteLn()
				if _, ok := in.(*ast.ParaNode); ok {
				if zsx.SymPara.IsEqual(inl.Car()) {
					v.writePrefixSpaces()
				}
			} else {
				second = true
			}
			ast.Walk(v, in)
			v.walk(inl, alst)
		}
	}
	v.prefix = v.prefix[:len(v.prefix)-1]
}

func (v *zmkVisitor) writePrefixSpaces() {
	if prefixLen := len(v.prefix); prefixLen > 0 {
		for i := 0; i <= prefixLen; i++ {
			v.b.WriteSpace()
		}
func (v *zmkVisitor) visitRegion(node *sx.Pair, alst *sx.Pair, delim string) {
	sym, attrs, blocks, inlines := zsx.GetRegion(node)
	//TODO: Scan rn.Blocks for embedded regions to adjust length of regionCode
	v.b.WriteString(delim)
	v.writeAttributes(attrs)
	v.b.WriteLn()
	if zsx.SymRegionVerse.IsEqualSymbol(sym) {
		alst = alst.Cons(sx.Cons(zsx.SymRegionVerse, sx.Nil()))
	}
	v.walk(zsx.MakeBlockList(blocks), alst)
	v.b.WriteLn()
	v.b.WriteString(delim)
	if inlines != nil {
		v.b.WriteSpace()
		v.walkList(inlines, alst)
	}
}

func (v *zmkVisitor) visitDescriptionList(dn *ast.DescriptionListNode) {
	for i, descr := range dn.Descriptions {
		if i > 0 {
func (v *zmkVisitor) visitDescription(node *sx.Pair, alst *sx.Pair) {
	_, termVals := zsx.GetDescription(node)
	first := true
	for n := termVals; n != nil; n = n.Tail() {
		if first {
			first = false
		} else {
			v.b.WriteLn()
		}
		v.b.WriteString("; ")
		if term := n.Head(); term != nil {
		ast.Walk(v, &descr.Term)

		for _, b := range descr.Descriptions {
			v.walkList(term, alst)
		}
		n = n.Tail()
		if n == nil {
			break
		}
		for bns := range zsx.GetBlock(n.Head()).Pairs() {
			v.b.WriteString("\n: ")
			second := false
			for jj, dn := range b {
				if jj > 0 {
			for pn := range zsx.GetBlock(bns.Head()).Pairs() {
				if second {
					v.b.WriteString("\n\n  ")
				} else {
					second = true
				}
				ast.Walk(v, dn)
				v.walk(pn.Head(), alst)
			}
		}
	}
}

var alignCode = map[ast.Alignment]string{
	ast.AlignDefault: "",
	ast.AlignLeft:    "<",
	ast.AlignCenter:  ":",
	ast.AlignRight:   ">",
}

func (v *zmkVisitor) visitTable(tn *ast.TableNode) {
	if header := tn.Header; len(header) > 0 {
		v.writeTableHeader(header, tn.Align)
		v.b.WriteLn()
func (v *zmkVisitor) visitTable(node *sx.Pair, alst *sx.Pair) {
	_, headerRow, rows := zsx.GetTable(node)
	if headerRow != nil {
		v.writeRow(headerRow, alst, "|=")
		v.b.WriteLn()
	}
	first := true
	for row := range rows.Pairs() {
		if first {
			first = false
		} else {
			v.b.WriteLn()
		}
		v.writeRow(row.Head(), alst, "|")
	}
}
func (v *zmkVisitor) writeRow(row *sx.Pair, alst *sx.Pair, delim string) {
	for i, row := range tn.Rows {
	for n := range row.Pairs() {
		if i > 0 {
			v.b.WriteLn()
		}
		v.b.WriteString(delim)
		v.walk(n.Head(), alst)
	}
		v.writeTableRow(row, tn.Align)
	}
}

}
func (v *zmkVisitor) visitCell(node *sx.Pair, alst *sx.Pair) {
	attrs, inlines := zsx.GetCell(node)
	align := ""
	if alignPair := attrs.Assoc(zsx.SymAttrAlign); alignPair != nil {
		if alignValue := alignPair.Cdr(); zsx.AttrAlignCenter.IsEqual(alignValue) {
			align = ":"
		} else if zsx.AttrAlignLeft.IsEqual(alignValue) {
			align = "<"
		} else if zsx.AttrAlignRight.IsEqual(alignValue) {
			align = ">"
		}
	}
func (v *zmkVisitor) writeTableHeader(header ast.TableRow, align []ast.Alignment) {
	for pos, cell := range header {
		v.b.WriteString("|=")
		colAlign := align[pos]
	v.b.WriteString(align)
	v.walkList(inlines, alst)
		if cell.Align != colAlign {
			v.b.WriteString(alignCode[cell.Align])
		}
		ast.Walk(v, &cell.Inlines)
}

		if colAlign != ast.AlignDefault {
			v.b.WriteString(alignCode[colAlign])
		}
func (v *zmkVisitor) visitVerbatim(node *sx.Pair, delim string) {
	sym, attrs, content := zsx.GetVerbatim(node)

	if zsx.SymVerbatimHTML.IsEqualSymbol(sym) {
		attrs = attrs.RemoveAssoc(sx.MakeString(meta.KeySyntax))
		attrs = attrs.Cons(sx.Cons(sx.MakeString(""), sx.MakeString(meta.ValueSyntaxHTML)))
	}
}

func (v *zmkVisitor) writeTableRow(row ast.TableRow, align []ast.Alignment) {
	for pos, cell := range row {
		_ = v.b.WriteByte('|')

	// TODO: scan cn.Lines to find embedded kind[0]s at beginning
	v.b.WriteString(delim)
	v.writeAttributes(attrs)
	v.b.WriteLn()
		if cell.Align != align[pos] {
			v.b.WriteString(alignCode[cell.Align])
	v.b.WriteString(content)
		}
		ast.Walk(v, &cell.Inlines)
	}
}
	v.b.WriteLn()
	v.b.WriteString(delim)
}


func (v *zmkVisitor) visitBLOB(bn *ast.BLOBNode) {
	if bn.Syntax == meta.ValueSyntaxSVG {
		v.b.WriteStrings("@@@", bn.Syntax, "\n")
func (v *zmkVisitor) visitBLOB(node *sx.Pair) {
	_, syntax, content, inlines := zsx.GetBLOBuncode(node)
	if syntax == meta.ValueSyntaxSVG {
		v.b.WriteString("@@@")
		_, _ = v.b.Write(bn.Blob)
		v.b.WriteString("\n@@@\n")
		v.b.WriteStrings(syntax, "\n", content, "\n@@@\n")
		return
	}
	var sb strings.Builder
	var textEnc TextEncoder
	_, _ = v.textEnc.WriteInlines(&sb, &bn.Description)
	v.b.WriteStrings("%% Unable to display BLOB with description '", sb.String(), "' and syntax '", bn.Syntax, "'.")
	_ = textEnc.WriteSz(&sb, zsx.MakeInlineList(inlines))
	v.b.WriteStrings("%% Unable to display BLOB with description '", sb.String(), "' and syntax '", syntax, "'.")
}

func (v *zmkVisitor) visitTransclude(node *sx.Pair, alst *sx.Pair) {
	attrs, ref, inlines := zsx.GetTransclusion(node)
	v.b.WriteString("{{{")
	if inlines != nil {
		v.walkList(inlines, alst)
		_ = v.b.WriteByte('|')
	}
	_ = sz.WriteReference(&v.b, ref)
	v.b.WriteString("}}}")
	v.writeAttributes(attrs)
}

var escapeSeqs = set.New(
	"\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==", "##",
)

func (v *zmkVisitor) visitText(tn *ast.TextNode) {
func (v *zmkVisitor) writeText(text string) {
	last := 0
	for i := 0; i < len(tn.Text); i++ {
		if b := tn.Text[i]; b == '\\' {
			v.b.WriteString(tn.Text[last:i])
	for i := 0; i < len(text); i++ {
		if b := text[i]; b == '\\' {
			v.b.WriteString(text[last:i])
			v.b.WriteBytes('\\', b)
			last = i + 1
			continue
		}
		if i < len(tn.Text)-1 {
			s := tn.Text[i : i+2]
		if i < len(text)-1 {
			s := text[i : i+2]
			if escapeSeqs.Contains(s) {
				v.b.WriteString(tn.Text[last:i])
				v.b.WriteString(text[last:i])
				for j := range len(s) {
					v.b.WriteBytes('\\', s[j])
				}
				i++
				last = i + 1
				continue
			}
		}
	}
	v.b.WriteString(tn.Text[last:])
	v.b.WriteString(text[last:])
}

func (v *zmkVisitor) visitBreak(bn *ast.BreakNode) {
	if bn.Hard {
func (v *zmkVisitor) writeBreak(isHard bool) {
	if isHard {
		v.b.WriteString("\\\n")
	} else {
		v.b.WriteLn()
	}
	v.writePrefixSpaces()
}

func (v *zmkVisitor) visitLink(ln *ast.LinkNode) {
func (v *zmkVisitor) writeAttributes(attrs *sx.Pair) {
	v.b.WriteString("[[")
	if len(ln.Inlines) > 0 {
		ast.Walk(v, &ln.Inlines)
		_ = v.b.WriteByte('|')
	}
	if ln.Ref.State == ast.RefStateBased {
		_ = v.b.WriteByte('/')
	}
	v.b.WriteStrings(ln.Ref.String(), "]]")
}

func (v *zmkVisitor) visitEmbedRef(en *ast.EmbedRefNode) {
	v.b.WriteString("{{")
	if len(en.Inlines) > 0 {
		ast.Walk(v, &en.Inlines)
		_ = v.b.WriteByte('|')
	}
	v.b.WriteStrings(en.Ref.String(), "}}")
}

func (v *zmkVisitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) {
	if en.Syntax == meta.ValueSyntaxSVG {
		v.b.WriteString("@@")
		_, _ = v.b.Write(en.Blob)
		v.b.WriteStrings("@@{=", en.Syntax, "}")
		return
	}
	v.b.WriteString("{{TODO: display inline BLOB}}")
}

func (v *zmkVisitor) visitCite(cn *ast.CiteNode) {
	v.b.WriteStrings("[@", cn.Key)
	if len(cn.Inlines) > 0 {
		v.b.WriteSpace()
		ast.Walk(v, &cn.Inlines)
	}
	_ = v.b.WriteByte(']')
	v.visitAttributes(cn.Attrs)
}

func (v *zmkVisitor) visitMark(mn *ast.MarkNode) {
	v.b.WriteStrings("[!", mn.Mark)
	if len(mn.Inlines) > 0 {
		_ = v.b.WriteByte('|')
		ast.Walk(v, &mn.Inlines)
	}
	_ = v.b.WriteByte(']')

}

var mapFormatKind = map[ast.FormatKind][]byte{
	ast.FormatEmph:   []byte("__"),
	ast.FormatStrong: []byte("**"),
	ast.FormatInsert: []byte(">>"),
	ast.FormatDelete: []byte("~~"),
	ast.FormatSuper:  []byte("^^"),
	ast.FormatSub:    []byte(",,"),
	ast.FormatQuote:  []byte(`""`),
	ast.FormatMark:   []byte("##"),
	ast.FormatSpan:   []byte("::"),
}

func (v *zmkVisitor) visitFormat(fn *ast.FormatNode) {
	kind, ok := mapFormatKind[fn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown format kind %d", fn.Kind))
	}
	_, _ = v.b.Write(kind)
	ast.Walk(v, &fn.Inlines)
	_, _ = v.b.Write(kind)
	v.visitAttributes(fn.Attrs)
}

func (v *zmkVisitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {
	case ast.LiteralCode:
		v.writeLiteral('`', ln.Attrs, ln.Content)
	case ast.LiteralMath:
		v.b.WriteStrings("$$", string(ln.Content), "$$")
		v.visitAttributes(ln.Attrs)
	case ast.LiteralInput:
		v.writeLiteral('\'', ln.Attrs, ln.Content)
	case ast.LiteralOutput:
		v.writeLiteral('=', ln.Attrs, ln.Content)
	case ast.LiteralComment:
		v.b.WriteString("%%")
		v.visitAttributes(ln.Attrs)
		v.b.WriteSpace()
		_, _ = v.b.Write(ln.Content)
	default:
		panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind))
	}
}

func (v *zmkVisitor) writeLiteral(code byte, a zsx.Attributes, content []byte) {
	v.b.WriteBytes(code, code)
	v.writeEscaped(string(content), code)
	v.b.WriteBytes(code, code)
	v.visitAttributes(a)
	a := zsx.GetAttributes(attrs)
}

// visitAttributes write HTML attributes
func (v *zmkVisitor) visitAttributes(a zsx.Attributes) {
	if a.IsEmpty() {
		return
	}
	_ = v.b.WriteByte('{')
	for i, k := range a.Keys() {
		if i > 0 {
			v.b.WriteSpace()
		}
		if k == "-" {
		if k == zsx.DefaultAttribute {
			_ = v.b.WriteByte('-')
			continue
		}
		v.b.WriteString(k)
		if vl := a[k]; len(vl) > 0 {
			v.b.WriteStrings("=\"", vl)
			v.b.WriteString("=\"")
			v.writeEscaped(vl, '"')
			_ = v.b.WriteByte('"')
		}
	}
	_ = v.b.WriteByte('}')
}

func (v *zmkVisitor) writeEscaped(s string, toEscape byte) {
	last := 0
	for i := range len(s) {
		if b := s[i]; b == toEscape || b == '\\' {
			v.b.WriteString(s[last:i])
			v.b.WriteBytes('\\', b)
			last = i + 1
		}
	}
	v.b.WriteString(s[last:])
}

func syntaxToHTML(a zsx.Attributes) zsx.Attributes {
	return a.Clone().Set("", meta.ValueSyntaxHTML).Remove(meta.KeySyntax)
}
func (v *zmkVisitor) writePrefixSpaces() {
	if prefixLen := len(v.prefix); prefixLen > 0 {
		for i := 0; i <= prefixLen; i++ {
			v.b.WriteSpace()
		}
	}
}
Added internal/evaluator/block.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore 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: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package evaluator interprets and evaluates the AST.
package evaluator

import (
	"errors"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/parser"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
)

func (e *evaluator) evalVerbatimEval(node *sx.Pair) *sx.Pair {
	_, attrs, content := zsx.GetVerbatim(node)
	if p := attrs.Assoc(sx.MakeString("")); p != nil {
		if s, isString := sx.GetString(p.Cdr()); isString && s.GetValue() == meta.ValueSyntaxDraw {
			return parser.ParseDrawBlock(attrs, []byte(content))
		}
	}
	return node
}

func (e *evaluator) evalVerbatimZettel(vn *sx.Pair) *sx.Pair {
	_, attrs, content := zsx.GetVerbatim(vn)
	m := meta.New(id.Invalid)
	m.Set(meta.KeySyntax, getSyntax(attrs, meta.ValueSyntaxText))
	zettel := zettel.Zettel{
		Meta:    m,
		Content: zettel.NewContent([]byte(content)),
	}
	e.transcludeCount++
	zn := e.evaluateEmbeddedZettel(zettel)
	return splicedBlocks(zn.Blocks)
}

func (e *evaluator) evalTransclusion(tn *sx.Pair) *sx.Pair {
	attrs, ref, text := zsx.GetTransclusion(tn)
	refSym, refVal := zsx.GetReference(ref)

	// To prevent e.embedCount from counting
	if errText := e.checkMaxTransclusions(ref); errText != nil {
		return makeBlock(errText)
	}
	if !sz.SymRefStateZettel.IsEqualSymbol(refSym) {
		switch refSym {
		case zsx.SymRefStateInvalid, sz.SymRefStateBroken:
			e.transcludeCount++
			return makeBlock(createInlineErrorText(ref, "Invalid or broken transclusion reference"))
		case zsx.SymRefStateSelf:
			e.transcludeCount++
			return makeBlock(createInlineErrorText(ref, "Self transclusion reference"))
		case sz.SymRefStateFound, zsx.SymRefStateExternal:
			return tn
		case zsx.SymRefStateHosted, sz.SymRefStateBased:
			return makeBlock(e.evalEmbed(zsx.MakeEmbed(attrs, ref, "", text)))
		case sz.SymRefStateQuery:
			e.transcludeCount++
			return e.evalQueryTransclusion(refVal)
		default:
			return makeBlock(createInlineErrorText(ref, "Illegal reference symvol "+refSym.GetValue()))
		}
	}

	zid := mustParseZid(ref, refVal)

	cost, ok := e.costMap[zid]
	zn := cost.zn
	if zn == e.marker {
		e.transcludeCount++
		return makeBlock(createInlineErrorText(ref, "Recursive transclusion"))
	}
	if !ok {
		zettel, err1 := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
		if err1 != nil {
			if errors.Is(err1, &box.ErrNotAllowed{}) {
				return nil
			}
			e.transcludeCount++
			return makeBlock(createInlineErrorText(ref, "Unable to get zettel"))
		}
		setMetadataFromAttributes(zettel.Meta, attrs)
		ec := e.transcludeCount
		e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec}
		zn = e.evaluateEmbeddedZettel(zettel)
		e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec}
		e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first.
	}
	e.transcludeCount++
	if ec := cost.ec; ec > 0 {
		e.transcludeCount += cost.ec
	}
	return splicedBlocks(zn.Blocks)
}

func (e *evaluator) evalQueryTransclusion(expr string) *sx.Pair {
	q := query.Parse(expr)
	ml, err := e.port.QueryMeta(e.ctx, q)
	if err != nil {
		if errors.Is(err, &box.ErrNotAllowed{}) {
			return nil
		}
		return makeBlock(createInlineErrorText(nil, "Unable to search zettel"))
	}
	result, _ := QueryAction(e.ctx, q, ml)
	if result != nil {
		result = mustPair(zsx.Walk(e, result, nil))
	}
	return result
}

func makeBlock(inl *sx.Pair) *sx.Pair { return zsx.MakePara(inl) }

func splicedBlocks(block *sx.Pair) *sx.Pair {
	blocks := zsx.GetBlock(block)
	if blocks.Tail() == nil {
		return blocks.Head()
	}
	return blocks.Cons(zsx.SymSpecialSplice)
}
Changes to internal/evaluator/evaluator.go.
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
433
434
435
436
437
438

439
440
441
442
443
444
445
446
447
448
449
450
451
452
453

454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525

526
527
528
529
530
531
532
533
534
535

536
537
538
539
540
541

542
543
544
545
546
547
548
549
550

551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567

568
569
570
571

572
573
574
575
576
577
578
579
580
581
582
583

584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
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







-

-

-
-

-

-
-
-
-
+
+
+
+



-














-
+



-
+

-
+

-
+



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


-
+







-
-
+
+

-
+
-









-
-
+
+



-
+


-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+

-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
-
-
-
+
-
-
-
-
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
+
-
-
-
+
-
-
-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+





-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+



-
-
+
+



-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

-
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-


-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package evaluator interprets and evaluates the AST.
package evaluator

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"path"
	"slices"
	"strconv"
	"strings"

	"t73f.de/r/sx/sxbuiltins"
	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/parser"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
)

// Port contains all methods to retrieve zettel (or part of it) to evaluate a zettel.
type Port interface {
	GetZettel(context.Context, id.Zid) (zettel.Zettel, error)
	QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error)
}

// EvaluateZettel evaluates the given zettel in the given context, with the
// given ports, and the given environment.
func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.ZettelNode) {
func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.Zettel) {
	switch zn.Syntax {
	case meta.ValueSyntaxNone:
		// AST is empty, evaluate to a description list of metadata.
		zn.BlocksAST = evaluateMetadata(zn.Meta)
		zn.Blocks = evaluateMetadata(zn.Meta)
	case meta.ValueSyntaxSxn:
		zn.BlocksAST = evaluateSxn(zn.BlocksAST)
		zn.Blocks = evaluateSxn(zn.Blocks)
	default:
		EvaluateBlock(ctx, port, rtConfig, &zn.BlocksAST)
		zn.Blocks = EvaluateBlock(ctx, port, rtConfig, zn.Blocks)
	}
}

func evaluateSxn(bs ast.BlockSlice) ast.BlockSlice {
	// Check for structure made in parser/plain/plain.go:parseSxnBlocks
	if len(bs) == 1 {
		// If len(bs) > 1 --> an error was found during parsing
		if vn, isVerbatim := bs[0].(*ast.VerbatimNode); isVerbatim && vn.Kind == ast.VerbatimCode {
			if classAttr, hasClass := vn.Attrs.Get(""); hasClass && classAttr == meta.ValueSyntaxSxn {
				rd := sxreader.MakeReader(bytes.NewReader(vn.Content))
				if objs, err := rd.ReadAll(); err == nil {
					result := make(ast.BlockSlice, len(objs))
					for i, obj := range objs {
						var buf bytes.Buffer
						_, _ = sxbuiltins.Print(&buf, obj)
						result[i] = &ast.VerbatimNode{
							Kind:    ast.VerbatimCode,
							Attrs:   zsx.Attributes{"": classAttr},
							Content: buf.Bytes(),
						}
					}
					return result
				}
			}
		}
	}
	return bs
}

// EvaluateBlock evaluates the given block list in the given context, with
// the given ports, and the given environment.
func EvaluateBlock(ctx context.Context, port Port, rtConfig config.Config, bns *ast.BlockSlice) {
func EvaluateBlock(ctx context.Context, port Port, rtConfig config.Config, block *sx.Pair) *sx.Pair {
	e := evaluator{
		ctx:             ctx,
		port:            port,
		rtConfig:        rtConfig,
		transcludeMax:   rtConfig.GetMaxTransclusions(),
		transcludeCount: 0,
		costMap:         map[id.Zid]transcludeCost{},
		embedMap:        map[string]ast.InlineSlice{},
		marker:          &ast.ZettelNode{},
		embedMap:        map[string]*sx.Pair{},
		marker:          &ast.Zettel{},
	}
	ast.Walk(&e, bns)
	return mustPair(zsx.Walk(&e, block, nil))
	parser.Clean(bns, true)
}

type evaluator struct {
	ctx             context.Context
	port            Port
	rtConfig        config.Config
	transcludeMax   int
	transcludeCount int
	costMap         map[id.Zid]transcludeCost
	marker          *ast.ZettelNode
	embedMap        map[string]ast.InlineSlice
	marker          *ast.Zettel
	embedMap        map[string]*sx.Pair
}

type transcludeCost struct {
	zn *ast.ZettelNode
	zn *ast.Zettel
	ec int
}

func (e *evaluator) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		e.visitBlockSlice(n)
	case *ast.InlineSlice:
		e.visitInlineSlice(n)
	default:
		return e
	}
	return nil
}

func (e *evaluator) visitBlockSlice(bs *ast.BlockSlice) {
	for i := 0; i < len(*bs); i++ {
		bn := (*bs)[i]
		ast.Walk(e, bn)
		switch n := bn.(type) {
		case *ast.VerbatimNode:
			i += transcludeNode(bs, i, e.evalVerbatimNode(n))
		case *ast.TranscludeNode:
			i += transcludeNode(bs, i, e.evalTransclusionNode(n))

func (e *evaluator) VisitBefore(_ *sx.Pair, _ *sx.Pair) (sx.Object, bool) {
	return sx.Nil(), false
}
func (e *evaluator) VisitAfter(node *sx.Pair, _ *sx.Pair) sx.Object {
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		switch sym {
		case zsx.SymLink:
			return e.evalLink(node)
		case zsx.SymEmbed:
			return e.evalEmbed(node)
		case zsx.SymVerbatimEval:
			return e.evalVerbatimEval(node)
		case zsx.SymTransclude:
			return e.evalTransclusion(node)
		case zsx.SymVerbatimZettel:
			return e.evalVerbatimZettel(node)
		}
	}
}

func transcludeNode(bln *ast.BlockSlice, i int, bn ast.BlockNode) int {
	if ln, ok := bn.(*ast.BlockSlice); ok {
		*bln = replaceWithBlockNodes(*bln, i, *ln)
		return len(*ln) - 1
	}
	return node
}
	if bn == nil {
		(*bln) = (*bln)[:i+copy((*bln)[i:], (*bln)[i+1:])]
		return -1
	}
	(*bln)[i] = bn
	return 0

func (e *evaluator) evaluateEmbeddedZettel(zettel zettel.Zettel) *ast.Zettel {
	zn := parser.ParseZettel(e.ctx, zettel, string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)), e.rtConfig)
	parser.Clean(zn.Blocks)
	zn.Blocks = mustPair(zsx.Walk(e, zn.Blocks, nil))
	return zn
}

func replaceWithBlockNodes(bns []ast.BlockNode, i int, replaceBns []ast.BlockNode) []ast.BlockNode {
	if len(replaceBns) == 1 {
		bns[i] = replaceBns[0]
		return bns
	}
	newIns := make([]ast.BlockNode, 0, len(bns)+len(replaceBns)-1)
	if i > 0 {
		newIns = append(newIns, bns[:i]...)
	}

func setMetadataFromAttributes(m *meta.Meta, attrs *sx.Pair) {
	for obj := range attrs.Values() {
		if pair, isPair := sx.GetPair(obj); isPair {
			if key, isKey := sx.GetString(pair.Car()); isKey && meta.KeyIsValid(key.GetValue()) {
				if val, isVal := sx.GetString(pair.Cdr()); isVal {
					m.Set(key.GetValue(), meta.Value(val.GetValue()))
				}
	if len(replaceBns) > 0 {
		newIns = append(newIns, replaceBns...)
	}
			}
	if i+1 < len(bns) {
		newIns = append(newIns, bns[i+1:]...)
	}
		}
	return newIns
}

func (e *evaluator) evalVerbatimNode(vn *ast.VerbatimNode) ast.BlockNode {
	}
}

	switch vn.Kind {
	case ast.VerbatimZettel:
		return e.evalVerbatimZettel(vn)
	case ast.VerbatimEval:
		if syntax, found := vn.Attrs.Get(""); found && syntax == meta.ValueSyntaxDraw {
			return parser.ParseDrawBlock(vn)
		}
func mustPair(obj sx.Object) *sx.Pair {
	p, isPair := sx.GetPair(obj)
	if !isPair {
		panic(fmt.Sprintf("not a pair after evaluate: %T/%v", obj, obj))
	}
	return p
}

	}
	return vn
}

func mustParseZid(ref *sx.Pair, refVal string) id.Zid {
	baseVal, _ := sz.SplitFragment(refVal)
	zid, err := id.Parse(baseVal)
	if err == nil {
		return zid
	}
	refState, _ := zsx.GetReference(ref)
func (e *evaluator) evalVerbatimZettel(vn *ast.VerbatimNode) ast.BlockNode {
	m := meta.New(id.Invalid)
	m.Set(meta.KeySyntax, getSyntax(vn.Attrs, meta.ValueSyntaxText))
	zettel := zettel.Zettel{
	panic(fmt.Sprintf("%v: %q (state %v) -> %v", err, refVal, refState, ref))
		Meta:    m,
		Content: zettel.NewContent(vn.Content),
	}
}
	e.transcludeCount++
	zn := e.evaluateEmbeddedZettel(zettel)
	return &zn.BlocksAST
}


func getSyntax(a zsx.Attributes, defSyntax meta.Value) meta.Value {
	if a != nil {
		if val, ok := a.Get(meta.KeySyntax); ok {
			return meta.Value(val)
		}
func getSyntax(attrs *sx.Pair, defSyntax meta.Value) meta.Value {
	for a := range attrs.Values() {
		if pair, isPair := sx.GetPair(a); isPair {
			car := pair.Car()
			if car.IsEqual(sx.MakeString(meta.KeySyntax)) || car.IsEqual(sx.MakeString("")) {
				if val, isString := sx.GetString(pair.Cdr()); isString {
					return meta.Value(val.GetValue())
				}
		if val, ok := a.Get(""); ok {
			return meta.Value(val)
			}
		}
	}
	return defSyntax
}

func (e *evaluator) evalTransclusionNode(tn *ast.TranscludeNode) ast.BlockNode {
	ref := tn.Ref

	// To prevent e.embedCount from counting
	if errText := e.checkMaxTransclusions(ref); errText != nil {
		return makeBlockNode(errText)
	}
	switch ref.State {
	case ast.RefStateZettel:
		// Only zettel references will be evaluated.
	case ast.RefStateInvalid, ast.RefStateBroken:
		e.transcludeCount++
		return makeBlockNode(createInlineErrorText(ref, "Invalid", "or", "broken", "transclusion", "reference"))
	case ast.RefStateSelf:
		e.transcludeCount++
		return makeBlockNode(createInlineErrorText(ref, "Self", "transclusion", "reference"))
	case ast.RefStateFound, ast.RefStateExternal:
		return tn
	case ast.RefStateHosted, ast.RefStateBased:
		if n := createEmbeddedNodeLocal(ref); n != nil {
			n.Attrs = tn.Attrs
			return makeBlockNode(n)
		}
		return tn
	case ast.RefStateQuery:
		e.transcludeCount++
		return e.evalQueryTransclusion(tn.Ref.Value)
	default:
		return makeBlockNode(createInlineErrorText(ref, "Illegal", "block", "state", strconv.Itoa(int(ref.State))))
	}

	zid, err := id.Parse(ref.URL.Path)
	if err != nil {
		panic(err)
	}

	cost, ok := e.costMap[zid]
	zn := cost.zn
	if zn == e.marker {
		e.transcludeCount++
		return makeBlockNode(createInlineErrorText(ref, "Recursive", "transclusion"))
	}
	if !ok {
		zettel, err1 := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
		if err1 != nil {
			if errors.Is(err1, &box.ErrNotAllowed{}) {
				return nil
			}
			e.transcludeCount++
			return makeBlockNode(createInlineErrorText(ref, "Unable", "to", "get", "zettel"))
		}
		setMetadataFromAttributes(zettel.Meta, tn.Attrs)
		ec := e.transcludeCount
		e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec}
		zn = e.evaluateEmbeddedZettel(zettel)
		e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec}
		e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first.
	}
	e.transcludeCount++
	if ec := cost.ec; ec > 0 {
		e.transcludeCount += cost.ec
	}
	return &zn.BlocksAST
}

func (e *evaluator) evalQueryTransclusion(expr string) ast.BlockNode {
	q := query.Parse(expr)
	ml, err := e.port.QueryMeta(e.ctx, q)
	if err != nil {
		if errors.Is(err, &box.ErrNotAllowed{}) {
			return nil
		}
		return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel"))
	}
	result, _ := QueryAction(e.ctx, q, ml)
	if result != nil {
		ast.Walk(e, result)
	}
	return result
}

func (e *evaluator) checkMaxTransclusions(ref *ast.Reference) ast.InlineNode {
func (e *evaluator) checkMaxTransclusions(ref *sx.Pair) *sx.Pair {
	if maxTrans := e.transcludeMax; e.transcludeCount > maxTrans {
		e.transcludeCount = maxTrans + 1
		return createInlineErrorText(ref,
			"Too", "many", "transclusions", "(must", "be", "at", "most", strconv.Itoa(maxTrans)+",",
			"see", "runtime", "configuration", "key", "max-transclusions)")
			"Too many transclusions (must be at most "+strconv.Itoa(maxTrans)+
				", see runtime configuration key max-transclusions)")
	}
	return nil
}

func makeBlockNode(in ast.InlineNode) ast.BlockNode { return ast.CreateParaNode(in) }

func setMetadataFromAttributes(m *meta.Meta, a zsx.Attributes) {
	for aKey, aVal := range a {
		if meta.KeyIsValid(aKey) {
			m.Set(aKey, meta.Value(aVal))
		}
	}
}

func (e *evaluator) visitInlineSlice(is *ast.InlineSlice) {
	for i := 0; i < len(*is); i++ {
		in := (*is)[i]
		ast.Walk(e, in)
		switch n := in.(type) {
		case *ast.LinkNode:
			(*is)[i] = e.evalLinkNode(n)
		case *ast.EmbedRefNode:
			i += embedNode(is, i, e.evalEmbedRefNode(n))
		}

	}
}

func createInlineErrorText(ref *sx.Pair, message string) *sx.Pair {
func embedNode(is *ast.InlineSlice, i int, in ast.InlineNode) int {
	if ln, ok := in.(*ast.InlineSlice); ok {
		*is = replaceWithInlineNodes(*is, i, *ln)
		return len(*ln) - 1
	}
	if in == nil {
		(*is) = (*is)[:i+copy((*is)[i:], (*is)[i+1:])]
		return -1
	}
	(*is)[i] = in
	return 0
}

	text := message
func replaceWithInlineNodes(ins ast.InlineSlice, i int, replaceIns ast.InlineSlice) ast.InlineSlice {
	if len(replaceIns) == 1 {
		ins[i] = replaceIns[0]
		return ins
	}
	newIns := make(ast.InlineSlice, 0, len(ins)+len(replaceIns)-1)
	if i > 0 {
		newIns = append(newIns, ins[:i]...)
	}
	if len(replaceIns) > 0 {
		newIns = append(newIns, replaceIns...)
	}
	if i+1 < len(ins) {
		newIns = append(newIns, ins[i+1:]...)
	}
	return newIns
}

func (e *evaluator) evalLinkNode(ln *ast.LinkNode) ast.InlineNode {
	if len(ln.Inlines) == 0 {
		ln.Inlines = ast.InlineSlice{&ast.TextNode{Text: ln.Ref.Value}}
	}
	ref := ln.Ref
	if ref == nil || ref.State != ast.RefStateZettel {
		return ln
	}

	zid := mustParseZid(ref)
	_, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
	if errors.Is(err, &box.ErrNotAllowed{}) {
		return &ast.FormatNode{
			Kind:    ast.FormatSpan,
			Attrs:   ln.Attrs,
			Inlines: getLinkInline(ln),
		}
	} else if err != nil {
	if ref != nil {
		ln.Ref.State = ast.RefStateBroken
		return ln
	}

		text += ": " + sz.ReferenceString(ref) + "."
	ln.Ref.State = ast.RefStateZettel
	return ln
}

func getLinkInline(ln *ast.LinkNode) ast.InlineSlice {
	if ln.Inlines != nil {
		return ln.Inlines
	}
	return ast.InlineSlice{&ast.TextNode{Text: ln.Ref.Value}}
}

func (e *evaluator) evalEmbedRefNode(en *ast.EmbedRefNode) ast.InlineNode {
	ref := en.Ref

	// To prevent e.embedCount from counting
	if errText := e.checkMaxTransclusions(ref); errText != nil {
		return errText
	}

	switch ref.State {
	case ast.RefStateZettel:
		// Only zettel references will be evaluated.
	case ast.RefStateInvalid, ast.RefStateBroken:
		e.transcludeCount++
		return createInlineErrorImage(en)
	case ast.RefStateSelf:
		e.transcludeCount++
		return createInlineErrorText(ref, "Self", "embed", "reference")
	case ast.RefStateFound, ast.RefStateExternal:
		return en
	case ast.RefStateHosted, ast.RefStateBased:
		if n := createEmbeddedNodeLocal(ref); n != nil {
			n.Attrs = en.Attrs
			n.Inlines = en.Inlines
			return n
		}
		return en
	default:
		return createInlineErrorText(ref, "Illegal", "inline", "state", strconv.Itoa(int(ref.State)))
	}

	ln := zsx.MakeLiteral(zsx.SymLiteralOutput, nil, text)
	zid := mustParseZid(ref)
	zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
	if err != nil {
		if errors.Is(err, &box.ErrNotAllowed{}) {
			return nil
		}
		e.transcludeCount++
		return createInlineErrorImage(en)
	}

	fn := zsx.MakeFormat(zsx.SymFormatStrong,
	if syntax := string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)); parser.IsImageFormat(syntax) {
		e.updateImageRefNode(en, zettel.Meta, syntax)
		return en
	} else if !parser.IsASTParser(syntax) {
		// Not embeddable.
		e.transcludeCount++
		return createInlineErrorText(ref, "Not", "embeddable (syntax="+syntax+")")
	}

		sx.MakeList(sx.Cons(sx.MakeString("class"), sx.MakeString("error"))),
	cost, ok := e.costMap[zid]
	zn := cost.zn
	if zn == e.marker {
		e.transcludeCount++
		return createInlineErrorText(ref, "Recursive", "transclusion")
	}
	if !ok {
		ec := e.transcludeCount
		e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec}
		zn = e.evaluateEmbeddedZettel(zettel)
		e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec}
		e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first.
	}
	e.transcludeCount++

		sx.MakeList(ln))
	result, ok := e.embedMap[ref.Value]
	if !ok {
		// Search for text to be embedded.
		result = findInlineSlice(&zn.BlocksAST, ref.URL.Fragment)
		e.embedMap[ref.Value] = result
	}
	if len(result) == 0 {
		return &ast.LiteralNode{
			Kind:    ast.LiteralComment,
			Attrs:   map[string]string{"-": ""},
			Content: append([]byte("Nothing to transclude: "), ref.String()...),
		}
	}

	if ec := cost.ec; ec > 0 {
		e.transcludeCount += cost.ec
	}
	return &result
}

func mustParseZid(ref *ast.Reference) id.Zid {
	zid, err := id.Parse(ref.URL.Path)
	if err != nil {
		panic(fmt.Sprintf("%v: %q (state %v) -> %v", err, ref.URL.Path, ref.State, ref))
	}
	return zid
}

func (e *evaluator) updateImageRefNode(en *ast.EmbedRefNode, m *meta.Meta, syntax string) {
	en.Syntax = syntax
	if len(en.Inlines) == 0 {
		is := parser.ParseDescriptionAST(m)
		if len(is) > 0 {
			ast.Walk(e, &is)
			if len(is) > 0 {
				en.Inlines = is
			}
		}
	}
}

func createInlineErrorImage(en *ast.EmbedRefNode) *ast.EmbedRefNode {
	errorZid := id.ZidEmoji
	en.Ref = ast.ParseReference(errorZid.String())
	if len(en.Inlines) == 0 {
		en.Inlines = ast.InlineSlice{&ast.TextNode{Text: "Error placeholder"}}
	}
	return en
}

func createInlineErrorText(ref *ast.Reference, msgWords ...string) ast.InlineNode {
	text := strings.Join(msgWords, " ")
	if ref != nil {
		text += ": " + ref.String() + "."
	}
	ln := &ast.LiteralNode{
		Kind:    ast.LiteralInput,
		Content: []byte(text),
	}
	fn := &ast.FormatNode{
		Kind:    ast.FormatStrong,
		Inlines: ast.InlineSlice{ln},
	}
	fn.Attrs = fn.Attrs.AddClass("error")
	return fn
}

func createEmbeddedNodeLocal(ref *ast.Reference) *ast.EmbedRefNode {
	ext := path.Ext(ref.Value)
	if ext != "" && ext[0] == '.' {
		ext = ext[1:]
	}

	pinfo := parser.Get(ext)
	if pinfo == nil || !pinfo.IsImageFormat {
		return nil
	}
	return &ast.EmbedRefNode{
		Ref:    ref,
		Syntax: ext,
	}
}

func createInlineErrorImage(attrs *sx.Pair, text *sx.Pair) *sx.Pair {
func (e *evaluator) evaluateEmbeddedZettel(zettel zettel.Zettel) *ast.ZettelNode {
	zn := parser.ParseZettel(e.ctx, zettel, string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)), e.rtConfig)
	ast.Walk(e, &zn.BlocksAST)
	return zn
}

	ref := sz.ScanReference(id.ZidEmoji.String())
func findInlineSlice(bs *ast.BlockSlice, fragment string) ast.InlineSlice {
	if fragment == "" {
		return firstInlinesToEmbed(*bs)
	}
	fs := fragmentSearcher{fragment: fragment}
	ast.Walk(&fs, bs)
	return fs.result
}

	if text == nil {
func firstInlinesToEmbed(bs ast.BlockSlice) ast.InlineSlice {
	if ins := bs.FirstParagraphInlines(); ins != nil {
		return ins
	}
	if len(bs) == 0 {
		return nil
	}
	if bn, ok := bs[0].(*ast.BLOBNode); ok {
		return ast.InlineSlice{&ast.EmbedBLOBNode{
			Blob:    bn.Blob,
			Syntax:  bn.Syntax,
			Inlines: bn.Description,
		}}
	}
	return nil
}

		text = sx.MakeList(zsx.MakeText("Error placeholder"))
type fragmentSearcher struct {
	fragment string
	result   ast.InlineSlice
}
	}

func (fs *fragmentSearcher) Visit(node ast.Node) ast.Visitor {
	if len(fs.result) > 0 {
		return nil
	}
	switch n := node.(type) {
	case *ast.BlockSlice:
		fs.visitBlockSlice(n)
	case *ast.InlineSlice:
		fs.visitInlineSlice(n)
	default:
		return fs
	return zsx.MakeEmbed(attrs, ref, "", text)
	}
	return nil
}

func (fs *fragmentSearcher) visitBlockSlice(bs *ast.BlockSlice) {
	for i, bn := range *bs {
		if hn, ok := bn.(*ast.HeadingNode); ok && hn.Fragment == fs.fragment {
			fs.result = (*bs)[i+1:].FirstParagraphInlines()
			return
		}
		ast.Walk(fs, bn)
	}
}

func (fs *fragmentSearcher) visitInlineSlice(is *ast.InlineSlice) {
	for i, in := range *is {
		if mn, ok := in.(*ast.MarkNode); ok && mn.Fragment == fs.fragment {
			ris := skipBreakeNodes((*is)[i+1:])
			if len(mn.Inlines) > 0 {
				fs.result = slices.Clone(mn.Inlines)
				fs.result = append(fs.result, &ast.TextNode{Text: " "})
				fs.result = append(fs.result, ris...)
			} else {
				fs.result = ris
			}
			return
		}
		ast.Walk(fs, in)
	}
}

func skipBreakeNodes(ins ast.InlineSlice) ast.InlineSlice {
	for i, in := range ins {
		switch in.(type) {
		case *ast.BreakNode:
		default:
			return ins[i:]
		}
	}
	return nil
}
Added internal/evaluator/inline.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore 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: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package evaluator interprets and evaluates the AST.
package evaluator

import (
	"errors"
	"path"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/parser"
)

func (e *evaluator) evalLink(node *sx.Pair) *sx.Pair {
	attrs, ref, inlines := zsx.GetLink(node)
	refState, refVal := zsx.GetReference(ref)
	newInlines := inlines
	if inlines == nil {
		newInlines = sx.MakeList(zsx.MakeText(refVal))
	}
	if !sz.SymRefStateZettel.IsEqualSymbol(refState) {
		if newInlines != inlines {
			return zsx.MakeLink(attrs, ref, newInlines)
		}
		return node
	}

	zid := mustParseZid(ref, refVal)
	_, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
	if errors.Is(err, &box.ErrNotAllowed{}) {
		return zsx.MakeFormat(zsx.SymFormatSpan, attrs, newInlines)
	}
	if err != nil {
		return zsx.MakeLink(attrs, zsx.MakeReference(sz.SymRefStateBroken, refVal), newInlines)
	}

	if newInlines != inlines {
		return zsx.MakeLink(attrs, ref, newInlines)
	}
	return node
}

func (e *evaluator) evalEmbed(en *sx.Pair) *sx.Pair {
	attrs, ref, _, inlines := zsx.GetEmbed(en)
	refSym, refVal := zsx.GetReference(ref)

	// To prevent e.embedCount from counting
	if errText := e.checkMaxTransclusions(ref); errText != nil {
		return errText
	}

	if !sz.SymRefStateZettel.IsEqualSymbol(refSym) {
		switch refSym {
		case zsx.SymRefStateInvalid, sz.SymRefStateBroken:
			e.transcludeCount++
			return createInlineErrorImage(attrs, inlines)
		case zsx.SymRefStateSelf:
			e.transcludeCount++
			return createInlineErrorText(ref, "Self embed reference")
		case sz.SymRefStateFound, zsx.SymRefStateExternal:
			return en
		case zsx.SymRefStateHosted, sz.SymRefStateBased:
			if n := createLocalEmbedded(attrs, ref, refVal, inlines); n != nil {
				return n
			}
			return en
		case sz.SymRefStateQuery:
			return createInlineErrorText(ref, "Query reference not allowed here")
		default:
			return createInlineErrorText(ref, "Illegal inline state "+refSym.GetValue())
		}
	}

	zid := mustParseZid(ref, refVal)
	zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
	if err != nil {
		if errors.Is(err, &box.ErrNotAllowed{}) {
			return nil
		}
		e.transcludeCount++
		return createInlineErrorImage(attrs, inlines)
	}

	if syntax := string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)); parser.IsImageFormat(syntax) {
		return e.updateImageRefNode(attrs, ref, inlines, zettel.Meta, syntax)
	} else if !parser.IsASTParser(syntax) {
		// Not embeddable.
		e.transcludeCount++
		return createInlineErrorText(ref, "Not embeddable (syntax="+syntax+")")
	}

	cost, ok := e.costMap[zid]
	zn := cost.zn
	if zn == e.marker {
		e.transcludeCount++
		return createInlineErrorText(ref, "Recursive transclusion")
	}
	if !ok {
		ec := e.transcludeCount
		e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec}
		zn = e.evaluateEmbeddedZettel(zettel)
		e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec}
		e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first.
	}
	e.transcludeCount++

	result, ok := e.embedMap[refVal]
	if !ok {
		// Search for text to be embedded.
		_, fragment := sz.SplitFragment(refVal)
		blocks := zsx.GetBlock(zn.Blocks)
		if fragment == "" {
			result = firstInlinesToEmbed(blocks)
		} else {
			result = findFragmentInBlocks(blocks, fragment)
		}
		e.embedMap[refVal] = result
	}
	if result == nil {
		return zsx.MakeLiteral(zsx.SymLiteralComment,
			sx.MakeList(sx.Cons(sx.MakeString(zsx.DefaultAttribute), sx.MakeString(""))),
			"Nothing to transclude: "+sz.ReferenceString(ref),
		)
	}

	if ec := cost.ec; ec > 0 {
		e.transcludeCount += cost.ec
	}
	if result.Tail() == nil {
		return result.Head()
	}
	return result.Cons(zsx.SymSpecialSplice)
}

func (e *evaluator) updateImageRefNode(
	attrs *sx.Pair, ref *sx.Pair, inlines *sx.Pair, m *meta.Meta, syntax string,
) *sx.Pair {
	if inlines != nil {
		if is := parser.ParseDescription(m); is != nil {
			if is = mustPair(zsx.Walk(e, is, nil)); is != nil {
				inlines = is
			}
		}
	}
	return zsx.MakeEmbed(attrs, ref, syntax, inlines)
}

func findFragmentInBlocks(blocks *sx.Pair, fragment string) *sx.Pair {
	fs := fragmentSearcher{fragment: fragment}
	zsx.WalkItList(&fs, blocks, 0, nil)
	return fs.result
}

type fragmentSearcher struct {
	result   *sx.Pair
	fragment string
}

func (fs *fragmentSearcher) VisitItBefore(node *sx.Pair, alst *sx.Pair) bool {
	if fs.result != nil {
		return true
	}
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		switch sym {
		case zsx.SymHeading:
			_, _, _, _, frag := zsx.GetHeading(node)
			if frag == fs.fragment {
				bn := zsx.GetWalkList(alst)
				fs.result = firstInlinesToEmbed(bn.Tail())
				return true
			}

		case zsx.SymMark:
			_, _, frag, inlines := zsx.GetMark(node)
			if frag == fs.fragment {
				next := zsx.GetWalkList(alst).Tail()
				for ; next != nil; next = next.Tail() {
					car := next.Head().Car()
					if !zsx.SymSoft.IsEqual(car) && !zsx.SymHard.IsEqual(car) {
						break
					}
				}
				if next == nil { // Mark is last in inline list
					fs.result = inlines
					return true
				}

				var lb sx.ListBuilder
				if inlines != nil {
					lb.Collect(inlines.Values())
				}
				lb.Collect(next.Values())
				fs.result = lb.List()
				return true
			}

		}
	}
	return false
}
func (fs *fragmentSearcher) VisitItAfter(*sx.Pair, *sx.Pair) {}

func firstInlinesToEmbed(blocks *sx.Pair) *sx.Pair {
	if blocks != nil {
		if ins := firstParagraphInlines(blocks); ins != nil {
			return ins
		}

		blk := blocks.Head()
		if sym, isSymbol := sx.GetSymbol(blk.Car()); isSymbol && zsx.SymBLOB.IsEqualSymbol(sym) {
			attrs, syntax, content, inlines := zsx.GetBLOBuncode(blk)
			return sx.MakeList(zsx.MakeEmbedBLOBuncode(attrs, syntax, content, inlines))
		}
	}
	return nil
}

// firstParagraphInlines returns the inline list of the first paragraph that
// contains a inline list.
func firstParagraphInlines(blocks *sx.Pair) *sx.Pair {
	for blockObj := range blocks.Values() {
		if block, isPair := sx.GetPair(blockObj); isPair {
			if sym, isSymbol := sx.GetSymbol(block.Car()); isSymbol && zsx.SymPara.IsEqualSymbol(sym) {
				if inlines := zsx.GetPara(block); inlines != nil {
					return inlines
				}
			}
		}
	}
	return nil
}

func createLocalEmbedded(attrs *sx.Pair, ref *sx.Pair, refValue string, inlines *sx.Pair) *sx.Pair {
	ext := path.Ext(refValue)
	if ext != "" && ext[0] == '.' {
		ext = ext[1:]
	}
	pinfo := parser.Get(ext)
	if pinfo == nil || !pinfo.IsImageFormat {
		return nil
	}
	return zsx.MakeEmbed(attrs, ref, ext, inlines)
}
Changes to internal/evaluator/list.go.
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
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







+


+


-



-
-
+
+




-
+











-
+







	"bytes"
	"context"
	"math"
	"slices"
	"strconv"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/query"
)

// QueryAction transforms a list of metadata according to query actions into a AST nested list.
func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta) (ast.BlockNode, int) {
// QueryAction transforms a list of metadata according to query actions into a SZ nested list.
func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta) (*sx.Pair, int) {
	ap := actionPara{
		ctx:    ctx,
		q:      q,
		ml:     ml,
		kind:   ast.NestedListUnordered,
		kind:   zsx.SymListUnordered,
		minVal: -1,
		maxVal: -1,
	}
	actions := q.Actions()
	if len(actions) == 0 {
		return ap.createBlockNodeMeta("")
	}

	acts := make([]string, 0, len(actions))
	for _, act := range actions {
		if strings.HasPrefix(act, api.NumberedAction[0:1]) {
			ap.kind = ast.NestedListOrdered
			ap.kind = zsx.SymListOrdered
			continue
		}
		if strings.HasPrefix(act, api.MinAction) {
			if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
				ap.minVal = num
				continue
			}
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
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







+











-
+




-
+





-
+

+
+


-
-
-
-
-
+
+
+
+
+
+


-
-
+
-
-
-


-
+







-
+

-

+
+


-
+


-
-
-
-
+
+
+
+
-
-
+
-
-
-
-
-
-
+
+
+
-

+


-
+







		case meta.TypeTagSet:
			return ap.createBlockNodeTagSet(key)
		}
		if firstUnknowAct == "" {
			firstUnknowAct = act
		}
	}

	bn, numItems := ap.createBlockNodeMeta(strings.ToLower(firstUnknowAct))
	if bn != nil && numItems == 0 && firstUnknowAct == strings.ToUpper(firstUnknowAct) {
		bn, numItems = ap.createBlockNodeMeta("")
	}
	return bn, numItems
}

type actionPara struct {
	ctx    context.Context
	q      *query.Query
	ml     []*meta.Meta
	kind   ast.NestedListKind
	kind   *sx.Symbol
	minVal int
	maxVal int
}

func (ap *actionPara) createBlockNodeWord(key string) (ast.BlockNode, int) {
func (ap *actionPara) createBlockNodeWord(key string) (*sx.Pair, int) {
	var buf bytes.Buffer
	ccs, bufLen := ap.prepareCatAction(key, &buf)
	if len(ccs) == 0 {
		return nil, 0
	}
	items := make([]ast.ItemSlice, 0, len(ccs))

	ccs.SortByName()
	var items sx.ListBuilder
	count := 0
	for _, cat := range ccs {
		buf.WriteString(string(cat.Name))
		items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{
			Attrs:   nil,
			Ref:     ast.ParseReference(buf.String()),
			Inlines: ast.InlineSlice{&ast.TextNode{Text: string(cat.Name)}},
		})})
		items.Add(zsx.MakeBlock(zsx.MakePara(
			zsx.MakeLink(nil,
				sz.ScanReference(buf.String()),
				sx.MakeList(zsx.MakeText(string(cat.Name)))),
		)))
		count++
		buf.Truncate(bufLen)
	}
	return &ast.NestedListNode{
		Kind:  ap.kind,
	return zsx.MakeList(ap.kind, nil, items.List()), count
		Items: items,
		Attrs: nil,
	}, len(items)
}

func (ap *actionPara) createBlockNodeTagSet(key string) (ast.BlockNode, int) {
func (ap *actionPara) createBlockNodeTagSet(key string) (*sx.Pair, int) {
	var buf bytes.Buffer
	ccs, bufLen := ap.prepareCatAction(key, &buf)
	if len(ccs) == 0 {
		return nil, 0
	}
	ccs.SortByCount()
	ccs = ap.limitTags(ccs)
	countMap := ap.calcFontSizes(ccs)
	countClassAttrs := ap.calcFontSizes(ccs)

	para := make(ast.InlineSlice, 0, len(ccs))
	ccs.SortByName()
	var tags sx.ListBuilder
	count := 0
	for i, cat := range ccs {
		if i > 0 {
			para = append(para, &ast.TextNode{Text: " "})
			tags.Add(zsx.MakeText(" "))
		}
		buf.WriteString(string(cat.Name))
		para = append(para,
			&ast.LinkNode{
				Attrs: countMap[cat.Count],
				Ref:   ast.ParseReference(buf.String()),
		tags.AddN(
			zsx.MakeLink(
				countClassAttrs[cat.Count],
				sz.ScanReference(buf.String()),
				Inlines: ast.InlineSlice{
					&ast.TextNode{Text: string(cat.Name)},
				sx.MakeList(zsx.MakeText(string(cat.Name)))),
				},
			},
			&ast.FormatNode{
				Kind:    ast.FormatSuper,
				Attrs:   nil,
				Inlines: ast.InlineSlice{&ast.TextNode{Text: strconv.Itoa(cat.Count)}},
			zsx.MakeFormat(zsx.SymFormatSuper,
				nil,
				sx.MakeList(zsx.MakeText(strconv.Itoa(cat.Count)))),
			},
		)
		count++
		buf.Truncate(bufLen)
	}
	return &ast.ParaNode{Inlines: para}, len(ccs)
	return zsx.MakeParaList(tags.List()), count
}

func (ap *actionPara) limitTags(ccs meta.CountedCategories) meta.CountedCategories {
	if minVal, maxVal := ap.minVal, ap.maxVal; minVal > 0 || maxVal > 0 {
		if minVal < 0 {
			minVal = ccs[len(ccs)-1].Count
		}
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
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







-
+














-
+
+










-
-
+
+
-
-
-
+
+
-
-
-
-
+
+
+
-
-
-
+
+
-
-
-
+
+
+

-
-
+
-
-
-


-
+



-
+
+






-
-
-
-
-
+
+
+
+
+
+

-
-
+
-
-
-







			}
			return temp
		}
	}
	return ccs
}

func (ap *actionPara) createBlockNodeMetaKeys() (ast.BlockNode, int) {
func (ap *actionPara) createBlockNodeMetaKeys() (*sx.Pair, int) {
	arr := make(meta.Arrangement, 128)
	for _, m := range ap.ml {
		for k := range m.Map() {
			arr[k] = append(arr[k], m)
		}
	}
	if len(arr) == 0 {
		return nil, 0
	}
	ccs := arr.Counted()
	ccs.SortByName()

	var buf bytes.Buffer
	bufLen := ap.prepareSimpleQuery(&buf)
	items := make([]ast.ItemSlice, 0, len(ccs))
	var items sx.ListBuilder
	count := 0
	for _, cat := range ccs {
		buf.WriteString(string(cat.Name))
		buf.WriteString(api.ExistOperator)
		q1 := buf.String()
		buf.Truncate(bufLen)
		buf.WriteString(api.ActionSeparator)
		buf.WriteString(string(cat.Name))
		q2 := buf.String()
		buf.Truncate(bufLen)

		items = append(items, ast.ItemSlice{ast.CreateParaNode(
			&ast.LinkNode{
		items.Add(zsx.MakeBlock(zsx.MakePara(
			zsx.MakeLink(nil,
				Attrs:   nil,
				Ref:     ast.ParseReference(q1),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: string(cat.Name)}},
				sz.ScanReference(q1),
				sx.MakeList(zsx.MakeText(string(cat.Name)))),
			},
			&ast.TextNode{Text: " "},
			&ast.TextNode{Text: "(" + strconv.Itoa(cat.Count) + ", "},
			&ast.LinkNode{
			zsx.MakeText(" "),
			zsx.MakeText("("+strconv.Itoa(cat.Count)+", "),
			zsx.MakeLink(nil,
				Attrs:   nil,
				Ref:     ast.ParseReference(q2),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: "values"}},
				sz.ScanReference(q2),
				sx.MakeList(zsx.MakeText("values"))),
			},
			&ast.TextNode{Text: ")"},
		)})
			zsx.MakeText(")"),
		)))
		count++
	}
	return &ast.NestedListNode{
		Kind:  ap.kind,
	return zsx.MakeList(ap.kind, nil, items.List()), count
		Items: items,
		Attrs: nil,
	}, len(items)
}

func (ap *actionPara) createBlockNodeMeta(key string) (ast.BlockNode, int) {
func (ap *actionPara) createBlockNodeMeta(key string) (*sx.Pair, int) {
	if len(ap.ml) == 0 {
		return nil, 0
	}
	items := make([]ast.ItemSlice, 0, len(ap.ml))
	var items sx.ListBuilder
	count := 0
	for _, m := range ap.ml {
		if key != "" {
			if _, found := m.Get(key); !found {
				continue
			}
		}
		items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{
			Attrs:   nil,
			Ref:     ast.ParseReference(m.Zid.String()),
			Inlines: ast.ParseSpacedText(m.GetTitle()),
		})})
		items.Add(zsx.MakeBlock(zsx.MakePara(
			zsx.MakeLink(nil,
				sz.ScanReference(m.Zid.String()),
				sx.MakeList(zsx.MakeText(sz.NormalizedSpacedText(m.GetTitle())))),
		)))
		count++
	}
	return &ast.NestedListNode{
		Kind:  ap.kind,
	return zsx.MakeList(ap.kind, nil, items.List()), count
		Items: items,
		Attrs: nil,
	}, len(items)
}

func (ap *actionPara) prepareCatAction(key string, buf *bytes.Buffer) (meta.CountedCategories, int) {
	if len(ap.ml) == 0 {
		return nil, 0
	}
	ccs := meta.CreateArrangement(ap.ml, key).Counted()
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
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







-
+

-
+








-
-
+
+
-

-
+













-
+








	return ccs, bufLen
}

func (ap *actionPara) prepareSimpleQuery(buf *bytes.Buffer) int {
	sea := ap.q.Clone()
	sea.RemoveActions()
	buf.WriteString(ast.QueryPrefix)
	buf.WriteString(api.QueryPrefix)
	sea.Print(buf)
	if buf.Len() > len(ast.QueryPrefix) {
	if buf.Len() > len(api.QueryPrefix) {
		buf.WriteByte(' ')
	}
	return buf.Len()
}

const fontSizes = 6 // Must be the number of CSS classes zs-font-size-* in base.css
const fontSizes64 = float64(fontSizes)

func (*actionPara) calcFontSizes(ccs meta.CountedCategories) map[int]zsx.Attributes {
	var fsAttrs [fontSizes]zsx.Attributes
func (*actionPara) calcFontSizes(ccs meta.CountedCategories) map[int]*sx.Pair {
	var fsAttrs [fontSizes]*sx.Pair
	var a zsx.Attributes
	for i := range fontSizes {
		fsAttrs[i] = a.AddClass("zs-font-size-" + strconv.Itoa(i))
		fsAttrs[i] = sx.MakeList(sx.Cons(sx.MakeString("class"), sx.MakeString("zs-font-size-"+strconv.Itoa(i))))
	}

	countMap := make(map[int]int, len(ccs))
	for _, cat := range ccs {
		countMap[cat.Count]++
	}

	countList := make([]int, 0, len(countMap))
	for count := range countMap {
		countList = append(countList, count)
	}
	slices.Sort(countList)

	result := make(map[int]zsx.Attributes, len(countList))
	result := make(map[int]*sx.Pair, len(countList))
	if len(countList) <= fontSizes {
		// If we have less different counts, center them inside the fsAttrs vector.
		curSize := (fontSizes - len(countList)) / 2
		for _, count := range countList {
			result[count] = fsAttrs[curSize]
			curSize++
		}
Changes to internal/evaluator/metadata.go.
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
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







+
+
+
+

-
-
+
+


-
-
+
+
+

-
-
-
-
-
-
+
-
-
+
-
-
-
+

+


-
-
+
+

-
+
-
-
-

-
+



+
-
-
-
-
+
+
+
+
+
+

-
+

-
-
+
-
-

-
+


+
-
+

// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package evaluator

import (
	"iter"

	"t73f.de/r/sx"
	zeroiter "t73f.de/r/zero/iter"
	"t73f.de/r/zsc/domain/meta"

	"zettelstore.de/z/internal/ast"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"
)

func evaluateMetadata(m *meta.Meta) ast.BlockSlice {
	descrlist := &ast.DescriptionListNode{}
func evaluateMetadata(m *meta.Meta) *sx.Pair {
	var lb sx.ListBuilder
	lb.AddN(zsx.SymDescription, nil)
	for key, val := range m.All() {
		descrlist.Descriptions = append(
			descrlist.Descriptions, getMetadataDescription(key, val))
	}
	return ast.BlockSlice{descrlist}
}

		lb.Add(sx.MakeList(zsx.MakeText(key)))
func getMetadataDescription(key string, value meta.Value) ast.Description {
	is := convertMetavalueToInlineSlice(value, meta.Type(key))
		inlines := convertMetavalueToInlineSlice(val, meta.Type(key))
	return ast.Description{
		Term:         ast.InlineSlice{&ast.TextNode{Text: key}},
		Descriptions: []ast.DescriptionSlice{{&ast.ParaNode{Inlines: is}}},
		lb.Add(zsx.MakeBlock(zsx.MakeBlock(zsx.MakeParaList(inlines))))
	}
	return zsx.MakeBlock(lb.List())
}

func convertMetavalueToInlineSlice(value meta.Value, dt *meta.DescriptionType) ast.InlineSlice {
	var sliceData []string
func convertMetavalueToInlineSlice(val meta.Value, dt *meta.DescriptionType) *sx.Pair {
	var vals iter.Seq[string]
	if dt.IsSet {
		sliceData = value.AsSlice()
		vals = val.Fields()
		if len(sliceData) == 0 {
			return nil
		}
	} else {
		sliceData = []string{string(value)}
		vals = zeroiter.OneSeq(string(val))
	}
	makeLink := dt == meta.TypeID || dt == meta.TypeIDSet

	var lb sx.ListBuilder
	result := make(ast.InlineSlice, 0, 2*len(sliceData)-1)
	for i, val := range sliceData {
		if i > 0 {
			result = append(result, &ast.TextNode{Text: " "})
	first := true
	for s := range vals {
		if first {
			first = false
		} else {
			lb.Add(zsx.MakeText(" "))
		}
		tn := &ast.TextNode{Text: val}
		tn := zsx.MakeText(s)
		if makeLink {
			result = append(result, &ast.LinkNode{
				Ref:     ast.ParseReference(val),
			lb.Add(zsx.MakeLink(nil, sz.ScanReference(s), sx.MakeList(tn)))
				Inlines: ast.InlineSlice{tn},
			})
		} else {
			result = append(result, tn)
			lb.Add(tn)
		}
	}

	return result
	return lb.List()
}
Added internal/evaluator/sxn.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) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore 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: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package evaluator

import (
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxbuiltins"
	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx"
)

func evaluateSxn(block *sx.Pair) *sx.Pair {
	if blocks := zsx.GetBlock(block); blocks != nil {
		blk := blocks.Head()
		if sym, isSymbol := sx.GetSymbol(blk.Car()); isSymbol && zsx.SymVerbatimCode.IsEqualSymbol(sym) {
			_, attrs, content := zsx.GetVerbatim(blk)
			if classAttr, hasClass := zsx.GetAttributes(attrs).Get(""); hasClass && classAttr == meta.ValueSyntaxSxn {
				rd := sxreader.MakeReader(strings.NewReader(content))
				if objs, err := rd.ReadAll(); err == nil {
					var lb sx.ListBuilder
					for _, obj := range objs {
						var sb strings.Builder
						_, _ = sxbuiltins.Print(&sb, obj)
						lb.Add(zsx.MakeVerbatim(zsx.SymVerbatimCode, attrs, sb.String()))
					}
					return zsx.MakeBlockList(lb.List())
				}
			}
		}
	}
	return block
}
Changes to internal/parser/blob.go.
53
54
55
56
57
58
59
60

61
62
63
64

65
53
54
55
56
57
58
59

60
61
62
63

64
65







-
+



-
+

		IsASTParser:   false,
		IsTextFormat:  false,
		IsImageFormat: true,
		Parse:         parseBlob,
	})
}

func parseBlob(inp *input.Input, m *meta.Meta, syntax string) *sx.Pair {
func parseBlob(inp *input.Input, m *meta.Meta, syntax string, _ *sx.Pair) *sx.Pair {
	if p := Get(syntax); p != nil {
		syntax = p.Name
	}
	return zsx.MakeBlock(zsx.MakeBLOB(nil, ParseDescription(m), syntax, string(inp.Src)))
	return zsx.MakeBlock(zsx.MakeBLOB(nil, syntax, inp.Src, ParseDescription(m)))
}
Changes to internal/parser/cleaner.go.
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
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







+

+

-



-
-
-
+
+
+
-
-
-
-
-
-
+
+
-
-
+
+



-
+
-
-
+
-
-
+
-


-
-
-
+
+
+
+
-
-
-
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
+
-
-
+
-
-
-
+
-
-
+
-
-
-
+
+
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
-
-
-
-
-
+
+
-
-
-
+
+
-
-
-
-
+
-
-
-
-
-
+



-
-
+
+




-
+



// cleaner provides functions to clean up the parsed AST.

import (
	"strconv"
	"strings"

	"t73f.de/r/sx"
	zerostrings "t73f.de/r/zero/strings"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/encoder"
)

// Clean cleans the given block list.
func Clean(bs *ast.BlockSlice, allowHTML bool) {
	cv := cleanVisitor{
// Clean the given SZ syntax tree.
func Clean(node *sx.Pair) {
	v1 := cleanPhase1{ids: idsNode{}}
		allowHTML: allowHTML,
		hasMark:   false,
		doMark:    false,
	}
	ast.Walk(&cv, bs)
	if cv.hasMark {
	zsx.WalkIt(&v1, node, nil)
	if v1.hasMark {
		cv.doMark = true
		ast.Walk(&cv, bs)
		v2 := cleanPhase2{ids: v1.ids}
		zsx.WalkIt(&v2, node, nil)
	}
}

type cleanVisitor struct {
type cleanPhase1 struct {
	textEnc   encoder.TextEncoder
	ids       map[string]ast.Node
	ids     idsNode
	allowHTML bool
	hasMark   bool
	hasMark bool // Mark nodes will be cleaned in phase 2 only
	doMark    bool
}

func (cv *cleanVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
func (v *cleanPhase1) VisitItBefore(node *sx.Pair, _ *sx.Pair) bool {
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		switch sym {
		case zsx.SymHeading:
		if !cv.allowHTML {
			cv.visitBlockSlice(n)
			return nil
			levelNode := node.Tail()
		}
	case *ast.InlineSlice:
		if !cv.allowHTML {
			attrsNode := levelNode.Tail()
			slugNode := attrsNode.Tail()
			fragmentNode := slugNode.Tail()
			cv.visitInlineSlice(n)
			return nil
		}
	case *ast.HeadingNode:
		cv.visitHeading(n)
		return nil
	case *ast.MarkNode:
		cv.visitMark(n)

			textNode := fragmentNode.Tail()
			var sb strings.Builder
			var textEnc encoder.TextEncoder
			if err := textEnc.WriteSz(&sb, textNode.Cons(zsx.SymPara)); err == nil {
				slugText := zerostrings.Slugify(sb.String())
				slugNode.SetCar(sx.MakeString(slugText))
				fragmentNode.SetCar(sx.MakeString(v.ids.addIdentifier(slugText, node)))
		return nil
	}
	return cv
}

			}
		case zsx.SymMark:
			v.hasMark = true
		}
	}
func (cv *cleanVisitor) visitBlockSlice(bs *ast.BlockSlice) {
	if bs == nil {
		return
	}
	return false
}
	if len(*bs) == 0 {
		*bs = nil
		return
	}
	for _, bn := range *bs {
		ast.Walk(cv, bn)
	}

func (v *cleanPhase1) VisitItAfter(*sx.Pair, *sx.Pair) {}
	fromPos, toPos := 0, 0
	for fromPos < len(*bs) {
		(*bs)[toPos] = (*bs)[fromPos]
		fromPos++
		switch bn := (*bs)[toPos].(type) {
		case *ast.VerbatimNode:
			if bn.Kind != ast.VerbatimHTML {
				toPos++
			}

		default:
			toPos++
		}
	}
	for pos := toPos; pos < len(*bs); pos++ {
		(*bs)[pos] = nil // Allow excess nodes to be garbage collected.
	}
	*bs = (*bs)[:toPos:toPos]
}

type cleanPhase2 struct {
func (cv *cleanVisitor) visitInlineSlice(is *ast.InlineSlice) {
	if is == nil {
		return
	}
	if len(*is) == 0 {
		*is = nil
	ids idsNode
		return
	}
}
	for _, bn := range *is {
		ast.Walk(cv, bn)
	}

}

func (v *cleanPhase2) VisitItBefore(node *sx.Pair, _ *sx.Pair) bool {
func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) {
	if cv.doMark || hn == nil || len(hn.Inlines) == 0 {
		return
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		switch sym {
	}
	if hn.Slug == "" {
		var sb strings.Builder
		_, err := cv.textEnc.WriteInlines(&sb, &hn.Inlines)
		case zsx.SymMark:
			stringNode := node.Tail()
			if markString, isString := sx.GetString(stringNode.Car()); isString {
				slugNode := stringNode.Tail()
				fragmentNode := slugNode.Tail()
		if err != nil {
			return
		}
		hn.Slug = zerostrings.Slugify(sb.String())

				slugText := zerostrings.Slugify(markString.GetValue())
	}
	if hn.Slug != "" {
		hn.Fragment = cv.addIdentifier(hn.Slug, hn)
	}
}

				slugNode.SetCar(sx.MakeString(slugText))
				fragmentNode.SetCar(sx.MakeString(v.ids.addIdentifier(slugText, node)))
			}
		}
	}
func (cv *cleanVisitor) visitMark(mn *ast.MarkNode) {
	if !cv.doMark {
		cv.hasMark = true
		return
	}
	return false
}
	if mn.Mark == "" {
		mn.Slug = ""
		mn.Fragment = cv.addIdentifier("*", mn)
		return
	}
func (v *cleanPhase2) VisitItAfter(*sx.Pair, *sx.Pair) {}

	if mn.Slug == "" {
		mn.Slug = zerostrings.Slugify(mn.Mark)
	}
type idsNode map[string]*sx.Pair

	mn.Fragment = cv.addIdentifier(mn.Slug, mn)
}

func (cv *cleanVisitor) addIdentifier(id string, node ast.Node) string {
func (ids idsNode) addIdentifier(id string, node *sx.Pair) string {
	if cv.ids == nil {
		cv.ids = map[string]ast.Node{id: node}
		return id
	}
	if n, ok := cv.ids[id]; ok && n != node {
	if n, ok := ids[id]; ok && n != node {
		prefix := id + "-"
		for count := 1; ; count++ {
			newID := prefix + strconv.Itoa(count)
			if n2, ok2 := cv.ids[newID]; !ok2 || n2 == node {
				cv.ids[newID] = node
			if n2, ok2 := ids[newID]; !ok2 || n2 == node {
				ids[newID] = node
				return newID
			}
		}
	}
	cv.ids[id] = node
	ids[id] = node
	return id
}
Added internal/parser/cleaner_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
72
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2025-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore 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: 2025-present Detlef Stern
//-----------------------------------------------------------------------------

package parser_test

import (
	"strings"
	"testing"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxreader"
	"zettelstore.de/z/internal/parser"
)

func TestCleaner(t *testing.T) {
	var testcases = []struct {
		name string
		src  string
		exp  string
	}{
		{name: "nil", src: "()", exp: "()"},

		{name: "simple heading",
			src: "(HEADING 1 () \"\" \"\" (TEXT \"Heading\"))",
			exp: "(HEADING 1 () \"heading\" \"heading\" (TEXT \"Heading\"))"},
		{name: "same simple heading",
			src: "(BLOCK (HEADING 1 () \"\" \"\" (TEXT \"Heading\")) (HEADING 1 () \"\" \"\" (TEXT \"Heading\")))",
			exp: "(BLOCK (HEADING 1 () \"heading\" \"heading\" (TEXT \"Heading\")) (HEADING 1 () \"heading\" \"heading-1\" (TEXT \"Heading\")))"},

		{name: "simple mark, no text",
			src: "(MARK \"m\" \"\" \"\")",
			exp: "(MARK \"m\" \"m\" \"m\")"},
		{name: "same simple mark, no text",
			src: "(PARA (MARK \"m\" \"\" \"\") (MARK \"m\" \"\" \"\"))",
			exp: "(PARA (MARK \"m\" \"m\" \"m\") (MARK \"m\" \"m\" \"m-1\"))"},
		{name: "mark before heading",
			src: "(BLOCK (HEADING 1 () \"\" \"\" (TEXT \"x\")) (PARA (MARK \"x\" \"\" \"\")))",
			exp: "(BLOCK (HEADING 1 () \"x\" \"x\" (TEXT \"x\")) (PARA (MARK \"x\" \"x\" \"x-1\")))"},
		{name: "mark in mark with text",
			src: `(MARK "m" "" "" (MARK "m" "" "" (TEXT "x")))`,
			exp: `(MARK "m" "m" "m" (MARK "m" "m" "m-1" (TEXT "x")))`},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			rd := sxreader.MakeReader(strings.NewReader(tc.src))
			obj, err := rd.Read()
			if err != nil {
				t.Error(err)
				return
			}
			node, isPair := sx.GetPair(obj)
			if !isPair {
				t.Error("not a pair:", obj)
			}
			parser.Clean(node)
			if got := node.String(); got != tc.exp {
				t.Errorf("\nexpected: %q\n but got: %q", tc.exp, got)
			}
		})
	}
}
Changes to internal/parser/draw.go.
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
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







-
-



















-
+


















-
+



-
+
+

-
+


-
-
+
+

-
+

-
+









-
+

-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+







	"strconv"

	"t73f.de/r/sx"
	"t73f.de/r/webs/aasvg"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/ast"
)

func init() {
	register(&Info{
		Name:          meta.ValueSyntaxDraw,
		AltNames:      []string{},
		IsASTParser:   true,
		IsTextFormat:  true,
		IsImageFormat: false,
		Parse:         parseDraw,
	})
}

const (
	defaultFont   = ""
	defaultScaleX = 10
	defaultScaleY = 20
)

func parseDraw(inp *input.Input, m *meta.Meta, _ string) *sx.Pair {
func parseDraw(inp *input.Input, m *meta.Meta, _ string, _ *sx.Pair) *sx.Pair {
	font := m.GetDefault("font", defaultFont)
	scaleX := m.GetNumber("x-scale", defaultScaleX)
	scaleY := m.GetNumber("y-scale", defaultScaleY)
	if scaleX < 1 || 1000000 < scaleX {
		scaleX = defaultScaleX
	}
	if scaleY < 1 || 1000000 < scaleY {
		scaleY = defaultScaleY
	}

	canvas, err := aasvg.NewCanvas(inp.Src[inp.Pos:])
	if err != nil {
		return zsx.MakeBlock(zsx.MakeParaList(canvasErrMsg(err)))
	}
	svg := aasvg.CanvasToSVG(canvas, string(font), int(scaleX), int(scaleY))
	if len(svg) == 0 {
		return zsx.MakeBlock(zsx.MakeParaList(noSVGErrMsg()))
	}
	return zsx.MakeBlock(zsx.MakeBLOB(nil, ParseDescription(m), meta.ValueSyntaxSVG, string(svg)))
	return zsx.MakeBlock(zsx.MakeBLOB(nil, meta.ValueSyntaxSVG, svg, ParseDescription(m)))
}

// ParseDrawBlock parses the content of an eval verbatim node into an SVG image BLOB.
func ParseDrawBlock(vn *ast.VerbatimNode) ast.BlockNode {
func ParseDrawBlock(attrs *sx.Pair, content []byte) *sx.Pair {
	a := zsx.GetAttributes(attrs)
	font := defaultFont
	if val, found := vn.Attrs.Get("font"); found {
	if val, found := a.Get("font"); found {
		font = val
	}
	scaleX := getScale(vn.Attrs, "x-scale", defaultScaleX)
	scaleY := getScale(vn.Attrs, "y-scale", defaultScaleY)
	scaleX := getScaleAST(a, "x-scale", defaultScaleX)
	scaleY := getScaleAST(a, "y-scale", defaultScaleY)

	canvas, err := aasvg.NewCanvas(vn.Content)
	canvas, err := aasvg.NewCanvas(content)
	if err != nil {
		return ast.CreateParaNode(ast.InlineSlice{&ast.TextNode{Text: "Error: " + err.Error()}}...)
		return zsx.MakePara(zsx.MakeText("Error: " + err.Error()))
	}
	if scaleX < 1 || 1000000 < scaleX {
		scaleX = defaultScaleX
	}
	if scaleY < 1 || 1000000 < scaleY {
		scaleY = defaultScaleY
	}
	svg := aasvg.CanvasToSVG(canvas, font, scaleX, scaleY)
	if len(svg) == 0 {
		return ast.CreateParaNode(ast.InlineSlice{&ast.TextNode{Text: "NO IMAGE"}}...)
		return zsx.MakePara(zsx.MakeText("NO IMAGE"))
	}
	return &ast.BLOBNode{
		Description: nil, // TODO: look for attribute "summary" / "title"
		Syntax:      meta.ValueSyntaxSVG,
		Blob:        svg,
	}
}
	return zsx.MakeBLOB(
		nil,
		meta.ValueSyntaxSVG,
		svg,
		nil, // TODO: look for attribute "summary" / "title" for a description.
	)
}


func getScale(a zsx.Attributes, key string, defVal int) int {
func getScaleAST(a zsx.Attributes, key string, defVal int) int {
	if val, found := a.Get(key); found {
		if n, err := strconv.Atoi(val); err == nil && 0 < n && n < 100000 {
			return n
		}
	}
	return defVal
}
Changes to internal/parser/draw_test.go.
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

31
32
15
16
17
18
19
20
21

22
23
24
25
26
27
28

29
30
31







-







-
+



import (
	"testing"

	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/parser"
)

func FuzzParseDraw(f *testing.F) {
	f.Fuzz(func(t *testing.T, src []byte) {
		t.Parallel()
		inp := input.NewInput(src)
		parser.Parse(inp, nil, meta.ValueSyntaxDraw, config.NoHTML)
		parser.Parse(inp, nil, meta.ValueSyntaxDraw, nil)
	})
}
Changes to internal/parser/markdown.go.
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
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







-




-
-













-
+



-
-
+




-
-
-
+
+
+







	"strings"

	gm "github.com/yuin/goldmark"
	gmAst "github.com/yuin/goldmark/ast"
	gmText "github.com/yuin/goldmark/text"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/encoder"
)

func init() {
	register(&Info{
		Name:          meta.ValueSyntaxMarkdown,
		AltNames:      []string{meta.ValueSyntaxMD},
		IsASTParser:   true,
		IsTextFormat:  true,
		IsImageFormat: false,
		Parse:         parseMarkdown,
	})
}

func parseMarkdown(inp *input.Input, _ *meta.Meta, _ string) *sx.Pair {
func parseMarkdown(inp *input.Input, _ *meta.Meta, _ string, alst *sx.Pair) *sx.Pair {
	source := []byte(inp.Src[inp.Pos:])
	parser := gm.DefaultParser()
	node := parser.Parse(gmText.NewReader(source))
	textEnc := encoder.Create(api.EncoderText, nil)
	p := mdP{source: source, docNode: node, textEnc: textEnc}
	p := mdP{source: source, docNode: node, allowHTML: alst.Assoc(SymAllowHTML) != nil}
	return p.acceptBlockChildren(p.docNode)
}

type mdP struct {
	source  []byte
	docNode gmAst.Node
	textEnc encoder.Encoder
	source    []byte
	docNode   gmAst.Node
	allowHTML bool
}

func (p *mdP) acceptBlockChildren(docNode gmAst.Node) *sx.Pair {
	if docNode.Type() != gmAst.TypeDocument {
		panic(fmt.Sprintf("Expected document, but got node type %v", docNode.Type()))
	}
	var result sx.ListBuilder
175
176
177
178
179
180
181
182

183
184
185
186
187
188
189
171
172
173
174
175
176
177

178
179
180
181
182
183
184
185







-
+







	return zsx.MakeList(kind, a.List(), items.List())
}

func (p *mdP) acceptItemSlice(node gmAst.Node) *sx.Pair {
	var result sx.ListBuilder
	for elem := node.FirstChild(); elem != nil; elem = elem.NextSibling() {
		if item := p.acceptBlock(elem); item != nil {
			result.Add(item)
			result.Add(zsx.MakeBlock(item))
		}
	}
	return result.List()
}

func (p *mdP) acceptTextBlock(node *gmAst.TextBlock) *sx.Pair {
	if is := p.acceptInlineChildren(node); is != nil {
200
201
202
203
204
205
206

207



208
209
210
211
212
213
214
196
197
198
199
200
201
202
203

204
205
206
207
208
209
210
211
212
213







+
-
+
+
+







			closure = closure[:l-1]
		}
		if len(content) > 1 {
			content = append(content, '\n')
		}
		content = append(content, closure...)
	}
	if p.allowHTML {
	return zsx.MakeVerbatim(zsx.SymVerbatimHTML, nil, string(content))
		return zsx.MakeVerbatim(zsx.SymVerbatimHTML, nil, string(content))
	}
	return zsx.MakeVerbatim(zsx.SymVerbatimCode, makeAttrHTML(), string(content))
}

func (p *mdP) acceptInlineChildren(node gmAst.Node) *sx.Pair {
	var result sx.ListBuilder
	for child := node.FirstChild(); child != nil; child = child.NextSibling() {
		n1, n2 := p.acceptInline(child)
		if n1 != nil {
365
366
367
368
369
370
371
372
373
374





375
376
377
364
365
366
367
368
369
370



371
372
373
374
375


376







-
-
-
+
+
+
+
+
-
-


func (p *mdP) acceptRawHTML(node *gmAst.RawHTML) (*sx.Pair, *sx.Pair) {
	segs := make([][]byte, 0, node.Segments.Len())
	for i := range node.Segments.Len() {
		segment := node.Segments.At(i)
		segs = append(segs, segment.Value(p.source))
	}
	return zsx.MakeLiteral(
		zsx.SymLiteralCode,
		sx.Cons(sx.Cons(sx.MakeString(""), sx.MakeString("html")), sx.Nil()),
	return zsx.MakeLiteral(zsx.SymLiteralCode, makeAttrHTML(), string(bytes.Join(segs, nil))), nil
}

func makeAttrHTML() *sx.Pair {
	return sx.Cons(sx.Cons(sx.MakeString(""), sx.MakeString("html")), sx.Nil())
		string(bytes.Join(segs, nil)),
	), nil
}
Changes to internal/parser/none.go.
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26
27
28
29
30
31



32
33
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







+












-
+
+
+


//-----------------------------------------------------------------------------

package parser

import (
	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx/input"
)

// none provides a none-parser, e.g. for zettel with just metadata.

func init() {
	register(&Info{
		Name:          meta.ValueSyntaxNone,
		AltNames:      []string{},
		IsASTParser:   false,
		IsTextFormat:  false,
		IsImageFormat: false,
		Parse:         func(*input.Input, *meta.Meta, string) *sx.Pair { return nil },
		Parse: func(inp *input.Input, _ *meta.Meta, _ string, _ *sx.Pair) *sx.Pair {
			return sz.ParseNoneBlocks(inp)
		},
	})
}
Changes to internal/parser/parser.go.
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
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







-



+




-














+
+
-
+








// Package parser provides a generic interface to a range of different parsers.
package parser

import (
	"context"
	"fmt"
	"log"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/ast/sztrans"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/zettel"
)

// Info describes a single parser.
//
// Before Parse() is called, ensure the input stream to be valid. This can be
// achieved on calling inp.Next() after the input stream was created.
type Info struct {
	Name          string
	AltNames      []string
	IsASTParser   bool
	IsTextFormat  bool
	IsImageFormat bool

	// Parse the input, with the given metadata, the given syntax, and the given config.
	Parse         func(*input.Input, *meta.Meta, string) *sx.Pair
	Parse func(*input.Input, *meta.Meta, string, *sx.Pair) *sx.Pair
}

var registry = map[string]*Info{}

// register the parser (info) for later retrieval.
func register(pi *Info) {
	if _, ok := registry[pi.Name]; ok {
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
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







-
-
-
+
+
+
-
-
-
-
-
+
-
-
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+








-
+


-
+





-
+








+
+
+
+
+





-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+


	pi, ok := registry[syntax]
	if !ok {
		return false
	}
	return pi.IsImageFormat
}

// Parse parses some input and returns a slice of block nodes.
func Parse(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice {
	if obj := Get(syntax).Parse(inp, m, syntax); obj != nil {
// Parse parses some input and returns both a Sx.Object and a slice of block nodes.
func Parse(inp *input.Input, m *meta.Meta, syntax string, alst *sx.Pair) *sx.Pair {
	return Get(syntax).Parse(inp, m, syntax, alst)
		bs, err := sztrans.GetBlockSlice(obj)
		if err == nil {
			Clean(&bs, hi.AllowHTML(syntax))
			return bs
		}
}
		log.Printf("sztrans error: %v, for %v\n", err, obj)
	}

	return nil
}

// SymAllowHTML signals a parser to allow HTML content during parsing.
// ParseDescriptionAST returns a suitable description stored in the metadata as an inline slice.
// This is done for an image in most cases.
func ParseDescriptionAST(m *meta.Meta) ast.InlineSlice {
	if m == nil {
		return nil
	}
	if summary, found := m.Get(meta.KeySummary); found {
		return ast.ParseSpacedText(string(summary))
	}
	if title, found := m.Get(meta.KeyTitle); found {
		return ast.ParseSpacedText(string(title))
	}
	return ast.InlineSlice{&ast.TextNode{Text: "Zettel without title/summary: " + m.Zid.String()}}
}
var SymAllowHTML = sx.MakeSymbol("ALLOW-HTML")

// ParseDescription returns a suitable description stored in the metadata as an inline list.
// This is done for an image in most cases.
func ParseDescription(m *meta.Meta) *sx.Pair {
	if m == nil {
		return nil
	}
	if summary, found := m.Get(meta.KeySummary); found {
		return sx.Cons(zsx.MakeText(ast.NormalizedSpacedText(string(summary))), sx.Nil())
		return sx.Cons(zsx.MakeText(sz.NormalizedSpacedText(string(summary))), sx.Nil())
	}
	if title, found := m.Get(meta.KeyTitle); found {
		return sx.Cons(zsx.MakeText(ast.NormalizedSpacedText(string(title))), sx.Nil())
		return sx.Cons(zsx.MakeText(sz.NormalizedSpacedText(string(title))), sx.Nil())
	}
	return sx.Cons(zsx.MakeText("Zettel without title/summary: "+m.Zid.String()), sx.Nil())
}

// ParseZettel parses the zettel based on the syntax.
func ParseZettel(ctx context.Context, zettel zettel.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode {
func ParseZettel(ctx context.Context, zettel zettel.Zettel, syntax string, rtConfig config.Config) *ast.Zettel {
	m := zettel.Meta
	inhMeta := m
	if rtConfig != nil {
		inhMeta = rtConfig.AddDefaultValues(ctx, inhMeta)
	}
	if syntax == "" {
		syntax = string(inhMeta.GetDefault(meta.KeySyntax, meta.DefaultSyntax))
	}
	var alst *sx.Pair
	if rtConfig != nil && rtConfig.GetHTMLInsecurity().AllowHTML(syntax) {
		alst = alst.Cons(sx.Cons(SymAllowHTML, nil))
	}

	parseMeta := inhMeta
	if syntax == meta.ValueSyntaxNone {
		parseMeta = m
	}

	hi := config.NoHTML
	if rtConfig != nil {
		hi = rtConfig.GetHTMLInsecurity()
	}
	bs := Parse(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax, hi)
	return &ast.ZettelNode{
		Meta:      m,
		Content:   zettel.Content,
		Zid:       m.Zid,
		InhMeta:   inhMeta,
		BlocksAST: bs,
		Syntax:    syntax,
	rootNode := Parse(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax, alst)
	return &ast.Zettel{
		Meta:    m,
		Content: zettel.Content,
		Zid:     m.Zid,
		InhMeta: inhMeta,
		Blocks:  rootNode,
		Syntax:  syntax,
	}
}
Changes to internal/parser/plain.go.
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
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







+



















-
+








import (
	"bytes"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"
	"t73f.de/r/zsx/input"
)

func init() {
	register(&Info{
		Name:          meta.ValueSyntaxTxt,
		AltNames:      []string{meta.ValueSyntaxPlain, meta.ValueSyntaxText},
		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: false,
		Parse:         parsePlain,
	})
	register(&Info{
		Name:          meta.ValueSyntaxHTML,
		AltNames:      []string{},
		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: false,
		Parse:         parsePlainHTML,
		Parse:         parsePlain,
	})
	register(&Info{
		Name:          meta.ValueSyntaxCSS,
		AltNames:      []string{},
		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: false,
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
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







-
-
-
+
+
+
+
+
-
-
+

-
-
-
+
+
+
+
+
+
+
+
+
-
-
-

+

-
+


-
+









-
+
















-
+







		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: false,
		Parse:         parsePlainSxn,
	})
}

func parsePlain(inp *input.Input, _ *meta.Meta, syntax string) *sx.Pair {
	return doParsePlain(inp, syntax, zsx.SymVerbatimCode)
}
func parsePlain(inp *input.Input, _ *meta.Meta, syntax string, alst *sx.Pair) *sx.Pair {
	result := sz.ParsePlainBlocks(inp, syntax)
	if syntax == meta.ValueSyntaxHTML && alst.Assoc(SymAllowHTML) == nil {
		zsx.WalkIt(removeHTMLVisitor{}, result, nil)
	}
func parsePlainHTML(inp *input.Input, _ *meta.Meta, syntax string) *sx.Pair {
	return doParsePlain(inp, syntax, zsx.SymVerbatimHTML)
	return result
}
func doParsePlain(inp *input.Input, syntax string, kind *sx.Symbol) *sx.Pair {
	return zsx.MakeBlock(zsx.MakeVerbatim(
		kind,

type removeHTMLVisitor struct{}

func (removeHTMLVisitor) VisitItBefore(node *sx.Pair, _ *sx.Pair) bool {
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol && zsx.SymVerbatimHTML.IsEqualSymbol(sym) {
		node.SetCar(zsx.SymVerbatimCode)
		return true
	}
	return false
		sx.Cons(sx.Cons(sx.MakeString(""), sx.MakeString(syntax)), sx.Nil()),
		string(inp.ScanLineContent()),
	))
}
func (removeHTMLVisitor) VisitItAfter(*sx.Pair, *sx.Pair) {}

func parsePlainSVG(inp *input.Input, _ *meta.Meta, syntax string) *sx.Pair {
func parsePlainSVG(inp *input.Input, _ *meta.Meta, syntax string, _ *sx.Pair) *sx.Pair {
	is := parseSVGInlines(inp, syntax)
	if is == nil {
		return nil
		return zsx.MakeBlock()
	}
	return zsx.MakeBlock(zsx.MakeParaList(is))
}

func parseSVGInlines(inp *input.Input, syntax string) *sx.Pair {
	svgSrc := scanSVG(inp)
	if svgSrc == "" {
		return nil
	}
	return sx.Cons(zsx.MakeEmbedBLOB(nil, syntax, svgSrc, nil), sx.Nil())
	return sx.Cons(zsx.MakeEmbedBLOBuncode(nil, syntax, svgSrc, nil), sx.Nil())
}

func scanSVG(inp *input.Input) string {
	inp.SkipSpace()
	pos := inp.Pos
	if !inp.Accept("<svg") {
		return ""
	}
	ch := inp.Ch
	if input.IsSpace(ch) || input.IsEOLEOS(ch) || ch == '>' {
		// TODO: check proper end </svg>
		return string(inp.Src[pos:])
	}
	return ""
}

func parsePlainSxn(inp *input.Input, _ *meta.Meta, syntax string) *sx.Pair {
func parsePlainSxn(inp *input.Input, _ *meta.Meta, syntax string, _ *sx.Pair) *sx.Pair {
	rd := sxreader.MakeReader(bytes.NewReader(inp.Src))
	_, err := rd.ReadAll()

	var blocks sx.ListBuilder
	blocks.Add(zsx.MakeVerbatim(
		zsx.SymVerbatimCode,
		sx.Cons(sx.Cons(sx.MakeString(""), sx.MakeString(syntax)), sx.Nil()),
Changes to internal/parser/plain_test.go.
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
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







+



-
-



-
+

-
-
-
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+




+
+
+
+
-
+
-
-
-
+





//-----------------------------------------------------------------------------

package parser_test

import (
	"testing"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/encoder"
	"zettelstore.de/z/internal/parser"
)

func TestParseSVG(t *testing.T) {
func TestParsePlain(t *testing.T) {
	testCases := []struct {
		name string
		src  string
		exp  string
		name      string
		syntax    string
		src       string
		allowHTML bool
		exp       string
	}{
		{name: "empty-default", syntax: "",
			src: "",
			exp: "(BLOCK (VERBATIM-CODE ((\"\" . \"\")) \"\"))"},
		{name: "empty-html", syntax: meta.ValueSyntaxHTML,
			src: "",
			exp: "(BLOCK (VERBATIM-CODE ((\"\" . \"html\")) \"\"))"},
		{name: "empty-html-allow", syntax: meta.ValueSyntaxHTML, allowHTML: true,
			src: "",
			exp: "(BLOCK (VERBATIM-HTML ((\"\" . \"html\")) \"\"))"},
		{name: "empty-sxn", syntax: meta.ValueSyntaxSxn,
			src: "",
			exp: "(BLOCK (VERBATIM-CODE ((\"\" . \"sxn\")) \"\"))"},
		{name: "valid-sxn", syntax: meta.ValueSyntaxSxn,
			src: "(+ 3 4)",
			exp: "(BLOCK (VERBATIM-CODE ((\"\" . \"sxn\")) \"(+ 3 4)\"))"},
		{name: "invalid-sxn", syntax: meta.ValueSyntaxSxn,
			src: "(+ 3 4",
			exp: "(BLOCK (VERBATIM-CODE ((\"\" . \"sxn\")) \"(+ 3 4\") (PARA (TEXT \"ReaderError 1-6: unexpected EOF\")))"},

		{name: "svg-common", syntax: meta.ValueSyntaxSVG,
			src: " <svg bla",
		{"common", " <svg bla", "(BLOCK (PARA (EMBED-BLOB () \"svg\" \"<svg bla\")))"},
		{"inkscape", "<svg\nbla", "(BLOCK (PARA (EMBED-BLOB () \"svg\" \"<svg\\nbla\")))"},
		{"selfmade", "<svg>", "(BLOCK (PARA (EMBED-BLOB () \"svg\" \"<svg>\")))"},
		{"error", "<svgbla", "(BLOCK)"},
		{"error-", "<svg-bla", "(BLOCK)"},
		{"error#", "<svg2bla", "(BLOCK)"},
			exp: "(BLOCK (PARA (EMBED-BLOB () \"svg\" \"<svg bla\")))"},
		{name: "svg-inkscape", syntax: meta.ValueSyntaxSVG,
			src: "<svg\nbla",
			exp: "(BLOCK (PARA (EMBED-BLOB () \"svg\" \"<svg\\nbla\")))"},
		{name: "svg-selfmade", syntax: meta.ValueSyntaxSVG,
			src: "<svg>",
			exp: "(BLOCK (PARA (EMBED-BLOB () \"svg\" \"<svg>\")))"},
		{name: "svg-error", syntax: meta.ValueSyntaxSVG,
			src: "<svgbla",
			exp: "(BLOCK)"},
		{name: "svg-error-", syntax: meta.ValueSyntaxSVG,
			src: "<svg-bla",
			exp: "(BLOCK)"},
		{name: "svg-error#", syntax: meta.ValueSyntaxSVG,
			src: "<svg2bla",
			exp: "(BLOCK)"},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			inp := input.NewInput([]byte(tc.src))
			alst := sx.Nil()
			if tc.allowHTML {
				alst = alst.Cons(sx.Cons(parser.SymAllowHTML, nil))
			}
			bs := parser.Parse(inp, nil, meta.ValueSyntaxSVG, config.NoHTML)
			node := parser.Parse(inp, nil, tc.syntax, alst)
			trans := encoder.NewSzTransformer()
			lst := trans.GetSz(&bs)
			if got := lst.String(); tc.exp != got {
			if got := node.String(); tc.exp != got {
				t.Errorf("\nexp: %q\ngot: %q", tc.exp, got)
			}
		})
	}
}
Changes to internal/parser/zettelmark.go.
25
26
27
28
29
30
31
32
33
34
35




36
37
38
25
26
27
28
29
30
31




32
33
34
35
36
37
38







-
-
-
-
+
+
+
+



func init() {
	register(&Info{
		Name:          meta.ValueSyntaxZmk,
		AltNames:      nil,
		IsASTParser:   true,
		IsTextFormat:  true,
		IsImageFormat: false,
		Parse: func(inp *input.Input, _ *meta.Meta, _ string) *sx.Pair {
			var parser zmk.Parser
			parser.Initialize(inp)
			return parser.Parse()
		Parse: func(inp *input.Input, _ *meta.Meta, _ string, _ *sx.Pair) *sx.Pair {
			var zmkParser zmk.Parser
			zmkParser.Initialize(inp) // TODO: add alst
			return zmkParser.Parse()
		},
	})
}
Deleted internal/parser/zettelmark_fuzz_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
































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore 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: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package parser_test

import (
	"testing"

	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/parser"
)

func FuzzParseZmk(f *testing.F) {
	f.Fuzz(func(t *testing.T, src []byte) {
		t.Parallel()
		inp := input.NewInput(src)
		parser.Parse(inp, nil, meta.ValueSyntaxZmk, config.NoHTML)
	})
}
Deleted internal/parser/zettelmark_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
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
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042


















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore 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: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package parser_test

import (
	"fmt"
	"strings"
	"testing"

	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/parser"
)

type TestCase struct{ source, want string }
type TestCases []TestCase

func replace(s string, tcs TestCases) TestCases {
	var testCases TestCases

	for _, tc := range tcs {
		source := strings.ReplaceAll(tc.source, "$", s)
		want := strings.ReplaceAll(tc.want, "$", s)
		testCases = append(testCases, TestCase{source, want})
	}
	return testCases
}

func checkTcs(t *testing.T, tcs TestCases) {
	t.Helper()

	for tcn, tc := range tcs {
		t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) {
			st.Helper()
			inp := input.NewInput([]byte(tc.source))
			bns := parser.Parse(inp, nil, meta.ValueSyntaxZmk, config.NoHTML)
			var tv TestVisitor
			ast.Walk(&tv, &bns)
			got := tv.String()
			if tc.want != got {
				st.Errorf("\nwant=%q\n got=%q", tc.want, got)
			}
		})
	}
}

func TestEOL(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"", ""},
		{"\n", ""},
		{"\r", ""},
		{"\r\n", ""},
		{"\n\n", ""},
	})
}

func TestText(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"abcd", "(PARA abcd)"},
		{"ab cd", "(PARA ab cd)"},
		{"abcd ", "(PARA abcd)"},
		{" abcd", "(PARA abcd)"},
		{"\\", "(PARA \\)"},
		{"\\\n", ""},
		{"\\\ndef", "(PARA HB def)"},
		{"\\\r", ""},
		{"\\\rdef", "(PARA HB def)"},
		{"\\\r\n", ""},
		{"\\\r\ndef", "(PARA HB def)"},
		{"\\a", "(PARA a)"},
		{"\\aa", "(PARA aa)"},
		{"a\\a", "(PARA aa)"},
		{"\\+", "(PARA +)"},
		{"\\ ", "(PARA \u00a0)"},
		{"http://a, http://b", "(PARA http://a, http://b)"},
	})
}

func TestSpace(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{" ", ""},
		{"\t", ""},
		{"  ", ""},
	})
}

func TestSoftBreak(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"x\ny", "(PARA x SB y)"},
		{"z\n", "(PARA z)"},
		{" \n ", ""},
		{" \n", ""},
	})
}

func TestHardBreak(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"x  \ny", "(PARA x HB y)"},
		{"z  \n", "(PARA z)"},
		{"   \n ", ""},
		{"   \n", ""},
	})
}

func TestLink(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[", "(PARA [)"},
		{"[[", "(PARA [[)"},
		{"[[|", "(PARA [[|)"},
		{"[[]", "(PARA [[])"},
		{"[[|]", "(PARA [[|])"},
		{"[[]]", "(PARA [[]])"},
		{"[[|]]", "(PARA [[|]])"},
		{"[[ ]]", "(PARA [[ ]])"},
		{"[[\n]]", "(PARA [[ SB ]])"},
		{"[[ a]]", "(PARA (LINK a))"},
		{"[[a ]]", "(PARA [[a ]])"},
		{"[[a\n]]", "(PARA [[a SB ]])"},
		{"[[a]]", "(PARA (LINK a))"},
		{"[[12345678901234]]", "(PARA (LINK 12345678901234))"},
		{"[[a]", "(PARA [[a])"},
		{"[[|a]]", "(PARA [[|a]])"},
		{"[[b|]]", "(PARA [[b|]])"},
		{"[[b|a]]", "(PARA (LINK a b))"},
		{"[[b| a]]", "(PARA (LINK a b))"},
		{"[[b%c|a]]", "(PARA (LINK a b%c))"},
		{"[[b%%c|a]]", "(PARA [[b {% c|a]]})"},
		{"[[b|a]", "(PARA [[b|a])"},
		{"[[b\nc|a]]", "(PARA (LINK a b SB c))"},
		{"[[b c|a#n]]", "(PARA (LINK a#n b c))"},
		{"[[a]]go", "(PARA (LINK a) go)"},
		{"[[b|a]]{go}", "(PARA (LINK a b)[ATTR go])"},
		{"[[[[a]]|b]]", "(PARA [[ (LINK a) |b]])"},
		{"[[a[b]c|d]]", "(PARA (LINK d a[b]c))"},
		{"[[[b]c|d]]", "(PARA [ (LINK d b]c))"},
		{"[[a[]c|d]]", "(PARA (LINK d a[]c))"},
		{"[[a[b]|d]]", "(PARA (LINK d a[b]))"},
		{"[[\\|]]", "(PARA (LINK %5C%7C))"},
		{"[[\\||a]]", "(PARA (LINK a |))"},
		{"[[b\\||a]]", "(PARA (LINK a b|))"},
		{"[[b\\|c|a]]", "(PARA (LINK a b|c))"},
		{"[[\\]]]", "(PARA (LINK %5C%5D))"},
		{"[[\\]|a]]", "(PARA (LINK a ]))"},
		{"[[b\\]|a]]", "(PARA (LINK a b]))"},
		{"[[\\]\\||a]]", "(PARA (LINK a ]|))"},
		{"[[http://a]]", "(PARA (LINK http://a))"},
		{"[[http://a|http://a]]", "(PARA (LINK http://a http://a))"},
		{"[[[[a]]]]", "(PARA [[ (LINK a) ]])"},
		{"[[query:title]]", "(PARA (LINK query:title))"},
		{"[[query:title syntax]]", "(PARA (LINK query:title syntax))"},
		{"[[query:title | action]]", "(PARA (LINK query:title | action))"},
		{"[[Text|query:title]]", "(PARA (LINK query:title Text))"},
		{"[[Text|query:title syntax]]", "(PARA (LINK query:title syntax Text))"},
		{"[[Text|query:title | action]]", "(PARA (LINK query:title | action Text))"},
	})
}

func TestCite(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[@", "(PARA [@)"},
		{"[@]", "(PARA [@])"},
		{"[@a]", "(PARA (CITE a))"},
		{"[@ a]", "(PARA [@ a])"},
		{"[@a ]", "(PARA (CITE a))"},
		{"[@a\n]", "(PARA (CITE a))"},
		{"[@a\nx]", "(PARA (CITE a SB x))"},
		{"[@a\n\n]", "(PARA [@a)(PARA ])"},
		{"[@a,\n]", "(PARA (CITE a))"},
		{"[@a,n]", "(PARA (CITE a n))"},
		{"[@a| n]", "(PARA (CITE a n))"},
		{"[@a|n ]", "(PARA (CITE a n))"},
		{"[@a,[@b]]", "(PARA (CITE a (CITE b)))"},
		{"[@a]{color=green}", "(PARA (CITE a)[ATTR color=green])"},
	})
}

func TestFootnote(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[^", "(PARA [^)"},
		{"[^]", "(PARA (FN))"},
		{"[^abc]", "(PARA (FN abc))"},
		{"[^abc ]", "(PARA (FN abc))"},
		{"[^abc\ndef]", "(PARA (FN abc SB def))"},
		{"[^abc\n\ndef]", "(PARA [^abc)(PARA def])"},
		{"[^abc[^def]]", "(PARA (FN abc (FN def)))"},
		{"[^abc]{-}", "(PARA (FN abc)[ATTR -])"},
	})
}

func TestEmbed(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"{", "(PARA {)"},
		{"{{", "(PARA {{)"},
		{"{{|", "(PARA {{|)"},
		{"{{}", "(PARA {{})"},
		{"{{|}", "(PARA {{|})"},
		{"{{}}", "(PARA {{}})"},
		{"{{|}}", "(PARA {{|}})"},
		{"{{ }}", "(PARA {{ }})"},
		{"{{\n}}", "(PARA {{ SB }})"},
		{"{{a }}", "(PARA {{a }})"},
		{"{{a\n}}", "(PARA {{a SB }})"},
		{"{{a}}", "(PARA (EMBED a))"},
		{"{{12345678901234}}", "(PARA (EMBED 12345678901234))"},
		{"{{ a}}", "(PARA (EMBED a))"},
		{"{{a}", "(PARA {{a})"},
		{"{{|a}}", "(PARA {{|a}})"},
		{"{{b|}}", "(PARA {{b|}})"},
		{"{{b|a}}", "(PARA (EMBED a b))"},
		{"{{b| a}}", "(PARA (EMBED a b))"},
		{"{{b|a}", "(PARA {{b|a})"},
		{"{{b\nc|a}}", "(PARA (EMBED a b SB c))"},
		{"{{b c|a#n}}", "(PARA (EMBED a#n b c))"},
		{"{{a}}{go}", "(PARA (EMBED a)[ATTR go])"},
		{"{{{{a}}|b}}", "(PARA {{ (EMBED a) |b}})"},
		{"{{\\|}}", "(PARA (EMBED %5C%7C))"},
		{"{{\\||a}}", "(PARA (EMBED a |))"},
		{"{{b\\||a}}", "(PARA (EMBED a b|))"},
		{"{{b\\|c|a}}", "(PARA (EMBED a b|c))"},
		{"{{\\}}}", "(PARA (EMBED %5C%7D))"},
		{"{{\\}|a}}", "(PARA (EMBED a }))"},
		{"{{b\\}|a}}", "(PARA (EMBED a b}))"},
		{"{{\\}\\||a}}", "(PARA (EMBED a }|))"},
		{"{{http://a}}", "(PARA (EMBED http://a))"},
		{"{{http://a|http://a}}", "(PARA (EMBED http://a http://a))"},
		{"{{{{a}}}}", "(PARA {{ (EMBED a) }})"},
	})
}

func TestMark(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[!", "(PARA [!)"},
		{"[!\n", "(PARA [!)"},
		{"[!]", "(PARA (MARK #*))"},
		{"[!][!]", "(PARA (MARK #*) (MARK #*-1))"},
		{"[! ]", "(PARA [! ])"},
		{"[!a]", "(PARA (MARK \"a\" #a))"},
		{"[!a][!a]", "(PARA (MARK \"a\" #a) (MARK \"a\" #a-1))"},
		{"[!a ]", "(PARA [!a ])"},
		{"[!a_]", "(PARA (MARK \"a_\" #a))"},
		{"[!a_][!a]", "(PARA (MARK \"a_\" #a) (MARK \"a\" #a-1))"},
		{"[!a-b]", "(PARA (MARK \"a-b\" #a-b))"},
		{"[!a|b]", "(PARA (MARK \"a\" #a b))"},
		{"[!a|]", "(PARA (MARK \"a\" #a))"},
		{"[!|b]", "(PARA (MARK #* b))"},
		{"[!|b c]", "(PARA (MARK #* b c))"},
	})
}

func TestComment(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"%", "(PARA %)"},
		{"%%", "(PARA {%})"},
		{"%\n", "(PARA %)"},
		{"%%\n", "(PARA {%})"},
		{"%%a", "(PARA {% a})"},
		{"%%%a", "(PARA {% a})"},
		{"%% a", "(PARA {% a})"},
		{"%%%  a", "(PARA {% a})"},
		{"%% % a", "(PARA {% % a})"},
		{"%%a", "(PARA {% a})"},
		{"a%%b", "(PARA a {% b})"},
		{"a %%b", "(PARA a  {% b})" /*"(PARA a {% b})"*/},
		{" %%b", "(PARA {% b})"},
		{"%%b ", "(PARA {% b })"},
		{"100%", "(PARA 100%)"},
	})
}

func TestFormat(t *testing.T) {
	t.Parallel()
	// Not for Insert / '>', because collision with quoted list
	for _, ch := range []string{"_", "*", "~", "^", ",", "\"", "#", ":"} {
		checkTcs(t, replace(ch, TestCases{
			{"$", "(PARA $)"},
			{"$$", "(PARA $$)"},
			{"$$$", "(PARA $$$)"},
			{"$$$$", "(PARA {$})"},
		}))
	}
	for _, ch := range []string{"_", "*", ">", "~", "^", ",", "\"", "#", ":"} {
		checkTcs(t, replace(ch, TestCases{
			{"$$a$$", "(PARA {$ a})"},
			{"$$a$$$", "(PARA {$ a} $)"},
			{"$$$a$$", "(PARA {$ $a})"},
			{"$$$a$$$", "(PARA {$ $a} $)"},
			{"$\\$", "(PARA $$)"},
			{"$\\$$", "(PARA $$$)"},
			{"$$\\$", "(PARA $$$)"},
			{"$$a\\$$", "(PARA $$a$$)"},
			{"$$a$\\$", "(PARA $$a$$)"},
			{"$$a\\$$$", "(PARA {$ a$})"},
			{"$$a\na$$", "(PARA {$ a SB a})"},
			{"$$a\n\na$$", "(PARA $$a)(PARA a$$)"},
			{"$$a$${go}", "(PARA {$ a}[ATTR go])"},
		}))
	}
	checkTcs(t, TestCases{
		{"__****__", "(PARA {_ {*}})"},
		{"__**a**__", "(PARA {_ {* a}})"},
		{"__**__**", "(PARA __ {* __})"},
	})
}

func TestLiteral(t *testing.T) {
	t.Parallel()
	for _, ch := range []string{"`", "'", "="} {
		checkTcs(t, replace(ch, TestCases{
			{"$", "(PARA $)"},
			{"$$", "(PARA $$)"},
			{"$$$", "(PARA $$$)"},
			{"$$$$", "(PARA {$})"},
			{"$$a$$", "(PARA {$ a})"},
			{"$$a$$$", "(PARA {$ a} $)"},
			{"$$$a$$", "(PARA {$ $a})"},
			{"$$$a$$$", "(PARA {$ $a} $)"},
			{"$\\$", "(PARA $$)"},
			{"$\\$$", "(PARA $$$)"},
			{"$$\\$", "(PARA $$$)"},
			{"$$a\\$$", "(PARA $$a$$)"},
			{"$$a$\\$", "(PARA $$a$$)"},
			{"$$a\\$$$", "(PARA {$ a$})"},
			{"$$a$${go}", "(PARA {$ a}[ATTR go])"},
		}))
	}
	checkTcs(t, TestCases{
		{"``<script `` abc", "(PARA {` <script }  abc)"},
		{"''````''", "(PARA {' ````})"},
		{"''``a``''", "(PARA {' ``a``})"},
		{"''``''``", "(PARA {' ``} ``)"},
		{"''\\'''", "(PARA {' '})"},
	})
}

func TestLiteralMath(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"$", "(PARA $)"},
		{"$$", "(PARA $$)"},
		{"$$$", "(PARA $$$)"},
		{"$$$$", "(PARA {$})"},
		{"$$a$$", "(PARA {$ a})"},
		{"$$a$$$", "(PARA {$ a} $)"},
		{"$$$a$$", "(PARA {$ $a})"},
		{"$$$a$$$", "(PARA {$ $a} $)"},
		{`$\$`, "(PARA $$)"},
		{`$\$$`, "(PARA $$$)"},
		{`$$\$`, "(PARA $$$)"},
		{`$$a\$$`, `(PARA {$ a\})`},
		{`$$a$\$`, "(PARA $$a$$)"},
		{`$$a\$$$`, `(PARA {$ a\} $)`},
		{"$$a$${go}", "(PARA {$ a}[ATTR go])"},
	})
}

func TestMixFormatCode(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"__abc__\n**def**", "(PARA {_ abc} SB {* def})"},
		{"''abc''\n==def==", "(PARA {' abc} SB {= def})"},
		{"__abc__\n==def==", "(PARA {_ abc} SB {= def})"},
		{"__abc__\n``def``", "(PARA {_ abc} SB {` def})"},
		{"\"\"ghi\"\"\n::abc::\n``def``\n", "(PARA {\" ghi} SB {: abc} SB {` def})"},
	})
}

func TestNDash(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"--", "(PARA \u2013)"},
		{"a--b", "(PARA a\u2013b)"},
	})
}

func TestEntity(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"&", "(PARA &)"},
		{"&;", "(PARA &;)"},
		{"&#;", "(PARA &#;)"},
		{"&#1a;", "(PARA &#1a;)"},
		{"&#x;", "(PARA &#x;)"},
		{"&#x0z;", "(PARA &#x0z;)"},
		{"&1;", "(PARA &1;)"},
		{"&#9;", "(PARA &#9;)"}, // No numeric entities below space are not allowed.
		{"&#x1f;", "(PARA &#x1f;)"},

		// Good cases
		{"&lt;", "(PARA <)"},
		{"&#48;", "(PARA 0)"},
		{"&#x4A;", "(PARA J)"},
		{"&#X4a;", "(PARA J)"},
		{"&hellip;", "(PARA \u2026)"},
		{"&nbsp;", "(PARA \u00a0)"},
		{"E: &amp;,&#63;;&#x63;.", "(PARA E: &,?;c.)"},
	})
}

func TestVerbatimZettel(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		// {"@@@\n@@@", "(ZETTEL)"},
		{"@@@\nabc\n@@@", "(ZETTEL\nabc)"},
		{"@@@@def\nabc\n@@@@", "(ZETTEL\nabc)[ATTR =def]"},
	})
}

func TestVerbatimCode(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		// {"```\n```", "(PROG)"},
		{"```\nabc\n```", "(PROG\nabc)"},
		{"```\nabc\n````", "(PROG\nabc)"},
		{"````\nabc\n````", "(PROG\nabc)"},
		{"````\nabc\n```\n````", "(PROG\nabc\n```)"},
		{"````go\nabc\n````", "(PROG\nabc)[ATTR =go]"},
	})
}

func TestVerbatimEval(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		// {"~~~\n~~~", "(EVAL)"},
		{"~~~\nabc\n~~~", "(EVAL\nabc)"},
		{"~~~\nabc\n~~~~", "(EVAL\nabc)"},
		{"~~~~\nabc\n~~~~", "(EVAL\nabc)"},
		{"~~~~\nabc\n~~~\n~~~~", "(EVAL\nabc\n~~~)"},
		{"~~~~go\nabc\n~~~~", "(EVAL\nabc)[ATTR =go]"},
	})
}

func TestVerbatimMath(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		// {"$$$\n$$$", "(MATH)"},
		{"$$$\nabc\n$$$", "(MATH\nabc)"},
		{"$$$\nabc\n$$$$", "(MATH\nabc)"},
		{"$$$$\nabc\n$$$$", "(MATH\nabc)"},
		{"$$$$\nabc\n$$$\n$$$$", "(MATH\nabc\n$$$)"},
		{"$$$$go\nabc\n$$$$", "(MATH\nabc)[ATTR =go]"},
	})
}

func TestVerbatimComment(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		// {"%%%\n%%%", "(COMMENT)"},
		{"%%%\nabc\n%%%", "(COMMENT\nabc)"},
		{"%%%%go\nabc\n%%%%", "(COMMENT\nabc)[ATTR =go]"},
	})
}

func TestPara(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"a\n\nb", "(PARA a)(PARA b)"},
		{"a\n \nb", "(PARA a)(PARA b)"},
	})
}

func TestSpanRegion(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		// {":::\n:::", "(SPAN)"},
		{":::\nabc\n:::", "(SPAN (PARA abc))"},
		{":::\nabc\n::::", "(SPAN (PARA abc))"},
		{"::::\nabc\n::::", "(SPAN (PARA abc))"},
		{"::::\nabc\n:::\ndef\n:::\n::::", "(SPAN (PARA abc)(SPAN (PARA def)))"},
		{":::{go}\na\n:::", "(SPAN (PARA a))[ATTR go]"},
		{":::\nabc\n::: def ", "(SPAN (PARA abc) (LINE def))"},
	})
}

func TestQuoteRegion(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		// {"<<<\n<<<", "(QUOTE)"},
		{"<<<\nabc\n<<<", "(QUOTE (PARA abc))"},
		{"<<<\nabc\n<<<<", "(QUOTE (PARA abc))"},
		{"<<<<\nabc\n<<<<", "(QUOTE (PARA abc))"},
		{"<<<<\nabc\n<<<\ndef\n<<<\n<<<<", "(QUOTE (PARA abc)(QUOTE (PARA def)))"},
		{"<<<go\na\n<<<", "(QUOTE (PARA a))[ATTR =go]"},
		{"<<<\nabc\n<<< def ", "(QUOTE (PARA abc) (LINE def))"},
	})
}

func TestVerseRegion(t *testing.T) {
	t.Parallel()
	checkTcs(t, replace("\"", TestCases{
		// {"$$$\n$$$", "(VERSE)"},
		{"$$$\nabc\n$$$", "(VERSE (PARA abc))"},
		{"$$$\nabc\n$$$$", "(VERSE (PARA abc))"},
		{"$$$$\nabc\n$$$$", "(VERSE (PARA abc))"},
		{"$$$\nabc\ndef\n$$$", "(VERSE (PARA abc HB def))"},
		{"$$$$\nabc\n$$$\ndef\n$$$\n$$$$", "(VERSE (PARA abc)(VERSE (PARA def)))"},
		{"$$$go\na\n$$$", "(VERSE (PARA a))[ATTR =go]"},
		{"$$$\nabc\n$$$ def ", "(VERSE (PARA abc) (LINE def))"},
	}))
}

func TestHeading(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"=h", "(PARA =h)"},
		{"= h", "(PARA = h)"},
		{"==h", "(PARA ==h)"},
		{"== h", "(PARA == h)"},
		{"===h", "(PARA ===h)"},
		{"=== h", "(H1 h #h)"},
		{"===  h", "(H1 h #h)"},
		{"==== h", "(H2 h #h)"},
		{"===== h", "(H3 h #h)"},
		{"====== h", "(H4 h #h)"},
		{"======= h", "(H5 h #h)"},
		{"======== h", "(H5 h #h)"},
		{"=", "(PARA =)"},
		{"=== h=__=a__", "(H1 h= {_ =a} #h-a)"},
		{"=\n", "(PARA =)"},
		{"a=", "(PARA a=)"},
		{" =", "(PARA =)"},
		{"=== h\na", "(H1 h #h)(PARA a)"},
		{"=== h i {-}", "(H1 h i #h-i)[ATTR -]"},
		{"=== h {{a}}", "(H1 h  (EMBED a) #h)"},
		{"=== h{{a}}", "(H1 h (EMBED a) #h)"},
		{"=== {{a}}", "(H1 (EMBED a))"},
		{"=== h {{a}}{-}", "(H1 h  (EMBED a)[ATTR -] #h)"},
		{"=== h {{a}} {-}", "(H1 h  (EMBED a) #h)[ATTR -]"},
		{"=== h {-}{{a}}", "(H1 h #h)[ATTR -]"},
		{"=== h{id=abc}", "(H1 h #h)[ATTR id=abc]"},
		{"=== h\n=== h", "(H1 h #h)(H1 h #h-1)"},
	})
}

func TestHRule(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"-", "(PARA -)"},
		{"---", "(HR)"},
		{"----", "(HR)"},
		{"---A", "(HR)[ATTR =A]"},
		{"---A-", "(HR)[ATTR =A-]"},
		{"-1", "(PARA -1)"},
		{"2-1", "(PARA 2-1)"},
		{"---  {  go  }  ", "(HR)[ATTR go]"},
		{"---  {  .go  }  ", "(HR)[ATTR class=go]"},
	})
}

func TestList(t *testing.T) {
	t.Parallel()
	// No ">" in the following, because quotation lists may have empty items.
	for _, ch := range []string{"*", "#"} {
		checkTcs(t, replace(ch, TestCases{
			{"$", "(PARA $)"},
			{"$$", "(PARA $$)"},
			{"$$$", "(PARA $$$)"},
			{"$ ", "(PARA $)"},
			{"$$ ", "(PARA $$)"},
			{"$$$ ", "(PARA $$$)"},
		}))
	}
	checkTcs(t, TestCases{
		{"* abc", "(UL {(PARA abc)})"},
		{"** abc", "(UL {(UL {(PARA abc)})})"},
		{"*** abc", "(UL {(UL {(UL {(PARA abc)})})})"},
		{"**** abc", "(UL {(UL {(UL {(UL {(PARA abc)})})})})"},
		{"** abc\n**** def", "(UL {(UL {(PARA abc)(UL {(UL {(PARA def)})})})})"},
		{"* abc\ndef", "(UL {(PARA abc)})(PARA def)"},
		{"* abc\n def", "(UL {(PARA abc)})(PARA def)"},
		{"* abc\n* def", "(UL {(PARA abc)} {(PARA def)})"},
		{"* abc\n  def", "(UL {(PARA abc SB def)})"},
		{"* abc\n   def", "(UL {(PARA abc SB def)})"},
		{"* abc\n\ndef", "(UL {(PARA abc)})(PARA def)"},
		{"* abc\n\n def", "(UL {(PARA abc)})(PARA def)"},
		{"* abc\n\n  def", "(UL {(PARA abc)(PARA def)})"},
		{"* abc\n\n   def", "(UL {(PARA abc)(PARA def)})"},
		{"* abc\n** def", "(UL {(PARA abc)(UL {(PARA def)})})"},
		{"* abc\n** def\n* ghi", "(UL {(PARA abc)(UL {(PARA def)})} {(PARA ghi)})"},
		{"* abc\n\n  def\n* ghi", "(UL {(PARA abc)(PARA def)} {(PARA ghi)})"},
		{"* abc\n** def\n   ghi\n  jkl", "(UL {(PARA abc)(UL {(PARA def SB ghi)})(PARA jkl)})"},

		// A list does not last beyond a region
		{":::\n# abc\n:::\n# def", "(SPAN (OL {(PARA abc)}))(OL {(PARA def)})"},

		// A HRule creates a new list
		{"* abc\n---\n* def", "(UL {(PARA abc)})(HR)(UL {(PARA def)})"},

		// Changing list type adds a new list
		{"* abc\n# def", "(UL {(PARA abc)})(OL {(PARA def)})"},

		// Quotation lists may have empty items
		{">", "(QL {})"},
	})
}

func TestQuoteList(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"> w1 w2", "(QL {(PARA w1 w2)})"},
		{"> w1\n> w2", "(QL {(PARA w1 SB w2)})"},
		{"> w1\n>\n>w2", "(QL {(PARA w1)} {})(PARA >w2)"},
	})
}

func TestEnumAfterPara(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"abc\n* def", "(PARA abc)(UL {(PARA def)})"},
		{"abc\n*def", "(PARA abc SB *def)"},
	})
}

func TestDefinition(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{";", "(PARA ;)"},
		{"; ", "(PARA ;)"},
		{"; abc", "(DL (DT abc))"},
		{"; abc\ndef", "(DL (DT abc))(PARA def)"},
		{"; abc\n def", "(DL (DT abc))(PARA def)"},
		{"; abc\n  def", "(DL (DT abc SB def))"},
		{":", "(PARA :)"},
		{": ", "(PARA :)"},
		{": abc", "(PARA : abc)"},
		{"; abc\n: def", "(DL (DT abc) (DD (PARA def)))"},
		{"; abc\n: def\nghi", "(DL (DT abc) (DD (PARA def)))(PARA ghi)"},
		{"; abc\n: def\n ghi", "(DL (DT abc) (DD (PARA def)))(PARA ghi)"},
		{"; abc\n: def\n  ghi", "(DL (DT abc) (DD (PARA def SB ghi)))"},
		{"; abc\n: def\n\n  ghi", "(DL (DT abc) (DD (PARA def)(PARA ghi)))"},
		{"; abc\n:", "(DL (DT abc))(PARA :)"},
		{"; abc\n: def\n: ghi", "(DL (DT abc) (DD (PARA def)) (DD (PARA ghi)))"},
		{"; abc\n: def\n; ghi\n: jkl", "(DL (DT abc) (DD (PARA def)) (DT ghi) (DD (PARA jkl)))"},
	})
}

func TestTable(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		// {"|", "(TAB (TR))"},
		{"||", "(TAB (TR (TD)))"},
		{"| |", "(TAB (TR (TD)))"},
		{"|a", "(TAB (TR (TD a)))"},
		{"|a|", "(TAB (TR (TD a)))"},
		{"|a| ", "(TAB (TR (TD a)(TD)))"},
		{"|a|b", "(TAB (TR (TD a)(TD b)))"},
		{"|a|b\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"},
		{"|%", ""},
		{"|a|b\n|%---\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"},
		{"|a|b\n|c", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD)))"},
	})
}

func TestTransclude(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"{{{a}}}", "(TRANSCLUDE a)"},
		{"{{{a}}}b", "(TRANSCLUDE a)[ATTR =b]"},
		{"{{{a}}}}", "(TRANSCLUDE a)"},
		{"{{{a\\}}}}", "(TRANSCLUDE a%5C%7D)"},
		{"{{{a\\}}}}b", "(TRANSCLUDE a%5C%7D)[ATTR =b]"},
		{"{{{a}}", "(PARA { (EMBED a))"},
		{"{{{a}}}{go=b}", "(TRANSCLUDE a)[ATTR go=b]"},
	})
}

func TestBlockAttr(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{":::go\na\n:::", "(SPAN (PARA a))[ATTR =go]"},
		{":::go=\na\n:::", "(SPAN (PARA a))[ATTR =go]"},
		{":::{}\na\n:::", "(SPAN (PARA a))"},
		{":::{ }\na\n:::", "(SPAN (PARA a))"},
		{":::{.go}\na\n:::", "(SPAN (PARA a))[ATTR class=go]"},
		{":::{=go}\na\n:::", "(SPAN (PARA a))[ATTR =go]"},
		{":::{go}\na\n:::", "(SPAN (PARA a))[ATTR go]"},
		{":::{go=py}\na\n:::", "(SPAN (PARA a))[ATTR go=py]"},
		{":::{.go=py}\na\n:::", "(SPAN (PARA a))"},
		{":::{go=}\na\n:::", "(SPAN (PARA a))[ATTR go]"},
		{":::{.go=}\na\n:::", "(SPAN (PARA a))"},
		{":::{go py}\na\n:::", "(SPAN (PARA a))[ATTR go py]"},
		{":::{go\npy}\na\n:::", "(SPAN (PARA a))[ATTR go py]"},
		{":::{.go py}\na\n:::", "(SPAN (PARA a))[ATTR class=go py]"},
		{":::{go .py}\na\n:::", "(SPAN (PARA a))[ATTR class=py go]"},
		{":::{.go py=3}\na\n:::", "(SPAN (PARA a))[ATTR class=go py=3]"},
		{":::  {  go  }  \na\n:::", "(SPAN (PARA a))[ATTR go]"},
		{":::  {  .go  }  \na\n:::", "(SPAN (PARA a))[ATTR class=go]"},
	})
	checkTcs(t, replace("\"", TestCases{
		{":::{py=3}\na\n:::", "(SPAN (PARA a))[ATTR py=3]"},
		{":::{py=$2 3$}\na\n:::", "(SPAN (PARA a))[ATTR py=$2 3$]"},
		{":::{py=$2\\$3$}\na\n:::", "(SPAN (PARA a))[ATTR py=2$3]"},
		{":::{py=2$3}\na\n:::", "(SPAN (PARA a))[ATTR py=2$3]"},
		{":::{py=$2\n3$}\na\n:::", "(SPAN (PARA a))[ATTR py=$2\n3$]"},
		{":::{py=$2 3}\na\n:::", "(SPAN (PARA a))"},
		{":::{py=2 py=3}\na\n:::", "(SPAN (PARA a))[ATTR py=$2 3$]"},
		{":::{.go .py}\na\n:::", "(SPAN (PARA a))[ATTR class=$go py$]"},
		{":::{go go}\na\n:::", "(SPAN (PARA a))[ATTR go]"},
		{":::{=py =go}\na\n:::", "(SPAN (PARA a))[ATTR =go]"},
	}))
}

func TestInlineAttr(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"::a::{}", "(PARA {: a})"},
		{"::a::{ }", "(PARA {: a})"},
		{"::a::{.go}", "(PARA {: a}[ATTR class=go])"},
		{"::a::{=go}", "(PARA {: a}[ATTR =go])"},
		{"::a::{go}", "(PARA {: a}[ATTR go])"},
		{"::a::{go=py}", "(PARA {: a}[ATTR go=py])"},
		{"::a::{.go=py}", "(PARA {: a} {.go=py})"},
		{"::a::{go=}", "(PARA {: a}[ATTR go])"},
		{"::a::{.go=}", "(PARA {: a} {.go=})"},
		{"::a::{go py}", "(PARA {: a}[ATTR go py])"},
		{"::a::{go\npy}", "(PARA {: a}[ATTR go py])"},
		{"::a::{.go py}", "(PARA {: a}[ATTR class=go py])"},
		{"::a::{go .py}", "(PARA {: a}[ATTR class=py go])"},
		{"::a::{  \n go \n .py\n  \n}", "(PARA {: a}[ATTR class=py go])"},
		{"::a::{  \n go \n .py\n\n}", "(PARA {: a}[ATTR class=py go])"},
		{"::a::{\ngo\n}", "(PARA {: a}[ATTR go])"},
	})
	checkTcs(t, replace("\"", TestCases{
		{"::a::{py=3}", "(PARA {: a}[ATTR py=3])"},
		{"::a::{py=$2 3$}", "(PARA {: a}[ATTR py=$2 3$])"},
		{"::a::{py=$2\\$3$}", "(PARA {: a}[ATTR py=2$3])"},
		{"::a::{py=2$3}", "(PARA {: a}[ATTR py=2$3])"},
		{"::a::{py=$2\n3$}", "(PARA {: a}[ATTR py=$2\n3$])"},
		{"::a::{py=$2 3}", "(PARA {: a} {py=$2 3})"},

		{"::a::{py=2 py=3}", "(PARA {: a}[ATTR py=$2 3$])"},
		{"::a::{.go .py}", "(PARA {: a}[ATTR class=$go py$])"},
	}))
}

func TestTemp(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"", ""},
	})
}

// --------------------------------------------------------------------------

// TestVisitor serializes the abstract syntax tree to a string.
type TestVisitor struct {
	sb strings.Builder
}

func (tv *TestVisitor) String() string { return tv.sb.String() }

func (tv *TestVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.InlineSlice:
		tv.visitInlineSlice(n)
	case *ast.ParaNode:
		tv.sb.WriteString("(PARA")
		ast.Walk(tv, &n.Inlines)
		tv.sb.WriteByte(')')
	case *ast.VerbatimNode:
		code, ok := mapVerbatimKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("Unknown verbatim code %v", n.Kind))
		}
		tv.sb.WriteString(code)
		if len(n.Content) > 0 {
			tv.sb.WriteByte('\n')
			tv.sb.Write(n.Content)
		}
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.RegionNode:
		code, ok := mapRegionKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("Unknown region code %v", n.Kind))
		}
		tv.sb.WriteString(code)
		if len(n.Blocks) > 0 {
			tv.sb.WriteByte(' ')
			ast.Walk(tv, &n.Blocks)
		}
		if len(n.Inlines) > 0 {
			tv.sb.WriteString(" (LINE")
			ast.Walk(tv, &n.Inlines)
			tv.sb.WriteByte(')')
		}
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.HeadingNode:
		fmt.Fprintf(&tv.sb, "(H%d", n.Level)
		ast.Walk(tv, &n.Inlines)
		if n.Fragment != "" {
			tv.sb.WriteString(" #")
			tv.sb.WriteString(n.Fragment)
		}
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.HRuleNode:
		tv.sb.WriteString("(HR)")
		tv.visitAttributes(n.Attrs)
	case *ast.NestedListNode:
		tv.sb.WriteString(mapNestedListKind[n.Kind])
		for _, item := range n.Items {
			tv.sb.WriteString(" {")
			ast.WalkItemSlice(tv, item)
			tv.sb.WriteByte('}')
		}
		tv.sb.WriteByte(')')
	case *ast.DescriptionListNode:
		tv.sb.WriteString("(DL")
		for _, def := range n.Descriptions {
			tv.sb.WriteString(" (DT")
			ast.Walk(tv, &def.Term)
			tv.sb.WriteByte(')')
			for _, b := range def.Descriptions {
				tv.sb.WriteString(" (DD ")
				ast.WalkDescriptionSlice(tv, b)
				tv.sb.WriteByte(')')
			}
		}
		tv.sb.WriteByte(')')
	case *ast.TableNode:
		tv.sb.WriteString("(TAB")
		if len(n.Header) > 0 {
			tv.sb.WriteString(" (TR")
			for _, cell := range n.Header {
				tv.sb.WriteString(" (TH")
				tv.sb.WriteString(alignString[cell.Align])
				ast.Walk(tv, &cell.Inlines)
				tv.sb.WriteString(")")
			}
			tv.sb.WriteString(")")
		}
		if len(n.Rows) > 0 {
			tv.sb.WriteString(" ")
			for _, row := range n.Rows {
				tv.sb.WriteString("(TR")
				for i, cell := range row {
					if i == 0 {
						tv.sb.WriteString(" ")
					}
					tv.sb.WriteString("(TD")
					tv.sb.WriteString(alignString[cell.Align])
					ast.Walk(tv, &cell.Inlines)
					tv.sb.WriteString(")")
				}
				tv.sb.WriteString(")")
			}
		}
		tv.sb.WriteString(")")
	case *ast.TranscludeNode:
		fmt.Fprintf(&tv.sb, "(TRANSCLUDE %v)", n.Ref) // FIXME n.Inlines
		tv.visitAttributes(n.Attrs)
	case *ast.BLOBNode:
		tv.sb.WriteString("(BLOB ")
		tv.sb.WriteString(n.Syntax)
		tv.sb.WriteString(")")
	case *ast.TextNode:
		tv.sb.WriteString(n.Text)
	case *ast.BreakNode:
		if n.Hard {
			tv.sb.WriteString("HB")
		} else {
			tv.sb.WriteString("SB")
		}
	case *ast.LinkNode:
		fmt.Fprintf(&tv.sb, "(LINK %v", n.Ref)
		ast.Walk(tv, &n.Inlines)
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.EmbedRefNode:
		fmt.Fprintf(&tv.sb, "(EMBED %v", n.Ref)
		if len(n.Inlines) > 0 {
			ast.Walk(tv, &n.Inlines)
		}
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.EmbedBLOBNode:
		panic("TODO: zmktest blob")
	case *ast.CiteNode:
		fmt.Fprintf(&tv.sb, "(CITE %s", n.Key)
		if len(n.Inlines) > 0 {
			ast.Walk(tv, &n.Inlines)
		}
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.FootnoteNode:
		tv.sb.WriteString("(FN")
		ast.Walk(tv, &n.Inlines)
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.MarkNode:
		tv.sb.WriteString("(MARK")
		if n.Mark != "" {
			tv.sb.WriteString(" \"")
			tv.sb.WriteString(n.Mark)
			tv.sb.WriteByte('"')
		}
		if n.Fragment != "" {
			tv.sb.WriteString(" #")
			tv.sb.WriteString(n.Fragment)
		}
		if len(n.Inlines) > 0 {
			ast.Walk(tv, &n.Inlines)
		}
		tv.sb.WriteByte(')')
	case *ast.FormatNode:
		fmt.Fprintf(&tv.sb, "{%c", mapFormatKind[n.Kind])
		ast.Walk(tv, &n.Inlines)
		tv.sb.WriteByte('}')
		tv.visitAttributes(n.Attrs)
	case *ast.LiteralNode:
		code, ok := mapLiteralKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("No element for code %v", n.Kind))
		}
		tv.sb.WriteByte('{')
		tv.sb.WriteRune(code)
		if len(n.Content) > 0 {
			tv.sb.WriteByte(' ')
			tv.sb.Write(n.Content)
		}
		tv.sb.WriteByte('}')
		tv.visitAttributes(n.Attrs)
	default:
		return tv
	}
	return nil
}

var mapVerbatimKind = map[ast.VerbatimKind]string{
	ast.VerbatimZettel:  "(ZETTEL",
	ast.VerbatimCode:    "(PROG",
	ast.VerbatimEval:    "(EVAL",
	ast.VerbatimMath:    "(MATH",
	ast.VerbatimComment: "(COMMENT",
}

var mapRegionKind = map[ast.RegionKind]string{
	ast.RegionSpan:  "(SPAN",
	ast.RegionQuote: "(QUOTE",
	ast.RegionVerse: "(VERSE",
}

var mapNestedListKind = map[ast.NestedListKind]string{
	ast.NestedListOrdered:   "(OL",
	ast.NestedListUnordered: "(UL",
	ast.NestedListQuote:     "(QL",
}

var alignString = map[ast.Alignment]string{
	ast.AlignDefault: "",
	ast.AlignLeft:    "l",
	ast.AlignCenter:  "c",
	ast.AlignRight:   "r",
}

var mapFormatKind = map[ast.FormatKind]rune{
	ast.FormatEmph:   '_',
	ast.FormatStrong: '*',
	ast.FormatInsert: '>',
	ast.FormatDelete: '~',
	ast.FormatSuper:  '^',
	ast.FormatSub:    ',',
	ast.FormatQuote:  '"',
	ast.FormatMark:   '#',
	ast.FormatSpan:   ':',
}

var mapLiteralKind = map[ast.LiteralKind]rune{
	ast.LiteralCode:    '`',
	ast.LiteralInput:   '\'',
	ast.LiteralOutput:  '=',
	ast.LiteralComment: '%',
	ast.LiteralMath:    '$',
}

func (tv *TestVisitor) visitInlineSlice(is *ast.InlineSlice) {
	for _, in := range *is {
		tv.sb.WriteByte(' ')
		ast.Walk(tv, in)
	}
}

func (tv *TestVisitor) visitAttributes(a zsx.Attributes) {
	if a.IsEmpty() {
		return
	}
	tv.sb.WriteString("[ATTR")

	for _, k := range a.Keys() {
		tv.sb.WriteByte(' ')
		tv.sb.WriteString(k)
		v := a[k]
		if len(v) > 0 {
			tv.sb.WriteByte('=')
			if quoteString(v) {
				tv.sb.WriteByte('"')
				tv.sb.WriteString(v)
				tv.sb.WriteByte('"')
			} else {
				tv.sb.WriteString(v)
			}
		}
	}

	tv.sb.WriteByte(']')
}

func quoteString(s string) bool {
	for _, ch := range s {
		if ch <= ' ' {
			return true
		}
	}
	return false
}
Changes to internal/query/unlinked.go.
42
43
44
45
46
47
48
49

50
51
52
42
43
44
45
46
47
48

49
50
51
52







-
+



	}
	result := make([]string, 0, len(metaSeq)*4) // Assumption: four words per title
	for _, m := range metaSeq {
		title, hasTitle := m.Get(meta.KeyTitle)
		if !hasTitle {
			continue
		}
		result = append(result, strings.MakeWords(string(title))...)
		result = append(result, strings.SplitWords(string(title))...)
	}
	return result
}
Changes to internal/usecase/evaluate.go.
12
13
14
15
16
17
18

19
20

21
22
23
24
25
26
27
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29







+


+







//-----------------------------------------------------------------------------

package usecase

import (
	"context"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/evaluator"
	"zettelstore.de/z/internal/parser"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
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
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







-
+








-
+





-
-
-
+
+
+


-
-
+
-











		rtConfig:    rtConfig,
		ucGetZettel: ucGetZettel,
		ucQuery:     ucQuery,
	}
}

// Run executes the use case.
func (uc *Evaluate) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) {
func (uc *Evaluate) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.Zettel, error) {
	zettel, err := uc.ucGetZettel.Run(ctx, zid)
	if err != nil {
		return nil, err
	}
	return uc.RunZettel(ctx, zettel, syntax), nil
}

// RunZettel executes the use case for a given zettel.
func (uc *Evaluate) RunZettel(ctx context.Context, zettel zettel.Zettel, syntax string) *ast.ZettelNode {
func (uc *Evaluate) RunZettel(ctx context.Context, zettel zettel.Zettel, syntax string) *ast.Zettel {
	zn := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig)
	evaluator.EvaluateZettel(ctx, uc, uc.rtConfig, zn)
	return zn
}

// RunBlockNode executes the use case for a metadata list.
func (uc *Evaluate) RunBlockNode(ctx context.Context, bn ast.BlockNode) ast.BlockSlice {
	if bn == nil {
// RunBlockNode executes the use case for a metadata list, formatted as a block.
func (uc *Evaluate) RunBlockNode(ctx context.Context, block *sx.Pair) *sx.Pair {
	if block == nil {
		return nil
	}
	bns := ast.BlockSlice{bn}
	evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, &bns)
	return evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, zsx.MakeBlock(block))
	return bns
}

// GetZettel retrieves the full zettel of a given zettel identifier.
func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	return uc.ucGetZettel.Run(ctx, zid)
}

// QueryMeta returns a list of metadata that comply to the given selection criteria.
func (uc *Evaluate) QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) {
	return uc.ucQuery.Run(ctx, q)
}
Changes to internal/usecase/get_references.go.
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
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







+


+
+

-











-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+


-
+



-
+

-
-
+
+
+
+
+







//-----------------------------------------------------------------------------

package usecase

import (
	"iter"

	"t73f.de/r/sx"
	zeroiter "t73f.de/r/zero/iter"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/collect"
)

// GetReferences is the usecase to retrieve references that occur in a zettel.
type GetReferences struct{}

// NewGetReferences creates a new usecase object.
func NewGetReferences() GetReferences { return GetReferences{} }

// RunByState returns all references of a zettel, sparated by their state:
// local, external, query. No zettel references are returned.
func (uc GetReferences) RunByState(zn *ast.ZettelNode) (local, ext, query []*ast.Reference) {
	for ref := range collect.ReferenceSeq(zn) {
		switch ref.State {
		case ast.RefStateHosted, ast.RefStateBased: // Local
			local = append(local, ref)
		case ast.RefStateExternal:
			ext = append(ext, ref)
		case ast.RefStateQuery:
			query = append(query, ref)
func (uc GetReferences) RunByState(block *sx.Pair) (local, ext, query *sx.Pair) {
	var lbLoc, lbQueries, lbExt sx.ListBuilder
	for ref := range collect.ReferenceSeq(block) {
		sym, _ := zsx.GetReference(ref)
		switch sym {
		case zsx.SymRefStateHosted, sz.SymRefStateBased:
			lbLoc.Add(ref)
		case zsx.SymRefStateExternal:
			lbExt.Add(ref)
		case sz.SymRefStateQuery:
			lbQueries.Add(ref)
		}
	}
	return local, ext, query
	return lbLoc.List(), lbExt.List(), lbQueries.List()
}

// RunByExternal returns an iterator of all external references of a zettel.
func (uc GetReferences) RunByExternal(zn *ast.ZettelNode) iter.Seq[*ast.Reference] {
func (uc GetReferences) RunByExternal(blocks *sx.Pair) iter.Seq[*sx.Pair] {
	return zeroiter.FilterSeq(
		collect.ReferenceSeq(zn),
		func(ref *ast.Reference) bool { return ref.State == ast.RefStateExternal })
		collect.ReferenceSeq(blocks),
		func(ref *sx.Pair) bool {
			sym, _ := zsx.GetReference(ref)
			return zsx.SymRefStateExternal.IsEqualSymbol(sym)
		})
}

// RunByMeta returns all URLs that are stored in the metadata.
func (uc GetReferences) RunByMeta(m *meta.Meta) iter.Seq[string] {
	return func(yield func(string) bool) {
		for key, val := range m.All() {
			if meta.Type(key) == meta.TypeURL && !yield(string(val)) {
Changes to internal/usecase/parse_zettel.go.
31
32
33
34
35
36
37
38

39
40
41
42
43
44



45
31
32
33
34
35
36
37

38
39
40
41
42
43

44
45
46
47







-
+





-
+
+
+


// NewParseZettel creates a new use case.
func NewParseZettel(rtConfig config.Config, getZettel GetZettel) ParseZettel {
	return ParseZettel{rtConfig: rtConfig, getZettel: getZettel}
}

// Run executes the use case.
func (uc ParseZettel) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) {
func (uc ParseZettel) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.Zettel, error) {
	zettel, err := uc.getZettel.Run(ctx, zid)
	if err != nil {
		return nil, err
	}

	return parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil
	z := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig)
	parser.Clean(z.Blocks)
	return z, nil
}
Changes to internal/usecase/query.go.
13
14
15
16
17
18
19

20
21

22
23
24
25


26
27
28
29
30
31
32
33
34
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







+


+




+
+

-








package usecase

import (
	"context"
	"errors"
	"fmt"
	"slices"
	"strings"

	"t73f.de/r/sx"
	zerostrings "t73f.de/r/zero/strings"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/id/idset"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/collect"
	"zettelstore.de/z/internal/parser"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/zettel"
)

128
129
130
131
132
133
134
135
136
137



138
139
140
141
142
143





144
145
146
147
148
149
150
131
132
133
134
135
136
137



138
139
140






141
142
143
144
145
146
147
148
149
150
151
152







-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+







func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(metaSeq))
	for _, m := range metaSeq {
		zn, err := uc.ucEvaluate.Run(ctx, m.Zid, string(m.GetDefault(meta.KeySyntax, meta.DefaultSyntax)))
		if err != nil {
			continue
		}
		for _, ln := range collect.Order(zn) {
			ref := ln.Ref
			if !ref.IsZettel() {
		for ln := range collect.Order(zn.Blocks).Pairs() {
			_, ref, _ := zsx.GetLink(ln.Head())
			if refSym, refVal := zsx.GetReference(ref); sz.SymRefStateZettel.IsEqualSymbol(refSym) {
				continue
			}

			if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil {
				if z, err3 := uc.port.GetZettel(ctx, collectedZid); err3 == nil {
					result = append(result, z.Meta)
				val, _ := sz.SplitFragment(refVal)
				if collectedZid, err2 := id.Parse(val); err2 == nil {
					if collectedMeta, err3 := uc.port.GetMeta(ctx, collectedZid); err3 == nil {
						result = append(result, collectedMeta)
					}
				}
			}
		}
	}
	return result
}

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

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







+





-
-
-
-
-






-
-
+
+
+
+






-
-
-
-


-



-
+
-
-
-
-
+
+
-
-
-
-

+
+
+
+
+
-
+
-
-
-
-
+
+
+
+
+
-
-
-
+
-
-
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-

-
+

+
		}
	}
	return result
}

func (uc *Query) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(candidates))
	ulv := unlinkedVisitor{words: words}
	for _, cand := range candidates {
		zettel, err := uc.port.GetZettel(ctx, cand.Zid)
		if err != nil {
			continue
		}
		v := unlinkedVisitor{
			words: words,
			found: false,
		}
		v.text = v.joinWords(words)

		syntax := string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax))
		if !parser.IsASTParser(syntax) {
			continue
		}
		zn := uc.ucEvaluate.RunZettel(ctx, zettel, syntax)
		ast.Walk(&v, &zn.BlocksAST)
		if v.found {

		ulv.found = false
		zsx.WalkIt(&ulv, zn.Blocks, nil)
		if ulv.found {
			result = append(result, cand)
		}
	}
	return result
}

func (*unlinkedVisitor) joinWords(words []string) string {
	return " " + strings.ToLower(strings.Join(words, " ")) + " "
}

type unlinkedVisitor struct {
	words []string
	text  string
	found bool
}

func (v *unlinkedVisitor) Visit(node ast.Node) ast.Visitor {
func (v *unlinkedVisitor) VisitItBefore(node *sx.Pair, _ *sx.Pair) bool {
	switch n := node.(type) {
	case *ast.InlineSlice:
		v.checkWords(n)
		return nil
	if v.found {
		return true
	case *ast.HeadingNode:
		return nil
	case *ast.LinkNode, *ast.EmbedRefNode, *ast.EmbedBLOBNode, *ast.CiteNode:
		return nil
	}
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		switch sym {
		case zsx.SymHeading,
			zsx.SymLink, zsx.SymEmbed, zsx.SymEmbedBLOB, zsx.SymCite:
			// No further search.
	return v
			return true
}

func (v *unlinkedVisitor) checkWords(is *ast.InlineSlice) {
	if len(*is) < 2*len(v.words)-1 {
		case zsx.SymText:
			// TODO: this is way too simple. For example, two text nodes may
			// be separated by a SOFT or HARD node.
			textWords := zerostrings.SplitWords(zsx.GetText(node))
			for i := 0; i+len(v.words) <= len(textWords); i++ {
		return
	}
	for _, text := range v.splitInlineTextList(is) {
				if slices.Equal(v.words, textWords[i:i+len(v.words)]) {
		if strings.Contains(text, v.text) {
			v.found = true
					v.found = true
		}
	}
}

					return true
func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string {
	var result []string
	var curList []string
	for _, in := range *is {
		switch n := in.(type) {
		case *ast.TextNode:
			curList = append(curList, zerostrings.MakeWords(n.Text)...)
		default:
			if curList != nil {
				result = append(result, v.joinWords(curList))
				curList = nil
			}
		}
	}
				}
			}
		}
	if curList != nil {
		result = append(result, v.joinWords(curList))
	}
	return result
	return false
}
func (*unlinkedVisitor) VisitItAfter(*sx.Pair, *sx.Pair) {}
Changes to internal/web/adapter/api/get_references.go.
18
19
20
21
22
23
24

25
26
27
28
29
30
31
32
33
18
19
20
21
22
23
24
25
26

27
28
29
30
31
32
33







+

-







	"iter"
	"net/http"

	"t73f.de/r/sx"
	zeroiter "t73f.de/r/zero/iter"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/usecase"
	"zettelstore.de/z/internal/web/content"
)

// MakeGetReferencesHandler creates a new HTTP handler to return various lists
// of zettel references.
func (a *API) MakeGetReferencesHandler(
49
50
51
52
53
54
55
56

57
58
59
60
61

62
63
64
65
66
67
68
49
50
51
52
53
54
55

56
57
58
59
60

61
62
63
64
65
66
67
68







-
+




-
+








		var seq iter.Seq[string]
		q := r.URL.Query()
		switch getPart(q, partZettel) {
		case partZettel:
			seq = zeroiter.CatSeq(
				ucGetReferences.RunByMeta(zn.InhMeta),
				getExternalURLs(zn, ucGetReferences),
				getExternalURLs(zn.Blocks, ucGetReferences),
			)
		case partMeta:
			seq = ucGetReferences.RunByMeta(zn.InhMeta)
		case partContent:
			seq = getExternalURLs(zn, ucGetReferences)
			seq = getExternalURLs(zn.Blocks, ucGetReferences)
		}

		enc, _ := getEncoding(r, q)
		if enc == api.EncoderData {
			var lb sx.ListBuilder
			lb.Collect(zeroiter.MapSeq(seq, func(s string) sx.Object { return sx.MakeString(s) }))
			if err = a.writeObject(w, zid, lb.List()); err != nil {
78
79
80
81
82
83
84
85

86
87
88





89
90
78
79
80
81
82
83
84

85
86


87
88
89
90
91
92
93







-
+

-
-
+
+
+
+
+


		}
		if err = writeBuffer(w, &buf, content.PlainText); err != nil {
			a.logger.Error("write plain data", "err", err, "zid", zid)
		}
	})
}

func getExternalURLs(zn *ast.ZettelNode, ucGetReferences usecase.GetReferences) iter.Seq[string] {
func getExternalURLs(blocks *sx.Pair, ucGetReferences usecase.GetReferences) iter.Seq[string] {
	return zeroiter.MapSeq(
		ucGetReferences.RunByExternal(zn),
		func(ref *ast.Reference) string { return ref.Value },
		ucGetReferences.RunByExternal(blocks),
		func(ref *sx.Pair) string {
			_, val := zsx.GetReference(ref)
			return val
		},
	)
}
Changes to internal/web/adapter/api/get_zettel.go.
53
54
55
56
57
58
59
60

61
62
63
64
65
66
67
53
54
55
56
57
58
59

60
61
62
63
64
65
66
67







-
+







		case api.EncoderPlain:
			a.writePlainData(ctx, w, zid, part, getZettel)

		case api.EncoderData:
			a.writeSzData(ctx, w, zid, part, getZettel)

		default:
			var zn *ast.ZettelNode
			var zn *ast.Zettel
			if q.Has(api.QueryKeyParseOnly) {
				zn, err = parseZettel.Run(ctx, zid, q.Get(meta.KeySyntax))
			} else {
				zn, err = evaluate.Run(ctx, zid, q.Get(meta.KeySyntax))
			}
			if err != nil {
				a.reportUsecaseError(w, err)
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
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







-
+















-
+

-
+

-
+







	if err = a.writeObject(w, zid, obj); err != nil {
		a.logger.Error("write sx data", "err", err, "zid", zid)
	}
}

func (a *API) writeEncodedZettelPart(
	ctx context.Context,
	w http.ResponseWriter, zn *ast.ZettelNode,
	w http.ResponseWriter, zn *ast.Zettel,
	enc api.EncodingEnum, encStr string, part partType,
) {
	encdr := encoder.Create(
		enc,
		&encoder.CreateParameter{
			Lang: a.rtConfig.Get(ctx, zn.InhMeta, meta.KeyLang),
		})
	if encdr == nil {
		adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in encoding %q", zn.Meta.Zid, encStr))
		return
	}
	var err error
	var buf bytes.Buffer
	switch part {
	case partZettel:
		_, err = encdr.WriteZettel(&buf, zn)
		err = encdr.WriteZettel(&buf, zn)
	case partMeta:
		_, err = encdr.WriteMeta(&buf, zn.InhMeta)
		err = encdr.WriteMeta(&buf, zn.InhMeta)
	case partContent:
		_, err = encdr.WriteBlocks(&buf, &zn.BlocksAST)
		err = encdr.WriteSz(&buf, zn.Blocks)
	}
	if err != nil {
		a.logger.Error("Unable to store data in buffer", "err", err, "zid", zn.Zid)
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	if buf.Len() == 0 {
Changes to internal/web/adapter/webui/create_zettel.go.
19
20
21
22
23
24
25

26
27
28
29
30
31
32
33
34
19
20
21
22
23
24
25
26
27

28
29
30
31
32
33
34







+

-







	"net/http"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/encoder"
	"zettelstore.de/z/internal/evaluator"
	"zettelstore.de/z/internal/usecase"
	"zettelstore.de/z/internal/web/adapter"
	"zettelstore.de/z/internal/zettel"
61
62
63
64
65
66
67
68
69


70
71
72
73
74
75
76
61
62
63
64
65
66
67


68
69
70
71
72
73
74
75
76







-
-
+
+







		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		switch op {
		case actionCopy:
			wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData)
		case actionFolge:
			wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData)
		case actionNew:
			title := ast.NormalizedSpacedText(origZettel.Meta.GetTitle())
			newTitle := ast.NormalizedSpacedText(q.Get(meta.KeyTitle))
			title := sz.NormalizedSpacedText(origZettel.Meta.GetTitle())
			newTitle := sz.NormalizedSpacedText(q.Get(meta.KeyTitle))
			wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel, newTitle), title, "", roleData, syntaxData)
		case actionSequel:
			wui.renderZettelForm(ctx, w, createZettel.PrepareSequel(origZettel), "Sequel Zettel", "", roleData, syntaxData)
		}
	})
}

169
170
171
172
173
174
175
176

177
178
179

180
181
182
183
184
185
186
187
169
170
171
172
173
174
175

176
177
178

179

180
181
182
183
184
185
186







-
+


-
+
-







		ctx := r.Context()
		metaSeq, err := queryMeta.Run(box.NoEnrichQuery(ctx, q), q)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		entries, _ := evaluator.QueryAction(ctx, q, metaSeq)
		bns := evaluate.RunBlockNode(ctx, entries)
		blocks := evaluate.RunBlockNode(ctx, entries)
		enc := encoder.Create(api.EncoderZmk, nil)
		var zmkContent bytes.Buffer
		_, err = enc.WriteBlocks(&zmkContent, &bns)
		if err = enc.WriteSz(&zmkContent, blocks); err != nil {
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		m := meta.New(id.Invalid)
		m.Set(meta.KeyTitle, meta.Value(q.Human()))
		m.Set(meta.KeySyntax, meta.ValueSyntaxZmk)
Changes to internal/web/adapter/webui/get_info.go.
20
21
22
23
24
25
26


27
28
29
30
31
32
33
34
35
20
21
22
23
24
25
26
27
28
29

30
31
32
33
34
35
36







+
+

-







	"strings"

	"t73f.de/r/sx"
	zerostrings "t73f.de/r/zero/strings"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/encoder"
	"zettelstore.de/z/internal/evaluator"
	"zettelstore.de/z/internal/query"
	"zettelstore.de/z/internal/usecase"
)
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
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







-
+

-
+












-
-
+
+







		getTextTitle := wui.makeGetTextTitle(ctx, ucGetZettel)
		var lbMetadata sx.ListBuilder
		for key, val := range zn.Meta.Computed() {
			sxval := wui.writeHTMLMetaValue(key, val, getTextTitle)
			lbMetadata.Add(sx.Cons(sx.MakeString(key), sxval))
		}

		locLinks, extLinks, queryLinks := wui.getLocalExtQueryLinks(ucGetReferences, zn)
		locLinks, extLinks, queryLinks := wui.getLocalExtQueryLinks(ucGetReferences, zn.Blocks)

		title := ast.NormalizedSpacedText(zn.InhMeta.GetTitle())
		title := sz.NormalizedSpacedText(zn.InhMeta.GetTitle())
		phrase := q.Get(api.QueryKeyPhrase)
		if phrase == "" {
			phrase = title
		}
		unlinkedMeta, err := ucQuery.Run(ctx, createUnlinkedQuery(zid, phrase))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		enc := wui.getSimpleHTMLEncoder(wui.getConfig(ctx, zn.InhMeta, meta.KeyLang))
		entries, _ := evaluator.QueryAction(ctx, nil, unlinkedMeta)
		bns := ucEvaluate.RunBlockNode(ctx, entries)
		unlinkedContent, _, err := enc.BlocksSxn(&bns)
		blocks := ucEvaluate.RunBlockNode(ctx, entries)
		unlinkedContent, _, err := enc.BlocksSxn(blocks)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		encTexts := encodingTexts()
		shadowLinks := getShadowLinks(ctx, zid, zn.InhMeta.GetDefault(meta.KeyBoxNumber, ""), ucGetAllZettel)

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
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







-
-
+
+

-
-
+
+
+

-
-
+
+
+

-
+
+


-
-
+
+









-
+







		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	})
}

func (wui *WebUI) getLocalExtQueryLinks(ucGetReferences usecase.GetReferences, zn *ast.ZettelNode) (locLinks, extLinks, queries *sx.Pair) {
	locRefs, extRefs, queryRefs := ucGetReferences.RunByState(zn)
func (wui *WebUI) getLocalExtQueryLinks(ucGetReferences usecase.GetReferences, blocks *sx.Pair) (locLinks, extLinks, queries *sx.Pair) {
	locRefs, extRefs, queryRefs := ucGetReferences.RunByState(blocks)
	var lbLoc, lbQueries, lbExt sx.ListBuilder
	for _, ref := range locRefs {
		lbLoc.Add(sx.MakeString(ref.String()))
	for ref := range locRefs.Pairs() {
		_, value := zsx.GetReference(ref.Head())
		lbLoc.Add(sx.MakeString(value))
	}
	for _, ref := range extRefs {
		lbExt.Add(sx.MakeString(ref.String()))
	for ref := range extRefs.Pairs() {
		_, value := zsx.GetReference(ref.Head())
		lbExt.Add(sx.MakeString(value))
	}
	for _, ref := range queryRefs {
	for ref := range queryRefs.Pairs() {
		_, value := zsx.GetReference(ref.Head())
		lbQueries.Add(
			sx.Cons(
				sx.MakeString(ref.Value),
				sx.MakeString(wui.NewURLBuilder('h').AppendQuery(ref.Value).String())))
				sx.MakeString(value),
				sx.MakeString(wui.NewURLBuilder('h').AppendQuery(value).String())))
	}
	return lbLoc.List(), lbExt.List(), lbQueries.List()
}

func createUnlinkedQuery(zid id.Zid, phrase string) *query.Query {
	var sb strings.Builder
	sb.Write(zid.Bytes())
	sb.WriteByte(' ')
	sb.WriteString(api.UnlinkedDirective)
	for _, word := range zerostrings.MakeWords(phrase) {
	for word := range zerostrings.SplitWordSeq(phrase) {
		sb.WriteByte(' ')
		sb.WriteString(api.PhraseDirective)
		sb.WriteByte(' ')
		sb.WriteString(word)
	}
	sb.WriteByte(' ')
	sb.WriteString(api.OrderDirective)
Changes to internal/web/adapter/webui/get_zettel.go.
20
21
22
23
24
25
26

27
28
29
30
31
32
33
34
35
20
21
22
23
24
25
26
27
28

29
30
31
32
33
34
35







+

-







	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"
	"t73f.de/r/zsc/sz"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/usecase"
)

// MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel".
52
53
54
55
56
57
58
59

60
61
62
63
64
65
66
67
68

69
70
71
72
73
74
75
52
53
54
55
56
57
58

59
60
61
62
63
64
65
66
67

68
69
70
71
72
73
74
75







-
+








-
+







			wui.reportError(ctx, w, err)
			return
		}

		zettelLang := wui.getConfig(ctx, zn.InhMeta, meta.KeyLang)
		enc := wui.getSimpleHTMLEncoder(zettelLang)
		metaObj := enc.MetaSxn(zn.InhMeta)
		content, endnotes, err := enc.BlocksSxn(&zn.BlocksAST)
		content, endnotes, err := enc.BlocksSxn(zn.Blocks)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		user := user.GetCurrentUser(ctx)
		getTextTitle := wui.makeGetTextTitle(ctx, getZettel)

		title := ast.NormalizedSpacedText(zn.InhMeta.GetTitle())
		title := sz.NormalizedSpacedText(zn.InhMeta.GetTitle())
		env, rb := wui.createRenderEnvironment(ctx, "zettel", zettelLang, title, user)
		rb.bindSymbol(symMetaHeader, metaObj)
		rb.bindString("heading", sx.MakeString(title))
		if role, found := zn.InhMeta.Get(meta.KeyRole); found && role != "" {
			rb.bindString(
				"role-url",
				sx.MakeString(wui.NewURLBuilder('h').AppendQuery(
Changes to internal/web/adapter/webui/htmlgen.go.
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
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







-









-




-




-








-
-
-
-
+
+
+
+



-
+

-
-
-
-
-
-
+
+
+
+
+
+





-
-
+
+



-
+







import (
	"maps"
	"net/url"
	"slices"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zero/set"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/encoder"
)

// Builder allows to build new URLs for the web service.
type urlBuilder interface {
	GetURLPrefix() string
	NewURLBuilder(key byte) *api.URLBuilder
}

type htmlGenerator struct {
	tx    encoder.SzTransformer
	th    *shtml.Evaluator
	lang  string
	symAt *sx.Symbol
}

func (wui *WebUI) createGenerator(builder urlBuilder, lang string) *htmlGenerator {
	th := shtml.NewEvaluator(1)

	findA := func(obj sx.Object) (attr, assoc, rest *sx.Pair) {
		pair, isPair := sx.GetPair(obj)
		if !isPair || !shtml.SymA.IsEqual(pair.Car()) {
			return nil, nil, nil
	findA := func(obj sx.Object) (assoc, rest *sx.Pair) {
		pair, isTag := sx.GetPair(obj)
		if !isTag || !shtml.SymA.IsEqual(pair.Car()) {
			return nil, nil
		}
		rest = pair.Tail()
		if rest == nil {
			return nil, nil, nil
			return nil, nil
		}
		objA := rest.Car()
		attr, isPair = sx.GetPair(objA)
		if !isPair || !sxhtml.SymAttr.IsEqual(attr.Car()) {
			return nil, nil, nil
		}
		return attr, attr.Tail(), rest.Tail()
		if attr, isAssoc := sx.GetPair(rest.Car()); isAssoc {
			if _, isAttr := sx.GetPair(attr.Car()); isAttr {
				return attr, rest.Tail()
			}
		}
		return nil, nil
	}

	rebindWrap(th, zsx.SymLink, func(args sx.Vector, env *shtml.Environment, prevFn shtml.EvalFn) sx.Object {
		refSym, _ := shtml.GetReference(args[1], env)
		obj := prevFn(args, env)
		attr, assoc, rest := findA(obj)
		if attr == nil {
		assoc, rest := findA(obj)
		if assoc == nil {
			return obj
		}
		if zsx.SymRefStateExternal.IsEqual(refSym) {
			a := zsx.GetAttributes(attr)
			a := zsx.GetAttributes(assoc)
			a = a.Set("target", "_blank")
			a = a.Add("rel", "external").Add("rel", "noreferrer")
			return rest.Cons(shtml.EvaluateAttributes(a)).Cons(shtml.SymA)
		}
		hrefP := assoc.Assoc(shtml.SymAttrHref)
		if hrefP == nil {
			return obj
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
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







-
+















-
+




-
+










-
+


+
+
+
+
-
+












-
+




-







			if err == nil {
				u = u.SetZid(zid)
			}
			if hasFragment {
				u = u.SetFragment(fragment)
			}
			assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String())))
			return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
			return rest.Cons(assoc).Cons(shtml.SymA)

		case sz.SymRefStateQuery:
			ur, err := url.Parse(href.GetValue())
			if err != nil {
				return obj
			}
			urlQuery := ur.Query()
			if !urlQuery.Has(api.QueryKeyQuery) {
				return obj
			}
			u := builder.NewURLBuilder('h')
			if q := urlQuery.Get(api.QueryKeyQuery); q != "" {
				u = u.AppendQuery(q)
			}
			assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String())))
			return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
			return rest.Cons(assoc).Cons(shtml.SymA)

		case sz.SymRefStateBased:
			u := builder.NewURLBuilder('/')
			assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String()+href.GetValue()[1:])))
			return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
			return rest.Cons(assoc).Cons(shtml.SymA)
		}
		return obj
	})

	rebind(th, zsx.SymEmbed, func(obj sx.Object) sx.Object {
		pair, isPair := sx.GetPair(obj)
		if !isPair || !shtml.SymIMG.IsEqual(pair.Car()) {
			return obj
		}
		attr, isPair := sx.GetPair(pair.Tail().Car())
		if !isPair || !sxhtml.SymAttr.IsEqual(attr.Car()) {
		if !isPair {
			return obj
		}
		_, isAttr := sx.GetPair(attr.Car())
		if !isAttr {
			return obj
		}
		srcP := attr.Tail().Assoc(shtml.SymAttrSrc)
		srcP := attr.Assoc(shtml.SymAttrSrc)
		if srcP == nil {
			return obj
		}
		src, isString := sx.GetString(srcP.Cdr())
		if !isString {
			return obj
		}
		zid, err := id.Parse(src.GetValue())
		if err != nil {
			return obj
		}
		u := builder.NewURLBuilder('z').SetZid(zid)
		imgAttr := attr.Tail().Cons(sx.Cons(shtml.SymAttrSrc, sx.MakeString(u.String()))).Cons(sxhtml.SymAttr)
		imgAttr := attr.Tail().Cons(sx.Cons(shtml.SymAttrSrc, sx.MakeString(u.String())))
		return pair.Tail().Tail().Cons(imgAttr).Cons(shtml.SymIMG)
	})

	return &htmlGenerator{
		tx:   encoder.NewSzTransformer(),
		th:   th,
		lang: lang,
	}
}

func rebind(ev *shtml.Evaluator, sym *sx.Symbol, fn func(sx.Object) sx.Object) {
	prevFn := ev.ResolveBinding(sym)
185
186
187
188
189
190
191
192

193
194
195
196
197
198
199
184
185
186
187
188
189
190

191
192
193
194
195
196
197
198







-
+








var mapMetaKey = map[string]string{
	meta.KeyCopyright: "copyright",
	meta.KeyLicense:   "license",
}

func (g *htmlGenerator) MetaSxn(m *meta.Meta) *sx.Pair {
	tm := g.tx.GetMeta(m)
	tm := ast.GetMetaSz(m)
	env := shtml.MakeEnvironment(g.lang)
	hm, err := g.th.Evaluate(tm, &env)
	if err != nil {
		return nil
	}

	ignore := set.New(meta.KeyTitle, meta.KeyLang)
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
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







-
-
+
+


-

-
+






-
+
-

-
+





	metaTags := sb.String()
	if len(metaTags) == 0 {
		return nil
	}
	return g.th.EvaluateMeta(zsx.Attributes{"name": "keywords", "content": metaTags})
}

func (g *htmlGenerator) BlocksSxn(bs *ast.BlockSlice) (content, endnotes *sx.Pair, _ error) {
	if bs == nil || len(*bs) == 0 {
func (g *htmlGenerator) BlocksSxn(block *sx.Pair) (content, endnotes *sx.Pair, _ error) {
	if block == nil || block.Tail() == nil {
		return nil, nil, nil
	}
	sx := g.tx.GetSz(bs)
	env := shtml.MakeEnvironment(g.lang)
	sh, err := g.th.Evaluate(sx, &env)
	sh, err := g.th.Evaluate(block, &env)
	if err != nil {
		return nil, nil, err
	}
	return sh, shtml.Endnotes(&env), nil
}

func (g *htmlGenerator) nodeSxHTML(node ast.Node) *sx.Pair {
func (g *htmlGenerator) szToSxHTML(node *sx.Pair) *sx.Pair {
	sz := g.tx.GetSz(node)
	env := shtml.MakeEnvironment(g.lang)
	sh, err := g.th.Evaluate(sz, &env)
	sh, err := g.th.Evaluate(node, &env)
	if err != nil {
		return nil
	}
	return sh
}
Changes to internal/web/adapter/webui/htmlmeta.go.
20
21
22
23
24
25
26

27
28
29
30
31
32
33
34
35
20
21
22
23
24
25
26
27
28

29
30
31
32
33
34
35







+

-








	"t73f.de/r/sx"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"
	"t73f.de/r/zsc/sz"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/usecase"
)

func (wui *WebUI) writeHTMLMetaValue(
	key string, value meta.Value,
	getTextTitle getTextTitleFunc,
48
49
50
51
52
53
54
55

56
57
58

59
60
61
62
63
64
65
48
49
50
51
52
53
54

55
56


57
58
59
60
61
62
63
64







-
+

-
-
+







	case meta.TypeString:
		return sx.MakeString(string(value))
	case meta.TypeTagSet:
		return wui.transformTagSet(key, value.AsSlice())
	case meta.TypeTimestamp:
		if ts, ok := value.AsTime(); ok {
			return sx.MakeList(
				sx.MakeSymbol("time"),
				sxhtml.MakeSymbol("time"),
				sx.MakeList(
					sxhtml.SymAttr,
					sx.Cons(sx.MakeSymbol("datetime"), sx.MakeString(ts.Format("2006-01-02T15:04:05"))),
					sx.Cons(sxhtml.MakeSymbol("datetime"), sx.MakeString(ts.Format("2006-01-02T15:04:05"))),
				),
				sx.MakeList(sxhtml.SymNoEscape, sx.MakeString(ts.Format("2006-01-02&nbsp;15:04:05"))),
			)
		}
		return sx.Nil()
	case meta.TypeURL:
		return wui.url2html(sx.MakeString(string(value)))
80
81
82
83
84
85
86
87

88
89
90

91
92
93
94
95
96
97
79
80
81
82
83
84
85

86
87
88

89
90
91
92
93
94
95
96







-
+


-
+







	switch {
	case found > 0:
		ub := wui.NewURLBuilder('h').SetZid(zid)
		attrs := sx.Nil()
		if title != "" {
			attrs = attrs.Cons(sx.Cons(shtml.SymAttrTitle, sx.MakeString(title)))
		}
		attrs = attrs.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(ub.String()))).Cons(sxhtml.SymAttr)
		attrs = attrs.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(ub.String())))
		return sx.Nil().Cons(sx.MakeString(zid.String())).Cons(attrs).Cons(shtml.SymA)
	case found == 0:
		return sx.MakeList(sx.MakeSymbol("s"), text)
		return sx.MakeList(sxhtml.MakeSymbol("s"), text)
	default: // case found < 0:
		return text
	}
}

var space = sx.MakeString(" ")

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
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







-
-
-
+
-















-
+


	}
	return buildHref(ub, text)
}

func buildHref(ub *api.URLBuilder, text string) *sx.Pair {
	return sx.MakeList(
		shtml.SymA,
		sx.MakeList(
			sxhtml.SymAttr,
			sx.Cons(shtml.SymAttrHref, sx.MakeString(ub.String())),
		sx.MakeList(sx.Cons(shtml.SymAttrHref, sx.MakeString(ub.String()))),
		),
		sx.MakeString(text),
	)
}

type getTextTitleFunc func(id.Zid) (string, int)

func (wui *WebUI) makeGetTextTitle(ctx context.Context, getZettel usecase.GetZettel) getTextTitleFunc {
	return func(zid id.Zid) (string, int) {
		z, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			if errors.Is(err, &box.ErrNotAllowed{}) {
				return "", -1
			}
			return "", 0
		}
		return ast.NormalizedSpacedText(z.Meta.GetTitle()), 1
		return sz.NormalizedSpacedText(z.Meta.GetTitle()), 1
	}
}
Changes to internal/web/adapter/webui/lists.go.
18
19
20
21
22
23
24
25
26
27
28
29

30
31
32
33
34
35
36
37
38
18
19
20
21
22
23
24

25
26
27
28
29
30

31
32
33
34
35
36
37







-




+

-







	"net/http"
	"net/url"
	"slices"
	"strconv"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/evaluator"
	"zettelstore.de/z/internal/usecase"
	"zettelstore.de/z/internal/web/adapter"
)

// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML.
70
71
72
73
74
75
76
77

78
79
80
81
82
83
84
69
70
71
72
73
74
75

76
77
78
79
80
81
82
83







-
+








		userLang := wui.getUserLang(ctx)

		var content, endnotes *sx.Pair
		numEntries := 0
		if bn, cnt := evaluator.QueryAction(ctx, q, metaSeq); bn != nil {
			enc := wui.getSimpleHTMLEncoder(userLang)
			content, endnotes, err = enc.BlocksSxn(&ast.BlockSlice{bn})
			content, endnotes, err = enc.BlocksSxn(zsx.MakeBlock(bn))
			if err != nil {
				wui.reportError(ctx, w, err)
				return
			}
			numEntries = cnt
		}

171
172
173
174
175
176
177
178
179
180

181
182
183
184
185
186
187
188
170
171
172
173
174
175
176



177

178
179
180
181
182
183
184







-
-
-
+
-







	}
	return withZettel, withoutZettel
}

func (wui *WebUI) prependZettelLink(sxZtl *sx.Pair, name string, u *api.URLBuilder) *sx.Pair {
	link := sx.MakeList(
		shtml.SymA,
		sx.MakeList(
			sxhtml.SymAttr,
			sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String())),
		sx.MakeList(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String()))),
		),
		sx.MakeString(name),
	)
	if sxZtl != nil {
		sxZtl = sxZtl.Cons(sx.MakeString(", "))
	}
	return sxZtl.Cons(link)
}
Changes to internal/web/adapter/webui/template.go.
27
28
29
30
31
32
33


34
35
36
37
38
39
40
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42







+
+







	"t73f.de/r/sx/sxeval"
	"t73f.de/r/sx/sxreader"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/auth/user"
	"zettelstore.de/z/internal/box"
	"zettelstore.de/z/internal/collect"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/logging"
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
137
138
139
140
141
142
143

144
145
146
147
148
149
150







-








func (wui *WebUI) url2html(text sx.String) sx.Object {
	if u, errURL := url.Parse(text.GetValue()); errURL == nil {
		if us := u.String(); us != "" {
			return sx.MakeList(
				shtml.SymA,
				sx.MakeList(
					sxhtml.SymAttr,
					sx.Cons(shtml.SymAttrHref, sx.MakeString(us)),
					sx.Cons(shtml.SymAttrTarget, sx.MakeString("_blank")),
					sx.Cons(shtml.SymAttrRel, sx.MakeString("external noreferrer")),
				),
				text)
		}
	}
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
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







-
+
















-
-
+
+














-
-
-
+
+
+
+
+


-
+










-
+












-
+







}
func (wui *WebUI) bindQueryURL(rb *renderBinder, strZid, symName, directive string) {
	rb.bindString(symName,
		sx.MakeString(wui.NewURLBuilder('h').AppendQuery(strZid+" "+directive+" "+api.DirectedDirective).String()))
}

func (wui *WebUI) buildListsMenuSxn(ctx context.Context, lang string) *sx.Pair {
	var zn *ast.ZettelNode
	var zn *ast.Zettel
	if menuZid, err := id.Parse(wui.getConfig(ctx, nil, config.KeyListsMenuZettel)); err == nil {
		if zn, err = wui.evalZettel.Run(ctx, menuZid, ""); err != nil {
			zn = nil
		}
	}
	if zn == nil {
		ctx = box.NoEnrichContext(ctx)
		ztl, err := wui.box.GetZettel(ctx, id.ZidTOCListsMenu)
		if err != nil {
			return nil
		}
		zn = wui.evalZettel.RunZettel(ctx, ztl, "")
	}

	htmlgen := wui.getSimpleHTMLEncoder(lang)
	var lb sx.ListBuilder
	for _, ln := range collect.Order(zn) {
		lb.Add(htmlgen.nodeSxHTML(ln))
	for ln := range collect.Order(zn.Blocks).Pairs() {
		lb.Add(htmlgen.szToSxHTML(ln.Head()))
	}
	return lb.List()
}

func (wui *WebUI) fetchNewTemplatesSxn(ctx context.Context, user *meta.Meta) *sx.Pair {
	if !wui.canCreate(ctx, user) {
		return nil
	}
	ctx = box.NoEnrichContext(ctx)
	menu, err := wui.box.GetZettel(ctx, id.ZidTOCNewTemplate)
	if err != nil {
		return nil
	}
	var lb sx.ListBuilder
	for _, ln := range collect.Order(parser.ParseZettel(ctx, menu, "", wui.rtConfig)) {
		ref := ln.Ref
		if !ref.IsZettel() {
	zn := parser.ParseZettel(ctx, menu, "", wui.rtConfig)
	for ln := range collect.Order(zn.Blocks).Pairs() {
		_, ref, _ := zsx.GetLink(ln.Head())
		sym, val := zsx.GetReference(ref)
		if !sz.SymRefStateZettel.IsEqualSymbol(sym) {
			continue
		}
		zid, err2 := id.Parse(ref.URL.Path)
		zid, err2 := id.Parse(val)
		if err2 != nil {
			continue
		}
		z, err2 := wui.box.GetZettel(ctx, zid)
		if err2 != nil {
			continue
		}
		if !wui.policy.CanRead(user, z.Meta) {
			continue
		}
		text := sx.MakeString(ast.NormalizedSpacedText(z.Meta.GetTitle()))
		text := sx.MakeString(sz.NormalizedSpacedText(z.Meta.GetTitle()))
		link := sx.MakeString(wui.NewURLBuilder('c').SetZid(zid).
			AppendKVQuery(queryKeyAction, valueActionNew).String())

		lb.Add(sx.Cons(text, link))
	}
	return lb.List()
}

func (wui *WebUI) calculateFooterSxn(ctx context.Context) *sx.Pair {
	if footerZid, err := id.Parse(wui.getConfig(ctx, nil, config.KeyFooterZettel)); err == nil {
		if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil {
			htmlEnc := wui.getSimpleHTMLEncoder(wui.getConfig(ctx, zn.InhMeta, meta.KeyLang)).SetUnique("footer-")
			if content, endnotes, err3 := htmlEnc.BlocksSxn(&zn.BlocksAST); err3 == nil {
			if content, endnotes, err3 := htmlEnc.BlocksSxn(zn.Blocks); err3 == nil {
				if content != nil && endnotes != nil {
					content.LastPair().SetCdr(sx.Cons(endnotes, nil))
				}
				return content
			}
		}
	}
480
481
482
483
484
485
486
487

488
489
490
491
492
493
494
495
483
484
485
486
487
488
489

490

491
492
493
494
495
496
497







-
+
-







	if err != nil {
		return err
	}
	wui.logger.Debug("render", "page", pageObj)

	gen := sxhtml.NewGenerator().SetNewline()
	var sb bytes.Buffer
	_, err = gen.WriteHTML(&sb, pageObj)
	if err = gen.WriteHTML(&sb, pageObj); err != nil {
	if err != nil {
		return err
	}
	wui.prepareAndWriteHeader(w, code)
	if _, err = w.Write(sb.Bytes()); err != nil {
		wui.logger.Error("Unable to write HTML via template", "err", err)
	}
	return nil // No error reporting, since we do not know what happended during write to client.
Changes to internal/web/adapter/webui/webui.go.
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
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







-
-
-















-
+






	return wui.policy.CanRefresh(user)
}

func (wui *WebUI) getSimpleHTMLEncoder(lang string) *htmlGenerator {
	return wui.createGenerator(wui, lang)
}

// GetURLPrefix returns the configured URL prefix of the web server.
func (wui *WebUI) GetURLPrefix() string { return wui.ab.GetURLPrefix() }

// NewURLBuilder creates a new URL builder object with the given key.
func (wui *WebUI) NewURLBuilder(key byte) *api.URLBuilder { return wui.ab.NewURLBuilder(key) }

func (wui *WebUI) clearToken(ctx context.Context, w http.ResponseWriter) context.Context {
	return wui.ab.ClearToken(ctx, w)
}

func (wui *WebUI) setToken(w http.ResponseWriter, token []byte) {
	wui.ab.SetToken(w, token, wui.tokenLifetime)
}

func (wui *WebUI) prepareAndWriteHeader(w http.ResponseWriter, statusCode int) {
	h := adapter.PrepareHeader(w, "text/html; charset=utf-8")
	h.Set("Content-Security-Policy", "default-src 'self'; img-src * data:; style-src 'self' 'unsafe-inline'")
	h.Set("Permissions-Policy", "payment=(), interest-cohort=()")
	h.Set("Referrer-Policy", "no-referrer")
	h.Set("Referrer-Policy", "same-origin")
	h.Set("X-Content-Type-Options", "nosniff")
	if !wui.debug {
		h.Set("X-Frame-Options", "sameorigin")
	}
	w.WriteHeader(statusCode)
}
Changes to internal/web/server/http.go.
31
32
33
34
35
36
37

38
39
40
41
42
43
44
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45







+







)

type webServer struct {
	log              *slog.Logger
	baseURL          string
	httpServer       httpServer
	router           httpRouter
	cop              *http.CrossOriginProtection
	persistentCookie bool
	secureCookie     bool
}

// ConfigData contains the data needed to configure a server.
type ConfigData struct {
	Log              *slog.Logger
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
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







+


















-
+











-
+
+
+
+


-
+
+
+
+






-
-
-

-
+











-
+







-
+







}

// New creates a new web server.
func New(sd ConfigData) Server {
	srv := webServer{
		log:              sd.Log,
		baseURL:          sd.BaseURL,
		cop:              http.NewCrossOriginProtection(),
		persistentCookie: sd.PersistentCookie,
		secureCookie:     sd.SecureCookie,
	}

	rd := routerData{
		log:            sd.Log,
		urlPrefix:      sd.URLPrefix,
		maxRequestSize: sd.MaxRequestSize,
		auth:           sd.Auth,
		loopbackIdent:  sd.LoopbackIdent,
		loopbackZid:    sd.LoopbackZid,
		profiling:      sd.Profiling,
	}
	srv.router.initializeRouter(rd)

	mwReqID := reqid.Config{WithContext: true}
	mwLogReq := logging.ReqConfig{
		Logger: sd.Log, Level: slog.LevelDebug,
		Message: "ServeHTTP", WithRequestID: true, WithRemote: true}
		Message: "ServeHTTP", WithRequestID: true, WithRemote: true, WithHeaders: true}
	mwLogResp := logging.RespConfig{Logger: sd.Log, Level: slog.LevelDebug,
		Message: "/ServeHTTP", WithRequestID: true}
	mw := middleware.NewChain(mwReqID.Build(), mwLogReq.Build(), mwLogResp.Build())

	srv.httpServer.initializeHTTPServer(sd.ListenAddr, middleware.Apply(mw, &srv.router))
	return &srv
}

func (srv *webServer) Handle(pattern string, handler http.Handler) {
	srv.router.Handle(pattern, handler)
}
func (srv *webServer) AddListRoute(key byte, method Method, handler http.Handler) {
func (srv *webServer) AddListRoute(isAPI bool, key byte, method Method, handler http.Handler) {
	if !isAPI {
		handler = srv.cop.Handler(handler)
	}
	srv.router.addListRoute(key, method, handler)
}
func (srv *webServer) AddZettelRoute(key byte, method Method, handler http.Handler) {
func (srv *webServer) AddZettelRoute(isAPI bool, key byte, method Method, handler http.Handler) {
	if !isAPI {
		handler = srv.cop.Handler(handler)
	}
	srv.router.addZettelRoute(key, method, handler)
}
func (srv *webServer) SetUserRetriever(ur UserRetriever) {
	srv.router.ur = ur
}

func (srv *webServer) GetURLPrefix() string {
	return srv.router.urlPrefix
}
func (srv *webServer) NewURLBuilder(key byte) *api.URLBuilder {
	return api.NewURLBuilder(srv.GetURLPrefix(), key)
	return api.NewURLBuilder(srv.router.urlPrefix, key)
}
func (srv *webServer) NewURLBuilderAbs(key byte) *api.URLBuilder {
	return api.NewURLBuilder(srv.baseURL, key)
}

const sessionName = "zsession"

func (srv *webServer) SetToken(w http.ResponseWriter, token []byte, d time.Duration) {
	cookie := http.Cookie{
		Name:     sessionName,
		Value:    string(token),
		Path:     srv.GetURLPrefix(),
		Path:     srv.router.urlPrefix,
		Secure:   srv.secureCookie,
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
	}
	if srv.persistentCookie && d > 0 {
		cookie.Expires = time.Now().Add(d).Add(30 * time.Second).UTC()
	}
	srv.log.Debug("SetToken", "token", token)
	srv.log.Debug("SetToken", "token", cookie.Value)
	if v := cookie.String(); v != "" {
		w.Header().Add("Set-Cookie", v)
		w.Header().Add("Cache-Control", `no-cache="Set-Cookie"`)
		w.Header().Add("Vary", "Cookie")
	}
}

Changes to internal/web/server/server.go.
41
42
43
44
45
46
47
48
49


50
51
52
53
54
55
56
57
58
59
60
61
62
41
42
43
44
45
46
47


48
49
50
51
52
53
54

55
56
57
58
59
60
61







-
-
+
+





-







	MethodDelete
	methodLAST // must always be the last one
)

// Router allows to state routes for various URL paths.
type Router interface {
	Handle(pattern string, handler http.Handler)
	AddListRoute(key byte, method Method, handler http.Handler)
	AddZettelRoute(key byte, method Method, handler http.Handler)
	AddListRoute(isAPI bool, key byte, method Method, handler http.Handler)
	AddZettelRoute(isAPI bool, key byte, method Method, handler http.Handler)
	SetUserRetriever(ur UserRetriever)
}

// Builder allows to build new URLs for the web service.
type Builder interface {
	GetURLPrefix() string
	NewURLBuilder(key byte) *api.URLBuilder
	NewURLBuilderAbs(key byte) *api.URLBuilder
}

// Auth is the authencation interface.
type Auth interface {
	// SetToken sends the token to the client.
Changes to tests/markdown_test.go.
17
18
19
20
21
22
23

24
25
26
27
28
29
30
31
32
33
34
35
36
17
18
19
20
21
22
23
24
25
26
27
28


29
30
31
32
33
34
35







+




-
-







	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"testing"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/ast"
	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/encoder"
	"zettelstore.de/z/internal/parser"
)

type markdownTestCase struct {
	Markdown  string `json:"markdown"`
	HTML      string `json:"html"`
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
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







-
-
-
+
+
+



-
-
+
+


-
+




-
+





-
+





-
+



-
+

-
+







-
+

-
+



-
+














-
+

-
-
+
+
-




	}
	var testcases []markdownTestCase
	if err = json.Unmarshal(content, &testcases); err != nil {
		panic(err)
	}

	for _, tc := range testcases {
		ast := createMDBlockSlice(tc.Markdown, config.NoHTML)
		testAllEncodings(t, tc, &ast)
		testZmkEncoding(t, tc, &ast)
		node := createMDBlockSlice(tc.Markdown)
		testAllEncodings(t, tc, node)
		testZmkEncoding(t, tc, node)
	}
}

func createMDBlockSlice(markdown string, hi config.HTMLInsecurity) ast.BlockSlice {
	return parser.Parse(input.NewInput([]byte(markdown)), nil, meta.ValueSyntaxMarkdown, hi)
func createMDBlockSlice(markdown string) *sx.Pair {
	return parser.Parse(input.NewInput([]byte(markdown)), nil, meta.ValueSyntaxMarkdown, nil)
}

func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
func testAllEncodings(t *testing.T, tc markdownTestCase, node *sx.Pair) {
	var sb strings.Builder
	testID := tc.Example*100 + 1
	for _, enc := range encodings {
		t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(*testing.T) {
			_, _ = encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN}).WriteBlocks(&sb, ast)
			_ = encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN}).WriteSz(&sb, node)
			sb.Reset()
		})
	}
}

func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
func testZmkEncoding(t *testing.T, tc markdownTestCase, node *sx.Pair) {
	zmkEncoder := encoder.Create(api.EncoderZmk, nil)
	var buf bytes.Buffer
	testID := tc.Example*100 + 1
	t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) {
		buf.Reset()
		_, _ = zmkEncoder.WriteBlocks(&buf, ast)
		_ = zmkEncoder.WriteSz(&buf, node)
		// gotFirst := buf.String()

		testID = tc.Example*100 + 2
		secondAst := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML)
		secondNode := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, nil)
		buf.Reset()
		_, _ = zmkEncoder.WriteBlocks(&buf, &secondAst)
		_ = zmkEncoder.WriteSz(&buf, secondNode)
		gotSecond := buf.String()

		// if gotFirst != gotSecond {
		// 	st.Errorf("\nCMD: %q\n1st: %q\n2nd: %q", tc.Markdown, gotFirst, gotSecond)
		// }

		testID = tc.Example*100 + 3
		thirdAst := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML)
		thirdNode := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, nil)
		buf.Reset()
		_, _ = zmkEncoder.WriteBlocks(&buf, &thirdAst)
		_ = zmkEncoder.WriteSz(&buf, thirdNode)
		gotThird := buf.String()

		if gotSecond != gotThird {
			st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird)
			st.Errorf("\ncmd: %q\n1st: %q\n2nd: %q", tc.Markdown, gotSecond, gotThird)
		}
	})
}

func TestAdditionalMarkdown(t *testing.T) {
	testcases := []struct {
		md  string
		exp string
	}{
		{`abc<br>def`, "abc``<br>``{=\"html\"}def"},
	}
	zmkEncoder := encoder.Create(api.EncoderZmk, nil)
	var sb strings.Builder
	for i, tc := range testcases {
		ast := createMDBlockSlice(tc.md, config.MarkdownHTML)
		node := createMDBlockSlice(tc.md)
		sb.Reset()
		_, _ = zmkEncoder.WriteBlocks(&sb, &ast)
		got := sb.String()
		_ = zmkEncoder.WriteSz(&sb, node)
		if got := sb.String(); got != tc.exp {
		if got != tc.exp {
			t.Errorf("%d: %q -> %q, but got %q", i, tc.md, tc.exp, got)
		}
	}
}
Changes to tests/naughtystrings_test.go.
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
19
20
21
22
23
24
25

26
27
28
29
30
31
32







-







	"os"
	"path/filepath"
	"testing"

	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsx/input"

	"zettelstore.de/z/internal/config"
	"zettelstore.de/z/internal/encoder"
	"zettelstore.de/z/internal/parser"

	_ "zettelstore.de/z/cmd"
)

// Test all parser / encoder with a list of "naughty strings", i.e. unusual strings
81
82
83
84
85
86
87
88

89
90

91
92
93
94
95
96
97
80
81
82
83
84
85
86

87
88

89

90
91
92
93
94
95







-
+

-
+
-






	}
	encs := getAllEncoder()
	if len(encs) == 0 {
		t.Fatal("no encoder found")
	}
	for _, s := range blns {
		for _, pinfo := range pinfos {
			bs := parser.Parse(input.NewInput([]byte(s)), &meta.Meta{}, pinfo.Name, config.NoHTML)
			node := parser.Parse(input.NewInput([]byte(s)), &meta.Meta{}, pinfo.Name, nil)
			for _, enc := range encs {
				_, err = enc.WriteBlocks(io.Discard, &bs)
				if err = enc.WriteSz(io.Discard, node); err != nil {
				if err != nil {
					t.Error(err)
				}
			}
		}
	}
}
Changes to tests/regression_test.go.
121
122
123
124
125
126
127
128

129
130
131
132
133

134
135
136
137
138
139
140
121
122
123
124
125
126
127

128
129
130
131
132

133
134
135
136
137
138
139
140







-
+




-
+







	u, err := url.Parse(p.Location())
	if err != nil {
		panic("Unable to parse URL '" + p.Location() + "': " + err.Error())
	}
	return u.Path[len(root):]
}

func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) {
func checkMetaFile(t *testing.T, resultName string, zn *ast.Zettel, enc api.EncodingEnum) {
	t.Helper()

	if enc := encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN}); enc != nil {
		var sf strings.Builder
		_, _ = enc.WriteMeta(&sf, zn.Meta)
		_ = enc.WriteMeta(&sf, zn.Meta)
		checkFileContent(t, resultName, sf.String())
		return
	}
	panic(fmt.Sprintf("Unknown writer encoding %q", enc))
}

func checkMetaBox(t *testing.T, p box.ManagedBox, wd, boxName string) {
Changes to tools/build/build.go.
59
60
61
62
63
64
65
66

67
68
69
70
71
72
73
59
60
61
62
63
64
65

66
67
68
69
70
71
72
73







-
+







const dirtySuffix = "-dirty"

func readFossilDirty() (string, error) {
	s, err := tools.ExecuteCommand(nil, "fossil", "status", "--differ")
	if err != nil {
		return "", err
	}
	for _, line := range zerostrings.SplitLines(s) {
	for line := range zerostrings.SplitLineSeq(s) {
		for _, prefix := range dirtyPrefixes {
			if strings.HasPrefix(line, prefix) {
				return dirtySuffix, nil
			}
		}
	}
	return "", nil
Changes to tools/tools.go.
90
91
92
93
94
95
96



97
98
99
100
101
102
103
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106







+
+
+







		return err
	}
	if err := checkStaticcheck(); err != nil {
		return err
	}
	if err := checkUnparam(forRelease); err != nil {
		return err
	}
	if err := checkDeadcode(); err != nil {
		return err
	}
	if err := checkRevive(); err != nil {
		return err
	}
	if err := checkErrCheck(); err != nil {
		return err
	}
114
115
116
117
118
119
120
121

122
123
124
125
126
127
128
117
118
119
120
121
122
123

124
125
126
127
128
129
130
131







-
+







	var env []string
	env = append(env, EnvDirectProxy...)
	env = append(env, EnvGoVCS...)
	args := []string{"test", pkg}
	args = append(args, testParams...)
	out, err := ExecuteCommand(env, "go", args...)
	if err != nil {
		for _, line := range zerostrings.SplitLines(out) {
		for line := range zerostrings.SplitLineSeq(out) {
			if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") {
				continue
			}
			fmt.Fprintln(os.Stderr, line)
		}
	}
	return err
156
157
158
159
160
161
162











163
164
165
166
167
168
169
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







+
+
+
+
+
+
+
+
+
+
+







func checkStaticcheck() error {
	out, err := ExecuteCommand(EnvGoVCS, "staticcheck", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some staticcheck problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkDeadcode() error {
	out, err := ExecuteCommand(EnvGoVCS, "deadcode", "./...")
	if err != nil || out != "" {
		fmt.Fprintln(os.Stderr, "Some deadcode problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkRevive() error {
	out, err := ExecuteCommand(EnvGoVCS, "revive", "./...")
	if err != nil || out != "" {
238
239
240
241
242
243
244

245
246




247
248
249
250
251
252
253
254
252
253
254
255
256
257
258
259


260
261
262
263
264
265
266
267
268
269
270
271







+
-
-
+
+
+
+








	out, err := ExecuteCommand(nil, "fossil", "extra")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'")
		return err
	}
	if len(out) > 0 {
		fmt.Fprint(os.Stderr, "Warning: unversioned file(s):")
		first := true
		for i, extra := range zerostrings.SplitLines(out) {
			if i > 0 {
		for extra := range zerostrings.SplitLineSeq(out) {
			if first {
				first = false
			} else {
				fmt.Fprint(os.Stderr, ",")
			}
			fmt.Fprintf(os.Stderr, " %q", extra)
		}
		fmt.Fprintln(os.Stderr)
	}
	return nil
}
Changes to www/changes.wiki.
1
2
3
4









5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20




+
+
+
+
+
+
+
+
+







<title>Change Log</title>

<a id="0_23"></a>
<h2>Changes for Version 0.23.0 (pending)</h2>
  *  SZ encoding of lists (ordered, unordered, and quotations) has been
     simplified by allowing only block elements. Previously, inline elements
     were also permitted to signal a compact list. However, this behavior is
     only relevant for HTML generation. Therefore, the detection of compact
     lists is now performed exclusively during HTML generation.
     (breaking)
  *  SZ encoding of BLOBs has be changed from <code>(BLOB attrs inlines syntax
     data)</code> to <code>(BLOG attrs syntax data inline ...)</code>.
     (breaking)

<a id="0_22"></a>
<h2>Changes for Version 0.22.0 (2025-07-07)</h2>
  *  Sx builtin <code>(bind-lookup ...)</code> is replaced with
     <code>(resolve-symbol ...)</code>. If you maintain your own Sx code to
     customize Zettelstore behaviour, you must update your code; otherwise it
     will break. If your code use <code>(ROLE-DEFAULT-action ...)</code>