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-10-20
16:46
Implement evaluators for links and verbatim-eval-draw as a sz-based walker ... (Leaf check-in: 9c53c97eb3 user: stern tags: trunk)
13:19
Remove almost all external ast references in evaluator ... (check-in: 845172de7d 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
66







+
+







-
+
-







		zettel.Zettel{
			Meta:    m,
			Content: zettel.NewContent(inp.Src[inp.Pos:]),
		},
		string(m.GetDefault(meta.KeySyntax, meta.DefaultSyntax)),
		nil,
	)
	parser.Clean(z.Blocks, false)
	parser.CleanAST(&z.BlocksAST, false)
	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/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-20251017161911-2f8291157520
	t73f.de/r/sxwebs v0.0.0-20251017162422-9f8d0174bc1f
	t73f.de/r/webs v0.0.0-20250930141330-11da1688d11c
	t73f.de/r/zero v0.0.0-20251017150835-a8859ec900ed
	t73f.de/r/zsc v0.0.0-20251020122118-ed75b99a947d
	t73f.de/r/zsx v0.0.0-20251020123811-57d9a3e9bbb9
)

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-20251017161911-2f8291157520 h1:XtiOULaPBescy4AI1MgrE/klJca1gtVDK9awrW6zjCc=
t73f.de/r/sx v0.0.0-20251017161911-2f8291157520/go.mod h1:qOhNY+S+pcINETviISh5YIEfNUMk81QTDij78fIVa+Q=
t73f.de/r/sxwebs v0.0.0-20251017162422-9f8d0174bc1f h1:E5UpgzY4MrjZiHqrctMLi/0xyonimxozFbhQN439Dd0=
t73f.de/r/sxwebs v0.0.0-20251017162422-9f8d0174bc1f/go.mod h1:FUgkRA2F031cYtEir1hnyFbZYpGbybaBPdqxWjpxxuI=
t73f.de/r/webs v0.0.0-20250930141330-11da1688d11c h1:6bHMcSJPl6mDWHZu2DuiC2FcoOt/+TxxvbIm5E63sPs=
t73f.de/r/webs v0.0.0-20250930141330-11da1688d11c/go.mod h1:G3vn6fCTvYWwQby5cVNmXzHlOGhgBDfbbo/9OgIxy0g=
t73f.de/r/zero v0.0.0-20251017150835-a8859ec900ed h1:Omh9Beo5pupvpC8yHnvlRlw1CBcWm8PrgWI0uhQ7Xk4=
t73f.de/r/zero v0.0.0-20251017150835-a8859ec900ed/go.mod h1:cNaE2o9BWPFqLkmDuYaWrMJQS7GOo+wwmB9y8VfAF6c=
t73f.de/r/zsc v0.0.0-20251020122118-ed75b99a947d h1:r6bbERNxOjkUMfRQL3lwhUkJobYR2kXh6pzDQ3tcFTg=
t73f.de/r/zsc v0.0.0-20251020122118-ed75b99a947d/go.mod h1:JVgWkDy24MTAgrYqImA2A9DltUV7YsyoKGsSFsti+Yo=
t73f.de/r/zsx v0.0.0-20251020123811-57d9a3e9bbb9 h1:bDGcg8PIKd9/YhVClY7K+IFwAJ0Qe4Qkx92yKi3r3DA=
t73f.de/r/zsx v0.0.0-20251020123811-57d9a3e9bbb9/go.mod h1:/wqH6y+cX2WnLYs8sqFydWPPiht90Jm1KTg12Rn33jU=
Changes to internal/ast/ast.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
10
11
12
13
14
15
16

17


















18
19
20
21
22
23
24







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







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

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

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

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/internal/zettel"
)

// ZettelNode 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
}

// Node is the interface, all nodes must implement.
type Node interface {
	WalkChildren(v Visitor)
}

// BlockNode is the interface that all block nodes must implement.
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
69
70
71
72
73
74
75

















-
-
-
-
-
-
-
-
-
-
	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
)

// 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)}}
}

// NormalizedSpacedText returns the given string, but normalize multiple spaces to one space.
func NormalizedSpacedText(s string) string { return strings.Join(strings.Fields(s), " ") }
Changes to internal/ast/block.go.
296
297
298
299
300
301
302
303

296
297
298
299
300
301
302

303







-
+
	Syntax      string
	Blob        []byte
}

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

// WalkChildren does nothing.
func (*BLOBNode) WalkChildren(Visitor) { /* No children*/ }
func (bn *BLOBNode) WalkChildren(v Visitor) { Walk(v, &bn.Description) }
Added internal/ast/sztrans/szenc.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 sztrans

import (
	"fmt"

	"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
	for _, item := range ln.Items {
		var itemObjs sx.ListBuilder
		for _, in := range item {
			itemObjs.Add(t.GetSz(in))
		}
		items.Add(zsx.MakeBlockList(itemObjs.List()))
	}
	return zsx.MakeList(mapGetS(mapNestedListKindS, ln.Kind), getAttributes(ln.Attrs), items.List())
}

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, sx.Nil(), 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 {
	return zsx.MakeBLOB(getAttributes(bn.Attrs), bn.Syntax, bn.Blob, t.getInlineList(bn.Description))
}

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 {
	return zsx.MakeEmbedBLOB(getAttributes(en.Attrs), en.Syntax, en.Blob, 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,
}

// 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()
		} 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))
}
Changes to internal/ast/sztrans/sztrans.go.
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
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







-
+




-










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

















-
-
+
+


-
-
-
+
+
-






-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+





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



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



-
-
+
+


-
+


-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+


-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-

-
+







// 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.
// abstract syntax tree and vice versa.
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{}

// MustGetBlock returns the sz representation as an AST BlockNode. Panic on error.
func MustGetBlock(pair *sx.Pair) ast.BlockNode {
	if pair == nil {
		return nil
	}
	var t transformer
	if obj := zsx.Walk(&t, pair, nil); !obj.IsNil() {
		if sxn, isNode := obj.(sxNode); isNode {
			if bn, ok := sxn.node.(ast.BlockNode); ok {
				return bn
			}
			panic(fmt.Sprintf("no BlockNode AST: %T/%v for %v", sxn.node, sxn.node, pair))
		}
		panic(fmt.Sprintf("no AST for %v: %v", pair, obj))
	}
	panic(fmt.Sprintf("error walking %v", pair))
}

// GetBlockSlice returns the sz representations as a AST BlockSlice
// GetBlockSlice returns the sz representation as an 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 {
func (t *transformer) VisitBefore(node *sx.Pair, _ *sx.Pair) (sx.Object, bool) {
	if sym, isSymbol := sx.GetSymbol(node.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
			if s := zsx.GetText(node); s != "" {
				return sxNode{&ast.TextNode{Text: s}}, 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())
			return handleLiteral(ast.LiteralCode, node)
		case zsx.SymLiteralComment:
			return handleLiteral(ast.LiteralComment, pair.Tail())
			return handleLiteral(ast.LiteralComment, node)
		case zsx.SymLiteralInput:
			return handleLiteral(ast.LiteralInput, pair.Tail())
			return handleLiteral(ast.LiteralInput, node)
		case zsx.SymLiteralMath:
			return handleLiteral(ast.LiteralMath, pair.Tail())
			return handleLiteral(ast.LiteralMath, node)
		case zsx.SymLiteralOutput:
			return handleLiteral(ast.LiteralOutput, pair.Tail())
			return handleLiteral(ast.LiteralOutput, node)
		case zsx.SymThematic:
			return sxNode{&ast.HRuleNode{Attrs: zsx.GetAttributes(pair.Tail().Head())}}, true
			return sxNode{&ast.HRuleNode{Attrs: zsx.GetAttributes(node.Tail().Head())}}, true
		case zsx.SymVerbatimComment:
			return handleVerbatim(ast.VerbatimComment, pair.Tail())
			return handleVerbatim(ast.VerbatimComment, node)
		case zsx.SymVerbatimEval:
			return handleVerbatim(ast.VerbatimEval, pair.Tail())
			return handleVerbatim(ast.VerbatimEval, node)
		case zsx.SymVerbatimHTML:
			return handleVerbatim(ast.VerbatimHTML, pair.Tail())
			return handleVerbatim(ast.VerbatimHTML, node)
		case zsx.SymVerbatimMath:
			return handleVerbatim(ast.VerbatimMath, pair.Tail())
			return handleVerbatim(ast.VerbatimMath, node)
		case zsx.SymVerbatimCode:
			return handleVerbatim(ast.VerbatimCode, pair.Tail())
			return handleVerbatim(ast.VerbatimCode, node)
		case zsx.SymVerbatimZettel:
			return handleVerbatim(ast.VerbatimZettel, pair.Tail())
			return handleVerbatim(ast.VerbatimZettel, node)
		}
	}
	return sx.Nil(), false
}

func handleLiteral(kind ast.LiteralKind, rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
func handleLiteral(kind ast.LiteralKind, node *sx.Pair) (sx.Object, bool) {
	if sym, attrs, content := zsx.GetLiteral(node); sym != 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 sxNode{&ast.LiteralNode{
			Kind:    kind,
			Attrs:   zsx.GetAttributes(attrs),
			Content: []byte(content)}}, true
	}
		}
	}
	return nil, false
}

func handleVerbatim(kind ast.VerbatimKind, rest *sx.Pair) (sx.Object, bool) {
	if rest != nil {
func handleVerbatim(kind ast.VerbatimKind, node *sx.Pair) (sx.Object, bool) {
	if sym, attrs, content := zsx.GetVerbatim(node); sym != 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 sxNode{&ast.VerbatimNode{
			Kind:    kind,
			Attrs:   zsx.GetAttributes(attrs),
			Content: []byte(content),
		}}, true
	}
		}
	}
	return nil, false
}

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

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

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)
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
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







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


-
-
+
+
-


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







				result = append(result, in)
			}
		}
	}
	return result
}

func handleHeading(rest *sx.Pair) sx.Object {
func handleHeading(node *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 level, attrs, inlines, slug, fragment := zsx.GetHeading(node); level > 0 && level < 6 {
				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()),
								}}
							}
		return sxNode{&ast.HeadingNode{
			Level:    level,
			Attrs:    zsx.GetAttributes(attrs),
			Slug:     slug,
			Fragment: fragment,
			Inlines:  collectInlines(inlines),
		}}
	}
						}
					}
				}
			}
		}
	}
	log.Println("HEAD", rest)
	return rest
	return node
}

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

	}
	return node
}
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 {
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
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







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

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

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

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

-
-
+
+

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


-
-
-
-
+
+
+
+

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

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











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


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


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


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














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




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


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

-
-
+
+


-
-
+


-
-
+
+
-


-
-
+
+


-
-
+








+





				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
			}
func handleDescription(node *sx.Pair) sx.Object {
	attrs, termsVals := zsx.GetDescription(node)

	var descs []ast.Description
	for curr := termsVals; 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
			}
		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
			}
		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)
			}
		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)
		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,
			}}
		}
		curr = curr.Tail()
	}
	if len(descs) > 0 {
		return sxNode{&ast.DescriptionListNode{
			Attrs:        zsx.GetAttributes(attrs),
			Descriptions: descs,
		}}
	}
	}
	log.Println("DESC", rest)
	return rest
	return node
}

func handleTable(rest *sx.Pair) sx.Object {
	if rest != nil {
		header := collectRow(rest.Head())
		cols := len(header)
func handleTable(node *sx.Pair) sx.Object {
	_, headerRow, rowList := zsx.GetTable(node)
	header := collectRow(headerRow)
	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
		}
	var rows []ast.TableRow
	for curr := range rowList.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,
		}}
	}
	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 handleCell(node *sx.Pair) sx.Object {
	attrs, inlines := zsx.GetCell(node)
	align := ast.AlignDefault
	if alignPair := attrs.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(inlines),
	}}
}

func handleRegion(kind ast.RegionKind, node *sx.Pair) sx.Object {
	if sym, attrs, blocks, inlines := zsx.GetRegion(node); sym != nil {
		return sxNode{&ast.RegionNode{
			Kind:    kind,
			Attrs:   zsx.GetAttributes(attrs),
			Blocks:  collectBlocks(blocks),
			Inlines: collectInlines(inlines),
		}}
	}
	return node
}

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

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

func handleLink(node *sx.Pair) sx.Object {
	if attrs, reference, inlines := zsx.GetLink(node); reference != nil {
		if ref := collectReference(reference); ref != nil {
			return sxNode{&ast.LinkNode{
				Attrs:   zsx.GetAttributes(attrs),
				Ref:     ref,
				Inlines: collectInlines(inlines),
			}}
		}
	}
	return node
}

func handleBLOB(rest *sx.Pair) sx.Object {
	if rest != nil {
func handleEmbed(node *sx.Pair) sx.Object {
	if attrs, reference, syntax, inlines := zsx.GetEmbed(node); reference != nil {
		attrs := zsx.GetAttributes(rest.Head())
		if curr := rest.Tail(); curr != nil {
			ins := collectInlines(curr.Head())
		if ref := collectReference(reference); ref != nil {
			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(),
			return sxNode{&ast.EmbedRefNode{
				Attrs:   zsx.GetAttributes(attrs),
				Ref:     ref,
				Syntax:  syntax,
								Blob:        []byte(blob.GetValue()),
							}}

						}
					}
				}
			}
		}
				Inlines: collectInlines(inlines),
			}}
		}
	}
	return node
}

func handleEmbedBLOB(node *sx.Pair) sx.Object {
	if attrs, syntax, data, inlines := zsx.GetEmbedBLOB(node); data != nil {
		return sxNode{&ast.EmbedBLOBNode{
			Attrs:   zsx.GetAttributes(attrs),
			Syntax:  syntax,
			Blob:    data,
			Inlines: collectInlines(inlines),
		}}
	}
	}
	log.Println("BLOB", rest)
	return rest
	return node
}

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 {
func collectReference(node *sx.Pair) *ast.Reference {
	if sym, val := zsx.GetReference(node); sym != 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]
		ref := ast.ParseReference(val)
		ref.State = mapRefState[sym]
						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 ref
				}
			}
		}
	}
	return nil
}

func handleCite(rest *sx.Pair) sx.Object {
func handleCite(node *sx.Pair) sx.Object {
	if rest != nil {
		attrs := zsx.GetAttributes(rest.Head())
	if attrs, key, inlines := zsx.GetCite(node); key != "" {
		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()),
				}}
			}
		return sxNode{&ast.CiteNode{
			Attrs:   zsx.GetAttributes(attrs),
			Key:     key,
			Inlines: collectInlines(inlines),
		}}
	}
		}
	}
	log.Println("CITE", rest)
	return rest
	return node
}

func handleMark(rest *sx.Pair) sx.Object {
func handleMark(node *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()),
							}}
						}
					}
				}
	if mark, slug, fragment, inlines := zsx.GetMark(node); mark != "" {
		return sxNode{&ast.MarkNode{
			Mark:     mark,
			Slug:     slug,
			Fragment: fragment,
			Inlines:  collectInlines(inlines),
		}}
	}
	return node
}

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

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

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

type sxNode struct {
	node ast.Node
}

func (sxNode) IsNil() bool        { return false }
func (sxNode) IsAtom() bool       { return true }
func (sxNode) IsTrue() 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()
}
Added internal/ast/zettel.go.


































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 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 provides the abstract syntax tree for parsed zettel content.
package ast

import (
	"t73f.de/r/sx"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"zettelstore.de/z/internal/zettel"
)

// Zettel is the root node of the abstract syntax tree.
// It is *not* part of the visitor pattern.
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.
	BlocksAST BlockSlice     // Zettel abstract syntax tree is a sequence of block nodes.
	Syntax    string         // Syntax / parser that produced the Ast
}
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) VisitBefore(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) VisitAfter(*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) VisitBefore(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) VisitAfter(*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
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







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







-
+








-
+


-
+







-
+





-
+

-
+

-
+




-
+







// 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 (
	"t73f.de/r/sx"
	"t73f.de/r/zsx"

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 {
	"zettelstore.de/z/internal/ast"
)

// 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 {
			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(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(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 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 zsx.SymEmbedBLOB:
				_, _, _, binlines := zsx.GetEmbedBLOBuncode(inl)
				result = firstInlineZettelLink(binlines)
			}
			if result != nil {
				return result
			}
		}
	}
	return nil
}

// OrderAST of internal links within the given zettel.
func OrderAST(bns *ast.BlockSlice) (result []*ast.LinkNode) {
	for _, bn := range *bns {
		ln, ok := bn.(*ast.NestedListNode)
		if !ok {
			continue
		}
		switch ln.Kind {
		case ast.NestedListOrdered, ast.NestedListUnordered:
			for _, is := range ln.Items {
				if ln := firstItemZettelLink(is); ln != nil {
				if ln := firstItemZettelLinkAST(is); ln != nil {
					result = append(result, ln)
				}
			}
		}
	}
	return result
}

func firstItemZettelLink(is ast.ItemSlice) *ast.LinkNode {
func firstItemZettelLinkAST(is ast.ItemSlice) *ast.LinkNode {
	for _, in := range is {
		if pn, ok := in.(*ast.ParaNode); ok {
			if ln := firstInlineZettelLink(pn.Inlines); ln != nil {
			if ln := firstInlineZettelLinkAST(pn.Inlines); ln != nil {
				return ln
			}
		}
	}
	return nil
}

func firstInlineZettelLink(is ast.InlineSlice) (result *ast.LinkNode) {
func firstInlineZettelLinkAST(is ast.InlineSlice) (result *ast.LinkNode) {
	for _, inl := range is {
		switch in := inl.(type) {
		case *ast.LinkNode:
			return in
		case *ast.EmbedRefNode:
			result = firstInlineZettelLink(in.Inlines)
			result = firstInlineZettelLinkAST(in.Inlines)
		case *ast.EmbedBLOBNode:
			result = firstInlineZettelLink(in.Inlines)
			result = firstInlineZettelLinkAST(in.Inlines)
		case *ast.CiteNode:
			result = firstInlineZettelLink(in.Inlines)
			result = firstInlineZettelLinkAST(in.Inlines)
		case *ast.FootnoteNode:
			// Ignore references in footnotes
			continue
		case *ast.FormatNode:
			result = firstInlineZettelLink(in.Inlines)
			result = firstInlineZettelLinkAST(in.Inlines)
		default:
			continue
		}
		if result != nil {
			return result
		}
	}
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
71

72
73
74
75
76
77
78
79







+





+





-
+


-
+

+
+
+

+
+
+
-
+









-
+









-
+






-
+







// 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"
	"zettelstore.de/z/internal/ast/sztrans"
)

// 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

	// WriteSz encodes  SZ represented zettel content.
	WriteSz(io.Writer, *sx.Pair) error

	// WiteBlocks encodes a block slice, i.e. the zettel content.
	//
	// This method is deprecated and will be removed, if all implementations
	// of WriteSz work correctly.
	WriteBlocks(io.Writer, *ast.BlockSlice) (int, error)
	WriteBlocks(io.Writer, *ast.BlockSlice) 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(),
			tx:   sztrans.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(),
			tx:   sztrans.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{trans: sztrans.NewSzTransformer()}
	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
63







-
-
+
+











-
-
+
+
+


			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, bs := parser.Parse(inp, m, tc.syntax)
		checkEncodings(t, testNum, node, false, tc.descr, tc.expect, "???")
		checkEncodingsAST(t, testNum+1000, bs, 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











-
+














-
-
+
+
















-
+











-
+











-
+















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












-
-
-
-
	},
	{
		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 syntax HTML",
		zmk:    "<h1>Hello</h1>\nWorld\n",
		syntax: meta.ValueSyntaxHTML,
		expect: expectMap{
			encoderHTML:  ``,
			encoderSz:    `(BLOCK)`,
			encoderSHTML: `()`,
			encoderText:  "",
			encoderZmk:   "",
		},
	},
	{
		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
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







+






-







+




















-
+





+
+
+
+
-
-
+
+
+
+
+




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


-
+







package encoder_test

import (
	"fmt"
	"strings"
	"testing"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxreader"
	"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
	syntax string
	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
		}
		bs := parser.Parse(inp, nil, meta.ValueSyntaxZmk, config.NoHTML)
		checkEncodings(t, testNum, bs, tc.inline, tc.descr, tc.expect, tc.zmk)
		node, bs := parser.Parse(inp, nil, syntax)
		parser.Clean(node, false)
		parser.CleanAST(&bs, false)
		checkEncodings(t, testNum, node, tc.inline, tc.descr, tc.expect, tc.zmk)
		checkEncodingsAST(t, testNum+1000, bs, tc.inline, tc.descr, tc.expect, tc.zmk)
		checkSz(t, testNum, bs, tc.inline, tc.descr)
	}
}

func checkEncodings(t *testing.T, testNum int, node *sx.Pair, isInline bool, descr string, expected expectMap, zmkDefault string) {
	for enc, exp := range expected {
		if enc == api.EncoderZmk {
			continue
		}
		encdr := encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN})
		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)
			continue
		}
		if enc == api.EncoderZmk && exp == useZmk {
			exp = zmkDefault
		}
		if got != exp {
			prefix := fmt.Sprintf("Test #%d", testNum)
			if d := descr; d != "" {
				prefix += "\nReason:   " + d
			}
			prefix += "\nMode:     " + mode(isInline)
			t.Errorf("%s\nEncoder:  %s\nExpected: %q\nGot:      %q", prefix, enc, exp, got)
		}
	}
}
func checkEncodings(t *testing.T, testNum int, bs ast.BlockSlice, isInline bool, descr string, expected expectMap, zmkDefault string) {
func checkEncodingsAST(t *testing.T, testNum int, bs ast.BlockSlice, 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 := encodeAST(encdr, bs)
		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)
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
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







-
+









-
+
-









+
+
+
+
+
-
+

-
+









		}
	}
}

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)
	exp, err := encodeAST(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 got := val.String(); exp != got {
	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, node *sx.Pair) (string, error) {
	var sb strings.Builder
	err := e.WriteSz(&sb, node)
	return sb.String(), err
}
func encode(e encoder.Encoder, bs ast.BlockSlice) (string, error) {
func encodeAST(e encoder.Encoder, bs ast.BlockSlice) (string, error) {
	var sb strings.Builder
	_, err := e.WriteBlocks(&sb, &bs)
	err := e.WriteBlocks(&sb, &bs)
	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
80
81
82







+
+


+




-
+






-
+

-
+

-
+






-
+
-
-
+

-
+






-
+






-
+




-
+







	"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"
	"zettelstore.de/z/internal/ast/sztrans"
)

// htmlEncoder contains all data needed for encoding.
type htmlEncoder struct {
	tx      SzTransformer
	tx      sztrans.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(sztrans.GetMetaSz(zn.InhMeta), &env)
	if err != nil {
		return 0, err
		return err
	}

	var isTitle ast.InlineSlice
	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)
	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.WriteInlines(&sb, &isTitle)
	} 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
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







-
+

-
+

-
+





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

-
+




-
+
-
-
+


-
+
-
-

-
+

	)

	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(sztrans.GetMetaSz(m), &env)
	if err != nil {
		return 0, err
		return err
	}
	gen := sxhtml.NewGenerator().SetNewline()
	return gen.WriteListHTML(w, hm)
}

// 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(node, &env)
	if err == nil {
		gen := sxhtml.NewGenerator()
		if err = gen.WriteListHTML(w, hobj); err != nil {
			return err
		}

		return gen.WriteHTML(w, shtml.Endnotes(&env))
	}
	return err
}

// WriteBlocks encodes a block slice.
func (he *htmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
func (he *htmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) error {
	env := shtml.MakeEnvironment(he.lang)
	hobj, err := he.th.Evaluate(he.tx.GetSz(bs), &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
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







+

+


+











-
-
-
+
+
+






-
+
-



-
+
+
+
+
+
+
+
+

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


-
-
-
+
+
+




-
-
+
+

-
+
-


-
-
+
+







-
-
+
+



-
+








-
+


-
+

-
+





-
+








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) {
	v := newMDVisitor(w, me.lang)
	v.acceptMeta(zn.InhMeta)
func (me *mdEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error {
	v := newMDVisitorAST(w, me.lang)
	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()
	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) {
func (*mdEncoder) WriteMeta(w io.Writer, m *meta.Meta) error {
	ew := newEncWriter(w)
	ew.WriteMeta(m)
	return ew.Flush()
}

// WriteSz encodes SZ represented zettel content.
func (me *mdEncoder) WriteSz(w io.Writer, node *sx.Pair) error {
	v := newMDVisitor(w, me.lang)
	zsx.WalkIt(&v, node, nil)
	return v.b.Flush()
}
	v.acceptMeta(m)
	length, err := v.b.Flush()
	return length, err

type mdVisitor struct {
	b            encWriter
	listInfo     []int
	listPrefix   string
	defLang      string
	quoteNesting uint
}

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

var symLang = sx.MakeSymbol("lang")

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()
		}
	}
	return v.defLang
}
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) 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) VisitBefore(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)

		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) VisitAfter(*sx.Pair, *sx.Pair) {}

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")
		}
		v.walk(bn.Head(), alst)
	}
}

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) visitReference(ref, inlines, alst *sx.Pair) {
	refState, val := zsx.GetReference(ref)
	if sz.SymRefStateQuery.IsEqualSymbol(refState) {
		v.walkList(inlines, alst)
	} else if inlines != nil {
		_ = v.b.WriteByte('[')
		v.walkList(inlines, alst)
		v.b.WriteStrings("](", val)
		_ = v.b.WriteByte(')')
	} else if isAutoLinkable(refState, val) {
		_ = v.b.WriteByte('<')
		v.b.WriteString(val)
		_ = v.b.WriteByte('>')
	} else {
		v.b.WriteStrings("[", val, "](", val, ")")
	}
}
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(node, alst *sx.Pair, delim1, delim2 string) {
	_, attrs, inlines := zsx.GetFormat(node)
	alst = v.setLanguage(alst, attrs)
	v.b.WriteString(delim1)
	v.walkList(inlines, alst)
	v.b.WriteString(delim2)
}
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++
	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) 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) acceptMeta(m *meta.Meta) {
	for key, val := range m.Computed() {
		v.b.WriteStrings(key, ": ", string(val), "\n")
func (v *mdVisitor) writeSpaces(n int) {
	for range n {
		v.b.WriteSpace()
	}
}

// WriteBlocks writes the content of a block slice to the writer.
func (me *mdEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newMDVisitor(w, me.lang)
func (me *mdEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) error {
	v := newMDVisitorAST(w, me.lang)
	ast.Walk(&v, bs)
	length, err := v.b.Flush()
	return v.b.Flush()
	return length, err
}

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

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

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

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

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

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

func (v *mdVisitor) Visit(node ast.Node) ast.Visitor {
func (v *mdVisitorAST) 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)
134
135
136
137
138
139
140
141

142
143
144
145
146
147
148
149
150

151
152
153
154
155
156
157
454
455
456
457
458
459
460

461
462
463
464
465
466
467
468
469

470
471
472
473
474
475
476
477







-
+








-
+







		v.visitLiteral(n)
	default:
		return v
	}
	return nil
}

func (v *mdVisitor) visitBlockSlice(bs *ast.BlockSlice) {
func (v *mdVisitorAST) visitBlockSlice(bs *ast.BlockSlice) {
	for i, bn := range *bs {
		if i > 0 {
			v.b.WriteString("\n\n")
		}
		ast.Walk(v, bn)
	}
}

func (v *mdVisitor) visitVerbatim(vn *ast.VerbatimNode) {
func (v *mdVisitorAST) 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++ {
172
173
174
175
176
177
178
179

180
181
182
183
184
185
186
492
493
494
495
496
497
498

499
500
501
502
503
504
505
506







-
+







		}
		v.b.WriteLn()
		v.writeSpaces(4)
		i = j - 1
	}
}

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

	first := true
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
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







-
+








-
+











-
+







		}
		first = false
		v.b.WriteString("> ")
		ast.Walk(v, &pn.Inlines)
	}
}

func (v *mdVisitor) visitHeading(hn *ast.HeadingNode) {
func (v *mdVisitorAST) 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) {
func (v *mdVisitorAST) 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) {
func (v *mdVisitorAST) 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()
		}
237
238
239
240
241
242
243
244

245
246
247
248
249
250
251
557
558
559
560
561
562
563

564
565
566
567
568
569
570
571







-
+







				}
			}
			ast.Walk(v, in)
		}
	}
}

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

	prefix := v.listPrefix
	v.listPrefix = "> "
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
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







-
+















-
+






-
+







-
+







-
+









-
+






-
+







			ast.Walk(v, in)
		}
	}

	v.listPrefix = prefix
}

func (v *mdVisitor) visitBreak(bn *ast.BreakNode) {
func (v *mdVisitorAST) visitBreak(bn *ast.BreakNode) {
	if bn.Hard {
		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 *mdVisitorAST) visitLink(ln *ast.LinkNode) {
	v.pushAttributes(ln.Attrs)
	defer v.popAttributes()

	v.writeReference(ln.Ref, ln.Inlines)
}

func (v *mdVisitor) visitEmbedRef(en *ast.EmbedRefNode) {
func (v *mdVisitorAST) visitEmbedRef(en *ast.EmbedRefNode) {
	v.pushAttributes(en.Attrs)
	defer v.popAttributes()

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

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

func isAutoLinkable(ref *ast.Reference) bool {
func isAutoLinkableAST(ref *ast.Reference) bool {
	if ref.State != ast.RefStateExternal || ref.URL == nil {
		return false
	}
	return ref.URL.Scheme != ""
}

func (v *mdVisitor) visitFormat(fn *ast.FormatNode) {
func (v *mdVisitorAST) visitFormat(fn *ast.FormatNode) {
	v.pushAttributes(fn.Attrs)
	defer v.popAttributes()

	switch fn.Kind {
	case ast.FormatEmph:
		_ = v.b.WriteByte('*')
		ast.Walk(v, &fn.Inlines)
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
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







-
+














-
+











-
+




		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("</mark>")
	default:
		ast.Walk(v, &fn.Inlines)
	}
}

func (v *mdVisitor) writeQuote(fn *ast.FormatNode) {
func (v *mdVisitorAST) writeQuote(fn *ast.FormatNode) {
	leftQ, rightQ, withNbsp := v.getQuotes()
	v.b.WriteString(leftQ)
	if withNbsp {
		v.b.WriteString("&nbsp;")
	}
	v.quoteNesting++
	ast.Walk(v, &fn.Inlines)
	v.quoteNesting--
	if withNbsp {
		v.b.WriteString("&nbsp;")
	}
	v.b.WriteString(rightQ)
}

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







+




-
+





-
+

-
+

-
+



-
+


+
-
+



-
+

-
+

-
+

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



-
+



-
+

+
-
+

	"io"

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

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

// shtmlEncoder contains all data needed for encoding.
type shtmlEncoder struct {
	tx   SzTransformer
	tx   sztrans.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(sztrans.GetMetaSz(zn.InhMeta), &env)
	if err != nil {
		return 0, err
		return err
	}
	contentSHTML, err := enc.th.Evaluate(enc.tx.GetSz(&zn.BlocksAST), &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(sztrans.GetMetaSz(m), &env)
	if err != nil {
		return 0, err
		return err
	}
	return sx.Print(w, metaSHTML)
	_, err = sx.Print(w, metaSHTML)
	return err
}

// 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(node, &env)
	if err != nil {
		return err
	}
	_, err = hval.Print(w)
	return err
}

// WriteBlocks writes a block slice to the writer
func (enc *shtmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
func (enc *shtmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) error {
	env := shtml.MakeEnvironment(enc.lang)
	hval, err := enc.th.Evaluate(enc.tx.GetSz(bs), &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.
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
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







+




-
+



-
+

-
-
+
+
+



-
-
+
+
+
+
+
+
+
+
+



-
-
+
+
+

import (
	"io"

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

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

// szEncoder contains all data needed for encoding.
type szEncoder struct {
	trans SzTransformer
	trans sztrans.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 := sztrans.GetMetaSz(zn.InhMeta)
	_, err := sx.MakeList(meta, content).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 := sztrans.GetMetaSz(m).Print(w)
	return err
}

// WriteSz encodes SZ represented zettel content.
func (*szEncoder) WriteSz(w io.Writer, node *sx.Pair) error {
	_, err := node.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)
func (enc *szEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) error {
	_, err := enc.trans.GetSz(bs).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
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







+

+









-
-
-
+
+
+

-
+
-



-
+









-
+
-











-



-
-
+
+

-
+
-



-
-
+
+

-
+
-


-
-
-
-
+
+
+
+
+

+
+
+




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








// 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) {
	v := newTextVisitor(w)
	_, _ = te.WriteMeta(&v.b, zn.InhMeta)
func (te *TextEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error {
	v := newTextVisitorAST(w)
	_ = te.WriteMeta(&v.b, zn.InhMeta)
	v.visitBlockSlice(&zn.BlocksAST)
	length, err := v.b.Flush()
	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()))
	}

}

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

// WriteInlines writes an inline slice to the writer
func (*TextEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newTextVisitor(w)
func (*TextEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) error {
	v := newTextVisitorAST(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 {
	b         encWriter
	inlinePos int
// WriteSz writes SZ encoded content to the writer.
func (*TextEncoder) WriteSz(w io.Writer, node *sx.Pair) error {
	v := newTextVisitor(w)
	v.walk(node, nil)
	return v.b.Flush()
}

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

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) walkList(lst, alst *sx.Pair) { zsx.WalkItList(v, lst, 0, alst) }
func (v *textVisitor) VisitBefore(node *sx.Pair, alst *sx.Pair) bool {
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		switch sym {
		case zsx.SymText:
			s := zsx.GetText(node)
			spaceFound := false
			for _, ch := range s {
				if input.IsSpace(ch) {
					if !spaceFound {
						v.b.WriteSpace()
						spaceFound = true

func (v *textVisitor) Visit(node ast.Node) ast.Visitor {
					}
					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

		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) VisitAfter(*sx.Pair, *sx.Pair) {}

// textVisitorAST writes the abstract syntax tree to an io.Writer.
type textVisitorAST struct {
	b         encWriter
	inlinePos int
}

func newTextVisitorAST(w io.Writer) textVisitorAST {
	return textVisitorAST{b: newEncWriter(w)}
}

func (v *textVisitorAST) 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
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
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







-
+





-
+









-
+













-
+










-
+






-
+






-
+







-
+














-
+




		if n.Kind != ast.LiteralComment {
			_, _ = v.b.Write(n.Content)
		}
	}
	return v
}

func (v *textVisitor) visitVerbatim(vn *ast.VerbatimNode) {
func (v *textVisitorAST) visitVerbatim(vn *ast.VerbatimNode) {
	if vn.Kind != ast.VerbatimComment {
		_, _ = v.b.Write(vn.Content)
	}
}

func (v *textVisitor) visitNestedList(ln *ast.NestedListNode) {
func (v *textVisitorAST) 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) {
func (v *textVisitorAST) 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 *textVisitorAST) visitTable(tn *ast.TableNode) {
	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)
	}
}

func (v *textVisitor) writeRow(row ast.TableRow) {
func (v *textVisitorAST) writeRow(row ast.TableRow) {
	for i, cell := range row {
		v.writePosChar(i, ' ')
		ast.Walk(v, &cell.Inlines)
	}
}

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

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

func (v *textVisitor) visitText(s string) {
func (v *textVisitorAST) 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))
	}
}

func (v *textVisitor) writePosChar(pos int, ch byte) {
func (v *textVisitorAST) writePosChar(pos int, ch byte) {
	if pos > 0 {
		_ = v.b.WriteByte(ch)
	}
}
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.
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
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







+

+

+









-
-
-
+
+
+






-
+
-



-
-
-
+
+
+
-
-
+


-
-
-
-
-
-
+
-
-
+

-
-
+
+
-


-

-
+
-
-
+
-
-




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


-
+

-
+
-


-

-
+

-
+

-
+


-
+

-
+

-
+

-
+


-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+




-
+

-
+

-
+

-
+






-
+







// 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/api"
	"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) {
	v := newZmkVisitor(w)
	v.acceptMeta(zn.InhMeta)
func (ze *zmkEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error {
	v := newZmkVisitorAST(w)
	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()
	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) VisitBefore(node *sx.Pair, alst *sx.Pair) bool {
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		switch sym {
		case zsx.SymText:
			v.writeText(zsx.GetText(node))
		case zsx.SymSoft:
			v.writeBreak(false)
		case zsx.SymHard:
			v.writeBreak(true)

		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.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)

		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 zsx.SymMark:
			v.visitMark(node, alst)

		default:
			return false
		}
		return true
	}
	return false
}
func (v *zmkVisitor) VisitAfter(*sx.Pair, *sx.Pair) {}

func (v *zmkVisitor) visitFormat(node *sx.Pair, alst *sx.Pair, delim string) {
	_, 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('|')
	}
	v.writeRef(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('|')
	}
	v.writeRef(ref)
	v.b.WriteString("}}")
	v.writeAttributes(attrs)
}

func (v *zmkVisitor) visitEndnote(node *sx.Pair, alst *sx.Pair) {
	attrs, inlines := zsx.GetEndnote(node)
	v.b.WriteString("[^")
	v.walkList(inlines, alst)
	_ = v.b.WriteByte(']')
	v.writeAttributes(attrs)
}

func (v *zmkVisitor) visitCite(node *sx.Pair, alst *sx.Pair) {
	attrs, key, inlines := zsx.GetCite(node)
	v.b.WriteStrings("[@", key)
	if inlines != nil {
		v.b.WriteSpace()
		v.walkList(inlines, alst)
	}
	_ = 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.b.WriteByte('|')
		v.walkList(inlines, alst)
	}
	_ = v.b.WriteByte(']')
}

func (v *zmkVisitor) writeText(text string) {
	last := 0
	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(text)-1 {
			s := text[i : i+2]
			if escapeSeqs.Contains(s) {
				v.b.WriteString(text[last:i])
				for j := range len(s) {
					v.b.WriteBytes('\\', s[j])
				}
				i++
				last = i + 1
				continue
			}
		}
	}
	v.b.WriteString(text[last:])
}
func (v *zmkVisitor) writeBreak(isHard bool) {
	if isHard {
		v.b.WriteString("\\\n")
	} else {
		v.b.WriteLn()
	}
	v.writePrefixSpaces()
}

func (v *zmkVisitor) writeAttributes(attrs *sx.Pair) {
	a := zsx.GetAttributes(attrs)
	if a.IsEmpty() {
		return
	}
	_ = v.b.WriteByte('{')
	for i, k := range a.Keys() {
		if i > 0 {
			v.b.WriteSpace()
		}
		if k == "-" {
			_ = v.b.WriteByte('-')
			continue
		}
		v.b.WriteString(k)
		if vl := a[k]; len(vl) > 0 {
			v.b.WriteString("=\"")
			v.writeEscaped(vl, '"')
			_ = v.b.WriteByte('"')
		}
	}
	_ = v.b.WriteByte('}')
}

func (v *zmkVisitor) writeRef(ref *sx.Pair) {
	refSym, refVal := zsx.GetReference(ref)
	if sz.SymRefStateBased.IsEqualSymbol(refSym) {
		_ = v.b.WriteByte('/')
	} else if sz.SymRefStateQuery.IsEqualSymbol(refSym) {
		v.b.WriteString(api.QueryPrefix)
	}
	v.b.WriteString(refVal)
}

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 (v *zmkVisitor) writePrefixSpaces() {
	if prefixLen := len(v.prefix); prefixLen > 0 {
		for i := 0; i <= prefixLen; i++ {
			v.b.WriteSpace()
		}
	}
}

// WriteBlocks writes the content of a block slice to the writer.
func (*zmkEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) error {
	v := newZmkVisitorAST(w)
	ast.Walk(&v, bs)
	return v.b.Flush()
}

// zmkVisitorAST writes the abstract syntax tree to an io.Writer.
type zmkVisitorAST struct {
	b       encWriter
	prefix  []byte
	inVerse bool
}

func newZmkVisitorAST(w io.Writer) zmkVisitorAST { return zmkVisitorAST{b: newEncWriter(w)} }

func (v *zmkVisitor) Visit(node ast.Node) ast.Visitor {
func (v *zmkVisitorAST) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		v.visitBlockSlice(n)
		v.visitBlockSliceAST(n)
	case *ast.InlineSlice:
		for i, in := range *n {
		for _, in := range *n {
			v.inlinePos = i
			ast.Walk(v, in)
		}
		v.inlinePos = 0
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
		v.visitVerbatimAST(n)
	case *ast.RegionNode:
		v.visitRegion(n)
		v.visitRegionAST(n)
	case *ast.HeadingNode:
		v.visitHeading(n)
		v.visitHeadingAST(n)
	case *ast.HRuleNode:
		v.b.WriteString("---")
		v.visitAttributes(n.Attrs)
		v.visitAttributesAST(n.Attrs)
	case *ast.NestedListNode:
		v.visitNestedList(n)
		v.visitNestedListAST(n)
	case *ast.DescriptionListNode:
		v.visitDescriptionList(n)
		v.visitDescriptionListAST(n)
	case *ast.TableNode:
		v.visitTable(n)
		v.visitTableAST(n)
	case *ast.TranscludeNode:
		v.b.WriteStrings("{{{", n.Ref.String(), "}}}") // FIXME n.Inlines
		v.visitAttributes(n.Attrs)
		v.visitAttributesAST(n.Attrs)
	case *ast.BLOBNode:
		v.visitBLOB(n)
		v.visitBLOBAST(n)
	case *ast.TextNode:
		v.visitText(n)
		v.visitTextAST(n)
	case *ast.BreakNode:
		v.visitBreak(n)
		v.visitBreakAST(n)
	case *ast.LinkNode:
		v.visitLink(n)
		v.visitLinkAST(n)
	case *ast.EmbedRefNode:
		v.visitEmbedRef(n)
		v.visitEmbedRefAST(n)
	case *ast.EmbedBLOBNode:
		v.visitEmbedBLOB(n)
		v.visitEmbedBLOBAST(n)
	case *ast.CiteNode:
		v.visitCite(n)
		v.visitCiteAST(n)
	case *ast.FootnoteNode:
		v.b.WriteString("[^")
		ast.Walk(v, &n.Inlines)
		_ = v.b.WriteByte(']')
		v.visitAttributes(n.Attrs)
		v.visitAttributesAST(n.Attrs)
	case *ast.MarkNode:
		v.visitMark(n)
		v.visitMarkAST(n)
	case *ast.FormatNode:
		v.visitFormat(n)
		v.visitFormatAST(n)
	case *ast.LiteralNode:
		v.visitLiteral(n)
		v.visitLiteralAST(n)
	default:
		return v
	}
	return nil
}

func (v *zmkVisitor) visitBlockSlice(bs *ast.BlockSlice) {
func (v *zmkVisitorAST) visitBlockSliceAST(bs *ast.BlockSlice) {
	var lastWasParagraph bool
	for i, bn := range *bs {
		if i > 0 {
			v.b.WriteLn()
			if lastWasParagraph && !v.inVerse {
				if _, ok := bn.(*ast.ParaNode); ok {
					v.b.WriteLn()
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
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







-
+











-
+












-
+






-
+













-
+



-
+








-
+




















-
+







-
+







	ast.VerbatimComment: "%%%",
	ast.VerbatimHTML:    "@@@", // Attribute is set to {="html"}
	ast.VerbatimCode:    "```",
	ast.VerbatimEval:    "~~~",
	ast.VerbatimMath:    "$$$",
}

func (v *zmkVisitor) visitVerbatim(vn *ast.VerbatimNode) {
func (v *zmkVisitorAST) visitVerbatimAST(vn *ast.VerbatimNode) {
	kind, ok := mapVerbatimKind[vn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown verbatim kind %d", vn.Kind))
	}
	attrs := vn.Attrs
	if vn.Kind == ast.VerbatimHTML {
		attrs = syntaxToHTML(attrs)
	}

	// TODO: scan cn.Lines to find embedded kind[0]s at beginning
	v.b.WriteString(kind)
	v.visitAttributes(attrs)
	v.visitAttributesAST(attrs)
	v.b.WriteLn()
	_, _ = v.b.Write(vn.Content)
	v.b.WriteLn()
	v.b.WriteString(kind)
}

var mapRegionKind = map[ast.RegionKind]string{
	ast.RegionSpan:  ":::",
	ast.RegionQuote: "<<<",
	ast.RegionVerse: "\"\"\"",
}

func (v *zmkVisitor) visitRegion(rn *ast.RegionNode) {
func (v *zmkVisitorAST) visitRegionAST(rn *ast.RegionNode) {
	// Scan rn.Blocks for embedded regions to adjust length of regionCode
	kind, ok := mapRegionKind[rn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown region kind %d", rn.Kind))
	}
	v.b.WriteString(kind)
	v.visitAttributes(rn.Attrs)
	v.visitAttributesAST(rn.Attrs)
	v.b.WriteLn()
	saveInVerse := v.inVerse
	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)
	}
}

func (v *zmkVisitor) visitHeading(hn *ast.HeadingNode) {
func (v *zmkVisitorAST) visitHeadingAST(hn *ast.HeadingNode) {
	const headingSigns = "========= "
	v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-3:])
	ast.Walk(v, &hn.Inlines)
	v.visitAttributes(hn.Attrs)
	v.visitAttributesAST(hn.Attrs)
}

var mapNestedListKind = map[ast.NestedListKind]byte{
	ast.NestedListOrdered:   '#',
	ast.NestedListUnordered: '*',
	ast.NestedListQuote:     '>',
}

func (v *zmkVisitor) visitNestedList(ln *ast.NestedListNode) {
func (v *zmkVisitorAST) visitNestedListAST(ln *ast.NestedListNode) {
	v.prefix = append(v.prefix, mapNestedListKind[ln.Kind])
	for i, item := range ln.Items {
		if i > 0 {
			v.b.WriteLn()
		}
		_, _ = v.b.Write(v.prefix)
		v.b.WriteSpace()
		for j, in := range item {
			if j > 0 {
				v.b.WriteLn()
				if _, ok := in.(*ast.ParaNode); ok {
					v.writePrefixSpaces()
				}
			}
			ast.Walk(v, in)
		}
	}
	v.prefix = v.prefix[:len(v.prefix)-1]
}

func (v *zmkVisitor) writePrefixSpaces() {
func (v *zmkVisitorAST) writePrefixSpaces() {
	if prefixLen := len(v.prefix); prefixLen > 0 {
		for i := 0; i <= prefixLen; i++ {
			v.b.WriteSpace()
		}
	}
}

func (v *zmkVisitor) visitDescriptionList(dn *ast.DescriptionListNode) {
func (v *zmkVisitorAST) visitDescriptionListAST(dn *ast.DescriptionListNode) {
	for i, descr := range dn.Descriptions {
		if i > 0 {
			v.b.WriteLn()
		}
		v.b.WriteString("; ")
		ast.Walk(v, &descr.Term)

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
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







-
+












-
+













-
+









-
+







+
-
+







-
+







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

func (v *zmkVisitor) visitTable(tn *ast.TableNode) {
func (v *zmkVisitorAST) visitTableAST(tn *ast.TableNode) {
	if header := tn.Header; len(header) > 0 {
		v.writeTableHeader(header, tn.Align)
		v.b.WriteLn()
	}
	for i, row := range tn.Rows {
		if i > 0 {
			v.b.WriteLn()
		}
		v.writeTableRow(row, tn.Align)
	}
}

func (v *zmkVisitor) writeTableHeader(header ast.TableRow, align []ast.Alignment) {
func (v *zmkVisitorAST) writeTableHeader(header ast.TableRow, align []ast.Alignment) {
	for pos, cell := range header {
		v.b.WriteString("|=")
		colAlign := align[pos]
		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) writeTableRow(row ast.TableRow, align []ast.Alignment) {
func (v *zmkVisitorAST) writeTableRow(row ast.TableRow, align []ast.Alignment) {
	for pos, cell := range row {
		_ = v.b.WriteByte('|')
		if cell.Align != align[pos] {
			v.b.WriteString(alignCode[cell.Align])
		}
		ast.Walk(v, &cell.Inlines)
	}
}

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

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

func (v *zmkVisitor) visitText(tn *ast.TextNode) {
func (v *zmkVisitorAST) visitTextAST(tn *ast.TextNode) {
	last := 0
	for i := 0; i < len(tn.Text); i++ {
		if b := tn.Text[i]; b == '\\' {
			v.b.WriteString(tn.Text[last:i])
			v.b.WriteBytes('\\', b)
			last = i + 1
			continue
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
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







-
+








-
+









+


-
+






+


-
+









-
+






-
+


-
+







				continue
			}
		}
	}
	v.b.WriteString(tn.Text[last:])
}

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

func (v *zmkVisitor) visitLink(ln *ast.LinkNode) {
func (v *zmkVisitorAST) visitLinkAST(ln *ast.LinkNode) {
	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(), "]]")
	v.visitAttributesAST(ln.Attrs)
}

func (v *zmkVisitor) visitEmbedRef(en *ast.EmbedRefNode) {
func (v *zmkVisitorAST) visitEmbedRefAST(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(), "}}")
	v.visitAttributesAST(en.Attrs)
}

func (v *zmkVisitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) {
func (v *zmkVisitorAST) visitEmbedBLOBAST(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) {
func (v *zmkVisitorAST) visitCiteAST(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)
	v.visitAttributesAST(cn.Attrs)
}

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

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
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







-
+







-
+


-
+





-
+






-
+







-
+



-
+


-
-
+
+














-
+
+






-
+







	ast.FormatSuper:  []byte("^^"),
	ast.FormatSub:    []byte(",,"),
	ast.FormatQuote:  []byte(`""`),
	ast.FormatMark:   []byte("##"),
	ast.FormatSpan:   []byte("::"),
}

func (v *zmkVisitor) visitFormat(fn *ast.FormatNode) {
func (v *zmkVisitorAST) visitFormatAST(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)
	v.visitAttributesAST(fn.Attrs)
}

func (v *zmkVisitor) visitLiteral(ln *ast.LiteralNode) {
func (v *zmkVisitorAST) visitLiteralAST(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)
		v.visitAttributesAST(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.visitAttributesAST(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) {
func (v *zmkVisitorAST) 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)
	v.visitAttributesAST(a)
}

// visitAttributes write HTML attributes
func (v *zmkVisitor) visitAttributes(a zsx.Attributes) {
// visitAttributesAST write HTML attributes
func (v *zmkVisitorAST) visitAttributesAST(a zsx.Attributes) {
	if a.IsEmpty() {
		return
	}
	_ = v.b.WriteByte('{')
	for i, k := range a.Keys() {
		if i > 0 {
			v.b.WriteSpace()
		}
		if k == "-" {
			_ = 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) {
func (v *zmkVisitorAST) 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
		}
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
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







-






-

-
-
-
-
+
+
+
+



+















-
+



-
+

-
+

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


-
+







-
-
+
+
+

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









-
-
+
+
+



-
+



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



-
+

-
+






-
+





+
-
+
+

-
+




-
+

-
+










-
+

















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

-
+





-
+



-
+











-
+



-
-
+
+






-
+


-
+



-
+

-
+




-
+

-
+











-
+








-
+

-
+


-
+










-
+






-
+

-
+






-
+


-
-
-
+
+
+




-
+

-
+







-
+




-
-

-
+




-
+

-
+










-
+

















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



-
+








-
+


-
+



-
+






-
+


-
+






-
+



-
+




-
+






-
+




-
+





-
+


-
-
+
+















-
+







-
+


-
+








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








-
-
+
+















-
+














-
+





-
+

-
+

-
+




-
+
















-
+




-
+





-
+

-
+






-
+









-
+


-
+













-
+









// 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/ast/sztrans"
	"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.BlocksAST = 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
	}

	if blk, err := sztrans.GetBlockSlice(zn.Blocks); err == nil {
		zn.BlocksAST = blk
	}
}
	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) ast.BlockSlice {
	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{},
		embedMapAST:     map[string]ast.InlineSlice{},
		marker:          &ast.Zettel{},
	}

	obj := zsx.Walk(&e, block, nil)
	evalBlock, isPair := sx.GetPair(obj)
	if !isPair {
		panic(fmt.Sprintf("not a pair after evaluate: %T/%v", obj, obj))
	}
	bns, err := sztrans.GetBlockSlice(evalBlock)
	if err != nil {
		panic(err)
	}

	// Now evaluate everything that was not evaluated by SZ-walker.

	ast.Walk(&e, bns)
	parser.Clean(bns, true)
	ast.Walk(&e, &bns)
	parser.CleanAST(&bns, true)
	return bns
}

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
	embedMapAST     map[string]ast.InlineSlice
}

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

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:
		case zsx.SymVerbatimEval:
			return e.evalVerbatimEval(node)
		}
	}
	return node
}

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) 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 mustParseZid(ref *sx.Pair, refVal string) id.Zid {
	zid, err := id.Parse(refVal)
	if err != nil {
		refState, _ := zsx.GetReference(ref)
		panic(fmt.Sprintf("%v: %q (state %v) -> %v", err, refVal, refState, ref))
	}
	return zid
}

// AST-based code, deprecated.

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

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

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

func replaceWithBlockNodes(bns []ast.BlockNode, i int, replaceBns []ast.BlockNode) []ast.BlockNode {
func replaceWithBlockNodesAST(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]...)
	}
	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)
		}
	}
	return vn
}

func (e *evaluator) evalVerbatimZettel(vn *ast.VerbatimNode) ast.BlockNode {
func (e *evaluator) evalVerbatimZettelNodeAST(vn *ast.VerbatimNode) ast.BlockNode {
	m := meta.New(id.Invalid)
	m.Set(meta.KeySyntax, getSyntax(vn.Attrs, meta.ValueSyntaxText))
	m.Set(meta.KeySyntax, getSyntaxAST(vn.Attrs, meta.ValueSyntaxText))
	zettel := zettel.Zettel{
		Meta:    m,
		Content: zettel.NewContent(vn.Content),
	}
	e.transcludeCount++
	zn := e.evaluateEmbeddedZettel(zettel)
	zn := e.evaluateEmbeddedZettelAST(zettel)
	return &zn.BlocksAST
}

func getSyntax(a zsx.Attributes, defSyntax meta.Value) meta.Value {
func getSyntaxAST(a zsx.Attributes, defSyntax meta.Value) meta.Value {
	if a != nil {
		if val, ok := a.Get(meta.KeySyntax); ok {
			return meta.Value(val)
		}
		if val, ok := a.Get(""); ok {
			return meta.Value(val)
		}
	}
	return defSyntax
}

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

	// To prevent e.embedCount from counting
	if errText := e.checkMaxTransclusions(ref); errText != nil {
		return makeBlockNode(errText)
	if errText := e.checkMaxTransclusionsAST(ref); errText != nil {
		return makeBlockNodeAST(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"))
		return makeBlockNodeAST(createInlineErrorTextAST(ref, "Invalid or broken transclusion reference"))
	case ast.RefStateSelf:
		e.transcludeCount++
		return makeBlockNode(createInlineErrorText(ref, "Self", "transclusion", "reference"))
		return makeBlockNodeAST(createInlineErrorTextAST(ref, "Self transclusion reference"))
	case ast.RefStateFound, ast.RefStateExternal:
		return tn
	case ast.RefStateHosted, ast.RefStateBased:
		if n := createEmbeddedNodeLocal(ref); n != nil {
		if n := createEmbeddedNodeLocalAST(ref); n != nil {
			n.Attrs = tn.Attrs
			return makeBlockNode(n)
			return makeBlockNodeAST(n)
		}
		return tn
	case ast.RefStateQuery:
		e.transcludeCount++
		return e.evalQueryTransclusion(tn.Ref.Value)
		return e.evalQueryTransclusionAST(tn.Ref.Value)
	default:
		return makeBlockNode(createInlineErrorText(ref, "Illegal", "block", "state", strconv.Itoa(int(ref.State))))
		return makeBlockNodeAST(createInlineErrorTextAST(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"))
		return makeBlockNodeAST(createInlineErrorTextAST(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"))
			return makeBlockNodeAST(createInlineErrorTextAST(ref, "Unable to get zettel"))
		}
		setMetadataFromAttributes(zettel.Meta, tn.Attrs)
		setMetadataFromAttributesAST(zettel.Meta, tn.Attrs)
		ec := e.transcludeCount
		e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec}
		zn = e.evaluateEmbeddedZettel(zettel)
		zn = e.evaluateEmbeddedZettelAST(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 {
func (e *evaluator) evalQueryTransclusionAST(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"))
		return makeBlockNodeAST(createInlineErrorTextAST(nil, "Unable to search zettel"))
	}
	result, _ := QueryAction(e.ctx, q, ml)
	result := QueryActionAST(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) checkMaxTransclusionsAST(ref *ast.Reference) ast.InlineNode {
	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)")
		return createInlineErrorTextAST(ref,
			"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 makeBlockNodeAST(in ast.InlineNode) ast.BlockNode { return ast.CreateParaNode(in) }

func setMetadataFromAttributes(m *meta.Meta, a zsx.Attributes) {
func setMetadataFromAttributesAST(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) {
func (e *evaluator) visitInlineSliceAST(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))
			i += embedNodeAST(is, i, e.evalEmbedRefNodeAST(n))
		}
	}
}

func embedNode(is *ast.InlineSlice, i int, in ast.InlineNode) int {
func embedNodeAST(is *ast.InlineSlice, i int, in ast.InlineNode) int {
	if ln, ok := in.(*ast.InlineSlice); ok {
		*is = replaceWithInlineNodes(*is, i, *ln)
		*is = replaceWithInlineNodesAST(*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
}

func replaceWithInlineNodes(ins ast.InlineSlice, i int, replaceIns ast.InlineSlice) ast.InlineSlice {
func replaceWithInlineNodesAST(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 {
		ln.Ref.State = ast.RefStateBroken
		return ln
	}

	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 {
func (e *evaluator) evalEmbedRefNodeAST(en *ast.EmbedRefNode) ast.InlineNode {
	ref := en.Ref

	// To prevent e.embedCount from counting
	if errText := e.checkMaxTransclusions(ref); errText != nil {
	if errText := e.checkMaxTransclusionsAST(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)
		return createInlineErrorImageAST(en)
	case ast.RefStateSelf:
		e.transcludeCount++
		return createInlineErrorText(ref, "Self", "embed", "reference")
		return createInlineErrorTextAST(ref, "Self embed reference")
	case ast.RefStateFound, ast.RefStateExternal:
		return en
	case ast.RefStateHosted, ast.RefStateBased:
		if n := createEmbeddedNodeLocal(ref); n != nil {
		if n := createEmbeddedNodeLocalAST(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)))
		return createInlineErrorTextAST(ref, "Illegal inline state"+strconv.Itoa(int(ref.State)))
	}

	zid := mustParseZid(ref)
	zid := mustParseZidAST(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)
		return createInlineErrorImageAST(en)
	}

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

	cost, ok := e.costMap[zid]
	zn := cost.zn
	if zn == e.marker {
		e.transcludeCount++
		return createInlineErrorText(ref, "Recursive", "transclusion")
		return createInlineErrorTextAST(ref, "Recursive transclusion")
	}
	if !ok {
		ec := e.transcludeCount
		e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec}
		zn = e.evaluateEmbeddedZettel(zettel)
		zn = e.evaluateEmbeddedZettelAST(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[ref.Value]
	result, ok := e.embedMapAST[ref.Value]
	if !ok {
		// Search for text to be embedded.
		result = findInlineSlice(&zn.BlocksAST, ref.URL.Fragment)
		e.embedMap[ref.Value] = result
		result = findInlineSliceAST(&zn.BlocksAST, ref.URL.Fragment)
		e.embedMapAST[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 {
func mustParseZidAST(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) {
func (e *evaluator) updateImageRefNodeAST(en *ast.EmbedRefNode, m *meta.Meta, syntax string) {
	en.Syntax = syntax
	if len(en.Inlines) == 0 {
		is := parser.ParseDescriptionAST(m)
		is := parseDescriptionAST(m)
		if len(is) > 0 {
			ast.Walk(e, &is)
			if len(is) > 0 {
				en.Inlines = is
			}
		}
	}
}
func parseDescriptionAST(m *meta.Meta) ast.InlineSlice {
	// Non-AST function in package parser.
	if m == nil {
		return nil

func createInlineErrorImage(en *ast.EmbedRefNode) *ast.EmbedRefNode {
	}
	if summary, found := m.Get(meta.KeySummary); found {
		return ast.InlineSlice{&ast.TextNode{Text: sz.NormalizedSpacedText(string(summary))}}
	}
	if title, found := m.Get(meta.KeyTitle); found {
		return ast.InlineSlice{&ast.TextNode{Text: sz.NormalizedSpacedText(string(title))}}
	}
	return ast.InlineSlice{&ast.TextNode{Text: "Zettel without title/summary: " + m.Zid.String()}}
}

func createInlineErrorImageAST(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, " ")
func createInlineErrorTextAST(ref *ast.Reference, message string) ast.InlineNode {
	text := message
	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 {
func createEmbeddedNodeLocalAST(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 (e *evaluator) evaluateEmbeddedZettel(zettel zettel.Zettel) *ast.ZettelNode {
func (e *evaluator) evaluateEmbeddedZettelAST(zettel zettel.Zettel) *ast.Zettel {
	zn := parser.ParseZettel(e.ctx, zettel, string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)), e.rtConfig)
	ast.Walk(e, &zn.BlocksAST)
	return zn
}

func findInlineSlice(bs *ast.BlockSlice, fragment string) ast.InlineSlice {
func findInlineSliceAST(bs *ast.BlockSlice, fragment string) ast.InlineSlice {
	if fragment == "" {
		return firstInlinesToEmbed(*bs)
		return firstInlinesToEmbedAST(*bs)
	}
	fs := fragmentSearcher{fragment: fragment}
	fs := fragmentSearcherAST{fragment: fragment}
	ast.Walk(&fs, bs)
	return fs.result
}

func firstInlinesToEmbed(bs ast.BlockSlice) ast.InlineSlice {
func firstInlinesToEmbedAST(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
}

type fragmentSearcher struct {
type fragmentSearcherAST struct {
	fragment string
	result   ast.InlineSlice
}

func (fs *fragmentSearcher) Visit(node ast.Node) ast.Visitor {
func (fs *fragmentSearcherAST) Visit(node ast.Node) ast.Visitor {
	if len(fs.result) > 0 {
		return nil
	}
	switch n := node.(type) {
	case *ast.BlockSlice:
		fs.visitBlockSlice(n)
		fs.visitBlockSliceAST(n)
	case *ast.InlineSlice:
		fs.visitInlineSlice(n)
		fs.visitInlineSliceAST(n)
	default:
		return fs
	}
	return nil
}

func (fs *fragmentSearcher) visitBlockSlice(bs *ast.BlockSlice) {
func (fs *fragmentSearcherAST) visitBlockSliceAST(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) {
func (fs *fragmentSearcherAST) visitInlineSliceAST(is *ast.InlineSlice) {
	for i, in := range *is {
		if mn, ok := in.(*ast.MarkNode); ok && mn.Fragment == fs.fragment {
			ris := skipBreakeNodes((*is)[i+1:])
			ris := skipBreakeNodesAST((*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 {
func skipBreakeNodesAST(ins ast.InlineSlice) ast.InlineSlice {
	for i, in := range ins {
		switch in.(type) {
		case *ast.BreakNode:
		default:
			return ins[i:]
		}
	}
	return nil
}
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

59
60
61
62
63
64
65
66







+


+



+



+
+
+
+
+
+
-
-
+
+




-
+











-
+







	"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/ast/sztrans"
	"zettelstore.de/z/internal/query"
)

// QueryActionAST transforms a list of metadata according to query actions into an AST nested list.
func QueryActionAST(ctx context.Context, q *query.Query, ml []*meta.Meta) ast.BlockNode {
	bn, _ := QueryAction(ctx, q, ml)
	return sztrans.MustGetBlock(bn)
}

// 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
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







+











-
+




-
+





-
+

+
+


-
-
-
-
-
+
+
+
+
+
+


-
-
+
-
-
-


-
+







-
+

-

+
+


-
+


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

+


-
+







		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
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







-
+














-
+
+










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

-
-
+
-
-
-


-
+



-
+
+






-
-
-
-
-
+
+
+
+
+
+

-
-
+
-
-
-







			}
			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()
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
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







-
-
+
+
-

-
+













-
+







	}
	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.
57
58
59
60
61
62
63
64

65
57
58
59
60
61
62
63

64
65







-
+

	})
}

func parseBlob(inp *input.Input, m *meta.Meta, syntax string) *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
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







+

+





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











-
+
-






-
+








// 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 the given SZ syntax tree.
func Clean(node *sx.Pair, allowHTML bool) {
	v1 := cleanPhase1{ids: idsNode{}, allowHTML: allowHTML}
	zsx.WalkIt(&v1, node, nil)
	if v1.hasMark {
		v2 := cleanPhase2{ids: v1.ids}
		zsx.WalkIt(&v2, node, nil)
	}
}

type cleanPhase1 struct {
	ids       idsNode
	allowHTML bool
	hasMark   bool // Mark nodes will be cleaned in phase 2 only
}

func (v *cleanPhase1) VisitBefore(node *sx.Pair, _ *sx.Pair) bool {
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		switch sym {
		case zsx.SymBlock:
			if !v.allowHTML {
				curr, next := node, node.Tail()
				for next != nil {
					sy, ok := sx.GetSymbol(next.Head().Car())
					if !ok || sy != zsx.SymVerbatimHTML {
						curr = next
						next = next.Tail()
					} else {
						next = next.Tail()
						curr.SetCdr(next)
					}
				}
			}

		case zsx.SymMark:
			v.hasMark = true
		}
	}
	return false
}
func (v *cleanPhase1) VisitAfter(node *sx.Pair, _ *sx.Pair) {
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		switch sym {
		case zsx.SymHeading:
			levelNode := node.Tail()
			attrsNode := levelNode.Tail()
			slugNode := attrsNode.Tail()
			fragmentNode := slugNode.Tail()

			textNode := fragmentNode.Tail()
			var sb strings.Builder
			var textEnc encoder.TextEncoder
			if err := textEnc.WriteSz(&sb, textNode.Cons(zsx.SymPara)); err != nil {
				return
			}

			slugText := zerostrings.Slugify(sb.String())
			slugNode.SetCar(sx.MakeString(slugText))
			fragmentNode.SetCar(sx.MakeString(v.ids.addIdentifier(slugText, node)))
		}
	}
}

type cleanPhase2 struct {
	ids idsNode
}

func (v *cleanPhase2) VisitBefore(node *sx.Pair, _ *sx.Pair) bool {
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		switch sym {
		case zsx.SymMark:
			stringNode := node.Tail()
			if markString, isString := sx.GetString(stringNode.Car()); isString {
				slugNode := stringNode.Tail()
				fragmentNode := slugNode.Tail()

				slugText := zerostrings.Slugify(markString.GetValue())
				slugNode.SetCar(sx.MakeString(slugText))
				fragmentNode.SetCar(sx.MakeString(v.ids.addIdentifier(slugText, node)))
			}
		}
	}
	return false
}
func (v *cleanPhase2) VisitAfter(*sx.Pair, *sx.Pair) {}

type idsNode map[string]*sx.Pair

func (ids idsNode) addIdentifier(id string, node *sx.Pair) string {
	if n, ok := ids[id]; ok && n != node {
		prefix := id + "-"
		for count := 1; ; count++ {
			newID := prefix + strconv.Itoa(count)
			if n2, ok2 := ids[newID]; !ok2 || n2 == node {
				ids[newID] = node
				return newID
			}
		}
	}
	ids[id] = node
	return id
}

// Clean cleans the given block list.
func Clean(bs *ast.BlockSlice, allowHTML bool) {
	cv := cleanVisitor{
// CleanAST cleans the given block list.
func CleanAST(bs *ast.BlockSlice, allowHTML bool) {
	cv := cleanASTVisitor{
		allowHTML: allowHTML,
		hasMark:   false,
		doMark:    false,
	}
	ast.Walk(&cv, bs)
	if cv.hasMark {
		cv.doMark = true
		ast.Walk(&cv, bs)
	}
}

type cleanVisitor struct {
type cleanASTVisitor struct {
	textEnc   encoder.TextEncoder
	ids       map[string]ast.Node
	allowHTML bool
	hasMark   bool
	doMark    bool
}

func (cv *cleanVisitor) Visit(node ast.Node) ast.Visitor {
func (cv *cleanASTVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		if !cv.allowHTML {
			cv.visitBlockSlice(n)
			return nil
		}
	case *ast.InlineSlice:
65
66
67
68
69
70
71
72

73
74
75
76
77
78
79
169
170
171
172
173
174
175

176
177
178
179
180
181
182
183







-
+







	case *ast.MarkNode:
		cv.visitMark(n)
		return nil
	}
	return cv
}

func (cv *cleanVisitor) visitBlockSlice(bs *ast.BlockSlice) {
func (cv *cleanASTVisitor) visitBlockSlice(bs *ast.BlockSlice) {
	if bs == nil {
		return
	}
	if len(*bs) == 0 {
		*bs = nil
		return
	}
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
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







-
+












-
+





+
-
+
-









-
+















-
+







	}
	for pos := toPos; pos < len(*bs); pos++ {
		(*bs)[pos] = nil // Allow excess nodes to be garbage collected.
	}
	*bs = (*bs)[:toPos:toPos]
}

func (cv *cleanVisitor) visitInlineSlice(is *ast.InlineSlice) {
func (cv *cleanASTVisitor) visitInlineSlice(is *ast.InlineSlice) {
	if is == nil {
		return
	}
	if len(*is) == 0 {
		*is = nil
		return
	}
	for _, bn := range *is {
		ast.Walk(cv, bn)
	}
}

func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) {
func (cv *cleanASTVisitor) visitHeading(hn *ast.HeadingNode) {
	if cv.doMark || hn == nil || len(hn.Inlines) == 0 {
		return
	}
	if hn.Slug == "" {
		var sb strings.Builder
		var textEnc encoder.TextEncoder
		_, err := cv.textEnc.WriteInlines(&sb, &hn.Inlines)
		if err := textEnc.WriteInlines(&sb, &hn.Inlines); err != nil {
		if err != nil {
			return
		}
		hn.Slug = zerostrings.Slugify(sb.String())
	}
	if hn.Slug != "" {
		hn.Fragment = cv.addIdentifier(hn.Slug, hn)
	}
}

func (cv *cleanVisitor) visitMark(mn *ast.MarkNode) {
func (cv *cleanASTVisitor) visitMark(mn *ast.MarkNode) {
	if !cv.doMark {
		cv.hasMark = true
		return
	}
	if mn.Mark == "" {
		mn.Slug = ""
		mn.Fragment = cv.addIdentifier("*", mn)
		return
	}
	if mn.Slug == "" {
		mn.Slug = zerostrings.Slugify(mn.Mark)
	}
	mn.Fragment = cv.addIdentifier(mn.Slug, mn)
}

func (cv *cleanVisitor) addIdentifier(id string, node ast.Node) string {
func (cv *cleanASTVisitor) addIdentifier(id string, node ast.Node) string {
	if cv.ids == nil {
		cv.ids = map[string]ast.Node{id: node}
		return id
	}
	if n, ok := cv.ids[id]; ok && n != node {
		prefix := id + "-"
		for count := 1; ; count++ {
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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
		allowHTML bool
		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: "remove-html-0",
			src: "(BLOCK (VERBATIM-HTML () \"<h1>Heading</h1>\"))",
			exp: "(BLOCK)"},
		{name: "remove-html-0-1",
			src: "(BLOCK (VERBATIM-HTML () \"<h1>Heading</h1>\") (PARA (TEXT \"ABC\")))",
			exp: "(BLOCK (PARA (TEXT \"ABC\")))"},
		{name: "remove-html-1-0",
			src: "(BLOCK (PARA (TEXT \"ABC\")) (VERBATIM-HTML () \"<h1>Heading</h1>\"))",
			exp: "(BLOCK (PARA (TEXT \"ABC\")))"},
		{name: "remove-html-1-2",
			src: "(BLOCK (PARA (TEXT \"ABC\")) (VERBATIM-HTML () \"<h1>Heading</h1>\") (VERBATIM-HTML () \"<h2>Head</h2>\"))",
			exp: "(BLOCK (PARA (TEXT \"ABC\")))"},

		{name: "allow HTML", allowHTML: true,
			src: "(BLOCK (VERBATIM-HTML () \"<h1>Heading</h1>\"))",
			exp: "(BLOCK (VERBATIM-HTML () \"<h1>Heading</h1>\"))"},
		{name: "allow-html-1-2", allowHTML: true,
			src: "(BLOCK (PARA (TEXT \"ABC\")) (VERBATIM-HTML () \"<h1>Heading</h1>\") (VERBATIM-HTML () \"<h2>Head</h2>\"))",
			exp: "(BLOCK (PARA (TEXT \"ABC\")) (VERBATIM-HTML () \"<h1>Heading</h1>\") (VERBATIM-HTML () \"<h2>Head</h2>\"))"},
	}

	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, tc.allowHTML)
			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
19
20
21
22
23
24
25


26
27
28
29
30
31
32







-
-







	"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,
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
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







-
+



-
+
+

-
+


-
-
+
+

-
+

-
+









-
+

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







	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)
	})
}
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 {
			return sz.ParseNoneBlocks(inp)
		},
	})
}
Changes to internal/parser/parser.go.
17
18
19
20
21
22
23

24
25
26
27
28
29
30
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31







+







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"
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 {
// 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) (*sx.Pair, ast.BlockSlice) {
	if obj := Get(syntax).Parse(inp, m, syntax); obj != nil {
		bs, err := sztrans.GetBlockSlice(obj)
		if err == nil {
			Clean(&bs, hi.AllowHTML(syntax))
			return bs
			return obj, bs
		}
		log.Printf("sztrans error: %v, for %v\n", err, obj)
	}
	return nil
	return nil, nil
}

// 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()}}
}

// 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))
	}
	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{
	rootNode, bs := Parse(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax)
	return &ast.Zettel{
		Meta:      m,
		Content:   zettel.Content,
		Zid:       m.Zid,
		InhMeta:   inhMeta,
		Blocks:    rootNode,
		BlocksAST: bs,
		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,
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
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







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





-
+









-
+







		IsTextFormat:  true,
		IsImageFormat: false,
		Parse:         parsePlainSxn,
	})
}

func parsePlain(inp *input.Input, _ *meta.Meta, syntax string) *sx.Pair {
	return doParsePlain(inp, syntax, zsx.SymVerbatimCode)
	return sz.ParsePlainBlocks(inp, syntax)
}
func parsePlainHTML(inp *input.Input, _ *meta.Meta, syntax string) *sx.Pair {
	return doParsePlain(inp, syntax, zsx.SymVerbatimHTML)
}
func doParsePlain(inp *input.Input, syntax string, kind *sx.Symbol) *sx.Pair {
	return zsx.MakeBlock(zsx.MakeVerbatim(
		kind,
		sx.Cons(sx.Cons(sx.MakeString(""), sx.MakeString(syntax)), sx.Nil()),
		string(inp.ScanLineContent()),
	))
}

func parsePlainSVG(inp *input.Input, _ *meta.Meta, syntax string) *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 ""
Changes to internal/parser/plain_test.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
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







-
+
-



















-
-
+
+
+
+
+








import (
	"testing"

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

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

func TestParseSVG(t *testing.T) {
	testCases := []struct {
		name string
		src  string
		exp  string
	}{
		{"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)"},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			inp := input.NewInput([]byte(tc.src))
			bs := parser.Parse(inp, nil, meta.ValueSyntaxSVG, config.NoHTML)
			trans := encoder.NewSzTransformer()
			node, bs := parser.Parse(inp, nil, meta.ValueSyntaxSVG)
			if got := node.String(); tc.exp != got {
				t.Errorf("\nexp: %q\ngot: %q", tc.exp, got)
			}
			trans := sztrans.NewSzTransformer()
			lst := trans.GetSz(&bs)
			if got := lst.String(); tc.exp != got {
				t.Errorf("\nexp: %q\ngot: %q", tc.exp, got)
			}
		})
	}
}
Changes to internal/parser/zettelmark.go.
26
27
28
29
30
31
32
33
34
35



36
37
38
26
27
28
29
30
31
32



33
34
35
36
37
38







-
-
-
+
+
+



	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()
			var zmkParser zmk.Parser
			zmkParser.Initialize(inp)
			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/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) ast.BlockSlice {
	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, uc.rtConfig.GetHTMLInsecurity().AllowHTML(syntax))
	return z, nil
}
Changes to internal/usecase/query.go.
128
129
130
131
132
133
134
135

136
137
138
139
140
141
142
128
129
130
131
132
133
134

135
136
137
138
139
140
141
142







-
+







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) {
		for _, ln := range collect.OrderAST(&zn.BlocksAST) {
			ref := ln.Ref
			if !ref.IsZettel() {
				continue
			}

			if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil {
				if z, err3 := uc.port.GetZettel(ctx, collectedZid); err3 == nil {
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
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







-
+


















-
+



-
+





-
+


-
+









-
+



-
+






-
+







func (uc *Query) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(candidates))
	for _, cand := range candidates {
		zettel, err := uc.port.GetZettel(ctx, cand.Zid)
		if err != nil {
			continue
		}
		v := unlinkedVisitor{
		v := unlinkedVisitorAST{
			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 {
			result = append(result, cand)
		}
	}
	return result
}

func (*unlinkedVisitor) joinWords(words []string) string {
func (*unlinkedVisitorAST) joinWords(words []string) string {
	return " " + strings.ToLower(strings.Join(words, " ")) + " "
}

type unlinkedVisitor struct {
type unlinkedVisitorAST struct {
	words []string
	text  string
	found bool
}

func (v *unlinkedVisitor) Visit(node ast.Node) ast.Visitor {
func (v *unlinkedVisitorAST) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.InlineSlice:
		v.checkWords(n)
		v.checkWordsAST(n)
		return nil
	case *ast.HeadingNode:
		return nil
	case *ast.LinkNode, *ast.EmbedRefNode, *ast.EmbedBLOBNode, *ast.CiteNode:
		return nil
	}
	return v
}

func (v *unlinkedVisitor) checkWords(is *ast.InlineSlice) {
func (v *unlinkedVisitorAST) checkWordsAST(is *ast.InlineSlice) {
	if len(*is) < 2*len(v.words)-1 {
		return
	}
	for _, text := range v.splitInlineTextList(is) {
	for _, text := range v.splitInlineTextListAST(is) {
		if strings.Contains(text, v.text) {
			v.found = true
		}
	}
}

func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string {
func (v *unlinkedVisitorAST) splitInlineTextListAST(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:
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
173
174







-
+















-
+

-
+

-
+
+
+







	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.WriteBlocks(&buf, &zn.BlocksAST)
	case partSz: // TEMP
		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/api/request.go.
67
68
69
70
71
72
73

74
75
76
77
78
79

80
81
82
83
84
85
86
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88







+






+







type partType int

const (
	_ partType = iota
	partMeta
	partContent
	partZettel
	partSz // TEMP: SZ encoded content
)

var partMap = map[string]partType{
	api.PartMeta:    partMeta,
	api.PartContent: partContent,
	api.PartZettel:  partZettel,
	"sz":            partSz,
}

func getPart(q url.Values, defPart partType) partType {
	if part, ok := partMap[q.Get(api.QueryKeyPart)]; ok {
		return part
	}
	return defPart
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)
		}
	})
}

172
173
174
175
176
177
178
179

180
181
182
183
184
185
186
187
172
173
174
175
176
177
178

179

180
181
182
183
184
185
186







-
+
-







			wui.reportError(ctx, w, err)
			return
		}
		entries, _ := evaluator.QueryAction(ctx, q, metaSeq)
		bns := evaluate.RunBlockNode(ctx, entries)
		enc := encoder.Create(api.EncoderZmk, nil)
		var zmkContent bytes.Buffer
		_, err = enc.WriteBlocks(&zmkContent, &bns)
		if err = enc.WriteBlocks(&zmkContent, &bns); 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)
		unlinkedContent, _, err := enc.BlocksSxnAST(&bns)
		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
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







-
-
+
+

-
-
+
+
+

-
-
+
+
+

-
+
+


-
-
+
+







		}
		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())
161
162
163
164
165
166
167




168
169
170


171
172
173
174
175
176
177
165
166
167
168
169
170
171
172
173
174
175
176


177
178
179
180
181
182
183
184
185







+
+
+
+

-
-
+
+







	return encTexts
}

var apiParts = []string{api.PartZettel, api.PartMeta, api.PartContent}

func (wui *WebUI) infoAPIMatrix(zid id.Zid, parseOnly bool, encTexts []string) *sx.Pair {
	matrix := sx.Nil()
	apiPartsAndSz := apiParts
	if parseOnly {
		apiPartsAndSz = append(apiPartsAndSz, "sz") // TEMP
	}
	u := wui.NewURLBuilder('z').SetZid(zid)
	for ip := len(apiParts) - 1; ip >= 0; ip-- {
		part := apiParts[ip]
	for ip := len(apiPartsAndSz) - 1; ip >= 0; ip-- {
		part := apiPartsAndSz[ip]
		row := sx.Nil()
		for je := len(encTexts) - 1; je >= 0; je-- {
			enc := encTexts[je]
			if parseOnly {
				u.AppendKVQuery(api.QueryKeyParseOnly, "")
			}
			u.AppendKVQuery(api.QueryKeyPart, part)
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.BlocksSxnAST(&zn.BlocksAST)
		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
81
82







-









-
+




-




-
+








-
-
-
-
+
+
+
+



-
+

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





-
-
+
+



-
+







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"
	"zettelstore.de/z/internal/ast/sztrans"
)

// 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
	tx    sztrans.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
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







-
+















-
+




-
+










-
+


+
+
+
+
-
+












-
+




-
+







			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(),
		tx:   sztrans.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
187
188
189
190
191
192
193

194
195
196
197
198
199
200
201







-
+








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 := sztrans.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
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







-
-
+
+


-

-
+





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

-
+





	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) BlocksSxnAST(bs *ast.BlockSlice) (content, endnotes *sx.Pair, _ error) {
	if bs == nil || len(*bs) == 0 {
		return nil, nil, nil

	}
func (g *htmlGenerator) nodeSxHTML(node ast.Node) *sx.Pair {
	sz := g.tx.GetSz(node)
	sx := g.tx.GetSz(bs)
	return g.BlocksSxn(sx)
}

func (g *htmlGenerator) szToSxHTML(node *sx.Pair) *sx.Pair {
	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.BlocksSxnAST(&zn.BlocksAST); 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
36







+





-







	"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
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







-
-
-
+
+
+
+
+



-
-
+
+


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




-
+





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





-
+



-
+

-
+







-
+

-
+


















-
+

+
+
+
+
+
-
-
+
+
-




	}
	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, ast := createMDBlockSlice(tc.Markdown)
		testAllEncodings(t, tc, node)
		testAllEncodingsAST(t, tc, &ast)
		testZmkEncoding(t, tc, node)
		testZmkEncodingAST(t, tc, &ast)
	}
}

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, ast.BlockSlice) {
	return parser.Parse(input.NewInput([]byte(markdown)), nil, meta.ValueSyntaxMarkdown)
}

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}).WriteSz(&sb, node)
			sb.Reset()
		})
	}
}
func testAllEncodingsAST(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	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}).WriteBlocks(&sb, ast)
			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.WriteSz(&buf, node)
		// gotFirst := buf.String()

		testID = tc.Example*100 + 2
		secondNode, _ := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk)
		buf.Reset()
		_ = 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
		thirdNode, _ := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk)
		buf.Reset()
		_ = zmkEncoder.WriteSz(&buf, thirdNode)
		gotThird := buf.String()

		if gotSecond != gotThird {
			st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird)
		}
	})
}
func testZmkEncodingAST(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	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.WriteBlocks(&buf, ast)
		// gotFirst := buf.String()

		testID = tc.Example*100 + 2
		secondAst := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML)
		_, secondAst := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk)
		buf.Reset()
		_, _ = zmkEncoder.WriteBlocks(&buf, &secondAst)
		_ = zmkEncoder.WriteBlocks(&buf, &secondAst)
		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)
		_, thirdAst := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk)
		buf.Reset()
		_, _ = zmkEncoder.WriteBlocks(&buf, &thirdAst)
		_ = zmkEncoder.WriteBlocks(&buf, &thirdAst)
		gotThird := buf.String()

		if gotSecond != gotThird {
			st.Errorf("\n1st: %q\n2nd: %q", 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, ast := createMDBlockSlice(tc.md)
		sb.Reset()
		_ = zmkEncoder.WriteSz(&sb, node)
		if got := sb.String(); got != tc.exp {
			t.Errorf("%d: %q -> %q, but got %q", i, tc.md, tc.exp, got)
		}
		sb.Reset()
		_, _ = zmkEncoder.WriteBlocks(&sb, &ast)
		got := sb.String()
		_ = zmkEncoder.WriteBlocks(&sb, &ast)
		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
96
97
98







-
+

+
+
+
-
+
-






	}
	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, bs := parser.Parse(input.NewInput([]byte(s)), &meta.Meta{}, pinfo.Name)
			for _, enc := range encs {
				if err = enc.WriteSz(io.Discard, node); err != nil {
					t.Error(err)
				}
				_, err = enc.WriteBlocks(io.Discard, &bs)
				if err = enc.WriteBlocks(io.Discard, &bs); 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 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>