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
0.22.0
|
1
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
		zettel.Zettel{
			Meta:    m,
			Content: zettel.NewContent(inp.Src[inp.Pos:]),
		},
		string(m.GetDefault(meta.KeySyntax, meta.DefaultSyntax)),
		nil,
	)


	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 != nil {
		return 2, err
	}
	fmt.Println()

	return 0, nil
}








>
>







|
<







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
	}
	if err = encdr.WriteZettel(os.Stdout, z); 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

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



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

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









>
>


|
|
|

|
|
|
|
|

|
|
|
|
|
|



|
|
|
|
|
|
|

|
|
|







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(!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(!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(!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(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(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
id: 00001004010000
title: Zettelstore startup configuration
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250627155145

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.







|







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: 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
  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'']







<







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

  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
id: 00001004051000
title: The ''run'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250627160733

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

```
zettelstore run [-a PORT] [-c CONFIGFILE] [-d DIR] [-debug] [-p PORT] [-r] [-v]
```






|







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

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







|
|
<

<







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 debugging of the internal web server.
  Same as setting [[''debug-mode''|00001004010000#debug-mode]] to ""true"".



; [!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
id: 00001006030000
title: Supported Key Types
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250115172354

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


| ''-role'' | [[Word|00001006035500]]
| ''-time'' | [[Timestamp|00001006034500]]
| ''-url'' | [[URL|00001006035000]]
| ''-zettel''  | [[Identifier|00001006032000]]
| ''-zid''  | [[Identifier|00001006032000]]
| ''-zids''  | [[IdentifierSet|00001006032500]]
| any other suffix | [[EString|00001006031500]]






|








>
>







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: 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
id: 00001010000000
title: Security
role: manual
tags: #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250102212014

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.






|







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







|
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, also for encryption|00001010090100]]
Changes to docs/manual/00001010090100.zettel.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001010090100
title: External server to encrypt message transport
role: manual
tags: #configuration #encryption #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20250701125905

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.

|




|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001010090100
title: External server, also to encrypt message transport
role: manual
tags: #configuration #encryption #manual #security #zettelstore
syntax: zmk
created: 20210126175322
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













}
```
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.




















>
>
>
>
>
>
>
>
>
>
>
>
>
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
id: 00001012053800
title: API: Retrieve references of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20250415154139
modified: 20250416181257

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.







|







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







|







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 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
id: 00001012920525
title: SHTML Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230316181044
modified: 20250102180003

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: 






|







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







|
|
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).
Changes to docs/manual/00001012931400.zettel.
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

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







|







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

=== ''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; '')''.

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

__QuotationList__ **=** ''(QUOTATION'' [[__Attributes__|00001012931000#attribute]] __ListElement__ &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







|

|

|


<
<
<
<
<







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]] [[__Block__|00001012931000#block]] &hellip; '')''.

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

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






=== ''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
:::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,, '')''.
:::
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.
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.







|





|










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]] 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.
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
id: 00001012931600
title: Encoding of Sz Inline Elements
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161845
modified: 20250313130428

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







|







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: 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
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,, '')''.
:::
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.
Otherwise a base64 encoding is used.

=== ''CITE''
:::syntax
__CiteBLOB__ **=** ''(CITE'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The string contains the citation key.







|




|







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,, [[__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.
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
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.







<
<
<
<
<














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





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
id: 00001018000000
title: Troubleshooting
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20211027105921
modified: 20250627130445

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.





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






|




















>
>
>
>







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: 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
module zettelstore.de/z

go 1.24

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
)

require (
	golang.org/x/sys v0.33.0 // indirect
	golang.org/x/text v0.26.0 // indirect
)


|



|
|
|
|
|
|
|
|
|



|
|

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

require (
	github.com/fsnotify/fsnotify v1.9.0
	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.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
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=


|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.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
// 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 (
	"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.







|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







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 "net/url"



















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







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










Changes to internal/ast/block.go.
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*/ }







|
296
297
298
299
300
301
302
303
	Syntax      string
	Blob        []byte
}

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

// WalkChildren does nothing.
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
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2025-present Detlef Stern
//-----------------------------------------------------------------------------

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

import (
	"fmt"
	"log"

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

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

type transformer struct{}



















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

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

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

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

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

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

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







|




<










>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|

















|
|


<
|
|
<






|

|

|

|

|

|

|

|

|

|

|

|





|
|
<
<
<
|
|
|
|
|
<
<



|
|
<
<
<
|
|
|
|
|
|
<
<



|
|


|


|

|

|

|

|

|

|

|

|

|

|

|

|


|

|

|

|

|

|

|

|

|

|

|

|

|

|

|

<

|







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 and vice versa.
package sztrans

import (
	"fmt"


	"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 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(node *sx.Pair, _ *sx.Pair) (sx.Object, bool) {
	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		switch sym {
		case zsx.SymText:

			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, node)
		case zsx.SymLiteralComment:
			return handleLiteral(ast.LiteralComment, node)
		case zsx.SymLiteralInput:
			return handleLiteral(ast.LiteralInput, node)
		case zsx.SymLiteralMath:
			return handleLiteral(ast.LiteralMath, node)
		case zsx.SymLiteralOutput:
			return handleLiteral(ast.LiteralOutput, node)
		case zsx.SymThematic:
			return sxNode{&ast.HRuleNode{Attrs: zsx.GetAttributes(node.Tail().Head())}}, true
		case zsx.SymVerbatimComment:
			return handleVerbatim(ast.VerbatimComment, node)
		case zsx.SymVerbatimEval:
			return handleVerbatim(ast.VerbatimEval, node)
		case zsx.SymVerbatimHTML:
			return handleVerbatim(ast.VerbatimHTML, node)
		case zsx.SymVerbatimMath:
			return handleVerbatim(ast.VerbatimMath, node)
		case zsx.SymVerbatimCode:
			return handleVerbatim(ast.VerbatimCode, node)
		case zsx.SymVerbatimZettel:
			return handleVerbatim(ast.VerbatimZettel, node)
		}
	}
	return sx.Nil(), false
}

func handleLiteral(kind ast.LiteralKind, node *sx.Pair) (sx.Object, bool) {
	if sym, attrs, content := zsx.GetLiteral(node); sym != nil {



		return sxNode{&ast.LiteralNode{
			Kind:    kind,
			Attrs:   zsx.GetAttributes(attrs),
			Content: []byte(content)}}, true
	}


	return nil, false
}

func handleVerbatim(kind ast.VerbatimKind, node *sx.Pair) (sx.Object, bool) {
	if sym, attrs, content := zsx.GetVerbatim(node); sym != nil {



		return sxNode{&ast.VerbatimNode{
			Kind:    kind,
			Attrs:   zsx.GetAttributes(attrs),
			Content: []byte(content),
		}}, true
	}


	return nil, false
}

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(node.Tail())
			return sxNode{&bns}
		case zsx.SymPara:
			return sxNode{&ast.ParaNode{Inlines: collectInlines(node.Tail())}}
		case zsx.SymHeading:
			return handleHeading(node)
		case zsx.SymListOrdered:
			return handleList(ast.NestedListOrdered, node)
		case zsx.SymListUnordered:
			return handleList(ast.NestedListUnordered, node)
		case zsx.SymListQuote:
			return handleList(ast.NestedListQuote, node)
		case zsx.SymDescription:
			return handleDescription(node)
		case zsx.SymTable:
			return handleTable(node)
		case zsx.SymCell:
			return handleCell(node)
		case zsx.SymRegionBlock:
			return handleRegion(ast.RegionSpan, node)
		case zsx.SymRegionQuote:
			return handleRegion(ast.RegionQuote, node)
		case zsx.SymRegionVerse:
			return handleRegion(ast.RegionVerse, node)
		case zsx.SymTransclude:
			return handleTransclude(node)
		case zsx.SymBLOB:
			return handleBLOB(node)

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

	}
	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
				result = append(result, in)
			}
		}
	}
	return result
}

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

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


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







|
<
<
<
|
<
<
<
<
|
|
|
|
|
|
|
|
<
<
<
<
<
<
<
|


|
|
<


|
|
|
<
<
|
>
|







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(node *sx.Pair) sx.Object {



	if level, attrs, inlines, slug, fragment := zsx.GetHeading(node); level > 0 && level < 6 {




		return sxNode{&ast.HeadingNode{
			Level:    level,
			Attrs:    zsx.GetAttributes(attrs),
			Slug:     slug,
			Fragment: fragment,
			Inlines:  collectInlines(inlines),
		}}
	}







	return node
}

func handleList(kind ast.NestedListKind, node *sx.Pair) sx.Object {
	if sym, attrs, items := zsx.GetList(node); sym != nil {

		return sxNode{&ast.NestedListNode{
			Kind:  kind,
			Items: collectItemSlices(items),
			Attrs: zsx.GetAttributes(attrs),
		}}


	}
	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
				result = append(result, items)
			}
		}
	}
	return result
}

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

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

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

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

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

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

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

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

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

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

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









	return rest
}

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

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


			}}


		}
	}





	log.Println("TRAN", rest)




	return rest
}

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

							}}

						}

					}
				}







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

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

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

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

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

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

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

func handleMark(rest *sx.Pair) sx.Object {
	if rest != nil {
		if sMark, isMarkS := sx.GetString(rest.Car()); isMarkS {
			if curr := rest.Tail(); curr != nil {
				if sSlug, isSlug := sx.GetString(curr.Car()); isSlug {
					if curr = curr.Tail(); curr != nil {
						if sUniq, isUniq := sx.GetString(curr.Car()); isUniq {

							return sxNode{&ast.MarkNode{
								Mark:     sMark.GetValue(),
								Slug:     sSlug.GetValue(),
								Fragment: sUniq.GetValue(),
								Inlines:  collectInlines(curr.Tail()),
							}}
						}

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

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

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

type sxNode struct {
	node ast.Node
}

func (sxNode) IsNil() bool        { return false }
func (sxNode) IsAtom() bool       { return true }

func (n sxNode) String() string   { return fmt.Sprintf("%T/%v", n.node, n.node) }
func (n sxNode) GoString() string { return n.String() }
func (n sxNode) IsEqual(other sx.Object) bool {
	return n.String() == other.String()
}







|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|

|
|
|
|
|
|
|
|
<
<
|


|
|
|
|

|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
<
<
<
<











|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>
>
>
>
>
>
>
>
>
|


|
|
<
|
<
|
<
|
|
|
|
|
|
<
<
|


|
|
<
<
<
|
|
<
|
>
>
|
>
>
|
|
>
>
>
>
>
|
>
>
>
>
|


|
|
<
<
|
<
<
<
<
|
|
|
|
<
>
|
|
|
>
|
|
>
>
>
>
>
>
>
|
|
<
<
|














|
|
<
<
<
<
<
<
<
<
<
|
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<




|
<
|
<
<
|
|
|
|
|
|
<
<
<
|


|
<
<
<
<
<
<
>
|
|
|
|
|
|
|
>
|
|
<
<
<
<
<
<
<
|
|
<

|
|


<
|


|
|
<


|
|


<
|








>





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

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

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

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

		curr = curr.Tail()
	}
	if len(descs) > 0 {
		return sxNode{&ast.DescriptionListNode{
			Attrs:        zsx.GetAttributes(attrs),
			Descriptions: descs,
		}}
	}


	return node
}

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




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(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 handleTransclude(node *sx.Pair) sx.Object {
	if attrs, reference, inlines := zsx.GetTransclusion(node); reference != nil {

		if ref := collectReference(reference); ref != nil {

			return sxNode{&ast.TranscludeNode{

				Attrs:   zsx.GetAttributes(attrs),
				Ref:     ref,
				Inlines: collectInlines(inlines),
			}}
		}
	}


	return node
}

func handleBLOB(node *sx.Pair) sx.Object {
	if attrs, syntax, data, inlines := zsx.GetBLOB(node); data != nil {



		return sxNode{&ast.BLOBNode{
			Attrs:       zsx.GetAttributes(attrs),

			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 handleEmbed(node *sx.Pair) sx.Object {
	if attrs, reference, syntax, inlines := zsx.GetEmbed(node); reference != nil {


		if ref := collectReference(reference); ref != nil {




			return sxNode{&ast.EmbedRefNode{
				Attrs:   zsx.GetAttributes(attrs),
				Ref:     ref,
				Syntax:  syntax,

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


	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 collectReference(node *sx.Pair) *ast.Reference {
	if sym, val := zsx.GetReference(node); sym != nil {









		ref := ast.ParseReference(val)
		ref.State = mapRefState[sym]





















































































		return ref



	}
	return nil
}

func handleCite(node *sx.Pair) sx.Object {

	if attrs, key, inlines := zsx.GetCite(node); key != "" {


		return sxNode{&ast.CiteNode{
			Attrs:   zsx.GetAttributes(attrs),
			Key:     key,
			Inlines: collectInlines(inlines),
		}}
	}



	return node
}

func handleMark(node *sx.Pair) sx.Object {






	if mark, slug, fragment, inlines := zsx.GetMark(node); mark != "" {
		return sxNode{&ast.MarkNode{
			Mark:     mark,
			Slug:     slug,
			Fragment: fragment,
			Inlines:  collectInlines(inlines),
		}}
	}
	return node
}








func handleEndnote(node *sx.Pair) sx.Object {
	if attrs, inlines := zsx.GetEndnote(node); inlines != nil {

		return sxNode{&ast.FootnoteNode{
			Attrs:   zsx.GetAttributes(attrs),
			Inlines: collectInlines(inlines),
		}}
	}

	return node
}

func handleFormat(kind ast.FormatKind, node *sx.Pair) sx.Object {
	if sym, attrs, inlines := zsx.GetFormat(node); sym != nil {

		return sxNode{&ast.FormatNode{
			Kind:    kind,
			Attrs:   zsx.GetAttributes(attrs),
			Inlines: collectInlines(inlines),
		}}
	}

	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
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(@@@@
(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-HEADER
  (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")
    ,@(if with-auth
      `((div (@ (class "zs-dropdown"))
        (button "User")
        (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"))
          )
      )))
    )
    (div (@ (class "zs-dropdown"))
      (button "Lists")
      (nav (@ (class "zs-dropdown-content"))
        ,@list-urls
        ,@(if (symbol-bound? 'refresh-url) `((a (@ (href ,refresh-url)) "Refresh")))
    ))
    ,@(if new-zettel-links
      `((div (@ (class "zs-dropdown"))
        (button "New")
        (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")))))
  )
  (main (@ (class "content")) ,DETAIL)
  ,@(if FOOTER `((footer (hr) ,@FOOTER)))
  ,@(if debug-mode '((div (b "WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!"))))
)))







|

|
|
|
|

|
|




|
|

|

|

|
|
|



|

|

|


|

|



|
|
|
|

|



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))))
(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-HEADER
  (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"))
    ,(wui-href home-url "Home")
    ,@(if with-auth
      `((div ((class "zs-dropdown"))
        (button "User")
        (nav ((class "zs-dropdown-content"))
          ,@(if user-is-valid
            `(,(wui-href user-zettel-url user-ident)
              ,(wui-href logout-url "Logout"))
            `(,(wui-href login-url "Login"))
          )
      )))
    )
    (div ((class "zs-dropdown"))
      (button "Lists")
      (nav ((class "zs-dropdown-content"))
        ,@list-urls
        ,(if (symbol-bound? 'refresh-url) (wui-href refresh-url "Refresh"))
    ))
    ,@(if new-zettel-links
      `((div ((class "zs-dropdown"))
        (button "New")
        (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")))))
  )
  (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
		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.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.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.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.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.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.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.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentListZettelSxn)},
	id.ZidErrorTemplate: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Error HTML Template",
			meta.KeyRole:       meta.ValueRoleConfiguration,







|









|









|









|









|









|









|







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:   "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:   "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:   "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:   "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:   "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:   "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:   "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
		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.KeyReadOnly:   meta.ValueTrue,
			meta.KeyVisibility: meta.ValueVisibilityExpert,
		},
		zettel.NewContent(contentBaseCodeSxn)},
	id.ZidBaseCSS: {
		constHeader{
			meta.KeyTitle:      "Zettelstore Base CSS",







|







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:   "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
;;; 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"))
      (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"))
      (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"))
      (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"))))
)







|





|






|






|

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"))
      (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"))
      (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"))
      (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"))))
)
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
;;;
;;; 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"))
  (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")
              (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")
              (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")
              (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")
                 (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")
              (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")
                   (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")))
  ))
)







|

|
|



|
|







|
|



|
|



|
|








|
|





|
|
|


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"))
  (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")
              (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")
              (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")
              (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")
                 (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")
              (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")
                   (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")))
  ))
)
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
;;; 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")
      ,@(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 "]")))
      ,@(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")))
    )
  )
  (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)))
  )
  (h2 "Parts and encodings")
  ,(wui-enc-matrix enc-eval)
  (h3 "Parsed (not evaluated)")
  ,(wui-enc-matrix enc-parsed)
  ,@(if shadow-links
    `((h2 "Shadowed Boxes")







|
|
|

|
|
|



|
|











|
|







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
      ,(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; [")  ,(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; ") ,(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)))
  )
  (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
;;;
;;; 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)
              (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))
    )
  ,@(if (symbol-bound? '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))
    )
  ,@(if (symbol-bound? '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)))
      ,(if (symbol-bound? 'data-url)
          `(@L "Other encodings"
               ,(if (> num-entries 3) `(@L " of these " ,num-entries " entries: ") ": ")
               (a (@ (href ,data-url)) "data")
               ", "
               (a (@ (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")))
        )
      )
  )
)







|
|



|


|


|


|



|



|

|



|
|
|




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)
              (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))
    )
  ,@(if (symbol-bound? '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))
    )
  ,@(if (symbol-bound? '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)))
      ,(if (symbol-bound? 'data-url)
          `(@L "Other encodings"
               ,(if (> num-entries 3) `(@L " of these " ,num-entries " entries: ") ": ")
               ,(wui-href data-url "data")
               ", "
               ,(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")))
        )
      )
  )
)
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
;;;
;;; 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 ""))
    (div
      (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.."))))
    (div
      (input (@ (class "zs-primary") (type "submit") (value "Login"))))
  )
)







|
|

|
|

|
|

|


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 ""))
    (div
      (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.."))))
    (div
      (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

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




;; wui-local-link translates a local link into HTML.
(defun wui-local-link (l) `(li (a (@ (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)))

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

;; wui-option-value returns a value for an HTML option element.
(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)))))

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







|

>
>
>

|


|












|


|





|







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

;; 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 ,(wui-href l l)))

;; wui-link takes a link (title . url) and returns a HTML reference.
(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 ,(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))))

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

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

;; 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"))))
      ,@(let ((sequel-url (resolve-symbol 'sequel-url frame)))
             (if (defined? sequel-url) `((@H " &#183; ") (a (@ (href ,sequel-url)) "Sequel"))))
      ,@(let ((folge-url (resolve-symbol 'folge-url frame)))
             (if (defined? folge-url) `((@H " &#183; ") (a (@ (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"))
             )
        )
    )
)

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

;; ROLE-DEFAULT-heading returns the default text for headings, below the
;; references of a zettel. In most cases it should be called from an







|














|











|

|

|







|









|







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

;; 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; ") ,(wui-href copy-url "Copy"))))
      ,@(let ((sequel-url (resolve-symbol 'sequel-url frame)))
             (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; ") ,(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 ,(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 ,(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
;;; 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; ")))
      ,zid (@H " &#183; ")
      (a (@ (href ,info-url)) "Info") (@H " &#183; ")
      "(" ,@(if (symbol-bound? 'role-url) `((a (@ (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)))
      ")"
      ,@(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")))
      ,@(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))
                       (summary ,(wui-optional-link "Folgezettel" 'folge-query-url))
                       (ul ,@(map wui-item-link folge-links)))))
      ,@(if sequel-links
            `((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)))))
     ))
  )
)







|
|

|
|

|


|
|














|



|


|
|



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) `(,(wui-href edit-url "Edit") (@H " &#183; ")))
      ,zid (@H " &#183; ")
      ,(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; ") ,(wui-href folge-role-url meta-folge-role)))
      ")"
      ,@(if tag-refs `((@H " &#183; ") ,@tag-refs))
      (@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))
                       (summary ,(wui-optional-link "Folgezettel" 'folge-query-url))
                       (ul ,@(map wui-item-link folge-links)))))
      ,@(if sequel-links
            `((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)))))
     ))
  )
)
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
//-----------------------------------------------------------------------------

package manager

import (
	"strings"


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



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







>



>
>

<















|
|


|
>
|
|
|
|
>
>
|
|
>
>
|
|
>
|
|
>
|
|
>
|
|
>
|
|
>
|
>
>
>
>
>
>
>
>
>
>
>
>







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
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/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(blocks *sx.Pair, data *collectData) {
	zsx.WalkIt(data, blocks, nil)
}

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















Changes to internal/box/manager/indexer.go.
148
149
150
151
152
153
154
155

156
157
158
159
160
161
162
	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)

	}

	m := zettel.Meta
	zi := store.NewZettelIndex(m)
	mgr.idxCollectFromMeta(ctx, m, zi, &cData)
	mgr.idxProcessData(ctx, zi, &cData)
	toCheck := mgr.idxStore.UpdateReferences(ctx, zi)







|
>







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


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

import (
	"iter"

	"zettelstore.de/z/internal/ast"

)

type refYielder struct {
	yield func(*ast.Reference) 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) {
		yielder := refYielder{yield, false}
		ast.Walk(&yielder, &zn.BlocksAST)
	}
}

// Visit all node to collect data for the summary.
func (y *refYielder) Visit(node ast.Node) ast.Visitor {
	if y.stop {
		return nil
	}
	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 {
		y.stop = true
		return nil
	}

	return y
}








|
>



|




|
|
|

|




|

|

|
|
|
>
|
|
>
>
|
|
>
>
|
|
|
<
|
|
>
|

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

	"t73f.de/r/sx"
	"t73f.de/r/zsx"
)

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

// ReferenceSeq returns an iterator of all references mentioned in the given
// 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}
		zsx.WalkIt(&yielder, block, nil)
	}
}

// Visit all node to collect data for the summary.
func (y *refYielder) VisitBefore(node *sx.Pair, _ *sx.Pair) bool {
	if y.stop {
		return true
	}
	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 {

			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
// Package collect_test provides some unit test for collectors.
package collect_test

import (
	"slices"
	"testing"




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

func parseRef(s string) *ast.Reference {
	r := ast.ParseReference(s)

	if !r.IsValid() {
		panic(s)
	}
	return r
}

func TestReferenceSeq(t *testing.T) {
	t.Parallel()
	zn := &ast.ZettelNode{}
	summary := slices.Collect(collect.ReferenceSeq(zn))
	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))
	if len(summary) != 2 {
		t.Error("2 refs expected, but got:", summary)
	}

	para.Inlines = append(para.Inlines, intNode)
	summary = slices.Collect(collect.ReferenceSeq(zn))
	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")})},
	}
	summary = slices.Collect(collect.ReferenceSeq(zn))
	if len(summary) != 1 {
		t.Error("Only one image ref expected, but got: ", summary)
	}
}







>
>
>
|



|
|
>
|







<
|




|
|
|
|




|
|




<
|
<
|




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/collect"
)

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

	summary := slices.Collect(collect.ReferenceSeq(nil))
	if len(summary) != 0 {
		t.Error("No references expected, but got:", summary)
	}

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


	blocks = zsx.MakeBlock(zsx.MakePara(zsx.MakeEmbed(nil, parseRef("12345678901234"), "", nil)))

	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
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

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





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


































































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

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

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







>
>
>
>
|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|







|








|


|







|





|

|

|




|







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"

	"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 := firstItemZettelLinkAST(is); ln != nil {
					result = append(result, ln)
				}
			}
		}
	}
	return result
}

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

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 = firstInlineZettelLinkAST(in.Inlines)
		case *ast.EmbedBLOBNode:
			result = firstInlineZettelLinkAST(in.Inlines)
		case *ast.CiteNode:
			result = firstInlineZettelLinkAST(in.Inlines)
		case *ast.FootnoteNode:
			// Ignore references in footnotes
			continue
		case *ast.FormatNode:
			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
// Package encoder provides a generic interface to encode the abstract syntax
// tree into some text form.
package encoder

import (
	"io"


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

	"zettelstore.de/z/internal/ast"

)

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

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




	// WiteBlocks encodes a block slice, i.e. the zettel content.



	WriteBlocks(io.Writer, *ast.BlockSlice) (int, error)
}

// Create builds a new encoder with the given options.
func Create(enc api.EncodingEnum, params *CreateParameter) Encoder {
	switch enc {
	case api.EncoderHTML:
		// We need a new transformer every time, because tx.inVerse must be unique.
		// If we can refactor it out, the transformer can be created only once.
		return &htmlEncoder{
			tx:   NewSzTransformer(),
			th:   shtml.NewEvaluator(1),
			lang: params.Lang,
		}
	case api.EncoderMD:
		return &mdEncoder{lang: params.Lang}
	case api.EncoderSHTML:
		// We need a new transformer every time, because tx.inVerse must be unique.
		// If we can refactor it out, the transformer can be created only once.
		return &shtmlEncoder{
			tx:   NewSzTransformer(),
			th:   shtml.NewEvaluator(1),
			lang: params.Lang,
		}
	case api.EncoderSz:
		// We need a new transformer every time, because trans.inVerse must be unique.
		// If we can refactor it out, the transformer can be created only once.
		return &szEncoder{trans: NewSzTransformer()}
	case api.EncoderText:
		return (*TextEncoder)(nil)
	case api.EncoderZmk:
		return (*zmkEncoder)(nil)
	}
	return nil
}







>





>





|


|

>
>
>

>
>
>
|









|









|






|







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

	// WriteMeta encodes just the metadata.
	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) 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:   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:   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: 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
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







<







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/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
			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==")))))`,
			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, "???")
	}
}







|
|











|
>
|


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 () "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)
		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
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package encoder_test







var tcsBlock = []zmkTestCase{
	{
		descr: "Empty Zettelmarkup should produce near nothing",
		zmk:   "",
		expect: expectMap{
			encoderHTML:  "",







>
>
>
>
>
>







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
	{
		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"))`,
			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"))))`,
			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")))))`,
			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"))))`,
			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",







|










|











|











|







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







|







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"))))`,
			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
	},
	{
		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"))))`,
			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"))))`,
			encoderSHTML: `((blockquote (@L "ToBeOrNotToBe")))`,
			encoderText:  "ToBeOrNotToBe",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Quote Block",







|











|







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 () (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 () (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
	{
		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\")))",
			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\")))",
			encoderText:  "Hello\n\\LaTeX",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Description List",
		zmk:   "; Zettel\n: Paper\n: Note\n; Zettelkasten\n: Slip box",







|











|







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\")))",
			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\")))",
			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
	},
	{
		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")))))`,
			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")))))`,
			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\"))))",
			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\"))))",
			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")))))`,
			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: "",
		zmk:   ``,
		expect: expectMap{
			encoderHTML:  ``,
			encoderSz:    `(BLOCK)`,
			encoderSHTML: `()`,
			encoderText:  "",
			encoderZmk:   useZmk,
		},
	},
}

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







|














|
|
















|











|











|















>
>
>
>
>
>
>
>
>
>
>
>












<
<
<
<
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")))))`,
			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")))))`,
			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\"))))",
			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\"))))",
			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")))))`,
			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,
		},
	},
}




Changes to internal/encoder/encoder_inline_test.go.
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;"))))`,
			encoderText:  `quotes`,
			encoderZmk:   `""quotes""{lang="de"}`,
		},
	},
	{
		descr: "Empty quotes (default)",
		zmk:   `""""`,







|







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;"))))`,
			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
	{
		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;"))))`,
			encoderText:  ``,
			encoderZmk:   `""""{lang="unknown"}`,
		},
	},
	{
		descr: "Nested quotes (default)",
		zmk:   `""say: ::""yes, ::""or?""::""::""`,







|







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;"))))`,
			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
	{
		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")))`,
			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;")))))`,
			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")))`,







|











|




>
>
>
>
>
>
>
>
>
>
>
>







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")))`,
			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;")))))`,
			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
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Comment after text and with -->",
		zmk:   `Text%%{-} comment --> end`,
		expect: expectMap{
			encoderHTML:  `<p>Text<!-- comment -&#45;> 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"))))`,
			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")))))`,
			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")))`,
			encoderText:  `with text`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Invalid Link",
		zmk:   `[[link|00000000000000]]`,







|














|











|











|







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 --&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"))))`,
			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")))))`,
			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")))`,
			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
	{
		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")))`,












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












			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},












	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSz:    `(BLOCK)`,
			encoderSHTML: `()`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
}







|
>
>
>
>
>
>
>
>
>
>
>
>











|











|











|











|











|











|











|











|












|










|











|











|











|
>
>
>
>
>
>
>
>
>
>
>
>




>
>
>
>
>
>
>
>
>
>
>
>













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")))`,
			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")))`,
			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")))`,
			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")))`,
			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")))`,
			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")))`,
			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")))`,
			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")))`,
			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")))`,
			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")))`,
			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")))`,
			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")))`,
			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")))`,
			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")))))`,
			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
package encoder_test

import (
	"fmt"
	"strings"
	"testing"


	"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

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

func executeTestCases(t *testing.T, testCases []zmkTestCase) {
	for testNum, tc := range testCases {
		inp := input.NewInput([]byte(tc.zmk))




		bs := parser.Parse(inp, nil, meta.ValueSyntaxZmk, config.NoHTML)



		checkEncodings(t, testNum, bs, tc.inline, tc.descr, tc.expect, tc.zmk)
		checkSz(t, testNum, bs, tc.inline, tc.descr)
	}
}






























func checkEncodings(t *testing.T, testNum int, bs ast.BlockSlice, isInline bool, descr string, expected expectMap, zmkDefault string) {
	for enc, exp := range expected {
		encdr := encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN})
		got, err := encode(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)







>






<







>




















|





>
>
>
>
|
>
>
>
|




>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|


|







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/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(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
		}
		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 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 := 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
		}
	}
}

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






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

func mode(isInline bool) string {
	if isInline {
		return "inline"
	}
	return "block"
}







|









|
<









>
>
>
>
>
|

|









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 := 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
	}
	if got := val.String(); 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 encodeAST(e encoder.Encoder, bs ast.BlockSlice) (string, error) {
	var sb strings.Builder
	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
	"io"
	"strings"

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



	"zettelstore.de/z/internal/ast"

)

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

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

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

	xast := he.tx.GetSz(&zn.BlocksAST)
	hast, err := he.th.Evaluate(xast, &env)
	if err != nil {
		return 0, 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),
	)
	head.ExtendBang(hm)
	var sb strings.Builder
	if hasTitle {
		_, _ = 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)







>
>


>




|






|

|

|






|
<
|

|






|






|




|







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      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.Zettel) error {
	env := shtml.MakeEnvironment(he.lang)
	hm, err := he.th.Evaluate(sztrans.GetMetaSz(zn.InhMeta), &env)
	if err != nil {
		return err
	}

	var isTitle ast.InlineSlice
	var htitle *sx.Pair
	plainTitle, hasTitle := zn.InhMeta.Get(meta.KeyTitle)
	if hasTitle {
		szTitle := zsx.MakeInline((zsx.MakeText(sz.NormalizedSpacedText(string(plainTitle)))))

		htitle, err = he.th.Evaluate(szTitle, &env)
		if err != nil {
			return err
		}
	}

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

	var head sx.ListBuilder
	head.AddN(
		shtml.SymHead,
		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)
	} 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
	)

	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) {
	env := shtml.MakeEnvironment(he.lang)
	hm, err := he.th.Evaluate(he.tx.GetMeta(m), &env)
	if err != nil {
		return 0, err
	}
	gen := sxhtml.NewGenerator().SetNewline()
	return gen.WriteListHTML(w, hm)
}
















// WriteBlocks encodes a block slice.
func (he *htmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	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 err2 != nil {
			return length, err2
		}

		l, err2 := gen.WriteHTML(w, shtml.Endnotes(&env))
		length += l
		return length, err2
	}
	return 0, err
}







|

|

|





>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

|




|
<
|


|
<
<

|

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) error {
	env := shtml.MakeEnvironment(he.lang)
	hm, err := he.th.Evaluate(sztrans.GetMetaSz(m), &env)
	if err != nil {
		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) error {
	env := shtml.MakeEnvironment(he.lang)
	hobj, err := he.th.Evaluate(he.tx.GetSz(bs), &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
}
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

package encoder

// Encodes the abstract syntax tree back into Markdown.

import (
	"io"



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

	"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)
	if zn.InhMeta.YamlSep {
		v.b.WriteString("---\n")
	} else {
		v.b.WriteLn()
	}
	ast.Walk(&v, &zn.BlocksAST)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as markdown.
func (me *mdEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {







	v := newMDVisitor(w, me.lang)



	v.acceptMeta(m)







	length, err := v.b.Flush()






















	return length, err
























































































































































































































































































}

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

// 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)
	ast.Walk(&v, bs)
	length, err := v.b.Flush()
	return length, err
}

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

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

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

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

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

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

func (v *mdVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		v.visitBlockSlice(n)
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
	case *ast.RegionNode:
		v.visitRegion(n)







>

>


>











|
|
|






|
<



|
>
>
>
>
>
>
>

>
>
>
|
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


|
|
|




|
|

|
<


|
|







|
|



|








|


|

|





|







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.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)
	return v.b.Flush()

}

// WriteMeta encodes meta data as markdown.
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()
}

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) 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) error {
	v := newMDVisitorAST(w, me.lang)
	ast.Walk(&v, bs)
	return v.b.Flush()

}

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

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 *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 *mdVisitorAST) popAttributes() { v.langStack.Pop() }

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

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 *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
		v.visitLiteral(n)
	default:
		return v
	}
	return nil
}

func (v *mdVisitor) 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) {
	lc := len(vn.Content)
	if vn.Kind != ast.VerbatimCode || lc == 0 {
		return
	}
	v.writeSpaces(4)
	lcm1 := lc - 1
	for i := 0; i < lc; i++ {







|








|







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 *mdVisitorAST) visitBlockSlice(bs *ast.BlockSlice) {
	for i, bn := range *bs {
		if i > 0 {
			v.b.WriteString("\n\n")
		}
		ast.Walk(v, bn)
	}
}

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
		}
		v.b.WriteLn()
		v.writeSpaces(4)
		i = j - 1
	}
}

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

	first := true







|







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 *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
		}
		first = false
		v.b.WriteString("> ")
		ast.Walk(v, &pn.Inlines)
	}
}

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

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

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

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







|








|











|







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 *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 *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 *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
				}
			}
			ast.Walk(v, in)
		}
	}
}

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

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







|







557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
				}
			}
			ast.Walk(v, in)
		}
	}
}

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
			ast.Walk(v, in)
		}
	}

	v.listPrefix = prefix
}

func (v *mdVisitor) 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) {
	v.pushAttributes(ln.Attrs)
	defer v.popAttributes()

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

func (v *mdVisitor) 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) {
	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) {
		_ = 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 {
	if ref.State != ast.RefStateExternal || ref.URL == nil {
		return false
	}
	return ref.URL.Scheme != ""
}

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







|















|






|







|







|









|






|







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 *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 *mdVisitorAST) visitLink(ln *ast.LinkNode) {
	v.pushAttributes(ln.Attrs)
	defer v.popAttributes()

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

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 *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 isAutoLinkableAST(ref) {
		_ = v.b.WriteByte('<')
		v.b.WriteString(ref.String())
		_ = v.b.WriteByte('>')
	} else {
		s := ref.String()
		v.b.WriteStrings("[", s, "](", s, ")")
	}
}

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

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
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("</mark>")
	default:
		ast.Walk(v, &fn.Inlines)
	}
}

func (v *mdVisitor) 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) {
	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) {
	for range n {
		v.b.WriteSpace()
	}
}







|














|











|




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

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

	"zettelstore.de/z/internal/ast"

)

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

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

	return result.Print(w)
}

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












}

// WriteBlocks writes a block slice to the writer
func (enc *shtmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	env := shtml.MakeEnvironment(enc.lang)
	hval, err := enc.th.Evaluate(enc.tx.GetSz(bs), &env)
	if err != nil {
		return 0, err
	}

	return sx.Print(w, hval)
}







>




|





|

|

|



|


>
|



|

|

|

|
>
>
>
>
>
>
>
>
>
>
>
>



|



|

>
|

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   sztrans.SzTransformer
	th   *shtml.Evaluator
	lang string
}

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

// WriteMeta encodes meta data as s-expression.
func (enc *shtmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) error {
	env := shtml.MakeEnvironment(enc.lang)
	metaSHTML, err := enc.th.Evaluate(sztrans.GetMetaSz(m), &env)
	if err != nil {
		return err
	}
	_, 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) error {
	env := shtml.MakeEnvironment(enc.lang)
	hval, err := enc.th.Evaluate(enc.tx.GetSz(bs), &env)
	if err != nil {
		return err
	}
	_, err = hval.Print(w)
	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
import (
	"io"

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

	"zettelstore.de/z/internal/ast"

)

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

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

}

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






}

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

}







>




|



|

|
|
>



|
>
|
>
>
>
>
>
>



|
|
>

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 sztrans.SzTransformer
}

// WriteZettel writes the encoded zettel to the writer.
func (enc *szEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error {
	content := enc.trans.GetSz(&zn.BlocksAST)
	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) 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) 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

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

import (
	"io"
	"iter"


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

	"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)
	v.visitBlockSlice(&zn.BlocksAST)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes metadata as text.
func (te *TextEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, 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 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)
	v.visitBlockSlice(bs)
	length, err := 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)
	ast.Walk(&v, is)
	length, err := v.b.Flush()
	return length, err
}

// textVisitor writes the abstract syntax tree to an io.Writer.

type textVisitor struct {
	b         encWriter
	inlinePos int
}




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


































































































































































func (v *textVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		v.visitBlockSlice(n)
		return nil
	case *ast.InlineSlice:
		v.visitInlineSlice(n)
		return nil







>

>









|
|
|

|
<



|









|
<











<



|
|

|
<



|
|

|
<


|
>
|
|
|

>
>
>




>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|







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.Zettel) error {
	v := newTextVisitorAST(w)
	_ = te.WriteMeta(&v.b, zn.InhMeta)
	v.visitBlockSlice(&zn.BlocksAST)
	return v.b.Flush()

}

// WriteMeta encodes metadata as text.
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()
	}
	return buf.Flush()

}

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) error {
	v := newTextVisitorAST(w)
	v.visitBlockSlice(bs)
	return v.b.Flush()

}

// WriteInlines writes an inline slice to the writer
func (*TextEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) error {
	v := newTextVisitorAST(w)
	ast.Walk(&v, is)
	return v.b.Flush()

}

// 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
					}
					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
		if n.Kind != ast.LiteralComment {
			_, _ = v.b.Write(n.Content)
		}
	}
	return v
}

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

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

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

func (v *textVisitor) visitTable(tn *ast.TableNode) {
	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) {
	for i, cell := range row {
		v.writePosChar(i, ' ')
		ast.Walk(v, &cell.Inlines)
	}
}

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

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







|





|









|













|










|






|






|







|














|




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 *textVisitorAST) visitVerbatim(vn *ast.VerbatimNode) {
	if vn.Kind != ast.VerbatimComment {
		_, _ = v.b.Write(vn.Content)
	}
}

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 *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 *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 *textVisitorAST) writeRow(row ast.TableRow) {
	for i, cell := range row {
		v.writePosChar(i, ' ')
		ast.Walk(v, &cell.Inlines)
	}
}

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

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

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 *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
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package encoder

import (
	"encoding/base64"
	"io"


)

// encWriter is a specialized writer for encoding zettel.
type encWriter struct {
	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.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})
	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(' ')
	}
}










// Flush returns the collected length and error.
func (w *encWriter) Flush() (int, error) { return w.length, w.err }







<

>
>




|
|
<











<








<
|
<











|
|
<
>






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














>
>
>
>
>
>
>
>
>

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

	"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

}

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

	return l, w.err
}

// WriteString writes the content of s.
func (w *encWriter) WriteString(s string) {
	if w.err != nil {
		return
	}

	_, w.err = io.WriteString(w.w, s)

}

// 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 {
	if w.err == nil {
		_, w.err = w.Write([]byte{b})

	}
	return w.err
}

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















// 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() 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
// zmkenc encodes the abstract syntax tree back into Zettelmarkup.

import (
	"fmt"
	"io"
	"strings"


	"t73f.de/r/zero/set"

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

	"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)
	if zn.InhMeta.YamlSep {
		v.b.WriteString("---\n")
	} else {
		v.b.WriteLn()
	}
	ast.Walk(&v, &zn.BlocksAST)
	length, err := 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)
	length, err := v.b.Flush()
	return length, err
}

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

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

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

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




















































































































































































































































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

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







>

>

>









|
|
|






|
<



|
|
|
<
|


<
<
<
<
<
|
<
|

|
|
<


<

|
<
|
<
<




>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|


|

|
<


<

|

|

|


|

|

|

|


|

|

|

|

|

|

|

|




|

|

|

|






|







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 (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)
	return v.b.Flush()

}

// WriteMeta encodes meta data as zmk.
func (ze *zmkEncoder) WriteMeta(w io.Writer, m *meta.Meta) error {
	ew := newEncWriter(w)
	ew.WriteMeta(m)

	return ew.Flush()
}






// WriteSz encodes SZ represented zettel content.

func (*zmkEncoder) WriteSz(w io.Writer, node *sx.Pair) error {
	v := newZmkVisitor(w)
	zsx.WalkIt(&v, node, nil)
	return v.b.Flush()

}


type zmkVisitor struct {
	b      encWriter

	prefix []byte


}

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 *zmkVisitorAST) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		v.visitBlockSliceAST(n)
	case *ast.InlineSlice:
		for _, in := range *n {

			ast.Walk(v, in)
		}

	case *ast.VerbatimNode:
		v.visitVerbatimAST(n)
	case *ast.RegionNode:
		v.visitRegionAST(n)
	case *ast.HeadingNode:
		v.visitHeadingAST(n)
	case *ast.HRuleNode:
		v.b.WriteString("---")
		v.visitAttributesAST(n.Attrs)
	case *ast.NestedListNode:
		v.visitNestedListAST(n)
	case *ast.DescriptionListNode:
		v.visitDescriptionListAST(n)
	case *ast.TableNode:
		v.visitTableAST(n)
	case *ast.TranscludeNode:
		v.b.WriteStrings("{{{", n.Ref.String(), "}}}") // FIXME n.Inlines
		v.visitAttributesAST(n.Attrs)
	case *ast.BLOBNode:
		v.visitBLOBAST(n)
	case *ast.TextNode:
		v.visitTextAST(n)
	case *ast.BreakNode:
		v.visitBreakAST(n)
	case *ast.LinkNode:
		v.visitLinkAST(n)
	case *ast.EmbedRefNode:
		v.visitEmbedRefAST(n)
	case *ast.EmbedBLOBNode:
		v.visitEmbedBLOBAST(n)
	case *ast.CiteNode:
		v.visitCiteAST(n)
	case *ast.FootnoteNode:
		v.b.WriteString("[^")
		ast.Walk(v, &n.Inlines)
		_ = v.b.WriteByte(']')
		v.visitAttributesAST(n.Attrs)
	case *ast.MarkNode:
		v.visitMarkAST(n)
	case *ast.FormatNode:
		v.visitFormatAST(n)
	case *ast.LiteralNode:
		v.visitLiteralAST(n)
	default:
		return v
	}
	return nil
}

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
	ast.VerbatimComment: "%%%",
	ast.VerbatimHTML:    "@@@", // Attribute is set to {="html"}
	ast.VerbatimCode:    "```",
	ast.VerbatimEval:    "~~~",
	ast.VerbatimMath:    "$$$",
}

func (v *zmkVisitor) visitVerbatim(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.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) {
	// 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.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) {
	const headingSigns = "========= "
	v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-3:])
	ast.Walk(v, &hn.Inlines)
	v.visitAttributes(hn.Attrs)
}

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

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

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








|











|












|






|













|



|








|




















|







|







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 *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.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 *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.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 *zmkVisitorAST) visitHeadingAST(hn *ast.HeadingNode) {
	const headingSigns = "========= "
	v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-3:])
	ast.Walk(v, &hn.Inlines)
	v.visitAttributesAST(hn.Attrs)
}

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

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

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
var alignCode = map[ast.Alignment]string{
	ast.AlignDefault: "",
	ast.AlignLeft:    "<",
	ast.AlignCenter:  ":",
	ast.AlignRight:   ">",
}

func (v *zmkVisitor) visitTable(tn *ast.TableNode) {
	if header := tn.Header; len(header) > 0 {
		v.writeTableHeader(header, tn.Align)
		v.b.WriteLn()
	}
	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) {
	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) {
	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) {
	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

	_, _ = v.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) {
	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







|












|













|









|







>
|







|







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 *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 *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 *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 *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
	_ = 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 *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
				continue
			}
		}
	}
	v.b.WriteString(tn.Text[last:])
}

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

func (v *zmkVisitor) visitLink(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(), "]]")

}

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

}

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

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

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








|








|









>


|






>


|









|






|


|







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 *zmkVisitorAST) visitBreakAST(bn *ast.BreakNode) {
	if bn.Hard {
		v.b.WriteString("\\\n")
	} else {
		v.b.WriteLn()
	}
	v.writePrefixSpaces()
}

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 *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 *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 *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.visitAttributesAST(cn.Attrs)
}

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
	ast.FormatSuper:  []byte("^^"),
	ast.FormatSub:    []byte(",,"),
	ast.FormatQuote:  []byte(`""`),
	ast.FormatMark:   []byte("##"),
	ast.FormatSpan:   []byte("::"),
}

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

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

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

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

			_ = v.b.WriteByte('"')
		}
	}
	_ = v.b.WriteByte('}')
}

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







|







|


|





|






|







|



|


|
|














|
>






|







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 *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.visitAttributesAST(fn.Attrs)
}

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.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.visitAttributesAST(ln.Attrs)
		v.b.WriteSpace()
		_, _ = v.b.Write(ln.Content)
	default:
		panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind))
	}
}

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.visitAttributesAST(a)
}

// 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.WriteString("=\"")
			v.writeEscaped(vl, '"')
			_ = v.b.WriteByte('"')
		}
	}
	_ = v.b.WriteByte('}')
}

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

	"zettelstore.de/z/internal/ast"

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

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

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

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


		}
	}
	return bs
}

// EvaluateBlock evaluates the given block list in the given context, with
// the given ports, and the given environment.
func EvaluateBlock(ctx context.Context, port Port, rtConfig config.Config, bns *ast.BlockSlice) {
	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{},
	}













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

}

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

}

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



































































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

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

			i += transcludeNode(bs, i, e.evalVerbatimNode(n))

		case *ast.TranscludeNode:
			i += transcludeNode(bs, i, e.evalTransclusionNode(n))
		}
	}
}

func transcludeNode(bln *ast.BlockSlice, i int, bn ast.BlockNode) int {
	if ln, ok := bn.(*ast.BlockSlice); ok {
		*bln = replaceWithBlockNodes(*bln, i, *ln)
		return len(*ln) - 1
	}
	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 {
	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 {
	m := meta.New(id.Invalid)
	m.Set(meta.KeySyntax, getSyntax(vn.Attrs, meta.ValueSyntaxText))
	zettel := zettel.Zettel{
		Meta:    m,
		Content: zettel.NewContent(vn.Content),
	}
	e.transcludeCount++
	zn := e.evaluateEmbeddedZettel(zettel)
	return &zn.BlocksAST
}

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

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

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

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

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

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

func (e *evaluator) checkMaxTransclusions(ref *ast.Reference) ast.InlineNode {
	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 nil
}

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

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

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

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

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

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

	zid := mustParseZid(ref)
	_, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
	if errors.Is(err, &box.ErrNotAllowed{}) {
		return &ast.FormatNode{
			Kind:    ast.FormatSpan,
			Attrs:   ln.Attrs,
			Inlines: getLinkInline(ln),
		}
	} else if err != nil {
		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 {
	ref := en.Ref

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

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

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

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

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

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

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

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

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














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

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

func createEmbeddedNodeLocal(ref *ast.Reference) *ast.EmbedRefNode {
	ext := path.Ext(ref.Value)
	if ext != "" && ext[0] == '.' {
		ext = ext[1:]
	}
	pinfo := parser.Get(ext)
	if pinfo == nil || !pinfo.IsImageFormat {
		return nil
	}
	return &ast.EmbedRefNode{
		Ref:    ref,
		Syntax: ext,
	}
}

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

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

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

type fragmentSearcher struct {
	fragment string
	result   ast.InlineSlice
}

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

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

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

func skipBreakeNodes(ins ast.InlineSlice) ast.InlineSlice {
	for i, in := range ins {
		switch in.(type) {
		case *ast.BreakNode:
		default:
			return ins[i:]
		}
	}
	return nil
}







<






<

|
|
|
|



>















|



|

|

|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
|
|
>
>
|
|
<
|
<


|







|
>
|

>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
>









|
|
>



|



>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>



|

|






|





>
|
>

|




|

|










|

















<
<
<
<
<
<
<
<
<
<
<
<
|

|





|



|











|



|
|






|


|



|

|




|

|











|








|

|


|










|






|

|






|


|
|
|




|

|







|




<
<

|




|

|










|

















<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|



|








|


|



|






|


|






|



|




|






|




|





|


|
|















|







|


|








>
>
>
>
|
>
>
>
>
>
>
>
>
>
|








|
|















|














|





|

|

|




|
















|




|





|

|






|









|


|













|









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 (

	"context"
	"errors"
	"fmt"
	"path"
	"slices"
	"strconv"


	"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.Zettel) {
	switch zn.Syntax {
	case meta.ValueSyntaxNone:
		// AST is empty, evaluate to a description list of metadata.
		zn.Blocks = evaluateMetadata(zn.Meta)
	case meta.ValueSyntaxSxn:
		zn.Blocks = evaluateSxn(zn.Blocks)
	default:
		zn.BlocksAST = EvaluateBlock(ctx, port, rtConfig, zn.Blocks)





















		return
	}

	if blk, err := sztrans.GetBlockSlice(zn.Blocks); err == nil {
		zn.BlocksAST = blk
	}
}



// 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, 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]*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.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.Zettel
	embedMap        map[string]*sx.Pair
	embedMapAST     map[string]ast.InlineSlice
}

type transcludeCost struct {
	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.visitBlockSliceAST(n)
	case *ast.InlineSlice:
		e.visitInlineSliceAST(n)
	default:
		return e
	}
	return nil
}

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 += transcludeNodeAST(bs, i, e.evalVerbatimZettelNodeAST(n))
			}
		case *ast.TranscludeNode:
			i += transcludeNodeAST(bs, i, e.evalTransclusionNodeAST(n))
		}
	}
}

func transcludeNodeAST(bln *ast.BlockSlice, i int, bn ast.BlockNode) int {
	if ln, ok := bn.(*ast.BlockSlice); ok {
		*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 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) evalVerbatimZettelNodeAST(vn *ast.VerbatimNode) ast.BlockNode {
	m := meta.New(id.Invalid)
	m.Set(meta.KeySyntax, getSyntaxAST(vn.Attrs, meta.ValueSyntaxText))
	zettel := zettel.Zettel{
		Meta:    m,
		Content: zettel.NewContent(vn.Content),
	}
	e.transcludeCount++
	zn := e.evaluateEmbeddedZettelAST(zettel)
	return &zn.BlocksAST
}

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) evalTransclusionNodeAST(tn *ast.TranscludeNode) ast.BlockNode {
	ref := tn.Ref

	// To prevent e.embedCount from counting
	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 makeBlockNodeAST(createInlineErrorTextAST(ref, "Invalid or broken transclusion reference"))
	case ast.RefStateSelf:
		e.transcludeCount++
		return makeBlockNodeAST(createInlineErrorTextAST(ref, "Self transclusion reference"))
	case ast.RefStateFound, ast.RefStateExternal:
		return tn
	case ast.RefStateHosted, ast.RefStateBased:
		if n := createEmbeddedNodeLocalAST(ref); n != nil {
			n.Attrs = tn.Attrs
			return makeBlockNodeAST(n)
		}
		return tn
	case ast.RefStateQuery:
		e.transcludeCount++
		return e.evalQueryTransclusionAST(tn.Ref.Value)
	default:
		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 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 makeBlockNodeAST(createInlineErrorTextAST(ref, "Unable to get zettel"))
		}
		setMetadataFromAttributesAST(zettel.Meta, tn.Attrs)
		ec := e.transcludeCount
		e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec}
		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) 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 makeBlockNodeAST(createInlineErrorTextAST(nil, "Unable to search zettel"))
	}
	result := QueryActionAST(e.ctx, q, ml)
	if result != nil {
		ast.Walk(e, result)
	}
	return result
}

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

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

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) visitInlineSliceAST(is *ast.InlineSlice) {
	for i := 0; i < len(*is); i++ {
		in := (*is)[i]
		ast.Walk(e, in)
		switch n := in.(type) {


		case *ast.EmbedRefNode:
			i += embedNodeAST(is, i, e.evalEmbedRefNodeAST(n))
		}
	}
}

func embedNodeAST(is *ast.InlineSlice, i int, in ast.InlineNode) int {
	if ln, ok := in.(*ast.InlineSlice); ok {
		*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 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) evalEmbedRefNodeAST(en *ast.EmbedRefNode) ast.InlineNode {
	ref := en.Ref

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

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

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

	cost, ok := e.costMap[zid]
	zn := cost.zn
	if zn == e.marker {
		e.transcludeCount++
		return createInlineErrorTextAST(ref, "Recursive transclusion")
	}
	if !ok {
		ec := e.transcludeCount
		e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec}
		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.embedMapAST[ref.Value]
	if !ok {
		// Search for text to be embedded.
		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 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) updateImageRefNodeAST(en *ast.EmbedRefNode, m *meta.Meta, syntax string) {
	en.Syntax = syntax
	if len(en.Inlines) == 0 {
		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
	}
	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 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 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) 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 findInlineSliceAST(bs *ast.BlockSlice, fragment string) ast.InlineSlice {
	if fragment == "" {
		return firstInlinesToEmbedAST(*bs)
	}
	fs := fragmentSearcherAST{fragment: fragment}
	ast.Walk(&fs, bs)
	return fs.result
}

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 fragmentSearcherAST struct {
	fragment string
	result   ast.InlineSlice
}

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

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 *fragmentSearcherAST) visitInlineSliceAST(is *ast.InlineSlice) {
	for i, in := range *is {
		if mn, ok := in.(*ast.MarkNode); ok && mn.Fragment == fs.fragment {
			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 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
	"bytes"
	"context"
	"math"
	"slices"
	"strconv"
	"strings"


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

	"t73f.de/r/zsx"

	"zettelstore.de/z/internal/ast"

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







// QueryAction transforms a list of metadata according to query actions into a AST nested list.
func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta) (ast.BlockNode, int) {
	ap := actionPara{
		ctx:    ctx,
		q:      q,
		ml:     ml,
		kind:   ast.NestedListUnordered,
		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
			continue
		}
		if strings.HasPrefix(act, api.MinAction) {
			if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
				ap.minVal = num
				continue
			}







>


>



>



>
>
>
>
>
>
|
|




|











|







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 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:   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 = 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
		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
	minVal int
	maxVal int
}

func (ap *actionPara) createBlockNodeWord(key string) (ast.BlockNode, 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()


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

		buf.Truncate(bufLen)
	}
	return &ast.NestedListNode{
		Kind:  ap.kind,
		Items: items,
		Attrs: nil,
	}, len(items)
}

func (ap *actionPara) createBlockNodeTagSet(key string) (ast.BlockNode, 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)

	para := make(ast.InlineSlice, 0, len(ccs))
	ccs.SortByName()


	for i, cat := range ccs {
		if i > 0 {
			para = append(para, &ast.TextNode{Text: " "})
		}
		buf.WriteString(string(cat.Name))
		para = append(para,

			&ast.LinkNode{
				Attrs: countMap[cat.Count],
				Ref:   ast.ParseReference(buf.String()),
				Inlines: ast.InlineSlice{
					&ast.TextNode{Text: string(cat.Name)},
				},
			},
			&ast.FormatNode{
				Kind:    ast.FormatSuper,
				Attrs:   nil,
				Inlines: ast.InlineSlice{&ast.TextNode{Text: strconv.Itoa(cat.Count)}},
			},
		)

		buf.Truncate(bufLen)
	}
	return &ast.ParaNode{Inlines: para}, len(ccs)
}

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
		}







>











|




|





|

>
>


<
>
|
|
|
|
>


<
|
<
<
<


|







|

<

>
>


|


<
>
|
|
|
<
|
<
<
<
|
|
|
<

>


|







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   *sx.Symbol
	minVal int
	maxVal 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
	}

	ccs.SortByName()
	var items sx.ListBuilder
	count := 0
	for _, cat := range ccs {
		buf.WriteString(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 zsx.MakeList(ap.kind, nil, items.List()), count



}

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)
	countClassAttrs := ap.calcFontSizes(ccs)


	ccs.SortByName()
	var tags sx.ListBuilder
	count := 0
	for i, cat := range ccs {
		if i > 0 {
			tags.Add(zsx.MakeText(" "))
		}
		buf.WriteString(string(cat.Name))

		tags.AddN(
			zsx.MakeLink(
				countClassAttrs[cat.Count],
				sz.ScanReference(buf.String()),

				sx.MakeList(zsx.MakeText(string(cat.Name)))),



			zsx.MakeFormat(zsx.SymFormatSuper,
				nil,
				sx.MakeList(zsx.MakeText(strconv.Itoa(cat.Count)))),

		)
		count++
		buf.Truncate(bufLen)
	}
	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
			}
			return temp
		}
	}
	return ccs
}

func (ap *actionPara) createBlockNodeMetaKeys() (ast.BlockNode, 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))

	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{
				Attrs:   nil,
				Ref:     ast.ParseReference(q1),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: string(cat.Name)}},
			},
			&ast.TextNode{Text: " "},
			&ast.TextNode{Text: "(" + strconv.Itoa(cat.Count) + ", "},
			&ast.LinkNode{
				Attrs:   nil,
				Ref:     ast.ParseReference(q2),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: "values"}},
			},
			&ast.TextNode{Text: ")"},
		)})

	}
	return &ast.NestedListNode{
		Kind:  ap.kind,
		Items: items,
		Attrs: nil,
	}, len(items)
}

func (ap *actionPara) createBlockNodeMeta(key string) (ast.BlockNode, int) {
	if len(ap.ml) == 0 {
		return nil, 0
	}
	items := make([]ast.ItemSlice, 0, len(ap.ml))

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

	}
	return &ast.NestedListNode{
		Kind:  ap.kind,
		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()







|














|
>










|
|
<
|
|
<
|
|
|
<
|
|
<
|
|
>

<
|
<
<
<


|



|
>






|
|
|
|
|
>

<
|
<
<
<







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() (*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)
	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.Add(zsx.MakeBlock(zsx.MakePara(
			zsx.MakeLink(nil,

				sz.ScanReference(q1),
				sx.MakeList(zsx.MakeText(string(cat.Name)))),

			zsx.MakeText(" "),
			zsx.MakeText("("+strconv.Itoa(cat.Count)+", "),
			zsx.MakeLink(nil,

				sz.ScanReference(q2),
				sx.MakeList(zsx.MakeText("values"))),

			zsx.MakeText(")"),
		)))
		count++
	}

	return zsx.MakeList(ap.kind, nil, items.List()), count



}

func (ap *actionPara) createBlockNodeMeta(key string) (*sx.Pair, int) {
	if len(ap.ml) == 0 {
		return nil, 0
	}
	var items sx.ListBuilder
	count := 0
	for _, m := range ap.ml {
		if key != "" {
			if _, found := m.Get(key); !found {
				continue
			}
		}
		items.Add(zsx.MakeBlock(zsx.MakePara(
			zsx.MakeLink(nil,
				sz.ScanReference(m.Zid.String()),
				sx.MakeList(zsx.MakeText(sz.NormalizedSpacedText(m.GetTitle())))),
		)))
		count++
	}

	return zsx.MakeList(ap.kind, nil, items.List()), count



}

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
	}
	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
	var a zsx.Attributes
	for i := range fontSizes {
		fsAttrs[i] = a.AddClass("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))
	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++
		}







|
|
<

|













|







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]*sx.Pair {
	var fsAttrs [fontSizes]*sx.Pair

	for i := range fontSizes {
		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]*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
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package evaluator

import (




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

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

func evaluateMetadata(m *meta.Meta) ast.BlockSlice {

	descrlist := &ast.DescriptionListNode{}
	for key, val := range m.All() {
		descrlist.Descriptions = append(
			descrlist.Descriptions, getMetadataDescription(key, val))
	}
	return ast.BlockSlice{descrlist}
}

func getMetadataDescription(key string, value meta.Value) ast.Description {
	is := convertMetavalueToInlineSlice(value, meta.Type(key))
	return ast.Description{
		Term:         ast.InlineSlice{&ast.TextNode{Text: key}},
		Descriptions: []ast.DescriptionSlice{{&ast.ParaNode{Inlines: is}}},

	}

}

func convertMetavalueToInlineSlice(value meta.Value, dt *meta.DescriptionType) ast.InlineSlice {
	var sliceData []string
	if dt.IsSet {
		sliceData = value.AsSlice()
		if len(sliceData) == 0 {
			return nil
		}
	} else {
		sliceData = []string{string(value)}
	}
	makeLink := dt == meta.TypeID || dt == meta.TypeIDSet


	result := make(ast.InlineSlice, 0, 2*len(sliceData)-1)
	for i, val := range sliceData {
		if i > 0 {
			result = append(result, &ast.TextNode{Text: " "})


		}
		tn := &ast.TextNode{Text: val}
		if makeLink {
			result = append(result, &ast.LinkNode{
				Ref:     ast.ParseReference(val),
				Inlines: ast.InlineSlice{tn},
			})
		} else {
			result = append(result, tn)
		}
	}

	return result
}







>
>
>
>

|
|


|
>
|

<
<
<
<
<
|
<
|
<
<
<
>

>


|
|

|
<
<
<

|



>
|
|
|
|
>
>

|

<
|
<
<

|


>
|

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"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"
)

func evaluateMetadata(m *meta.Meta) *sx.Pair {
	var lb sx.ListBuilder
	lb.AddN(zsx.SymDescription, nil)
	for key, val := range m.All() {





		lb.Add(sx.MakeList(zsx.MakeText(key)))

		inlines := convertMetavalueToInlineSlice(val, meta.Type(key))



		lb.Add(zsx.MakeBlock(zsx.MakeBlock(zsx.MakeParaList(inlines))))
	}
	return zsx.MakeBlock(lb.List())
}

func convertMetavalueToInlineSlice(val meta.Value, dt *meta.DescriptionType) *sx.Pair {
	var vals iter.Seq[string]
	if dt.IsSet {
		vals = val.Fields()



	} else {
		vals = zeroiter.OneSeq(string(val))
	}
	makeLink := dt == meta.TypeID || dt == meta.TypeIDSet

	var lb sx.ListBuilder
	first := true
	for s := range vals {
		if first {
			first = false
		} else {
			lb.Add(zsx.MakeText(" "))
		}
		tn := zsx.MakeText(s)
		if makeLink {

			lb.Add(zsx.MakeLink(nil, sz.ScanReference(s), sx.MakeList(tn)))


		} else {
			lb.Add(tn)
		}
	}

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

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







|

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

// cleaner provides functions to clean up the parsed AST.

import (
	"strconv"
	"strings"


	zerostrings "t73f.de/r/zero/strings"


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








































































































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

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

func (cv *cleanVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		if !cv.allowHTML {
			cv.visitBlockSlice(n)
			return nil
		}
	case *ast.InlineSlice:







>

>





>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|











|
<






|







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
}

// 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 cleanASTVisitor struct {

	ids       map[string]ast.Node
	allowHTML bool
	hasMark   bool
	doMark    bool
}

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
	case *ast.MarkNode:
		cv.visitMark(n)
		return nil
	}
	return cv
}

func (cv *cleanVisitor) visitBlockSlice(bs *ast.BlockSlice) {
	if bs == nil {
		return
	}
	if len(*bs) == 0 {
		*bs = nil
		return
	}







|







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 *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
	}
	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) {
	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) {
	if cv.doMark || hn == nil || len(hn.Inlines) == 0 {
		return
	}
	if hn.Slug == "" {
		var sb strings.Builder

		_, err := cv.textEnc.WriteInlines(&sb, &hn.Inlines)
		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) {
	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 {
	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++ {







|












|





>
|
<









|















|







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 *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 *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
		if err := textEnc.WriteInlines(&sb, &hn.Inlines); err != nil {

			return
		}
		hn.Slug = zerostrings.Slugify(sb.String())
	}
	if hn.Slug != "" {
		hn.Fragment = cv.addIdentifier(hn.Slug, hn)
	}
}

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







<
<







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"


)

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

// ParseDrawBlock parses the content of an eval verbatim node into an SVG image BLOB.
func ParseDrawBlock(vn *ast.VerbatimNode) ast.BlockNode {

	font := defaultFont
	if val, found := vn.Attrs.Get("font"); found {
		font = val
	}
	scaleX := getScale(vn.Attrs, "x-scale", defaultScaleX)
	scaleY := getScale(vn.Attrs, "y-scale", defaultScaleY)

	canvas, err := aasvg.NewCanvas(vn.Content)
	if err != nil {
		return ast.CreateParaNode(ast.InlineSlice{&ast.TextNode{Text: "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 &ast.BLOBNode{
		Description: nil, // TODO: look for attribute "summary" / "title"
		Syntax:      meta.ValueSyntaxSVG,
		Blob:        svg,


	}
}

func getScale(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
}







|



|
>

|


|
|

|

|









|

|
|
|
|
>
>
|
|
<
|







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, meta.ValueSyntaxSVG, svg, ParseDescription(m)))
}

// ParseDrawBlock parses the content of an eval verbatim node into an SVG image BLOB.
func ParseDrawBlock(attrs *sx.Pair, content []byte) *sx.Pair {
	a := zsx.GetAttributes(attrs)
	font := defaultFont
	if val, found := a.Get("font"); found {
		font = val
	}
	scaleX := getScaleAST(a, "x-scale", defaultScaleX)
	scaleY := getScaleAST(a, "y-scale", defaultScaleY)

	canvas, err := aasvg.NewCanvas(content)
	if err != nil {
		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 zsx.MakePara(zsx.MakeText("NO IMAGE"))
	}
	return zsx.MakeBLOB(
		nil,
		meta.ValueSyntaxSVG,
		svg,
		nil, // TODO: look for attribute "summary" / "title" for a description.
	)
}


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

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







<







|


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

package parser

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

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


	})
}







>












|
>
>


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(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
import (
	"context"
	"fmt"
	"log"

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

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







>







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
	pi, ok := registry[syntax]
	if !ok {
		return false
	}
	return pi.IsImageFormat
}

// Parse parses some input and returns a slice of block nodes.
func Parse(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice {
	if obj := Get(syntax).Parse(inp, m, syntax); obj != nil {
		bs, err := sztrans.GetBlockSlice(obj)
		if err == nil {
			Clean(&bs, hi.AllowHTML(syntax))
			return bs
		}
		log.Printf("sztrans error: %v, for %v\n", err, obj)
	}
	return nil
}

// 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())
	}
	if title, found := m.Get(meta.KeyTitle); found {
		return sx.Cons(zsx.MakeText(ast.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 {
	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{
		Meta:      m,
		Content:   zettel.Content,
		Zid:       m.Zid,
		InhMeta:   inhMeta,

		BlocksAST: bs,
		Syntax:    syntax,
	}
}







|
|



<
|



|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<









|


|





|













<
<
<
<
|
|




>




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

			return obj, bs
		}
		log.Printf("sztrans error: %v, for %v\n", err, obj)
	}
	return nil, nil















}

// 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(sz.NormalizedSpacedText(string(summary))), sx.Nil())
	}
	if title, found := m.Get(meta.KeyTitle); found {
		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.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
	}





	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

import (
	"bytes"

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

	"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,
	})
	register(&Info{
		Name:          meta.ValueSyntaxCSS,
		AltNames:      []string{},
		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: false,







>



















|







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:         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
		IsTextFormat:  true,
		IsImageFormat: false,
		Parse:         parsePlainSxn,
	})
}

func parsePlain(inp *input.Input, _ *meta.Meta, syntax string) *sx.Pair {
	return doParsePlain(inp, syntax, zsx.SymVerbatimCode)
}
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(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())
}

func scanSVG(inp *input.Input) string {
	inp.SkipSpace()
	pos := inp.Pos
	if !inp.Accept("<svg") {
		return ""







|
<
<
<
<
<
<
<
<
<
<





|









|







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 sz.ParsePlainBlocks(inp, syntax)










}

func parsePlainSVG(inp *input.Input, _ *meta.Meta, syntax string) *sx.Pair {
	is := parseSVGInlines(inp, syntax)
	if is == 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.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

import (
	"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"
)

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()
			lst := trans.GetSz(&bs)
			if got := lst.String(); tc.exp != got {
				t.Errorf("\nexp: %q\ngot: %q", tc.exp, got)
			}
		})
	}
}







|
<



















|
>
>
>
|







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/ast/sztrans"

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







|
|
|



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

package usecase

import (
	"context"


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


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







>


>







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
		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) {
	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 {
	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 {
		return nil
	}
	bns := ast.BlockSlice{bn}
	evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, &bns)
	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)
}







|








|





|
|
|


<
|
<











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.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.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, formatted as a block.
func (uc *Evaluate) RunBlockNode(ctx context.Context, block *sx.Pair) ast.BlockSlice {
	if block == nil {
		return nil
	}

	return evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, zsx.MakeBlock(block))

}

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

package usecase

import (
	"iter"


	zeroiter "t73f.de/r/zero/iter"
	"t73f.de/r/zsc/domain/meta"



	"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)
		}
	}
	return local, ext, query
}

// RunByExternal returns an iterator of all external references of a zettel.
func (uc GetReferences) RunByExternal(zn *ast.ZettelNode) iter.Seq[*ast.Reference] {
	return zeroiter.FilterSeq(
		collect.ReferenceSeq(zn),


		func(ref *ast.Reference) bool { return ref.State == ast.RefStateExternal })

}

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







>


>
>

<











|
>
|
>
|
|
|
|
|
|
|


|



|

|
>
>
|
>







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/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(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 lbLoc.List(), lbExt.List(), lbQueries.List()
}

// RunByExternal returns an iterator of all external references of a zettel.
func (uc GetReferences) RunByExternal(blocks *sx.Pair) iter.Seq[*sx.Pair] {
	return zeroiter.FilterSeq(
		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

// 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) {
	zettel, err := uc.getZettel.Run(ctx, zid)
	if err != nil {
		return nil, err
	}

	return parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil


}







|





|
>
>

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.Zettel, error) {
	zettel, err := uc.getZettel.Run(ctx, zid)
	if err != nil {
		return nil, err
	}

	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
func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(metaSeq))
	for _, m := range metaSeq {
		zn, err := uc.ucEvaluate.Run(ctx, m.Zid, string(m.GetDefault(meta.KeySyntax, meta.DefaultSyntax)))
		if err != nil {
			continue
		}
		for _, ln := range collect.Order(zn) {
			ref := ln.Ref
			if !ref.IsZettel() {
				continue
			}

			if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil {
				if z, err3 := uc.port.GetZettel(ctx, collectedZid); err3 == nil {







|







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.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
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{
			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 {
	return " " + strings.ToLower(strings.Join(words, " ")) + " "
}

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

func (v *unlinkedVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.InlineSlice:
		v.checkWords(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) {
	if len(*is) < 2*len(v.words)-1 {
		return
	}
	for _, text := range v.splitInlineTextList(is) {
		if strings.Contains(text, v.text) {
			v.found = true
		}
	}
}

func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string {
	var result []string
	var curList []string
	for _, in := range *is {
		switch n := in.(type) {
		case *ast.TextNode:
			curList = append(curList, zerostrings.MakeWords(n.Text)...)
		default:







|


















|



|





|


|









|



|






|







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 := 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 (*unlinkedVisitorAST) joinWords(words []string) string {
	return " " + strings.ToLower(strings.Join(words, " ")) + " "
}

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

func (v *unlinkedVisitorAST) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.InlineSlice:
		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 *unlinkedVisitorAST) checkWordsAST(is *ast.InlineSlice) {
	if len(*is) < 2*len(v.words)-1 {
		return
	}
	for _, text := range v.splitInlineTextListAST(is) {
		if strings.Contains(text, v.text) {
			v.found = true
		}
	}
}

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
	"iter"
	"net/http"

	"t73f.de/r/sx"
	zeroiter "t73f.de/r/zero/iter"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"


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







>

<







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

		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),
			)
		case partMeta:
			seq = ucGetReferences.RunByMeta(zn.InhMeta)
		case partContent:
			seq = getExternalURLs(zn, 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 {







|




|







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.Blocks, ucGetReferences),
			)
		case partMeta:
			seq = ucGetReferences.RunByMeta(zn.InhMeta)
		case partContent:
			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
		}
		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] {
	return zeroiter.MapSeq(
		ucGetReferences.RunByExternal(zn),
		func(ref *ast.Reference) string { return ref.Value },



	)
}







|

|
|
>
>
>


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(blocks *sx.Pair, ucGetReferences usecase.GetReferences) iter.Seq[string] {
	return zeroiter.MapSeq(
		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
		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
			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)







|







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.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
	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,
	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)
	case partMeta:
		_, err = encdr.WriteMeta(&buf, zn.InhMeta)
	case partContent:
		_, err = encdr.WriteBlocks(&buf, &zn.BlocksAST)


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







|















|

|

|
>
>







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.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)
	case partMeta:
		err = encdr.WriteMeta(&buf, zn.InhMeta)
	case partContent:
		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
type partType int

const (
	_ partType = iota
	partMeta
	partContent
	partZettel

)

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

}

func getPart(q url.Values, defPart partType) partType {
	if part, ok := partMap[q.Get(api.QueryKeyPart)]; ok {
		return part
	}
	return defPart







>






>







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
	"net/http"
	"strings"

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


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







>

<







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/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
		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))
			wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel, newTitle), title, "", roleData, syntaxData)
		case actionSequel:
			wui.renderZettelForm(ctx, w, createZettel.PrepareSequel(origZettel), "Sequel Zettel", "", roleData, syntaxData)
		}
	})
}








|
|







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 := 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
			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 != 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)







|
<







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
		if err = enc.WriteBlocks(&zmkContent, &bns); 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
	"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"



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







>
>

<







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

		title := ast.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)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		encTexts := encodingTexts()
		shadowLinks := getShadowLinks(ctx, zid, zn.InhMeta.GetDefault(meta.KeyBoxNumber, ""), ucGetAllZettel)








|

|













|







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

		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.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
		}
		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)
	var lbLoc, lbQueries, lbExt sx.ListBuilder
	for _, ref := range locRefs {

		lbLoc.Add(sx.MakeString(ref.String()))
	}
	for _, ref := range extRefs {

		lbExt.Add(sx.MakeString(ref.String()))
	}
	for _, ref := range queryRefs {

		lbQueries.Add(
			sx.Cons(
				sx.MakeString(ref.Value),
				sx.MakeString(wui.NewURLBuilder('h').AppendQuery(ref.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())







|
|

|
>
|

|
>
|

|
>


|
|







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, blocks *sx.Pair) (locLinks, extLinks, queries *sx.Pair) {
	locRefs, extRefs, queryRefs := ucGetReferences.RunByState(blocks)
	var lbLoc, lbQueries, lbExt sx.ListBuilder
	for ref := range locRefs.Pairs() {
		_, value := zsx.GetReference(ref.Head())
		lbLoc.Add(sx.MakeString(value))
	}
	for ref := range extRefs.Pairs() {
		_, value := zsx.GetReference(ref.Head())
		lbExt.Add(sx.MakeString(value))
	}
	for ref := range queryRefs.Pairs() {
		_, value := zsx.GetReference(ref.Head())
		lbQueries.Add(
			sx.Cons(
				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
	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()




	u := wui.NewURLBuilder('z').SetZid(zid)
	for ip := len(apiParts) - 1; ip >= 0; ip-- {
		part := apiParts[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)







>
>
>
>

|
|







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


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







>

<







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/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
			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)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		user := user.GetCurrentUser(ctx)
		getTextTitle := wui.makeGetTextTitle(ctx, getZettel)

		title := ast.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(







|








|







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.BlocksSxnAST(&zn.BlocksAST)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		user := user.GetCurrentUser(ctx)
		getTextTitle := wui.makeGetTextTitle(ctx, getZettel)

		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
import (
	"maps"
	"net/url"
	"slices"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zero/set"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/shtml"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"

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

// Builder allows to build new URLs for the web service.
type urlBuilder interface {
	GetURLPrefix() string
	NewURLBuilder(key byte) *api.URLBuilder
}

type htmlGenerator struct {
	tx    encoder.SzTransformer
	th    *shtml.Evaluator
	lang  string
	symAt *sx.Symbol
}

func (wui *WebUI) createGenerator(builder urlBuilder, lang string) *htmlGenerator {
	th := shtml.NewEvaluator(1)

	findA := func(obj sx.Object) (attr, assoc, rest *sx.Pair) {
		pair, isPair := sx.GetPair(obj)
		if !isPair || !shtml.SymA.IsEqual(pair.Car()) {
			return nil, nil, nil
		}
		rest = pair.Tail()
		if rest == nil {
			return nil, 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()
	}

	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 {
			return obj
		}
		if zsx.SymRefStateExternal.IsEqual(refSym) {
			a := zsx.GetAttributes(attr)
			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







<









|




<




|








|
|
|
|



|

<
|
|
|
|
>
|





|
|



|







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/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/ast/sztrans"
)

// Builder allows to build new URLs for the web service.
type urlBuilder interface {

	NewURLBuilder(key byte) *api.URLBuilder
}

type htmlGenerator struct {
	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) (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
		}

		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)
		assoc, rest := findA(obj)
		if assoc == nil {
			return obj
		}
		if zsx.SymRefStateExternal.IsEqual(refSym) {
			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
			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)

		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)

		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 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()) {
			return obj
		}




		srcP := attr.Tail().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)
		return pair.Tail().Tail().Cons(imgAttr).Cons(shtml.SymIMG)
	})

	return &htmlGenerator{
		tx:   encoder.NewSzTransformer(),
		th:   th,
		lang: lang,
	}
}

func rebind(ev *shtml.Evaluator, sym *sx.Symbol, fn func(sx.Object) sx.Object) {
	prevFn := ev.ResolveBinding(sym)







|















|




|










|


>
>
>
>
|












|




|







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(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(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(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 {
			return obj
		}
		_, isAttr := sx.GetPair(attr.Car())
		if !isAttr {
			return obj
		}
		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())))
		return pair.Tail().Tail().Cons(imgAttr).Cons(shtml.SymIMG)
	})

	return &htmlGenerator{
		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

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)
	env := shtml.MakeEnvironment(g.lang)
	hm, err := g.th.Evaluate(tm, &env)
	if err != nil {
		return nil
	}

	ignore := set.New(meta.KeyTitle, meta.KeyLang)







|







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 := 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
	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 {
		return nil, nil, nil
	}
	sx := g.tx.GetSz(bs)
	env := shtml.MakeEnvironment(g.lang)
	sh, err := g.th.Evaluate(sx, &env)
	if err != nil {
		return nil, nil, err
	}
	return sh, shtml.Endnotes(&env), nil
}




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




	env := shtml.MakeEnvironment(g.lang)
	sh, err := g.th.Evaluate(sz, &env)
	if err != nil {
		return nil
	}
	return sh
}







|
|


<

|





>
>
>
|
<
|
>
>
>
>

|





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(block *sx.Pair) (content, endnotes *sx.Pair, _ error) {
	if block == nil || block.Tail() == nil {
		return nil, nil, nil
	}

	env := shtml.MakeEnvironment(g.lang)
	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
	}

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

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


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







>

<







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/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
	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"),
				sx.MakeList(
					sxhtml.SymAttr,
					sx.Cons(sx.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)))







|

<
|







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(
				sxhtml.MakeSymbol("time"),
				sx.MakeList(

					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
	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)
		return sx.Nil().Cons(sx.MakeString(zid.String())).Cons(attrs).Cons(shtml.SymA)
	case found == 0:
		return sx.MakeList(sx.MakeSymbol("s"), text)
	default: // case found < 0:
		return text
	}
}

var space = sx.MakeString(" ")








|


|







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())))
		return sx.Nil().Cons(sx.MakeString(zid.String())).Cons(attrs).Cons(shtml.SymA)
	case found == 0:
		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
	}
	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.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
	}
}







<
<
|
<















|


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


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







<




>

<







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

		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})
			if err != nil {
				wui.reportError(ctx, w, err)
				return
			}
			numEntries = cnt
		}








|







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(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
	}
	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.MakeString(name),
	)
	if sxZtl != nil {
		sxZtl = sxZtl.Cons(sx.MakeString(", "))
	}
	return sxZtl.Cons(link)
}







<
<
|
<







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



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







>
>







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

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







<







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(

					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
}
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
	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))
	}
	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() {
			continue
		}
		zid, err2 := id.Parse(ref.URL.Path)
		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()))
		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 != nil && endnotes != nil {
					content.LastPair().SetCdr(sx.Cons(endnotes, nil))
				}
				return content
			}
		}
	}







|
















|
|














|
|
>
>
|


|










|












|







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.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.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
	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(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(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.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
	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 != 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.







|
<







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
	if err = gen.WriteHTML(&sb, pageObj); 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
	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("X-Content-Type-Options", "nosniff")
	if !wui.debug {
		h.Set("X-Frame-Options", "sameorigin")
	}
	w.WriteHeader(statusCode)
}







<
<
<















|






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




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

type webServer struct {
	log              *slog.Logger
	baseURL          string
	httpServer       httpServer
	router           httpRouter

	persistentCookie bool
	secureCookie     bool
}

// ConfigData contains the data needed to configure a server.
type ConfigData struct {
	Log              *slog.Logger







>







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
}

// New creates a new web server.
func New(sd ConfigData) Server {
	srv := webServer{
		log:              sd.Log,
		baseURL:          sd.BaseURL,

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



	srv.router.addListRoute(key, method, handler)
}
func (srv *webServer) AddZettelRoute(key byte, method Method, handler http.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)
}
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(),
		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)
	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")
	}
}








>


















|











|
>
>
>


|
>
>
>






<
<
<

|











|







|







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, 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(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(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) NewURLBuilder(key byte) *api.URLBuilder {
	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.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", 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
	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)
	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.







|
|





<







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

	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
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"testing"


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







>





<







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

	}
}

func createMDBlockSlice(markdown string, hi config.HTMLInsecurity) ast.BlockSlice {
	return parser.Parse(input.NewInput([]byte(markdown)), nil, meta.ValueSyntaxMarkdown, hi)
}

func testAllEncodings(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)
			sb.Reset()
		})
	}
}

func testZmkEncoding(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)
		// gotFirst := buf.String()

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





		_, _ = zmkEncoder.WriteBlocks(&sb, &ast)
		got := sb.String()
		if got != tc.exp {
			t.Errorf("%d: %q -> %q, but got %q", i, tc.md, tc.exp, got)
		}
	}
}







|
|
>
|
>



|
|


|
>
>
>
>
>
>
>
>
>
>




|





|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>





|



|

|







|

|


















|

>
>
>
>
>
|
|
<




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

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)
			sb.Reset()
		})
	}
}

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)
		// gotFirst := buf.String()

		testID = tc.Example*100 + 2
		_, secondAst := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk)
		buf.Reset()
		_ = 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)
		buf.Reset()
		_ = 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 {
		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)
		if got := sb.String(); 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
	"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







<







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/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
	}
	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)
			for _, enc := range encs {



				_, err = enc.WriteBlocks(io.Discard, &bs)
				if err != nil {
					t.Error(err)
				}
			}
		}
	}
}







|

>
>
>
|
<






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 {
			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)
				}
				if err = enc.WriteBlocks(io.Discard, &bs); 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
	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) {
	t.Helper()

	if enc := encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN}); enc != nil {
		var sf strings.Builder
		_, _ = 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) {







|




|







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.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)
		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
<title>Change Log</title>

<a id="0_23"></a>
<h2>Changes for Version 0.23.0 (pending)</h2>










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




>
>
>
>
>
>
>
>
>







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>