DELETED .github/dependabot.yml Index: .github/dependabot.yml ================================================================== --- .github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -# 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" Index: VERSION ================================================================== --- VERSION +++ VERSION @@ -1,1 +1,1 @@ -0.22.0 +0.23.0-dev Index: cmd/cmd_file.go ================================================================== --- cmd/cmd_file.go +++ cmd/cmd_file.go @@ -45,19 +45,20 @@ Content: zettel.NewContent(inp.Src[inp.Pos:]), }, string(m.GetDefault(meta.KeySyntax, meta.DefaultSyntax)), nil, ) + parser.Clean(z.Blocks, false) + parser.CleanAST(&z.BlocksAST, false) encdr := encoder.Create( api.Encoder(enc), &encoder.CreateParameter{Lang: string(m.GetDefault(meta.KeyLang, meta.ValueLangEN))}) if encdr == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc) return 2, nil } - _, err = encdr.WriteZettel(os.Stdout, z) - if err != nil { + if err = encdr.WriteZettel(os.Stdout, z); err != nil { return 2, err } fmt.Println() return 0, nil Index: cmd/cmd_run.go ================================================================== --- cmd/cmd_run.go +++ cmd/cmd_run.go @@ -93,43 +93,45 @@ if assetDir := kern.GetConfig(kernel.WebService, kernel.WebAssetDir).(string); assetDir != "" { const assetPrefix = "/assets/" webSrv.Handle(assetPrefix, http.StripPrefix(assetPrefix, http.FileServer(http.Dir(assetDir)))) webSrv.Handle("/favicon.ico", wui.MakeFaviconHandler(assetDir)) } + + const isAPI = true // Web user interface if !authManager.IsReadonly() { - webSrv.AddListRoute('c', server.MethodGet, wui.MakeGetZettelFromListHandler(&ucQuery, &ucEvaluate, ucListRoles, ucListSyntax)) - webSrv.AddListRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel)) - webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCreateZettelHandler( - 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( + 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('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate)) - webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler()) - webSrv.AddZettelRoute('r', server.MethodGet, a.MakeGetReferencesHandler(ucParseZettel, ucGetReferences)) - webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion)) - webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh)) - webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex)) - webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel, ucParseZettel, ucEvaluate)) + webSrv.AddListRoute(isAPI, 'a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate)) + webSrv.AddListRoute(isAPI, 'a', server.MethodPut, a.MakeRenewAuthHandler()) + webSrv.AddZettelRoute(isAPI, 'r', server.MethodGet, a.MakeGetReferencesHandler(ucParseZettel, ucGetReferences)) + webSrv.AddListRoute(isAPI, 'x', server.MethodGet, a.MakeGetDataHandler(ucVersion)) + webSrv.AddListRoute(isAPI, 'x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh)) + webSrv.AddListRoute(isAPI, 'z', server.MethodGet, a.MakeQueryHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex)) + webSrv.AddZettelRoute(isAPI, 'z', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel, ucParseZettel, ucEvaluate)) if !authManager.IsReadonly() { - webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel)) - webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate)) - webSrv.AddZettelRoute('z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete)) + webSrv.AddListRoute(isAPI, 'z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel)) + webSrv.AddZettelRoute(isAPI, 'z', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate)) + webSrv.AddZettelRoute(isAPI, 'z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete)) } if authManager.WithAuth() { webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager)) } Index: docs/manual/00001004010000.zettel ================================================================== --- docs/manual/00001004010000.zettel +++ docs/manual/00001004010000.zettel @@ -2,11 +2,11 @@ title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20250627155145 +modified: 20250828135622 The configuration file, specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options. These cannot be stored in a [[configuration zettel|00001004020000]] because they are needed before Zettelstore can start or because of security reasons. For example, Zettelstore needs to know in advance on which network address it must listen or where zettel are stored. An attacker that is able to change the owner can do anything. @@ -49,11 +49,10 @@ 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. Index: docs/manual/00001004051000.zettel ================================================================== --- docs/manual/00001004051000.zettel +++ docs/manual/00001004051000.zettel @@ -2,11 +2,11 @@ title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20250627160733 +modified: 20250828135353 === ``zettelstore run`` This starts the web service. ``` @@ -24,15 +24,13 @@ ; [!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. +: Allows debugging of the internal web server. + Same as setting [[''debug-mode''|00001004010000#debug-mode]] to ""true"". - [[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. Index: docs/manual/00001006030000.zettel ================================================================== --- docs/manual/00001006030000.zettel +++ docs/manual/00001006030000.zettel @@ -2,19 +2,21 @@ title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 -modified: 20250115172354 +modified: 20250708154024 All [[supported metadata keys|00001006020000]] conform to a type. User-defined metadata keys conform also to a type, based on the suffix of the key. |=Suffix|Type | ''-date'' | [[Timestamp|00001006034500]] | ''-number'' | [[Number|00001006033000]] +| ''-ref'' | [[Identifier|00001006032000]] +| ''-refs'' | [[IdentifierSet|00001006032500]] | ''-role'' | [[Word|00001006035500]] | ''-time'' | [[Timestamp|00001006034500]] | ''-url'' | [[URL|00001006035000]] | ''-zettel'' | [[Identifier|00001006032000]] | ''-zid'' | [[Identifier|00001006032000]] Index: docs/manual/00001010000000.zettel ================================================================== --- docs/manual/00001010000000.zettel +++ docs/manual/00001010000000.zettel @@ -2,11 +2,11 @@ title: Security role: manual tags: #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 -modified: 20250102212014 +modified: 20250828121450 Your zettel may contain sensitive content. You probably want to ensure that only authorized persons can read and/or modify them. Zettelstore ensures this in various ways. @@ -63,6 +63,6 @@ To enforce encryption, [[authenticated sessions|00001010040700]] are marked as secure by default. If you still want to access the Zettelstore remotely without encryption, you must change the startup configuration. Otherwise, authentication will not work. -* [[Use a server for encryption|00001010090100]] +* [[Use a server, also for encryption|00001010090100]] Index: docs/manual/00001010090100.zettel ================================================================== --- docs/manual/00001010090100.zettel +++ docs/manual/00001010090100.zettel @@ -1,12 +1,12 @@ id: 00001010090100 -title: External server to encrypt message transport +title: External server, also to encrypt message transport role: manual tags: #configuration #encryption #manual #security #zettelstore syntax: zmk created: 20210126175322 -modified: 20250701125905 +modified: 20250828135632 Since Zettelstore does not encrypt the messages it exchanges with its clients, you may need some additional software to enable encryption. === Public-key encryption To enable encryption, you probably use some kind of encryption keys. @@ -67,5 +67,18 @@ 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. Index: docs/manual/00001012053800.zettel ================================================================== --- docs/manual/00001012053800.zettel +++ docs/manual/00001012053800.zettel @@ -2,11 +2,11 @@ title: API: Retrieve references of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20250415154139 -modified: 20250416181257 +modified: 20251013185705 The [[endpoint|00001012920000]] to retrieve references of a specific zettel is ''/r/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. A zettel may contain references to external material, in the form of an URI. External material may be referenced via Zettelmarkup [[links|00001007040310]], [[inline embedding|00001007040320]], or a [[block transclusion|00001007031100]]. @@ -39,11 +39,11 @@ https://github.github.com/gfm/ https://spec.commonmark.org/0.31.2/ https://github.com/yuin/goldmark ``` -These examples should make clean, that no duplicates are removed and no sorting takes place. +These examples illustrate that the data remains unsorted and that duplicates are preserved. The client must handle this, e.g.: ```sh # curl 'http://127.0.0.1:23123/r/00001008010500' | sort -u https://commonmark.org/ https://github.com/yuin/goldmark Index: docs/manual/00001012920525.zettel ================================================================== --- docs/manual/00001012920525.zettel +++ docs/manual/00001012920525.zettel @@ -2,11 +2,11 @@ title: SHTML Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230316181044 -modified: 20250102180003 +modified: 20250806192348 A zettel representation that is a [[s-expression|00001012930000]], syntactically similar to the [[Sz encoding|00001012920516]], but denotes [[HTML|00001012920510]] semantics. It is derived from a XML encoding in s-expressions, called [[SXML|https://en.wikipedia.org/wiki/SXML]]. It is (relatively) easy to parse and contains everything to transform it into real HTML. @@ -30,7 +30,7 @@ This allows a more space economic storage of data. An HTML tag like ``< a href="link">Text`` is encoded in SHTML with a list, where the first element is a symbol named a the tag. The second element is an optional encoding of the tag's attributes. Further elements are either other tag encodings or a string. -The above tag is encoded as ``(a (@ (href . "link")) "Text")``. -Also possible is to encode the attribute without pairs: ``(a (@ (href "link")) "Text")`` (note the missing full stop character). +The above tag is encoded as ``(a ((href . "link")) "Text")``. +Also possible is to encode the attribute without pairs: ``(a ((href "link")) "Text")`` (note the missing full stop character). Index: docs/manual/00001012931400.zettel ================================================================== --- docs/manual/00001012931400.zettel +++ docs/manual/00001012931400.zettel @@ -2,11 +2,11 @@ title: Encoding of Sz Block Elements role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161803 -modified: 20250317143910 +modified: 20251007144738 === ''PARA'' :::syntax __Paragraph__ **=** ''(PARA'' [[__InlineElement__|00001012931600]] … '')''. ::: @@ -29,22 +29,17 @@ === ''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__ … '')''. +__OrderedList__ **=** ''(ORDERED'' [[__Attributes__|00001012931000#attribute]] [[__Block__|00001012931000#block]] … '')''. -__UnorderedList__ **=** ''(UNORDERED'' [[__Attributes__|00001012931000#attribute]] __ListElement__ … '')''. +__UnorderedList__ **=** ''(UNORDERED'' [[__Attributes__|00001012931000#attribute]] [[__Block__|00001012931000#block]] … '')''. -__QuotationList__ **=** ''(QUOTATION'' [[__Attributes__|00001012931000#attribute]] __ListElement__ … '')''. +__QuotationList__ **=** ''(QUOTATION'' [[__Attributes__|00001012931000#attribute]] [[__Block__|00001012931000#block]] … '')''. ::: -:::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__ … '')''. ::: A description is a sequence of one or more terms and values. @@ -149,17 +144,17 @@ ::: The string contains text that should be treated as (nested) zettel content. === ''BLOB'' :::syntax -__BLOB__ **=** ''(BLOB'' [[__Attributes__|00001012931000#attribute]] ''('' [[__InlineElement__|00001012931600]] … '')'' String,,1,, String,,2,, '')''. +__BLOB__ **=** ''(BLOB'' [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, [[__InlineElement__|00001012931600]] … '')''. ::: A BLOB contains an image in block mode. The inline elements states some description. The first string contains the syntax of the image. The second string contains the actual image. -If the syntax is ""SVG"", then the second string contains the SVG code. +If the syntax is ""''svg''"", then the second string contains the SVG code. Otherwise the (binary) image data is encoded with base64. === ''TRANSCLUDE'' :::syntax __Transclude__ **=** ''(TRANSCLUDE'' [[__Attributes__|00001012931000#attribute]] [[__Reference__|00001012931900]] [[__InlineElement__|00001012931600]] … '')''. Index: docs/manual/00001012931600.zettel ================================================================== --- docs/manual/00001012931600.zettel +++ docs/manual/00001012931600.zettel @@ -2,11 +2,11 @@ title: Encoding of Sz Inline Elements role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161845 -modified: 20250313130428 +modified: 20251007144653 === ''TEXT'' :::syntax __Text__ **=** ''(TEXT'' String '')''. ::: @@ -47,16 +47,16 @@ The __InlineElement__ at the end of the list is interpreted as describing text. === ''EMBED-BLOB'' :::syntax -__EmbedBLOB__ **=** ''(EMBED-BLOB'' [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, '')''. +__EmbedBLOB__ **=** ''(EMBED-BLOB'' [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, [[__InlineElement__|00001012931600]] … '')''. ::: If used if some processed image has to be embedded inside some inline material. The first string specifies the syntax of the image content. The second string contains the image content. -If the syntax is ""SVG"", the image content is not encoded further. +If the syntax is ""''svg''"", the image content is not encoded further. Otherwise a base64 encoding is used. === ''CITE'' :::syntax __CiteBLOB__ **=** ''(CITE'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] … '')''. @@ -139,15 +139,10 @@ :::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. @@ -158,10 +153,5 @@ :::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. Index: docs/manual/00001018000000.zettel ================================================================== --- docs/manual/00001018000000.zettel +++ docs/manual/00001018000000.zettel @@ -2,11 +2,11 @@ title: Troubleshooting role: manual tags: #manual #zettelstore syntax: zmk created: 20211027105921 -modified: 20250627130445 +modified: 20250828140447 This page lists some problems and their solutions that may occur when using your Zettelstore. === Installation * **Problem:** When you double-click on the Zettelstore executable icon, macOS complains that Zettelstore is an application from an unknown developer. @@ -23,10 +23,14 @@ ** **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. Index: go.mod ================================================================== --- go.mod +++ go.mod @@ -1,21 +1,21 @@ module zettelstore.de/z -go 1.24 +go 1.25 require ( github.com/fsnotify/fsnotify v1.9.0 - github.com/yuin/goldmark v1.7.12 - golang.org/x/crypto v0.39.0 - golang.org/x/term v0.32.0 - t73f.de/r/sx v0.0.0-20250707071435-95b82f7d24bb - t73f.de/r/sxwebs v0.0.0-20250707071704-c44197610ee4 - t73f.de/r/webs v0.0.0-20250707071548-227f3e99db55 - t73f.de/r/zero v0.0.0-20250703105709-bb38976d4455 - t73f.de/r/zsc v0.0.0-20250707072124-be388711ad2a - t73f.de/r/zsx v0.0.0-20250707071920-5e29047e4db7 + github.com/yuin/goldmark v1.7.13 + golang.org/x/crypto v0.43.0 + golang.org/x/term v0.36.0 + t73f.de/r/sx v0.0.0-20251017161911-2f8291157520 + t73f.de/r/sxwebs v0.0.0-20251017162422-9f8d0174bc1f + t73f.de/r/webs v0.0.0-20250930141330-11da1688d11c + t73f.de/r/zero v0.0.0-20251017150835-a8859ec900ed + t73f.de/r/zsc v0.0.0-20251020122118-ed75b99a947d + t73f.de/r/zsx v0.0.0-20251020123811-57d9a3e9bbb9 ) require ( - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect ) Index: go.sum ================================================================== --- go.sum +++ go.sum @@ -1,24 +1,24 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY= -github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -t73f.de/r/sx v0.0.0-20250707071435-95b82f7d24bb h1:cYvTOpaJinh/EPB7i8nx7PtT7hniuSP+NZr74P9U+fE= -t73f.de/r/sx v0.0.0-20250707071435-95b82f7d24bb/go.mod h1:uglbFdRHlcpQVVyCNh4Fd7jbKo8alGBCjRp0aZv8IIg= -t73f.de/r/sxwebs v0.0.0-20250707071704-c44197610ee4 h1:WbT8qQAQjqx1S0syci6yWi+YiNQFFYVBkOSfSYIRid4= -t73f.de/r/sxwebs v0.0.0-20250707071704-c44197610ee4/go.mod h1:zSel+qtBHV9NglxDHlFFPwvaetlZh4H0pLyd7OkpkpQ= -t73f.de/r/webs v0.0.0-20250707071548-227f3e99db55 h1:gl4XpbrzrtAFDY+4V9bixLTUruhzEcwvfKiZi1NqJY4= -t73f.de/r/webs v0.0.0-20250707071548-227f3e99db55/go.mod h1:b8/5E5Pe6WSWqh+T+sxLO5ZLiGVkuL5tgh86kx2OAIg= -t73f.de/r/zero v0.0.0-20250703105709-bb38976d4455 h1:TFRPPexX2WrwuF03hC+Be2ONx2bPzMMBlNDn0rk88eI= -t73f.de/r/zero v0.0.0-20250703105709-bb38976d4455/go.mod h1:Ovx7CYsjz45BNuIEMGZfqA7NdQxERydJqUGnOBoQaXQ= -t73f.de/r/zsc v0.0.0-20250707072124-be388711ad2a h1:p12BTQ8TdKZy6GcCRgVINKSeoMz0wBPcNG7ssYvLNrc= -t73f.de/r/zsc v0.0.0-20250707072124-be388711ad2a/go.mod h1:JnkeoahGBxNK0gDcTnAIfDP2rAP33BtnAU1/rCEewC8= -t73f.de/r/zsx v0.0.0-20250707071920-5e29047e4db7 h1:ERxpb1Hqln+NXoZDK6sqjmX3BzeoLO+O64f4bK0B6dk= -t73f.de/r/zsx v0.0.0-20250707071920-5e29047e4db7/go.mod h1:64/AjQ1GnEBoBhXI1D0bDMGDj7JCbtZUTT3WoA7kS0s= +github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +t73f.de/r/sx v0.0.0-20251017161911-2f8291157520 h1:XtiOULaPBescy4AI1MgrE/klJca1gtVDK9awrW6zjCc= +t73f.de/r/sx v0.0.0-20251017161911-2f8291157520/go.mod h1:qOhNY+S+pcINETviISh5YIEfNUMk81QTDij78fIVa+Q= +t73f.de/r/sxwebs v0.0.0-20251017162422-9f8d0174bc1f h1:E5UpgzY4MrjZiHqrctMLi/0xyonimxozFbhQN439Dd0= +t73f.de/r/sxwebs v0.0.0-20251017162422-9f8d0174bc1f/go.mod h1:FUgkRA2F031cYtEir1hnyFbZYpGbybaBPdqxWjpxxuI= +t73f.de/r/webs v0.0.0-20250930141330-11da1688d11c h1:6bHMcSJPl6mDWHZu2DuiC2FcoOt/+TxxvbIm5E63sPs= +t73f.de/r/webs v0.0.0-20250930141330-11da1688d11c/go.mod h1:G3vn6fCTvYWwQby5cVNmXzHlOGhgBDfbbo/9OgIxy0g= +t73f.de/r/zero v0.0.0-20251017150835-a8859ec900ed h1:Omh9Beo5pupvpC8yHnvlRlw1CBcWm8PrgWI0uhQ7Xk4= +t73f.de/r/zero v0.0.0-20251017150835-a8859ec900ed/go.mod h1:cNaE2o9BWPFqLkmDuYaWrMJQS7GOo+wwmB9y8VfAF6c= +t73f.de/r/zsc v0.0.0-20251020122118-ed75b99a947d h1:r6bbERNxOjkUMfRQL3lwhUkJobYR2kXh6pzDQ3tcFTg= +t73f.de/r/zsc v0.0.0-20251020122118-ed75b99a947d/go.mod h1:JVgWkDy24MTAgrYqImA2A9DltUV7YsyoKGsSFsti+Yo= +t73f.de/r/zsx v0.0.0-20251020123811-57d9a3e9bbb9 h1:bDGcg8PIKd9/YhVClY7K+IFwAJ0Qe4Qkx92yKi3r3DA= +t73f.de/r/zsx v0.0.0-20251020123811-57d9a3e9bbb9/go.mod h1:/wqH6y+cX2WnLYs8sqFydWPPiht90Jm1KTg12Rn33jU= Index: internal/ast/ast.go ================================================================== --- internal/ast/ast.go +++ internal/ast/ast.go @@ -12,29 +12,11 @@ //----------------------------------------------------------------------------- // 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 -} +import "net/url" // Node is the interface, all nodes must implement. type Node interface { WalkChildren(v Visitor) } @@ -89,15 +71,5 @@ 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), " ") } Index: internal/ast/block.go ================================================================== --- internal/ast/block.go +++ internal/ast/block.go @@ -298,6 +298,6 @@ } func (*BLOBNode) blockNode() { /* Just a marker */ } // WalkChildren does nothing. -func (*BLOBNode) WalkChildren(Visitor) { /* No children*/ } +func (bn *BLOBNode) WalkChildren(v Visitor) { Walk(v, &bn.Description) } ADDED internal/ast/sztrans/szenc.go Index: internal/ast/sztrans/szenc.go ================================================================== --- /dev/null +++ internal/ast/sztrans/szenc.go @@ -0,0 +1,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)) +} Index: internal/ast/sztrans/sztrans.go ================================================================== --- internal/ast/sztrans/sztrans.go +++ internal/ast/sztrans/sztrans.go @@ -10,16 +10,15 @@ // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2025-present Detlef Stern //----------------------------------------------------------------------------- // Package sztrans allows to transform a sz representation of text into an -// abstract syntax tree. +// abstract syntax tree and vice versa. package sztrans import ( "fmt" - "log" "t73f.de/r/sx" "t73f.de/r/zsc/sz" "t73f.de/r/zsx" @@ -26,11 +25,29 @@ "zettelstore.de/z/internal/ast" ) type transformer struct{} -// GetBlockSlice returns the sz representations as a AST BlockSlice +// 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 @@ -44,150 +61,137 @@ return nil, fmt.Errorf("no AST for %v: %v", pair, obj) } return nil, fmt.Errorf("error walking %v", pair) } -func (t *transformer) VisitBefore(pair *sx.Pair, _ *sx.Pair) (sx.Object, bool) { - if sym, isSymbol := sx.GetSymbol(pair.Car()); isSymbol { +func (t *transformer) VisitBefore(node *sx.Pair, _ *sx.Pair) (sx.Object, bool) { + if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol { switch sym { case zsx.SymText: - if p := pair.Tail(); p != nil { - if s, isString := sx.GetString(p.Car()); isString { - return sxNode{&ast.TextNode{Text: s.GetValue()}}, true - } + if s := zsx.GetText(node); s != "" { + return sxNode{&ast.TextNode{Text: s}}, true } case zsx.SymSoft: return sxNode{&ast.BreakNode{Hard: false}}, true case zsx.SymHard: return sxNode{&ast.BreakNode{Hard: true}}, true case zsx.SymLiteralCode: - return handleLiteral(ast.LiteralCode, pair.Tail()) + return handleLiteral(ast.LiteralCode, node) case zsx.SymLiteralComment: - return handleLiteral(ast.LiteralComment, pair.Tail()) + return handleLiteral(ast.LiteralComment, node) case zsx.SymLiteralInput: - return handleLiteral(ast.LiteralInput, pair.Tail()) + return handleLiteral(ast.LiteralInput, node) case zsx.SymLiteralMath: - return handleLiteral(ast.LiteralMath, pair.Tail()) + return handleLiteral(ast.LiteralMath, node) case zsx.SymLiteralOutput: - return handleLiteral(ast.LiteralOutput, pair.Tail()) + return handleLiteral(ast.LiteralOutput, node) case zsx.SymThematic: - return sxNode{&ast.HRuleNode{Attrs: zsx.GetAttributes(pair.Tail().Head())}}, true + return sxNode{&ast.HRuleNode{Attrs: zsx.GetAttributes(node.Tail().Head())}}, true case zsx.SymVerbatimComment: - return handleVerbatim(ast.VerbatimComment, pair.Tail()) + return handleVerbatim(ast.VerbatimComment, node) case zsx.SymVerbatimEval: - return handleVerbatim(ast.VerbatimEval, pair.Tail()) + return handleVerbatim(ast.VerbatimEval, node) case zsx.SymVerbatimHTML: - return handleVerbatim(ast.VerbatimHTML, pair.Tail()) + return handleVerbatim(ast.VerbatimHTML, node) case zsx.SymVerbatimMath: - return handleVerbatim(ast.VerbatimMath, pair.Tail()) + return handleVerbatim(ast.VerbatimMath, node) case zsx.SymVerbatimCode: - return handleVerbatim(ast.VerbatimCode, pair.Tail()) + return handleVerbatim(ast.VerbatimCode, node) case zsx.SymVerbatimZettel: - return handleVerbatim(ast.VerbatimZettel, pair.Tail()) + return handleVerbatim(ast.VerbatimZettel, node) } } return sx.Nil(), false } -func handleLiteral(kind ast.LiteralKind, rest *sx.Pair) (sx.Object, bool) { - if rest != nil { - 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 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 { @@ -208,49 +212,33 @@ } } 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()) +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(rest.Tail()), - Attrs: attrs}} + Items: collectItemSlices(items), + Attrs: zsx.GetAttributes(attrs), + }} } - log.Println("LIST", kind, rest) - return rest + return node } - func collectItemSlices(lst *sx.Pair) (result []ast.ItemSlice) { for val := range lst.Values() { if sxn, isNode := val.(sxNode); isNode { if bns, isBlockSlice := sxn.node.(*ast.BlockSlice); isBlockSlice { items := make(ast.ItemSlice, len(*bns)) @@ -273,98 +261,92 @@ } } 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 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) @@ -372,89 +354,102 @@ } } 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 +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, @@ -465,187 +460,70 @@ 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 - } - } - } +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(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 +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 Index: internal/ast/zettel.go ================================================================== --- /dev/null +++ internal/ast/zettel.go @@ -0,0 +1,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 +} Index: internal/box/constbox/base.sxn ================================================================== --- internal/box/constbox/base.sxn +++ internal/box/constbox/base.sxn @@ -10,53 +10,53 @@ ;;; SPDX-License-Identifier: EUPL-1.2 ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(@@@@ -(html ,@(if lang `((@ (lang ,lang)))) +(html ,@(if lang `(((lang ,lang)))) (head - (meta (@ (charset "utf-8"))) - (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0"))) - (meta (@ (name "generator") (content "Zettelstore"))) - (meta (@ (name "format-detection") (content "telephone=no"))) + (meta ((charset "utf-8"))) + (meta ((name "viewport") (content "width=device-width, initial-scale=1.0"))) + (meta ((name "generator") (content "Zettelstore"))) + (meta ((name "format-detection") (content "telephone=no"))) ,@META-HEADER - (link (@ (rel "stylesheet") (href ,css-base-url))) - (link (@ (rel "stylesheet") (href ,css-user-url))) + (link ((rel "stylesheet") (href ,css-base-url))) + (link ((rel "stylesheet") (href ,css-user-url))) ,@(ROLE-DEFAULT-meta (current-frame)) ,@(let* ((frame (current-frame))(rem (resolve-symbol 'ROLE-EXTRA-meta frame))) (if (defined? rem) (rem frame))) (title ,title)) (body - (nav (@ (class "zs-menu")) - (a (@ (href ,home-url)) "Home") - ,@(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) + (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!")))) ))) Index: internal/box/constbox/constbox.go ================================================================== --- internal/box/constbox/constbox.go +++ internal/box/constbox/constbox.go @@ -164,71 +164,71 @@ constHeader{ meta.KeyTitle: "Zettelstore Base HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230510155100", - meta.KeyModified: "20250623131400", + meta.KeyModified: "20250806182400", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentBaseSxn)}, id.ZidLoginTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Login Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", - meta.KeyModified: "20240219145200", + meta.KeyModified: "20250806182600", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentLoginSxn)}, id.ZidZettelTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Zettel HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230510155300", - meta.KeyModified: "20250626113800", + meta.KeyModified: "20250806182700", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentZettelSxn)}, id.ZidInfoTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Info HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", - meta.KeyModified: "20250624160000", + meta.KeyModified: "20250806182500", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentInfoSxn)}, id.ZidFormTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", - meta.KeyModified: "20250612180300", + meta.KeyModified: "20250806182500", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentFormSxn)}, id.ZidDeleteTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Delete HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", - meta.KeyModified: "20250612180200", + meta.KeyModified: "20250806182500", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentDeleteSxn)}, id.ZidListTemplate: { constHeader{ meta.KeyTitle: "Zettelstore List Zettel HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230704122100", - meta.KeyModified: "20250612180200", + meta.KeyModified: "20250806182600", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentListZettelSxn)}, id.ZidErrorTemplate: { constHeader{ @@ -255,11 +255,11 @@ constHeader{ meta.KeyTitle: "Zettelstore Sxn Base Code", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230619132800", - meta.KeyModified: "20250624200200", + meta.KeyModified: "20250806182700", meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentBaseCodeSxn)}, id.ZidBaseCSS: { Index: internal/box/constbox/delete.sxn ================================================================== --- internal/box/constbox/delete.sxn +++ internal/box/constbox/delete.sxn @@ -13,27 +13,27 @@ `(article (header (h1 "Delete Zettel " ,zid)) (p "Do you really want to delete this zettel?") ,@(if shadowed-box - `((div (@ (class "zs-info")) + `((div ((class "zs-info")) (h2 "Information") (p "If you delete this zettel, the previously shadowed zettel from overlayed box " ,shadowed-box " becomes available.") )) ) ,@(if incoming - `((div (@ (class "zs-warning")) + `((div ((class "zs-warning")) (h2 "Warning!") (p "If you delete this zettel, incoming references from the following zettel will become invalid.") (ul ,@(map wui-item-link incoming)) )) ) ,@(if (and (symbol-bound? 'useless) useless) - `((div (@ (class "zs-warning")) + `((div ((class "zs-warning")) (h2 "Warning!") (p "Deleting this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.") (ul ,@(map wui-item useless)) )) ) ,(wui-meta-desc metapairs) - (form (@ (method "POST")) (input (@ (class "zs-primary") (type "submit") (value "Delete")))) + (form ((method "POST")) (input ((class "zs-primary") (type "submit") (value "Delete")))) ) Index: internal/box/constbox/form.sxn ================================================================== --- internal/box/constbox/form.sxn +++ internal/box/constbox/form.sxn @@ -11,53 +11,53 @@ ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(article (header (h1 ,heading)) - (form (@ (action ,form-action-url) (method "POST") (enctype "multipart/form-data")) + (form ((action ,form-action-url) (method "POST") (enctype "multipart/form-data")) (div - (label (@ (for "zs-title")) "Title " (a (@ (title "Main heading of this zettel.")) (@H "ⓘ"))) - (input (@ (class "zs-input") (type "text") (id "zs-title") (name "title") + (label ((for "zs-title")) "Title " (a ((title "Main heading of this zettel.")) (@H "ⓘ"))) + (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 "ⓘ"))) - (input (@ (class "zs-input") (type "text") (pattern "\\w*") (id "zs-role") (name "role") + (label ((for "zs-role")) "Role " (a ((title "One word, without spaces, to set the main role of this zettel.")) (@H "ⓘ"))) + (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 "ⓘ"))) - (input (@ (class "zs-input") (type "text") (id "zs-tags") (name "tags") + (label ((for "zs-tags")) "Tags " (a ((title "Tags must begin with an '#' sign. They are separated by spaces.")) (@H "ⓘ"))) + (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 "ⓘ"))) - (textarea (@ (class "zs-input") (id "zs-meta") (name "meta") (rows "4") + (label ((for "zs-meta")) "Metadata " (a ((title "Other metadata for this zettel. Each line contains a key/value pair, separated by a colon ':'.")) (@H "ⓘ"))) + (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 "ⓘ"))) - (input (@ (class "zs-input") (type "text") (pattern "\\w*") (id "zs-syntax") (name "syntax") + (label ((for "zs-syntax")) "Syntax " (a ((title "Syntax of zettel content below, one word. Typically 'zmk' (for zettelmarkup).")) (@H "ⓘ"))) + (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 "ⓘ"))) - (textarea (@ (class "zs-input zs-content") (id "zs-content") (name "content") (rows "20") + (label ((for "zs-content")) "Content " (a ((title "Content for this zettel, according to above syntax.")) (@H "ⓘ"))) + (textarea ((class "zs-input zs-content") (id "zs-content") (name "content") (rows "20") (title "Zettel content, according to the given syntax") (placeholder "Zettel content..") (dir "auto")) ,content) )) ) (div - (input (@ (class "zs-primary") (type "submit") (value "Submit"))) - (input (@ (class "zs-secondary") (type "submit") (value "Save") (formaction "?save"))) - (input (@ (class "zs-upload") (type "file") (id "zs-file") (name "file"))) + (input ((class "zs-primary") (type "submit") (value "Submit"))) + (input ((class "zs-secondary") (type "submit") (value "Save") (formaction "?save"))) + (input ((class "zs-upload") (type "file") (id "zs-file") (name "file"))) )) ) Index: internal/box/constbox/info.sxn ================================================================== --- internal/box/constbox/info.sxn +++ internal/box/constbox/info.sxn @@ -12,22 +12,22 @@ ;;;---------------------------------------------------------------------------- `(article (header (h1 "Information for Zettel " ,zid) (p - (a (@ (href ,web-url)) "Web") - ,@(if (symbol-bound? 'edit-url) `((@H " · ") (a (@ (href ,edit-url)) "Edit"))) - (@H " · ") (a (@ (href ,context-full-url)) "Full Context") + ,(wui-href web-url "Web") + ,@(if (symbol-bound? 'edit-url) `((@H " · ") ,(wui-href edit-url "Edit"))) + (@H " · ") ,(wui-href context-full-url "Full Context") ,@(if (symbol-bound? 'thread-query-url) - `((@H " · [") (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 " · [") ,(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 " · ") (a (@ (href ,reindex-url)) "Reindex"))) - ,@(if (symbol-bound? 'delete-url) `((@H " · ") (a (@ (href ,delete-url)) "Delete"))) + ,@(if (symbol-bound? 'reindex-url) `((@H " · ") ,(wui-href reindex-url "Reindex"))) + ,@(if (symbol-bound? 'delete-url) `((@H " · ") ,(wui-href delete-url "Delete"))) ) ) (h2 "Interpreted Metadata") (table ,@(map wui-info-meta-table-row metadata)) (h2 "References") @@ -35,12 +35,12 @@ ,@(if query-links `((h3 "Queries") (ul ,@(map wui-item-link query-links)))) ,@(if ext-links `((h3 "External") (ul ,@(map wui-item-popup-link ext-links)))) (h3 "Unlinked") ,@unlinked-content (form - (label (@ (for "phrase")) "Search Phrase") - (input (@ (class "zs-input") (type "text") (id "phrase") (name ,query-key-phrase) (placeholder "Phrase..") (value ,phrase))) + (label ((for "phrase")) "Search Phrase") + (input ((class "zs-input") (type "text") (id "phrase") (name ,query-key-phrase) (placeholder "Phrase..") (value ,phrase))) ) (h2 "Parts and encodings") ,(wui-enc-matrix enc-eval) (h3 "Parsed (not evaluated)") ,(wui-enc-matrix enc-parsed) Index: internal/box/constbox/listzettel.sxn ================================================================== --- internal/box/constbox/listzettel.sxn +++ internal/box/constbox/listzettel.sxn @@ -11,40 +11,40 @@ ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(article (header (h1 ,heading)) - (search (form (@ (action ,search-url)) - (input (@ (class "zs-input") (type "search") (inputmode "search") (name ,query-key-query) + (search (form ((action ,search-url)) + (input ((class "zs-input") (type "search") (inputmode "search") (name ,query-key-query) (title "Contains the search that leads to the list below. You're allowed to modify it") (placeholder "Search..") (value ,query-value) (dir "auto"))))) ,@(if (symbol-bound? 'tag-zettel) - `((p (@ (class "zs-meta-zettel")) "Tag zettel: " ,@tag-zettel)) + `((p ((class "zs-meta-zettel")) "Tag zettel: " ,@tag-zettel)) ) ,@(if (symbol-bound? 'create-tag-zettel) - `((p (@ (class "zs-meta-zettel")) "Create tag zettel: " ,@create-tag-zettel)) + `((p ((class "zs-meta-zettel")) "Create tag zettel: " ,@create-tag-zettel)) ) ,@(if (symbol-bound? 'role-zettel) - `((p (@ (class "zs-meta-zettel")) "Role zettel: " ,@role-zettel)) + `((p ((class "zs-meta-zettel")) "Role zettel: " ,@role-zettel)) ) ,@(if (symbol-bound? 'create-role-zettel) - `((p (@ (class "zs-meta-zettel")) "Create role zettel: " ,@create-role-zettel)) + `((p ((class "zs-meta-zettel")) "Create role zettel: " ,@create-role-zettel)) ) ,@content ,@endnotes - (form (@ (action ,(if (symbol-bound? 'create-url) create-url))) + (form ((action ,(if (symbol-bound? 'create-url) create-url))) ,(if (symbol-bound? 'data-url) `(@L "Other encodings" ,(if (> num-entries 3) `(@L " of these " ,num-entries " entries: ") ": ") - (a (@ (href ,data-url)) "data") + ,(wui-href data-url "data") ", " - (a (@ (href ,plain-url)) "plain") + ,(wui-href plain-url "plain") ) ) ,@(if (symbol-bound? 'create-url) - `((input (@ (type "hidden") (name ,query-key-query) (value ,query-value))) - (input (@ (type "hidden") (name ,query-key-seed) (value ,seed))) - (input (@ (class "zs-primary") (type "submit") (value "Save As Zettel"))) + `((input ((type "hidden") (name ,query-key-query) (value ,query-value))) + (input ((type "hidden") (name ,query-key-seed) (value ,seed))) + (input ((class "zs-primary") (type "submit") (value "Save As Zettel"))) ) ) ) ) Index: internal/box/constbox/login.sxn ================================================================== --- internal/box/constbox/login.sxn +++ internal/box/constbox/login.sxn @@ -11,17 +11,17 @@ ;;; 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")))) + ,@(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")))) ) ) Index: internal/box/constbox/wuicode.sxn ================================================================== --- internal/box/constbox/wuicode.sxn +++ internal/box/constbox/wuicode.sxn @@ -17,17 +17,20 @@ (defun wui-item (s) `(li ,s)) ;; wui-info-meta-table-row takes a pair and translates it into a HTML table row ;; with two columns. (defun wui-info-meta-table-row (p) - `(tr (td (@ (class zs-info-meta-key)) ,(car p)) (td (@ (class zs-info-meta-value)) ,(cdr p)))) + `(tr (td ((class zs-info-meta-key)) ,(car p)) (td ((class zs-info-meta-value)) ,(cdr p)))) + +;; wui-href builds an HTML link +(defun wui-href (url text . attrs) `(a ((href ,url) ,@attrs) ,text)) ;; wui-local-link translates a local link into HTML. -(defun wui-local-link (l) `(li (a (@ (href ,l )) ,l))) +(defun wui-local-link (l) `(li ,(wui-href l l))) ;; wui-link takes a link (title . url) and returns a HTML reference. -(defun wui-link (q) `(a (@ (href ,(cdr q))) ,(car q))) +(defun wui-link (q) (wui-href (cdr q) (car q))) ;; wui-item-link taks a pair (text . url) and returns a HTML link inside ;; a list item. (defun wui-item-link (q) `(li ,(wui-link q))) @@ -36,20 +39,20 @@ (defun wui-tdata-link (q) `(td ,(wui-link q))) ;; wui-item-popup-link is like 'wui-item-link, but the HTML link will open ;; a new tab / window. (defun wui-item-popup-link (e) - `(li (a (@ (href ,e) (target "_blank") (rel "external noreferrer")) ,e))) + `(li ,(wui-href e e '(target "_blank") '(rel "external noreferrer")))) ;; wui-option-value returns a value for an HTML option element. -(defun wui-option-value (v) `(option (@ (value ,v)))) +(defun wui-option-value (v) `(option ((value ,v)))) ;; wui-datalist returns a HTML datalist with the given HTML identifier and a ;; list of values. (defun wui-datalist (id lst) (if lst - `((datalist (@ (id ,id)) ,@(map wui-option-value lst))))) + `((datalist ((id ,id)) ,@(map wui-option-value lst))))) ;; wui-pair-desc-item takes a pair '(term . text) and returns a list with ;; a HTML description term and a HTML description data. (defun wui-pair-desc-item (p) `((dt ,(car p)) (dd ,(cdr p)))) @@ -67,11 +70,11 @@ ;; wui-optional-link puts the text into a link, if symbol is defined. Otherwise just return the text (defun wui-optional-link (text url-sym) (let ((url (resolve-symbol url-sym))) (if (defined? url) - `(a (@ (href ,(resolve-symbol url-sym))) ,text) + (wui-href url text) text))) ;; CSS-ROLE-map is a mapping (pair list, assoc list) of role names to zettel ;; identifier. It is used in the base template to update the metadata of the ;; HTML page to include some role specific CSS code. @@ -82,11 +85,11 @@ ;; specific code should include the returned list of this function. (defun ROLE-DEFAULT-meta (frame) `(,@(let* ((meta-role (resolve-symbol 'meta-role frame)) (entry (assoc CSS-ROLE-map meta-role))) (if (pair? entry) - `((link (@ (rel "stylesheet") (href ,(zid-content-path (cdr entry)))))) + `((link ((rel "stylesheet") (href ,(zid-content-path (cdr entry)))))) ) ) ) ) @@ -94,33 +97,33 @@ (defvar ACTION-SEPARATOR '(@H " · ")) ;; 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 " · ") (a (@ (href ,copy-url)) "Copy")))) + (if (defined? copy-url) `((@H " · ") ,(wui-href copy-url "Copy")))) ,@(let ((sequel-url (resolve-symbol 'sequel-url frame))) - (if (defined? sequel-url) `((@H " · ") (a (@ (href ,sequel-url)) "Sequel")))) + (if (defined? sequel-url) `((@H " · ") ,(wui-href sequel-url "Sequel")))) ,@(let ((folge-url (resolve-symbol 'folge-url frame))) - (if (defined? folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge")))) + (if (defined? folge-url) `((@H " · ") ,(wui-href folge-url "Folge")))) ) ) ;; ROLE-tag-actions returns an additional action "Zettel" for zettel with role "tag". (defun ROLE-tag-actions (frame) `(,@(let ((title (resolve-symbol 'title frame))) (if (and (defined? title) title) - `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "tags:" title)))) "Zettel")) + `(,ACTION-SEPARATOR ,(wui-href (query->url (concat "tags:" title)) "Zettel")) ) ) ) ) ;; ROLE-role-actions returns an additional action "Zettel" for zettel with role "role". (defun ROLE-role-actions (frame) `(,@(let ((title (resolve-symbol 'title frame))) (if (and (defined? title) title) - `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "role:" title)))) "Zettel")) + `(,ACTION-SEPARATOR ,(wui-href (query->url (concat "role:" title)) "Zettel")) ) ) ) ) Index: internal/box/constbox/zettel.sxn ================================================================== --- internal/box/constbox/zettel.sxn +++ internal/box/constbox/zettel.sxn @@ -12,21 +12,21 @@ ;;;---------------------------------------------------------------------------- `(article (header (h1 ,heading) - (div (@ (class "zs-meta")) - ,@(if (symbol-bound? 'edit-url) `((a (@ (href ,edit-url)) "Edit") (@H " · "))) + (div ((class "zs-meta")) + ,@(if (symbol-bound? 'edit-url) `(,(wui-href edit-url "Edit") (@H " · "))) ,zid (@H " · ") - (a (@ (href ,info-url)) "Info") (@H " · ") - "(" ,@(if (symbol-bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role))) + ,(wui-href info-url "Info") (@H " · ") + "(" ,@(if (symbol-bound? 'role-url) `(,(wui-href role-url meta-role))) ,@(if (and (symbol-bound? 'folge-role-url) (symbol-bound? 'meta-folge-role)) - `((@H " → ") (a (@ (href ,folge-role-url)) ,meta-folge-role))) + `((@H " → ") ,(wui-href folge-role-url meta-folge-role))) ")" ,@(if tag-refs `((@H " · ") ,@tag-refs)) - (@H " · ") (a (@ (href ,context-url)) "Context") - ,@(if (symbol-bound? 'thread-query-url) `((@H " · ") (a (@ (href ,thread-query-url)) "Thread"))) + (@H " · ") ,(wui-href context-url "Context") + ,@(if (symbol-bound? 'thread-query-url) `((@H " · ") ,(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)) @@ -37,17 +37,17 @@ ,@content ,endnotes ,@(if (or folge-links sequel-links back-links subordinate-links) `((nav ,@(if folge-links - `((details (@ (,folge-open)) + `((details ((,folge-open)) (summary ,(wui-optional-link "Folgezettel" 'folge-query-url)) (ul ,@(map wui-item-link folge-links))))) ,@(if sequel-links - `((details (@ (,sequel-open)) + `((details ((,sequel-open)) (summary ,(wui-optional-link "Sequel" 'sequel-query-url)) (ul ,@(map wui-item-link sequel-links))))) - ,@(if subordinate-links `((details (@ (,subordinate-open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links))))) - ,@(if back-links `((details (@ (,back-open)) (summary "Incoming") (ul ,@(map wui-item-link back-links))))) + ,@(if subordinate-links `((details ((,subordinate-open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links))))) + ,@(if back-links `((details ((,back-open)) (summary "Incoming") (ul ,@(map wui-item-link back-links))))) )) ) ) Index: internal/box/manager/collect.go ================================================================== --- internal/box/manager/collect.go +++ internal/box/manager/collect.go @@ -14,15 +14,17 @@ package manager import ( "strings" + "t73f.de/r/sx" zerostrings "t73f.de/r/zero/strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" + "t73f.de/r/zsc/sz" + "t73f.de/r/zsx" - "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box/manager/store" ) type collectData struct { refs *idset.Set @@ -34,49 +36,56 @@ 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 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) } } - -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) - } -} Index: internal/box/manager/indexer.go ================================================================== --- internal/box/manager/indexer.go +++ internal/box/manager/indexer.go @@ -150,11 +150,12 @@ func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel zettel.Zettel) { var cData collectData cData.initialize() if mustIndexZettel(zettel.Meta) { - collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData) + zn := parser.ParseZettel(ctx, zettel, "", mgr.rtConfig) + collectZettelIndexData(zn.Blocks, &cData) } m := zettel.Meta zi := store.NewZettelIndex(m) mgr.idxCollectFromMeta(ctx, m, zi, &cData) Index: internal/collect/collect.go ================================================================== --- internal/collect/collect.go +++ internal/collect/collect.go @@ -15,42 +15,49 @@ package collect import ( "iter" - "zettelstore.de/z/internal/ast" + "t73f.de/r/sx" + "t73f.de/r/zsx" ) type refYielder struct { - yield func(*ast.Reference) bool + yield func(*sx.Pair) bool stop bool } // ReferenceSeq returns an iterator of all references mentioned in the given -// zettel. This also includes references to images. -func ReferenceSeq(zn *ast.ZettelNode) iter.Seq[*ast.Reference] { - return func(yield func(*ast.Reference) bool) { +// block slice. This also includes references to images. +func ReferenceSeq(block *sx.Pair) iter.Seq[*sx.Pair] { + return func(yield func(*sx.Pair) bool) { yielder := refYielder{yield, false} - ast.Walk(&yielder, &zn.BlocksAST) + zsx.WalkIt(&yielder, block, nil) } } // Visit all node to collect data for the summary. -func (y *refYielder) Visit(node ast.Node) ast.Visitor { +func (y *refYielder) VisitBefore(node *sx.Pair, _ *sx.Pair) bool { if y.stop { - return nil - } - 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 -} + 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) {} Index: internal/collect/collect_test.go ================================================================== --- internal/collect/collect_test.go +++ internal/collect/collect_test.go @@ -16,47 +16,48 @@ import ( "slices" "testing" - "zettelstore.de/z/internal/ast" + "t73f.de/r/sx" + "t73f.de/r/zsc/sz" + "t73f.de/r/zsx" + "zettelstore.de/z/internal/collect" ) -func parseRef(s string) *ast.Reference { - r := ast.ParseReference(s) - if !r.IsValid() { +func parseRef(s string) *sx.Pair { + r := sz.ScanReference(s) + sym, _ := zsx.GetReference(r) + if zsx.SymRefStateInvalid.IsEqualSymbol(sym) { panic(s) } return r } func TestReferenceSeq(t *testing.T) { t.Parallel() - zn := &ast.ZettelNode{} - summary := slices.Collect(collect.ReferenceSeq(zn)) + summary := slices.Collect(collect.ReferenceSeq(nil)) if len(summary) != 0 { t.Error("No references expected, but got:", summary) } - intNode := &ast.LinkNode{Ref: parseRef("01234567890123")} - para := ast.CreateParaNode(intNode, &ast.LinkNode{Ref: parseRef("https://zettelstore.de/z")}) - zn.BlocksAST = ast.BlockSlice{para} - summary = slices.Collect(collect.ReferenceSeq(zn)) + intNode := zsx.MakeLink(nil, parseRef("01234567890123"), nil) + para := zsx.MakePara(intNode, zsx.MakeLink(nil, parseRef("https://zettelstore.de/z"), nil)) + blocks := zsx.MakeBlock(para) + summary = slices.Collect(collect.ReferenceSeq(blocks)) if len(summary) != 2 { t.Error("2 refs expected, but got:", summary) } - para.Inlines = append(para.Inlines, intNode) - summary = slices.Collect(collect.ReferenceSeq(zn)) + para.LastPair().AppendBang(intNode) + summary = slices.Collect(collect.ReferenceSeq(blocks)) if cnt := len(summary); cnt != 3 { t.Error("Ref count does not work. Expected: 3, got", summary) } - zn = &ast.ZettelNode{ - BlocksAST: ast.BlockSlice{ast.CreateParaNode(&ast.EmbedRefNode{Ref: parseRef("12345678901234")})}, - } - summary = slices.Collect(collect.ReferenceSeq(zn)) + 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) } } Index: internal/collect/order.go ================================================================== --- internal/collect/order.go +++ internal/collect/order.go @@ -12,58 +12,127 @@ //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect -import "zettelstore.de/z/internal/ast" +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 +} -// Order of internal links within the given zettel. -func Order(zn *ast.ZettelNode) (result []*ast.LinkNode) { - for _, bn := range zn.BlocksAST { +// OrderAST of internal links within the given zettel. +func OrderAST(bns *ast.BlockSlice) (result []*ast.LinkNode) { + for _, bn := range *bns { ln, ok := bn.(*ast.NestedListNode) if !ok { continue } switch ln.Kind { case ast.NestedListOrdered, ast.NestedListUnordered: for _, is := range ln.Items { - if ln := firstItemZettelLink(is); ln != nil { + if ln := firstItemZettelLinkAST(is); ln != nil { result = append(result, ln) } } } } return result } -func firstItemZettelLink(is ast.ItemSlice) *ast.LinkNode { +func firstItemZettelLinkAST(is ast.ItemSlice) *ast.LinkNode { for _, in := range is { if pn, ok := in.(*ast.ParaNode); ok { - if ln := firstInlineZettelLink(pn.Inlines); ln != nil { + if ln := firstInlineZettelLinkAST(pn.Inlines); ln != nil { return ln } } } return nil } -func firstInlineZettelLink(is ast.InlineSlice) (result *ast.LinkNode) { +func firstInlineZettelLinkAST(is ast.InlineSlice) (result *ast.LinkNode) { for _, inl := range is { switch in := inl.(type) { case *ast.LinkNode: return in case *ast.EmbedRefNode: - result = firstInlineZettelLink(in.Inlines) + result = firstInlineZettelLinkAST(in.Inlines) case *ast.EmbedBLOBNode: - result = firstInlineZettelLink(in.Inlines) + result = firstInlineZettelLinkAST(in.Inlines) case *ast.CiteNode: - result = firstInlineZettelLink(in.Inlines) + result = firstInlineZettelLinkAST(in.Inlines) case *ast.FootnoteNode: // Ignore references in footnotes continue case *ast.FormatNode: - result = firstInlineZettelLink(in.Inlines) + result = firstInlineZettelLinkAST(in.Inlines) default: continue } if result != nil { return result Index: internal/encoder/encoder.go ================================================================== --- internal/encoder/encoder.go +++ internal/encoder/encoder.go @@ -16,54 +16,62 @@ package encoder import ( "io" + "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/internal/ast" + "zettelstore.de/z/internal/ast/sztrans" ) // Encoder is an interface that allows to encode different parts of a zettel. type Encoder interface { // WriteZettel encodes a whole zettel and writes it to the Writer. - WriteZettel(io.Writer, *ast.ZettelNode) (int, error) + WriteZettel(io.Writer, *ast.Zettel) error // WriteMeta encodes just the metadata. - WriteMeta(io.Writer, *meta.Meta) (int, error) + WriteMeta(io.Writer, *meta.Meta) error + + // WriteSz encodes SZ represented zettel content. + WriteSz(io.Writer, *sx.Pair) error // WiteBlocks encodes a block slice, i.e. the zettel content. - WriteBlocks(io.Writer, *ast.BlockSlice) (int, error) + // + // 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: NewSzTransformer(), + tx: sztrans.NewSzTransformer(), th: shtml.NewEvaluator(1), lang: params.Lang, } case api.EncoderMD: return &mdEncoder{lang: params.Lang} case api.EncoderSHTML: // We need a new transformer every time, because tx.inVerse must be unique. // If we can refactor it out, the transformer can be created only once. return &shtmlEncoder{ - tx: NewSzTransformer(), + tx: sztrans.NewSzTransformer(), th: shtml.NewEvaluator(1), lang: params.Lang, } case api.EncoderSz: // We need a new transformer every time, because trans.inVerse must be unique. // If we can refactor it out, the transformer can be created only once. - return &szEncoder{trans: NewSzTransformer()} + return &szEncoder{trans: sztrans.NewSzTransformer()} case api.EncoderText: return (*TextEncoder)(nil) case api.EncoderZmk: return (*zmkEncoder)(nil) } Index: internal/encoder/encoder_blob_test.go ================================================================== --- internal/encoder/encoder_blob_test.go +++ internal/encoder/encoder_blob_test.go @@ -18,11 +18,10 @@ "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 @@ -42,12 +41,12 @@ 0x06, 0x00, 0x03, 0x36, 0x37, 0x7c, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, }, expect: expectMap{ encoderHTML: `

Minimal PNG

`, - encoderSz: `(BLOCK (BLOB () ((TEXT "Minimal PNG")) "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="))`, - encoderSHTML: `((p (img (@ (alt . "Minimal PNG") (src . "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==")))))`, + encoderSz: `(BLOCK (BLOB () "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==" (TEXT "Minimal PNG")))`, + encoderSHTML: `((p (img ((alt . "Minimal PNG") (src . "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==")))))`, encoderText: "", encoderZmk: `%% Unable to display BLOB with description 'Minimal PNG' and syntax 'png'.`, }, }, } @@ -55,9 +54,10 @@ func TestBlob(t *testing.T) { m := meta.New(id.Invalid) for testNum, tc := range pngTestCases { m.Set(meta.KeyTitle, meta.Value(tc.descr)) inp := input.NewInput(tc.blob) - bs := parser.Parse(inp, m, tc.syntax, config.NoHTML) - checkEncodings(t, testNum, bs, false, tc.descr, tc.expect, "???") + node, bs := parser.Parse(inp, m, tc.syntax) + checkEncodings(t, testNum, node, false, tc.descr, tc.expect, "???") + checkEncodingsAST(t, testNum+1000, bs, false, tc.descr, tc.expect, "???") } } Index: internal/encoder/encoder_block_test.go ================================================================== --- internal/encoder/encoder_block_test.go +++ internal/encoder/encoder_block_test.go @@ -10,10 +10,16 @@ // 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: "", @@ -67,11 +73,11 @@ zmk: `=== Top Job`, expect: expectMap{ encoderHTML: "

Top Job

", encoderMD: "# Top Job", encoderSz: `(BLOCK (HEADING 1 () "top-job" "top-job" (TEXT "Top Job")))`, - encoderSHTML: `((h2 (@ (id . "top-job")) "Top Job"))`, + encoderSHTML: `((h2 ((id . "top-job")) "Top Job"))`, encoderText: `Top Job`, encoderZmk: useZmk, }, }, { @@ -78,11 +84,11 @@ descr: "Simple List", zmk: "* A\n* B\n* C", expect: expectMap{ encoderHTML: "", encoderMD: "* A\n* B\n* C", - encoderSz: `(BLOCK (UNORDERED () (INLINE (TEXT "A")) (INLINE (TEXT "B")) (INLINE (TEXT "C"))))`, + encoderSz: `(BLOCK (UNORDERED () (BLOCK (PARA (TEXT "A"))) (BLOCK (PARA (TEXT "B"))) (BLOCK (PARA (TEXT "C")))))`, encoderSHTML: `((ul (li "A") (li "B") (li "C")))`, encoderText: "A\nB\nC", encoderZmk: useZmk, }, }, @@ -90,11 +96,11 @@ descr: "Nested List", zmk: "* T1\n*# T2\n* T3\n** T4\n** T5\n* T6", expect: expectMap{ encoderHTML: ``, encoderMD: "* T1\n 1. T2\n* T3\n * T4\n * T5\n* T6", - encoderSz: `(BLOCK (UNORDERED () (BLOCK (PARA (TEXT "T1")) (ORDERED () (INLINE (TEXT "T2")))) (BLOCK (PARA (TEXT "T3")) (UNORDERED () (INLINE (TEXT "T4")) (INLINE (TEXT "T5")))) (BLOCK (PARA (TEXT "T6")))))`, + encoderSz: `(BLOCK (UNORDERED () (BLOCK (PARA (TEXT "T1")) (ORDERED () (BLOCK (PARA (TEXT "T2"))))) (BLOCK (PARA (TEXT "T3")) (UNORDERED () (BLOCK (PARA (TEXT "T4"))) (BLOCK (PARA (TEXT "T5"))))) (BLOCK (PARA (TEXT "T6")))))`, encoderSHTML: `((ul (li (p "T1") (ol (li "T2"))) (li (p "T3") (ul (li "T4") (li "T5"))) (li (p "T6"))))`, encoderText: "T1\nT2\nT3\nT4\nT5\nT6", encoderZmk: useZmk, }, }, @@ -102,11 +108,11 @@ descr: "Sequence of two lists", zmk: "* Item1.1\n* Item1.2\n* Item1.3\n\n* Item2.1\n* Item2.2", expect: expectMap{ encoderHTML: "", encoderMD: "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2", - encoderSz: `(BLOCK (UNORDERED () (INLINE (TEXT "Item1.1")) (INLINE (TEXT "Item1.2")) (INLINE (TEXT "Item1.3")) (INLINE (TEXT "Item2.1")) (INLINE (TEXT "Item2.2"))))`, + encoderSz: `(BLOCK (UNORDERED () (BLOCK (PARA (TEXT "Item1.1"))) (BLOCK (PARA (TEXT "Item1.2"))) (BLOCK (PARA (TEXT "Item1.3"))) (BLOCK (PARA (TEXT "Item2.1"))) (BLOCK (PARA (TEXT "Item2.2")))))`, encoderSHTML: `((ul (li "Item1.1") (li "Item1.2") (li "Item1.3") (li "Item2.1") (li "Item2.2")))`, encoderText: "Item1.1\nItem1.2\nItem1.3\nItem2.1\nItem2.2", encoderZmk: "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2", }, }, @@ -127,11 +133,11 @@ zmk: `---{lang="zmk"}`, expect: expectMap{ encoderHTML: `
`, encoderMD: "---", encoderSz: `(BLOCK (THEMATIC (("lang" . "zmk"))))`, - encoderSHTML: `((hr (@ (lang . "zmk"))))`, + encoderSHTML: `((hr ((lang . "zmk"))))`, encoderText: ``, encoderZmk: useZmk, }, }, { @@ -150,11 +156,11 @@ descr: "A list after paragraph", zmk: "Text\n# abc", expect: expectMap{ encoderHTML: "

Text

  1. abc
", encoderMD: "Text\n\n1. abc", - encoderSz: `(BLOCK (PARA (TEXT "Text")) (ORDERED () (INLINE (TEXT "abc"))))`, + encoderSz: `(BLOCK (PARA (TEXT "Text")) (ORDERED () (BLOCK (PARA (TEXT "abc")))))`, encoderSHTML: `((p "Text") (ol (li "abc")))`, encoderText: "Text\nabc", encoderZmk: useZmk, }, }, @@ -162,11 +168,11 @@ descr: "Simple List Quote", zmk: "> ToBeOrNotToBe", expect: expectMap{ encoderHTML: "
ToBeOrNotToBe
", encoderMD: "> ToBeOrNotToBe", - encoderSz: `(BLOCK (QUOTATION () (INLINE (TEXT "ToBeOrNotToBe"))))`, + encoderSz: `(BLOCK (QUOTATION () (BLOCK (PARA (TEXT "ToBeOrNotToBe")))))`, encoderSHTML: `((blockquote (@L "ToBeOrNotToBe")))`, encoderText: "ToBeOrNotToBe", encoderZmk: useZmk, }, }, @@ -259,11 +265,11 @@ zmk: "~~~\nHello\nWorld\n~~~", expect: expectMap{ encoderHTML: "
Hello\nWorld
", encoderMD: "", encoderSz: `(BLOCK (VERBATIM-EVAL () "Hello\nWorld"))`, - encoderSHTML: "((pre (code (@ (class . \"zs-eval\")) \"Hello\\nWorld\")))", + encoderSHTML: "((pre (code ((class . \"zs-eval\")) \"Hello\\nWorld\")))", encoderText: "Hello\nWorld", encoderZmk: useZmk, }, }, { @@ -271,11 +277,11 @@ zmk: "$$$\nHello\n\\LaTeX\n$$$", expect: expectMap{ encoderHTML: "
Hello\n\\LaTeX
", encoderMD: "", encoderSz: `(BLOCK (VERBATIM-MATH () "Hello\n\\LaTeX"))`, - encoderSHTML: "((pre (code (@ (class . \"zs-math\")) \"Hello\\n\\\\LaTeX\")))", + encoderSHTML: "((pre (code ((class . \"zs-math\")) \"Hello\\n\\\\LaTeX\")))", encoderText: "Hello\n\\LaTeX", encoderZmk: useZmk, }, }, { @@ -318,11 +324,11 @@ descr: "Simple Table", zmk: "|c1|c2|c3\n|d1||d3", expect: expectMap{ encoderHTML: `
c1c2c3
d1d3
`, encoderMD: "", - encoderSz: `(BLOCK (TABLE () ((CELL () (TEXT "c1")) (CELL () (TEXT "c2")) (CELL () (TEXT "c3"))) ((CELL () (TEXT "d1")) (CELL ()) (CELL () (TEXT "d3")))))`, + encoderSz: `(BLOCK (TABLE () () ((CELL () (TEXT "c1")) (CELL () (TEXT "c2")) (CELL () (TEXT "c3"))) ((CELL () (TEXT "d1")) (CELL ()) (CELL () (TEXT "d3")))))`, encoderSHTML: `((table (tbody (tr (td "c1") (td "c2") (td "c3")) (tr (td "d1") (td) (td "d3")))))`, encoderText: "c1 c2 c3\nd1 d3", encoderZmk: useZmk, }, }, @@ -333,12 +339,12 @@ |h1h2h3c1c2c3f1f2=f3`, encoderMD: "", - encoderSz: `(BLOCK (TABLE ((CELL ((align . "right")) (TEXT "h1")) (CELL () (TEXT "h2")) (CELL ((align . "center")) (TEXT "h3"))) ((CELL ((align . "left")) (TEXT "c1")) (CELL () (TEXT "c2")) (CELL ((align . "center")) (TEXT "c3"))) ((CELL ((align . "right")) (TEXT "f1")) (CELL () (TEXT "f2")) (CELL ((align . "center")) (TEXT "=f3")))))`, - encoderSHTML: `((table (thead (tr (th (@ (class . "right")) "h1") (th "h2") (th (@ (class . "center")) "h3"))) (tbody (tr (td (@ (class . "left")) "c1") (td "c2") (td (@ (class . "center")) "c3")) (tr (td (@ (class . "right")) "f1") (td "f2") (td (@ (class . "center")) "=f3")))))`, + encoderSz: `(BLOCK (TABLE () ((CELL ((align . "right")) (TEXT "h1")) (CELL () (TEXT "h2")) (CELL ((align . "center")) (TEXT "h3"))) ((CELL ((align . "left")) (TEXT "c1")) (CELL () (TEXT "c2")) (CELL ((align . "center")) (TEXT "c3"))) ((CELL ((align . "right")) (TEXT "f1")) (CELL () (TEXT "f2")) (CELL ((align . "center")) (TEXT "=f3")))))`, + encoderSHTML: `((table (thead (tr (th ((class . "right")) "h1") (th "h2") (th ((class . "center")) "h3"))) (tbody (tr (td ((class . "left")) "c1") (td "c2") (td ((class . "center")) "c3")) (tr (td ((class . "right")) "f1") (td "f2") (td ((class . "center")) "=f3")))))`, encoderText: "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3", encoderZmk: /*`|=h1>|=h2|=h3: |h1|=h2|=:h3 @@ -351,11 +357,11 @@ zmk: `Text[^Endnote]`, expect: expectMap{ encoderHTML: "

Text1

  1. Endnote \u21a9\ufe0e
", encoderMD: "Text", encoderSz: `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (TEXT "Endnote"))))`, - encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))", + encoderSHTML: "((p \"Text\" (sup ((id . \"fnref:1\")) (a ((class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))", encoderText: "Text Endnote", encoderZmk: useZmk, }, }, { @@ -363,11 +369,11 @@ zmk: `Text[^Endnote[^Nested]]`, expect: expectMap{ encoderHTML: "

Text1

  1. Endnote2 \u21a9\ufe0e
  2. Nested \u21a9\ufe0e
", encoderMD: "Text", encoderSz: `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (TEXT "Endnote") (ENDNOTE () (TEXT "Nested")))))`, - encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))", + encoderSHTML: "((p \"Text\" (sup ((id . \"fnref:1\")) (a ((class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))", encoderText: "Text Endnote Nested", encoderZmk: useZmk, }, }, { @@ -375,11 +381,11 @@ zmk: `{{{http://example.com/image}}}{width="100px"}`, expect: expectMap{ encoderHTML: `

`, encoderMD: "", encoderSz: `(BLOCK (TRANSCLUDE (("width" . "100px")) (EXTERNAL "http://example.com/image")))`, - encoderSHTML: `((p (img (@ (class . "external") (src . "http://example.com/image") (width . "100px")))))`, + encoderSHTML: `((p (img ((class . "external") (src . "http://example.com/image") (width . "100px")))))`, encoderText: "", encoderZmk: useZmk, }, }, { @@ -391,10 +397,22 @@ // encoderSHTML: ``, encoderText: "", encoderZmk: useZmk, }, }, + { + descr: "Zettel with syntax HTML", + zmk: "

Hello

\nWorld\n", + syntax: meta.ValueSyntaxHTML, + expect: expectMap{ + encoderHTML: ``, + encoderSz: `(BLOCK)`, + encoderSHTML: `()`, + encoderText: "", + encoderZmk: "", + }, + }, { descr: "", zmk: ``, expect: expectMap{ encoderHTML: ``, @@ -403,9 +421,5 @@ encoderText: "", encoderZmk: useZmk, }, }, } - -// func TestEncoderBlock(t *testing.T) { -// executeTestCases(t, tcsBlock) -// } Index: internal/encoder/encoder_inline_test.go ================================================================== --- internal/encoder/encoder_inline_test.go +++ internal/encoder/encoder_inline_test.go @@ -163,11 +163,11 @@ zmk: `""quotes""{lang=de}`, expect: expectMap{ encoderHTML: `

„quotes“

`, encoderMD: "„quotes“", encoderSz: `(BLOCK (PARA (FORMAT-QUOTE (("lang" . "de")) (TEXT "quotes"))))`, - encoderSHTML: `((p (span (@ (lang . "de")) (@H "„") "quotes" (@H "“"))))`, + encoderSHTML: `((p (span ((lang . "de")) (@H "„") "quotes" (@H "“"))))`, encoderText: `quotes`, encoderZmk: `""quotes""{lang="de"}`, }, }, { @@ -187,11 +187,11 @@ zmk: `""""{lang=unknown}`, expect: expectMap{ encoderHTML: `

""

`, encoderMD: """", encoderSz: `(BLOCK (PARA (FORMAT-QUOTE (("lang" . "unknown")))))`, - encoderSHTML: `((p (span (@ (lang . "unknown")) (@H """ """))))`, + encoderSHTML: `((p (span ((lang . "unknown")) (@H """ """))))`, encoderText: ``, encoderZmk: `""""{lang="unknown"}`, }, }, { @@ -307,11 +307,11 @@ zmk: `$$\TeX$$`, expect: expectMap{ encoderHTML: `

\TeX

`, encoderMD: "\\TeX", encoderSz: `(BLOCK (PARA (LITERAL-MATH () "\\TeX")))`, - encoderSHTML: `((p (code (@ (class . "zs-math")) "\\TeX")))`, + encoderSHTML: `((p (code ((class . "zs-math")) "\\TeX")))`, encoderText: `\TeX`, encoderZmk: useZmk, }, }, { @@ -319,15 +319,27 @@ zmk: `::""abc""::{lang=fr}`, expect: expectMap{ encoderHTML: `

« abc »

`, encoderMD: "« abc »", encoderSz: `(BLOCK (PARA (FORMAT-SPAN (("lang" . "fr")) (FORMAT-QUOTE () (TEXT "abc")))))`, - encoderSHTML: `((p (span (@ (lang . "fr")) (@L (@H "«" " ") "abc" (@H " " "»")))))`, + encoderSHTML: `((p (span ((lang . "fr")) (@L (@H "«" " ") "abc" (@H " " "»")))))`, encoderText: `abc`, encoderZmk: `::""abc""::{lang="fr"}`, }, }, + { + descr: "Nested Insert Quote formatting", + zmk: `>>""abc"">>{lang=fr}`, + expect: expectMap{ + encoderHTML: `

« abc »

`, + encoderMD: "« abc »", + encoderSz: `(BLOCK (PARA (FORMAT-INSERT (("lang" . "fr")) (FORMAT-QUOTE () (TEXT "abc")))))`, + encoderSHTML: `((p (ins ((lang . "fr")) (@L (@H "«" " ") "abc" (@H " " "»")))))`, + encoderText: `abc`, + encoderZmk: `>>""abc"">>{lang="fr"}`, + }, + }, { descr: "Simple Citation", zmk: `[@Stern18]`, expect: expectMap{ encoderHTML: `

Stern18

`, // TODO @@ -399,11 +411,11 @@ }, { descr: "Comment after text and with -->", zmk: `Text%%{-} comment --> end`, expect: expectMap{ - encoderHTML: `

Text

`, + encoderHTML: `

Text

`, encoderMD: "Text", encoderSz: `(BLOCK (PARA (TEXT "Text") (LITERAL-COMMENT (("-" . "")) "comment --> end")))`, encoderSHTML: `((p "Text" (@@ "comment --> end")))`, encoderText: `Text`, encoderZmk: useZmk, @@ -414,11 +426,11 @@ zmk: `[^endnote]`, expect: expectMap{ encoderHTML: `

1

  1. endnote ↩︎
`, encoderMD: "", encoderSz: `(BLOCK (PARA (ENDNOTE () (TEXT "endnote"))))`, - encoderSHTML: `((p (sup (@ (id . "fnref:1")) (a (@ (class . "zs-noteref") (href . "#fn:1") (role . "doc-noteref")) "1"))))`, + encoderSHTML: `((p (sup ((id . "fnref:1")) (a ((class . "zs-noteref") (href . "#fn:1") (role . "doc-noteref")) "1"))))`, encoderText: `endnote`, encoderZmk: useZmk, }, }, { @@ -426,11 +438,11 @@ zmk: `[!mark]`, expect: expectMap{ encoderHTML: `

`, encoderMD: "", encoderSz: `(BLOCK (PARA (MARK "mark" "mark" "mark")))`, - encoderSHTML: `((p (a (@ (id . "mark")))))`, + encoderSHTML: `((p (a ((id . "mark")))))`, encoderText: ``, encoderZmk: useZmk, }, }, { @@ -438,11 +450,11 @@ zmk: `[!mark|with text]`, expect: expectMap{ encoderHTML: `

with text

`, encoderMD: "with text", encoderSz: `(BLOCK (PARA (MARK "mark" "mark" "mark" (TEXT "with text"))))`, - encoderSHTML: `((p (a (@ (id . "mark")) "with text")))`, + encoderSHTML: `((p (a ((id . "mark")) "with text")))`, encoderText: `with text`, encoderZmk: useZmk, }, }, { @@ -474,11 +486,23 @@ zmk: `[[abc]]`, expect: expectMap{ encoderHTML: `

abc

`, encoderMD: "[abc](abc)", encoderSz: `(BLOCK (PARA (LINK () (HOSTED "abc"))))`, - encoderSHTML: `((p (a (@ (href . "abc")) "abc")))`, + encoderSHTML: `((p (a ((href . "abc")) "abc")))`, + encoderText: ``, + encoderZmk: useZmk, + }, + }, + { + descr: "Dummy Link with attribute", + zmk: `[[abc]]{a="b"}`, + expect: expectMap{ + encoderHTML: `

abc

`, + encoderMD: "[abc](abc)", + encoderSz: `(BLOCK (PARA (LINK (("a" . "b")) (HOSTED "abc"))))`, + encoderSHTML: `((p (a ((a . "b") (href . "abc")) "abc")))`, encoderText: ``, encoderZmk: useZmk, }, }, { @@ -486,11 +510,11 @@ zmk: `[[https://zettelstore.de]]`, expect: expectMap{ encoderHTML: `

https://zettelstore.de

`, encoderMD: "", encoderSz: `(BLOCK (PARA (LINK () (EXTERNAL "https://zettelstore.de"))))`, - encoderSHTML: `((p (a (@ (href . "https://zettelstore.de") (rel . "external")) "https://zettelstore.de")))`, + encoderSHTML: `((p (a ((href . "https://zettelstore.de") (rel . "external")) "https://zettelstore.de")))`, encoderText: ``, encoderZmk: useZmk, }, }, { @@ -498,11 +522,11 @@ zmk: `[[Home|https://zettelstore.de]]`, expect: expectMap{ encoderHTML: `

Home

`, encoderMD: "[Home](https://zettelstore.de)", encoderSz: `(BLOCK (PARA (LINK () (EXTERNAL "https://zettelstore.de") (TEXT "Home"))))`, - encoderSHTML: `((p (a (@ (href . "https://zettelstore.de") (rel . "external")) "Home")))`, + encoderSHTML: `((p (a ((href . "https://zettelstore.de") (rel . "external")) "Home")))`, encoderText: `Home`, encoderZmk: useZmk, }, }, { @@ -510,11 +534,11 @@ zmk: `[[00000000000100]]`, expect: expectMap{ encoderHTML: `

00000000000100

`, encoderMD: "[00000000000100](00000000000100)", encoderSz: `(BLOCK (PARA (LINK () (ZETTEL "00000000000100"))))`, - encoderSHTML: `((p (a (@ (href . "00000000000100")) "00000000000100")))`, + encoderSHTML: `((p (a ((href . "00000000000100")) "00000000000100")))`, encoderText: ``, encoderZmk: useZmk, }, }, { @@ -522,11 +546,11 @@ zmk: `[[Config|00000000000100]]`, expect: expectMap{ encoderHTML: `

Config

`, encoderMD: "[Config](00000000000100)", encoderSz: `(BLOCK (PARA (LINK () (ZETTEL "00000000000100") (TEXT "Config"))))`, - encoderSHTML: `((p (a (@ (href . "00000000000100")) "Config")))`, + encoderSHTML: `((p (a ((href . "00000000000100")) "Config")))`, encoderText: `Config`, encoderZmk: useZmk, }, }, { @@ -534,11 +558,11 @@ zmk: `[[00000000000100#frag]]`, expect: expectMap{ encoderHTML: `

00000000000100#frag

`, encoderMD: "[00000000000100#frag](00000000000100#frag)", encoderSz: `(BLOCK (PARA (LINK () (ZETTEL "00000000000100#frag"))))`, - encoderSHTML: `((p (a (@ (href . "00000000000100#frag")) "00000000000100#frag")))`, + encoderSHTML: `((p (a ((href . "00000000000100#frag")) "00000000000100#frag")))`, encoderText: ``, encoderZmk: useZmk, }, }, { @@ -546,11 +570,11 @@ zmk: `[[Config|00000000000100#frag]]`, expect: expectMap{ encoderHTML: `

Config

`, encoderMD: "[Config](00000000000100#frag)", encoderSz: `(BLOCK (PARA (LINK () (ZETTEL "00000000000100#frag") (TEXT "Config"))))`, - encoderSHTML: `((p (a (@ (href . "00000000000100#frag")) "Config")))`, + encoderSHTML: `((p (a ((href . "00000000000100#frag")) "Config")))`, encoderText: `Config`, encoderZmk: useZmk, }, }, { @@ -558,11 +582,11 @@ zmk: `[[#frag]]`, expect: expectMap{ encoderHTML: `

#frag

`, encoderMD: "[#frag](#frag)", encoderSz: `(BLOCK (PARA (LINK () (SELF "#frag"))))`, - encoderSHTML: `((p (a (@ (href . "#frag")) "#frag")))`, + encoderSHTML: `((p (a ((href . "#frag")) "#frag")))`, encoderText: ``, encoderZmk: useZmk, }, }, { @@ -570,11 +594,11 @@ zmk: `[[H|/hosted]]`, expect: expectMap{ encoderHTML: `

H

`, encoderMD: "[H](/hosted)", encoderSz: `(BLOCK (PARA (LINK () (HOSTED "/hosted") (TEXT "H"))))`, - encoderSHTML: `((p (a (@ (href . "/hosted")) "H")))`, + encoderSHTML: `((p (a ((href . "/hosted")) "H")))`, encoderText: `H`, encoderZmk: useZmk, }, }, { @@ -583,11 +607,11 @@ expect: expectMap{ encoderHTML: `

B

`, encoderMD: "[B](/based)", encoderSz: `(BLOCK (PARA (LINK () (BASED "/based") (TEXT "B"))))`, encoderText: `B`, - encoderSHTML: `((p (a (@ (href . "/based")) "B")))`, + encoderSHTML: `((p (a ((href . "/based")) "B")))`, encoderZmk: useZmk, }, }, { descr: "Relative link", @@ -594,11 +618,11 @@ zmk: `[[R|../relative]]`, expect: expectMap{ encoderHTML: `

R

`, encoderMD: "[R](../relative)", encoderSz: `(BLOCK (PARA (LINK () (HOSTED "../relative") (TEXT "R"))))`, - encoderSHTML: `((p (a (@ (href . "../relative")) "R")))`, + encoderSHTML: `((p (a ((href . "../relative")) "R")))`, encoderText: `R`, encoderZmk: useZmk, }, }, { @@ -606,11 +630,11 @@ zmk: `[[query:title:syntax]]`, expect: expectMap{ encoderHTML: `

title:syntax

`, encoderMD: "", encoderSz: `(BLOCK (PARA (LINK () (QUERY "title:syntax"))))`, - encoderSHTML: `((p (a (@ (href . "?q=title%3Asyntax")) "title:syntax")))`, + encoderSHTML: `((p (a ((href . "?q=title%3Asyntax")) "title:syntax")))`, encoderText: ``, encoderZmk: useZmk, }, }, { @@ -618,11 +642,11 @@ zmk: `[[Q|query:title:syntax]]`, expect: expectMap{ encoderHTML: `

Q

`, encoderMD: "Q", encoderSz: `(BLOCK (PARA (LINK () (QUERY "title:syntax") (TEXT "Q"))))`, - encoderSHTML: `((p (a (@ (href . "?q=title%3Asyntax")) "Q")))`, + encoderSHTML: `((p (a ((href . "?q=title%3Asyntax")) "Q")))`, encoderText: `Q`, encoderZmk: useZmk, }, }, { @@ -630,15 +654,39 @@ zmk: `{{abc}}`, expect: expectMap{ encoderHTML: `

`, encoderMD: "![abc](abc)", encoderSz: `(BLOCK (PARA (EMBED () (HOSTED "abc") "")))`, - encoderSHTML: `((p (img (@ (src . "abc")))))`, + encoderSHTML: `((p (img ((src . "abc")))))`, + encoderText: ``, + encoderZmk: useZmk, + }, + }, + { + descr: "Dummy Embed with attributes", + zmk: `{{abc}}{a="b"}`, + expect: expectMap{ + encoderHTML: `

`, + 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: `

text

`, + 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: ``, Index: internal/encoder/encoder_test.go ================================================================== --- internal/encoder/encoder_test.go +++ internal/encoder/encoder_test.go @@ -16,24 +16,25 @@ import ( "fmt" "strings" "testing" + "t73f.de/r/sx" "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/ast" - "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/encoder" "zettelstore.de/z/internal/parser" ) type zmkTestCase struct { descr string zmk string + syntax string inline bool expect expectMap } type expectMap map[api.EncodingEnum]string @@ -50,26 +51,62 @@ func TestEncoder(t *testing.T) { for i := range tcsInline { tcsInline[i].inline = true } - executeTestCases(t, append(tcsBlock, tcsInline...)) + executeTestCases(t, append(append([]zmkTestCase{}, tcsBlock...), tcsInline...)) } func executeTestCases(t *testing.T, testCases []zmkTestCase) { for testNum, tc := range testCases { inp := input.NewInput([]byte(tc.zmk)) - bs := parser.Parse(inp, nil, meta.ValueSyntaxZmk, config.NoHTML) - checkEncodings(t, testNum, bs, tc.inline, tc.descr, tc.expect, 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, bs ast.BlockSlice, isInline bool, descr string, expected expectMap, zmkDefault string) { +func checkEncodings(t *testing.T, testNum int, node *sx.Pair, isInline bool, descr string, expected expectMap, zmkDefault string) { + for enc, exp := range expected { + 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 := encode(encdr, bs) + got, err := encodeAST(encdr, bs) if err != nil { prefix := fmt.Sprintf("Test #%d", testNum) if d := descr; d != "" { prefix += "\nReason: " + d } @@ -92,38 +129,42 @@ } func checkSz(t *testing.T, testNum int, bs ast.BlockSlice, isInline bool, descr string) { t.Helper() encdr := encoder.Create(encoderSz, nil) - exp, err := encode(encdr, bs) + exp, err := encodeAST(encdr, bs) if err != nil { t.Error(err) return } val, err := sxreader.MakeReader(strings.NewReader(exp)).Read() if err != nil { t.Error(err) return } - got := val.String() - if exp != got { + 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, bs ast.BlockSlice) (string, error) { +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) + err := e.WriteBlocks(&sb, &bs) return sb.String(), err } func mode(isInline bool) string { if isInline { return "inline" } return "block" } Index: internal/encoder/htmlenc.go ================================================================== --- internal/encoder/htmlenc.go +++ internal/encoder/htmlenc.go @@ -21,58 +21,60 @@ "t73f.de/r/sx" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" + "t73f.de/r/zsc/sz" + "t73f.de/r/zsx" "zettelstore.de/z/internal/ast" + "zettelstore.de/z/internal/ast/sztrans" ) // htmlEncoder contains all data needed for encoding. type htmlEncoder struct { - tx SzTransformer + tx sztrans.SzTransformer th *shtml.Evaluator lang string textEnc TextEncoder } // WriteZettel encodes a full zettel as HTML5. -func (he *htmlEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { +func (he *htmlEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error { env := shtml.MakeEnvironment(he.lang) - hm, err := he.th.Evaluate(he.tx.GetMeta(zn.InhMeta), &env) + hm, err := he.th.Evaluate(sztrans.GetMetaSz(zn.InhMeta), &env) if err != nil { - return 0, err + return err } var isTitle ast.InlineSlice var htitle *sx.Pair plainTitle, hasTitle := zn.InhMeta.Get(meta.KeyTitle) if hasTitle { - isTitle = ast.ParseSpacedText(string(plainTitle)) - xtitle := he.tx.GetSz(&isTitle) - htitle, err = he.th.Evaluate(xtitle, &env) + szTitle := zsx.MakeInline((zsx.MakeText(sz.NormalizedSpacedText(string(plainTitle))))) + htitle, err = he.th.Evaluate(szTitle, &env) if err != nil { - return 0, err + return err } } xast := he.tx.GetSz(&zn.BlocksAST) hast, err := he.th.Evaluate(xast, &env) if err != nil { - return 0, err + return err } hen := shtml.Endnotes(&env) var head sx.ListBuilder head.AddN( shtml.SymHead, - sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sx.MakeSymbol("charset"), sx.MakeString("utf-8"))).Cons(sxhtml.SymAttr)).Cons(shtml.SymMeta), + sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sxhtml.MakeSymbol("charset"), sx.MakeString("utf-8")))).Cons(shtml.SymMeta), ) head.ExtendBang(hm) var sb strings.Builder if hasTitle { - _, _ = he.textEnc.WriteInlines(&sb, &isTitle) + _ = he.textEnc.WriteInlines(&sb, &isTitle) } else { sb.Write(zn.Meta.Zid.Bytes()) } head.Add(sx.MakeList(shtml.SymAttrTitle, sx.MakeString(sb.String()))) @@ -94,32 +96,44 @@ gen := sxhtml.NewGenerator().SetNewline() return gen.WriteHTML(w, doc) } // WriteMeta encodes meta data as HTML5. -func (he *htmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { +func (he *htmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) error { env := shtml.MakeEnvironment(he.lang) - hm, err := he.th.Evaluate(he.tx.GetMeta(m), &env) + hm, err := he.th.Evaluate(sztrans.GetMetaSz(m), &env) if err != nil { - return 0, err + return err } gen := sxhtml.NewGenerator().SetNewline() return gen.WriteListHTML(w, hm) } + +// WriteSz encodes SZ represented zettel content. +func (he *htmlEncoder) WriteSz(w io.Writer, node *sx.Pair) error { + env := shtml.MakeEnvironment(he.lang) + hobj, err := he.th.Evaluate(node, &env) + if err == nil { + gen := sxhtml.NewGenerator() + if err = gen.WriteListHTML(w, hobj); err != nil { + return err + } + + return gen.WriteHTML(w, shtml.Endnotes(&env)) + } + return err +} // WriteBlocks encodes a block slice. -func (he *htmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { +func (he *htmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) error { env := shtml.MakeEnvironment(he.lang) hobj, err := he.th.Evaluate(he.tx.GetSz(bs), &env) if err == nil { gen := sxhtml.NewGenerator() - length, err2 := gen.WriteListHTML(w, hobj) - if err2 != nil { - return length, err2 + if err = gen.WriteListHTML(w, hobj); err != nil { + return err } - l, err2 := gen.WriteHTML(w, shtml.Endnotes(&env)) - length += l - return length, err2 + return gen.WriteHTML(w, shtml.Endnotes(&env)) } - return 0, err + return err } Index: internal/encoder/mdenc.go ================================================================== --- internal/encoder/mdenc.go +++ internal/encoder/mdenc.go @@ -15,13 +15,16 @@ // 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" ) @@ -29,80 +32,397 @@ type mdEncoder struct { lang string } // WriteZettel writes the encoded zettel to the writer. -func (me *mdEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { - v := newMDVisitor(w, me.lang) - v.acceptMeta(zn.InhMeta) +func (me *mdEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error { + v := newMDVisitorAST(w, me.lang) + v.b.WriteMeta(zn.InhMeta) if zn.InhMeta.YamlSep { v.b.WriteString("---\n") } else { v.b.WriteLn() } ast.Walk(&v, &zn.BlocksAST) - length, err := v.b.Flush() - return length, err + return v.b.Flush() } // WriteMeta encodes meta data as markdown. -func (me *mdEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { +func (*mdEncoder) WriteMeta(w io.Writer, m *meta.Meta) error { + ew := newEncWriter(w) + ew.WriteMeta(m) + return ew.Flush() +} + +// WriteSz encodes SZ represented zettel content. +func (me *mdEncoder) WriteSz(w io.Writer, node *sx.Pair) error { v := newMDVisitor(w, me.lang) - v.acceptMeta(m) - length, err := v.b.Flush() - return length, err + 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, "", "") + 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(" ") + } + v.quoteNesting++ + v.walkList(inlines, alst) + v.quoteNesting-- + if withNbsp { + v.b.WriteString(" ") + } + v.b.WriteString(rightQ) +} + +const enumOrdered = "1. " +const enumUnordered = "* " + +func (v *mdVisitor) visitNestedList(node *sx.Pair, alst *sx.Pair, enum string) { + v.listInfo = append(v.listInfo, len(enum)) + regIndent := 4*len(v.listInfo) - 4 + paraIndent := regIndent + len(enum) + _, attrs, blocks := zsx.GetList(node) + alst = v.setLanguage(alst, attrs) + firstBlk := true + for blk := range blocks.Pairs() { + if firstBlk { + firstBlk = false + } else { + v.b.WriteLn() + } + v.writeSpaces(regIndent) + v.b.WriteString(enum) + first := true + for item := range blk.Head().Tail().Pairs() { + in := item.Head() + if first { + first = false + } else { + v.b.WriteLn() + if zsx.SymPara.IsEqual(in.Car()) { + v.writeSpaces(paraIndent) + } + } + v.walk(in, alst) + } + } + v.listInfo = v.listInfo[:len(v.listInfo)-1] +} +func (v *mdVisitor) visitListQuote(node *sx.Pair, alst *sx.Pair) { + v.listInfo = []int{0} + oldPrefix := v.listPrefix + v.listPrefix = "> " + + _, attrs, blocks := zsx.GetList(node) + alst = v.setLanguage(alst, attrs) + firstBlk := true + for blk := range blocks.Pairs() { + if firstBlk { + firstBlk = false + } else { + v.b.WriteLn() + } + v.b.WriteString(v.listPrefix) + first := true + for item := range blk.Head().Tail().Pairs() { + in := item.Head() + if first { + first = false + } else { + v.b.WriteLn() + if zsx.SymPara.IsEqual(in.Car()) { + v.b.WriteString(v.listPrefix) + } + } + v.walk(in, alst) + } + } + v.listPrefix = oldPrefix + v.listInfo = nil +} + +func (v *mdVisitor) visitVerbatim(node *sx.Pair) { + if _, _, content := zsx.GetVerbatim(node); content != "" { + lc := len(content) + v.writeSpaces(4) + lcm1 := lc - 1 + for i := 0; i < lc; i++ { + b := content[i] + if b != '\n' && b != '\r' { + _ = v.b.WriteByte(b) + continue + } + j := i + 1 + for ; j < lc; j++ { + c := content[j] + if c != '\n' && c != '\r' { + break + } + } + if j >= lcm1 { + break + } + v.b.WriteLn() + v.writeSpaces(4) + i = j - 1 + } + } +} + +func (v *mdVisitor) visitRegion(node *sx.Pair, alst *sx.Pair) { + _, attrs, blocks, _ := zsx.GetRegion(node) + alst = v.setLanguage(alst, attrs) + + first := true + for n := range blocks.Pairs() { + blk := n.Head() + if zsx.SymPara.IsEqual(blk.Car()) { + if first { + first = false + } else { + v.b.WriteString("\n>\n") + } + v.b.WriteString("> ") + v.walk(blk, alst) + } + } } -func (v *mdVisitor) acceptMeta(m *meta.Meta) { - for key, val := range m.Computed() { - v.b.WriteStrings(key, ": ", string(val), "\n") +func (v *mdVisitor) writeSpaces(n int) { + for range n { + v.b.WriteSpace() } } // WriteBlocks writes the content of a block slice to the writer. -func (me *mdEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { - v := newMDVisitor(w, me.lang) +func (me *mdEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) error { + v := newMDVisitorAST(w, me.lang) ast.Walk(&v, bs) - length, err := v.b.Flush() - return length, err + return v.b.Flush() } -// mdVisitor writes the abstract syntax tree to an EncWriter. -type mdVisitor struct { +// mdVisitorAST writes the abstract syntax tree to an EncWriter. +type mdVisitorAST struct { b encWriter listInfo []int listPrefix string langStack shtml.LangStack quoteNesting uint } -func newMDVisitor(w io.Writer, lang string) mdVisitor { - return mdVisitor{b: newEncWriter(w), langStack: shtml.NewLangStack(lang)} +func newMDVisitorAST(w io.Writer, lang string) mdVisitorAST { + return mdVisitorAST{b: newEncWriter(w), langStack: shtml.NewLangStack(lang)} } // pushAttribute adds the current attributes to the visitor. -func (v *mdVisitor) pushAttributes(a zsx.Attributes) { +func (v *mdVisitorAST) pushAttributes(a zsx.Attributes) { if value, ok := a.Get("lang"); ok { v.langStack.Push(value) } else { v.langStack.Dup() } } // popAttributes removes the current attributes from the visitor. -func (v *mdVisitor) popAttributes() { v.langStack.Pop() } +func (v *mdVisitorAST) popAttributes() { v.langStack.Pop() } // getLanguage returns the current language, -func (v *mdVisitor) getLanguage() string { return v.langStack.Top() } +func (v *mdVisitorAST) getLanguage() string { return v.langStack.Top() } -func (v *mdVisitor) getQuotes() (string, string, bool) { +func (v *mdVisitorAST) getQuotes() (string, string, bool) { qi := shtml.GetQuoteInfo(v.getLanguage()) leftQ, rightQ := qi.GetQuotes(v.quoteNesting) return leftQ, rightQ, qi.GetNBSp() } -func (v *mdVisitor) Visit(node ast.Node) ast.Visitor { +func (v *mdVisitorAST) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: v.visitBlockSlice(n) case *ast.VerbatimNode: v.visitVerbatim(n) @@ -136,20 +456,20 @@ return v } return nil } -func (v *mdVisitor) visitBlockSlice(bs *ast.BlockSlice) { +func (v *mdVisitorAST) visitBlockSlice(bs *ast.BlockSlice) { for i, bn := range *bs { if i > 0 { v.b.WriteString("\n\n") } ast.Walk(v, bn) } } -func (v *mdVisitor) visitVerbatim(vn *ast.VerbatimNode) { +func (v *mdVisitorAST) visitVerbatim(vn *ast.VerbatimNode) { lc := len(vn.Content) if vn.Kind != ast.VerbatimCode || lc == 0 { return } v.writeSpaces(4) @@ -174,11 +494,11 @@ v.writeSpaces(4) i = j - 1 } } -func (v *mdVisitor) visitRegion(rn *ast.RegionNode) { +func (v *mdVisitorAST) visitRegion(rn *ast.RegionNode) { if rn.Kind != ast.RegionQuote { return } v.pushAttributes(rn.Attrs) defer v.popAttributes() @@ -196,20 +516,20 @@ v.b.WriteString("> ") ast.Walk(v, &pn.Inlines) } } -func (v *mdVisitor) visitHeading(hn *ast.HeadingNode) { +func (v *mdVisitorAST) visitHeading(hn *ast.HeadingNode) { v.pushAttributes(hn.Attrs) defer v.popAttributes() const headingSigns = "###### " v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-1:]) ast.Walk(v, &hn.Inlines) } -func (v *mdVisitor) visitNestedList(ln *ast.NestedListNode) { +func (v *mdVisitorAST) visitNestedList(ln *ast.NestedListNode) { switch ln.Kind { case ast.NestedListOrdered: v.writeNestedList(ln, "1. ") case ast.NestedListUnordered: v.writeNestedList(ln, "* ") @@ -217,11 +537,11 @@ v.writeListQuote(ln) } v.listInfo = v.listInfo[:len(v.listInfo)-1] } -func (v *mdVisitor) writeNestedList(ln *ast.NestedListNode, enum string) { +func (v *mdVisitorAST) writeNestedList(ln *ast.NestedListNode, enum string) { v.listInfo = append(v.listInfo, len(enum)) regIndent := 4*len(v.listInfo) - 4 paraIndent := regIndent + len(enum) for i, item := range ln.Items { if i > 0 { @@ -239,11 +559,11 @@ ast.Walk(v, in) } } } -func (v *mdVisitor) writeListQuote(ln *ast.NestedListNode) { +func (v *mdVisitorAST) writeListQuote(ln *ast.NestedListNode) { v.listInfo = append(v.listInfo, 0) if len(v.listInfo) > 1 { return } @@ -267,11 +587,11 @@ } v.listPrefix = prefix } -func (v *mdVisitor) visitBreak(bn *ast.BreakNode) { +func (v *mdVisitorAST) visitBreak(bn *ast.BreakNode) { if bn.Hard { v.b.WriteString("\\\n") } else { v.b.WriteLn() } @@ -283,51 +603,51 @@ v.b.WriteString(v.listPrefix) } } } -func (v *mdVisitor) visitLink(ln *ast.LinkNode) { +func (v *mdVisitorAST) visitLink(ln *ast.LinkNode) { v.pushAttributes(ln.Attrs) defer v.popAttributes() v.writeReference(ln.Ref, ln.Inlines) } -func (v *mdVisitor) visitEmbedRef(en *ast.EmbedRefNode) { +func (v *mdVisitorAST) visitEmbedRef(en *ast.EmbedRefNode) { v.pushAttributes(en.Attrs) defer v.popAttributes() _ = v.b.WriteByte('!') v.writeReference(en.Ref, en.Inlines) } -func (v *mdVisitor) writeReference(ref *ast.Reference, is ast.InlineSlice) { +func (v *mdVisitorAST) writeReference(ref *ast.Reference, is ast.InlineSlice) { if ref.State == ast.RefStateQuery { ast.Walk(v, &is) } else if len(is) > 0 { _ = v.b.WriteByte('[') ast.Walk(v, &is) v.b.WriteStrings("](", ref.String()) _ = v.b.WriteByte(')') - } else if isAutoLinkable(ref) { + } else if isAutoLinkableAST(ref) { _ = v.b.WriteByte('<') v.b.WriteString(ref.String()) _ = v.b.WriteByte('>') } else { s := ref.String() v.b.WriteStrings("[", s, "](", s, ")") } } -func isAutoLinkable(ref *ast.Reference) bool { +func isAutoLinkableAST(ref *ast.Reference) bool { if ref.State != ast.RefStateExternal || ref.URL == nil { return false } return ref.URL.Scheme != "" } -func (v *mdVisitor) visitFormat(fn *ast.FormatNode) { +func (v *mdVisitorAST) visitFormat(fn *ast.FormatNode) { v.pushAttributes(fn.Attrs) defer v.popAttributes() switch fn.Kind { case ast.FormatEmph: @@ -347,11 +667,11 @@ default: ast.Walk(v, &fn.Inlines) } } -func (v *mdVisitor) writeQuote(fn *ast.FormatNode) { +func (v *mdVisitorAST) writeQuote(fn *ast.FormatNode) { leftQ, rightQ, withNbsp := v.getQuotes() v.b.WriteString(leftQ) if withNbsp { v.b.WriteString(" ") } @@ -362,11 +682,11 @@ v.b.WriteString(" ") } v.b.WriteString(rightQ) } -func (v *mdVisitor) visitLiteral(ln *ast.LiteralNode) { +func (v *mdVisitorAST) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralCode, ast.LiteralInput, ast.LiteralOutput: _ = v.b.WriteByte('`') _, _ = v.b.Write(ln.Content) _ = v.b.WriteByte('`') @@ -374,10 +694,10 @@ default: _, _ = v.b.Write(ln.Content) } } -func (v *mdVisitor) writeSpaces(n int) { +func (v *mdVisitorAST) writeSpaces(n int) { for range n { v.b.WriteSpace() } } Index: internal/encoder/shtmlenc.go ================================================================== --- internal/encoder/shtmlenc.go +++ internal/encoder/shtmlenc.go @@ -21,48 +21,63 @@ "t73f.de/r/sx" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/internal/ast" + "zettelstore.de/z/internal/ast/sztrans" ) // shtmlEncoder contains all data needed for encoding. type shtmlEncoder struct { - tx SzTransformer + tx sztrans.SzTransformer th *shtml.Evaluator lang string } // WriteZettel writes the encoded zettel to the writer. -func (enc *shtmlEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { +func (enc *shtmlEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error { env := shtml.MakeEnvironment(enc.lang) - metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(zn.InhMeta), &env) + metaSHTML, err := enc.th.Evaluate(sztrans.GetMetaSz(zn.InhMeta), &env) if err != nil { - return 0, err + return err } contentSHTML, err := enc.th.Evaluate(enc.tx.GetSz(&zn.BlocksAST), &env) if err != nil { - return 0, err + return err } result := sx.Cons(metaSHTML, contentSHTML) - return result.Print(w) + _, err = result.Print(w) + return err } // WriteMeta encodes meta data as s-expression. -func (enc *shtmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { +func (enc *shtmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) error { + env := shtml.MakeEnvironment(enc.lang) + metaSHTML, err := enc.th.Evaluate(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) - metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(m), &env) + hval, err := enc.th.Evaluate(node, &env) if err != nil { - return 0, err + return err } - return sx.Print(w, metaSHTML) + _, err = hval.Print(w) + return err } // WriteBlocks writes a block slice to the writer -func (enc *shtmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { +func (enc *shtmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) error { env := shtml.MakeEnvironment(enc.lang) hval, err := enc.th.Evaluate(enc.tx.GetSz(bs), &env) if err != nil { - return 0, err + return err } - return sx.Print(w, hval) + _, err = hval.Print(w) + return err } Index: internal/encoder/szenc.go ================================================================== --- internal/encoder/szenc.go +++ internal/encoder/szenc.go @@ -20,28 +20,38 @@ "t73f.de/r/sx" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" + "zettelstore.de/z/internal/ast/sztrans" ) // szEncoder contains all data needed for encoding. type szEncoder struct { - trans SzTransformer + trans sztrans.SzTransformer } // WriteZettel writes the encoded zettel to the writer. -func (enc *szEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { +func (enc *szEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error { content := enc.trans.GetSz(&zn.BlocksAST) - meta := enc.trans.GetMeta(zn.InhMeta) - return sx.MakeList(meta, content).Print(w) + meta := sztrans.GetMetaSz(zn.InhMeta) + _, err := sx.MakeList(meta, content).Print(w) + return err } // WriteMeta encodes meta data as s-expression. -func (enc *szEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { - return enc.trans.GetMeta(m).Print(w) +func (enc *szEncoder) WriteMeta(w io.Writer, m *meta.Meta) error { + _, err := sztrans.GetMetaSz(m).Print(w) + return err +} + +// WriteSz encodes SZ represented zettel content. +func (*szEncoder) WriteSz(w io.Writer, node *sx.Pair) error { + _, err := node.Print(w) + return err } // WriteBlocks writes a block slice to the writer -func (enc *szEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { - return enc.trans.GetSz(bs).Print(w) +func (enc *szEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) error { + _, err := enc.trans.GetSz(bs).Print(w) + return err } DELETED internal/encoder/sztransform.go Index: internal/encoder/sztransform.go ================================================================== --- internal/encoder/sztransform.go +++ /dev/null @@ -1,359 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 "" -} Index: internal/encoder/textenc.go ================================================================== --- internal/encoder/textenc.go +++ internal/encoder/textenc.go @@ -17,41 +17,41 @@ import ( "io" "iter" + "t73f.de/r/sx" "t73f.de/r/zsc/domain/meta" + "t73f.de/r/zsx" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/ast" ) // TextEncoder encodes just the text and ignores any formatting. type TextEncoder struct{} // WriteZettel writes metadata and content. -func (te *TextEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { - v := newTextVisitor(w) - _, _ = te.WriteMeta(&v.b, zn.InhMeta) +func (te *TextEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error { + v := newTextVisitorAST(w) + _ = te.WriteMeta(&v.b, zn.InhMeta) v.visitBlockSlice(&zn.BlocksAST) - length, err := v.b.Flush() - return length, err + return v.b.Flush() } // WriteMeta encodes metadata as text. -func (te *TextEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { +func (te *TextEncoder) WriteMeta(w io.Writer, m *meta.Meta) error { buf := newEncWriter(w) for key, val := range m.Computed() { if meta.Type(key) == meta.TypeTagSet { writeTagSet(&buf, val.Elems()) } else { buf.WriteString(string(val)) } buf.WriteLn() } - length, err := buf.Flush() - return length, err + return buf.Flush() } func writeTagSet(buf *encWriter, tags iter.Seq[meta.Value]) { first := true for tag := range tags { @@ -59,40 +59,202 @@ buf.WriteSpace() } first = false buf.WriteString(string(tag.CleanTag())) } - } // WriteBlocks writes the content of a block slice to the writer. -func (*TextEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { - v := newTextVisitor(w) +func (*TextEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) error { + v := newTextVisitorAST(w) v.visitBlockSlice(bs) - length, err := v.b.Flush() - return length, err + return v.b.Flush() } // WriteInlines writes an inline slice to the writer -func (*TextEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { - v := newTextVisitor(w) +func (*TextEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) error { + v := newTextVisitorAST(w) ast.Walk(&v, is) - length, err := v.b.Flush() - return length, err + 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 abstract syntax tree to an io.Writer. -type textVisitor struct { - b encWriter - inlinePos int -} +// 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 *textVisitor) Visit(node ast.Node) ast.Visitor { +func (v *textVisitorAST) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: v.visitBlockSlice(n) return nil case *ast.InlineSlice: @@ -152,27 +314,27 @@ } } return v } -func (v *textVisitor) visitVerbatim(vn *ast.VerbatimNode) { +func (v *textVisitorAST) visitVerbatim(vn *ast.VerbatimNode) { if vn.Kind != ast.VerbatimComment { _, _ = v.b.Write(vn.Content) } } -func (v *textVisitor) visitNestedList(ln *ast.NestedListNode) { +func (v *textVisitorAST) visitNestedList(ln *ast.NestedListNode) { for i, item := range ln.Items { v.writePosChar(i, '\n') for j, it := range item { v.writePosChar(j, '\n') ast.Walk(v, it) } } } -func (v *textVisitor) visitDescriptionList(dl *ast.DescriptionListNode) { +func (v *textVisitorAST) visitDescriptionList(dl *ast.DescriptionListNode) { for i, descr := range dl.Descriptions { v.writePosChar(i, '\n') ast.Walk(v, &descr.Term) for _, b := range descr.Descriptions { v.b.WriteLn() @@ -182,11 +344,11 @@ } } } } -func (v *textVisitor) visitTable(tn *ast.TableNode) { +func (v *textVisitorAST) visitTable(tn *ast.TableNode) { if len(tn.Header) > 0 { v.writeRow(tn.Header) v.b.WriteLn() } for i, row := range tn.Rows { @@ -193,33 +355,33 @@ v.writePosChar(i, '\n') v.writeRow(row) } } -func (v *textVisitor) writeRow(row ast.TableRow) { +func (v *textVisitorAST) writeRow(row ast.TableRow) { for i, cell := range row { v.writePosChar(i, ' ') ast.Walk(v, &cell.Inlines) } } -func (v *textVisitor) visitBlockSlice(bs *ast.BlockSlice) { +func (v *textVisitorAST) visitBlockSlice(bs *ast.BlockSlice) { for i, bn := range *bs { v.writePosChar(i, '\n') ast.Walk(v, bn) } } -func (v *textVisitor) visitInlineSlice(is *ast.InlineSlice) { +func (v *textVisitorAST) visitInlineSlice(is *ast.InlineSlice) { for i, in := range *is { v.inlinePos = i ast.Walk(v, in) } v.inlinePos = 0 } -func (v *textVisitor) visitText(s string) { +func (v *textVisitorAST) visitText(s string) { spaceFound := false for _, ch := range s { if input.IsSpace(ch) { if !spaceFound { v.b.WriteSpace() @@ -230,10 +392,10 @@ spaceFound = false v.b.WriteString(string(ch)) } } -func (v *textVisitor) writePosChar(pos int, ch byte) { +func (v *textVisitorAST) writePosChar(pos int, ch byte) { if pos > 0 { _ = v.b.WriteByte(ch) } } Index: internal/encoder/write.go ================================================================== --- internal/encoder/write.go +++ internal/encoder/write.go @@ -12,19 +12,19 @@ //----------------------------------------------------------------------------- package encoder import ( - "encoding/base64" "io" + + "t73f.de/r/zsc/domain/meta" ) // encWriter is a specialized writer for encoding zettel. type encWriter struct { - w io.Writer // The io.Writer to write to - err error // Collect error - length int // Collected length + 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} } @@ -32,22 +32,19 @@ 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 + _, w.err = io.WriteString(w.w, s) } // WriteStrings writes the content of sl. func (w *encWriter) WriteStrings(sl ...string) { for _, s := range sl { @@ -55,33 +52,19 @@ } } // 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 + 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) } -// 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') } @@ -91,8 +74,17 @@ func (w *encWriter) WriteSpace() { if w.err == nil { w.err = w.WriteByte(' ') } } + +// WriteMeta write the content of the meta data in a standard way: +// +// key: val +func (w *encWriter) WriteMeta(m *meta.Meta) { + for key, val := range m.Computed() { + w.WriteStrings(key, ": ", string(val), "\n") + } +} // Flush returns the collected length and error. -func (w *encWriter) Flush() (int, error) { return w.length, w.err } +func (w *encWriter) Flush() error { return w.err } Index: internal/encoder/zmkenc.go ================================================================== --- internal/encoder/zmkenc.go +++ internal/encoder/zmkenc.go @@ -18,127 +18,358 @@ import ( "fmt" "io" "strings" + "t73f.de/r/sx" "t73f.de/r/zero/set" + "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" + "t73f.de/r/zsc/sz" "t73f.de/r/zsx" "zettelstore.de/z/internal/ast" ) // zmkEncoder contains all data needed for encoding. type zmkEncoder struct{} // WriteZettel writes the encoded zettel to the writer. -func (*zmkEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { - v := newZmkVisitor(w) - v.acceptMeta(zn.InhMeta) +func (ze *zmkEncoder) WriteZettel(w io.Writer, zn *ast.Zettel) error { + v := newZmkVisitorAST(w) + v.b.WriteMeta(zn.InhMeta) if zn.InhMeta.YamlSep { v.b.WriteString("---\n") } else { v.b.WriteLn() } ast.Walk(&v, &zn.BlocksAST) - length, err := v.b.Flush() - return length, err + return v.b.Flush() } // 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 (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) Visit(node ast.Node) ast.Visitor { +// 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.visitBlockSlice(n) + v.visitBlockSliceAST(n) case *ast.InlineSlice: - for i, in := range *n { - v.inlinePos = i + for _, in := range *n { ast.Walk(v, in) } - v.inlinePos = 0 case *ast.VerbatimNode: - v.visitVerbatim(n) + v.visitVerbatimAST(n) case *ast.RegionNode: - v.visitRegion(n) + v.visitRegionAST(n) case *ast.HeadingNode: - v.visitHeading(n) + v.visitHeadingAST(n) case *ast.HRuleNode: v.b.WriteString("---") - v.visitAttributes(n.Attrs) + v.visitAttributesAST(n.Attrs) case *ast.NestedListNode: - v.visitNestedList(n) + v.visitNestedListAST(n) case *ast.DescriptionListNode: - v.visitDescriptionList(n) + v.visitDescriptionListAST(n) case *ast.TableNode: - v.visitTable(n) + v.visitTableAST(n) case *ast.TranscludeNode: v.b.WriteStrings("{{{", n.Ref.String(), "}}}") // FIXME n.Inlines - v.visitAttributes(n.Attrs) + v.visitAttributesAST(n.Attrs) case *ast.BLOBNode: - v.visitBLOB(n) + v.visitBLOBAST(n) case *ast.TextNode: - v.visitText(n) + v.visitTextAST(n) case *ast.BreakNode: - v.visitBreak(n) + v.visitBreakAST(n) case *ast.LinkNode: - v.visitLink(n) + v.visitLinkAST(n) case *ast.EmbedRefNode: - v.visitEmbedRef(n) + v.visitEmbedRefAST(n) case *ast.EmbedBLOBNode: - v.visitEmbedBLOB(n) + v.visitEmbedBLOBAST(n) case *ast.CiteNode: - v.visitCite(n) + v.visitCiteAST(n) case *ast.FootnoteNode: v.b.WriteString("[^") ast.Walk(v, &n.Inlines) _ = v.b.WriteByte(']') - v.visitAttributes(n.Attrs) + v.visitAttributesAST(n.Attrs) case *ast.MarkNode: - v.visitMark(n) + v.visitMarkAST(n) case *ast.FormatNode: - v.visitFormat(n) + v.visitFormatAST(n) case *ast.LiteralNode: - v.visitLiteral(n) + v.visitLiteralAST(n) default: return v } return nil } -func (v *zmkVisitor) visitBlockSlice(bs *ast.BlockSlice) { +func (v *zmkVisitorAST) visitBlockSliceAST(bs *ast.BlockSlice) { var lastWasParagraph bool for i, bn := range *bs { if i > 0 { v.b.WriteLn() if lastWasParagraph && !v.inVerse { @@ -159,11 +390,11 @@ ast.VerbatimCode: "```", ast.VerbatimEval: "~~~", ast.VerbatimMath: "$$$", } -func (v *zmkVisitor) visitVerbatim(vn *ast.VerbatimNode) { +func (v *zmkVisitorAST) visitVerbatimAST(vn *ast.VerbatimNode) { kind, ok := mapVerbatimKind[vn.Kind] if !ok { panic(fmt.Sprintf("Unknown verbatim kind %d", vn.Kind)) } attrs := vn.Attrs @@ -171,11 +402,11 @@ attrs = syntaxToHTML(attrs) } // TODO: scan cn.Lines to find embedded kind[0]s at beginning v.b.WriteString(kind) - v.visitAttributes(attrs) + v.visitAttributesAST(attrs) v.b.WriteLn() _, _ = v.b.Write(vn.Content) v.b.WriteLn() v.b.WriteString(kind) } @@ -184,18 +415,18 @@ ast.RegionSpan: ":::", ast.RegionQuote: "<<<", ast.RegionVerse: "\"\"\"", } -func (v *zmkVisitor) visitRegion(rn *ast.RegionNode) { +func (v *zmkVisitorAST) visitRegionAST(rn *ast.RegionNode) { // Scan rn.Blocks for embedded regions to adjust length of regionCode kind, ok := mapRegionKind[rn.Kind] if !ok { panic(fmt.Sprintf("Unknown region kind %d", rn.Kind)) } v.b.WriteString(kind) - v.visitAttributes(rn.Attrs) + v.visitAttributesAST(rn.Attrs) v.b.WriteLn() saveInVerse := v.inVerse v.inVerse = rn.Kind == ast.RegionVerse ast.Walk(v, &rn.Blocks) v.inVerse = saveInVerse @@ -205,24 +436,24 @@ v.b.WriteSpace() ast.Walk(v, &rn.Inlines) } } -func (v *zmkVisitor) visitHeading(hn *ast.HeadingNode) { +func (v *zmkVisitorAST) visitHeadingAST(hn *ast.HeadingNode) { const headingSigns = "========= " v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-3:]) ast.Walk(v, &hn.Inlines) - v.visitAttributes(hn.Attrs) + v.visitAttributesAST(hn.Attrs) } var mapNestedListKind = map[ast.NestedListKind]byte{ ast.NestedListOrdered: '#', ast.NestedListUnordered: '*', ast.NestedListQuote: '>', } -func (v *zmkVisitor) visitNestedList(ln *ast.NestedListNode) { +func (v *zmkVisitorAST) visitNestedListAST(ln *ast.NestedListNode) { v.prefix = append(v.prefix, mapNestedListKind[ln.Kind]) for i, item := range ln.Items { if i > 0 { v.b.WriteLn() } @@ -239,19 +470,19 @@ } } v.prefix = v.prefix[:len(v.prefix)-1] } -func (v *zmkVisitor) writePrefixSpaces() { +func (v *zmkVisitorAST) writePrefixSpaces() { if prefixLen := len(v.prefix); prefixLen > 0 { for i := 0; i <= prefixLen; i++ { v.b.WriteSpace() } } } -func (v *zmkVisitor) visitDescriptionList(dn *ast.DescriptionListNode) { +func (v *zmkVisitorAST) visitDescriptionListAST(dn *ast.DescriptionListNode) { for i, descr := range dn.Descriptions { if i > 0 { v.b.WriteLn() } v.b.WriteString("; ") @@ -274,11 +505,11 @@ ast.AlignLeft: "<", ast.AlignCenter: ":", ast.AlignRight: ">", } -func (v *zmkVisitor) visitTable(tn *ast.TableNode) { +func (v *zmkVisitorAST) visitTableAST(tn *ast.TableNode) { if header := tn.Header; len(header) > 0 { v.writeTableHeader(header, tn.Align) v.b.WriteLn() } for i, row := range tn.Rows { @@ -287,11 +518,11 @@ } v.writeTableRow(row, tn.Align) } } -func (v *zmkVisitor) writeTableHeader(header ast.TableRow, align []ast.Alignment) { +func (v *zmkVisitorAST) writeTableHeader(header ast.TableRow, align []ast.Alignment) { for pos, cell := range header { v.b.WriteString("|=") colAlign := align[pos] if cell.Align != colAlign { v.b.WriteString(alignCode[cell.Align]) @@ -301,37 +532,38 @@ v.b.WriteString(alignCode[colAlign]) } } } -func (v *zmkVisitor) writeTableRow(row ast.TableRow, align []ast.Alignment) { +func (v *zmkVisitorAST) writeTableRow(row ast.TableRow, align []ast.Alignment) { for pos, cell := range row { _ = v.b.WriteByte('|') if cell.Align != align[pos] { v.b.WriteString(alignCode[cell.Align]) } ast.Walk(v, &cell.Inlines) } } -func (v *zmkVisitor) visitBLOB(bn *ast.BLOBNode) { +func (v *zmkVisitorAST) visitBLOBAST(bn *ast.BLOBNode) { if bn.Syntax == meta.ValueSyntaxSVG { v.b.WriteStrings("@@@", bn.Syntax, "\n") _, _ = v.b.Write(bn.Blob) v.b.WriteString("\n@@@\n") return } var sb strings.Builder - _, _ = v.textEnc.WriteInlines(&sb, &bn.Description) + 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 *zmkVisitor) visitText(tn *ast.TextNode) { +func (v *zmkVisitorAST) visitTextAST(tn *ast.TextNode) { last := 0 for i := 0; i < len(tn.Text); i++ { if b := tn.Text[i]; b == '\\' { v.b.WriteString(tn.Text[last:i]) v.b.WriteBytes('\\', b) @@ -352,61 +584,63 @@ } } v.b.WriteString(tn.Text[last:]) } -func (v *zmkVisitor) visitBreak(bn *ast.BreakNode) { +func (v *zmkVisitorAST) visitBreakAST(bn *ast.BreakNode) { if bn.Hard { v.b.WriteString("\\\n") } else { v.b.WriteLn() } v.writePrefixSpaces() } -func (v *zmkVisitor) visitLink(ln *ast.LinkNode) { +func (v *zmkVisitorAST) visitLinkAST(ln *ast.LinkNode) { v.b.WriteString("[[") if len(ln.Inlines) > 0 { ast.Walk(v, &ln.Inlines) _ = v.b.WriteByte('|') } if ln.Ref.State == ast.RefStateBased { _ = v.b.WriteByte('/') } v.b.WriteStrings(ln.Ref.String(), "]]") + v.visitAttributesAST(ln.Attrs) } -func (v *zmkVisitor) visitEmbedRef(en *ast.EmbedRefNode) { +func (v *zmkVisitorAST) visitEmbedRefAST(en *ast.EmbedRefNode) { v.b.WriteString("{{") if len(en.Inlines) > 0 { ast.Walk(v, &en.Inlines) _ = v.b.WriteByte('|') } v.b.WriteStrings(en.Ref.String(), "}}") + v.visitAttributesAST(en.Attrs) } -func (v *zmkVisitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) { +func (v *zmkVisitorAST) visitEmbedBLOBAST(en *ast.EmbedBLOBNode) { if en.Syntax == meta.ValueSyntaxSVG { v.b.WriteString("@@") _, _ = v.b.Write(en.Blob) v.b.WriteStrings("@@{=", en.Syntax, "}") return } v.b.WriteString("{{TODO: display inline BLOB}}") } -func (v *zmkVisitor) visitCite(cn *ast.CiteNode) { +func (v *zmkVisitorAST) visitCiteAST(cn *ast.CiteNode) { v.b.WriteStrings("[@", cn.Key) if len(cn.Inlines) > 0 { v.b.WriteSpace() ast.Walk(v, &cn.Inlines) } _ = v.b.WriteByte(']') - v.visitAttributes(cn.Attrs) + v.visitAttributesAST(cn.Attrs) } -func (v *zmkVisitor) visitMark(mn *ast.MarkNode) { +func (v *zmkVisitorAST) visitMarkAST(mn *ast.MarkNode) { v.b.WriteStrings("[!", mn.Mark) if len(mn.Inlines) > 0 { _ = v.b.WriteByte('|') ast.Walk(v, &mn.Inlines) } @@ -424,51 +658,51 @@ ast.FormatQuote: []byte(`""`), ast.FormatMark: []byte("##"), ast.FormatSpan: []byte("::"), } -func (v *zmkVisitor) visitFormat(fn *ast.FormatNode) { +func (v *zmkVisitorAST) visitFormatAST(fn *ast.FormatNode) { kind, ok := mapFormatKind[fn.Kind] if !ok { panic(fmt.Sprintf("Unknown format kind %d", fn.Kind)) } _, _ = v.b.Write(kind) ast.Walk(v, &fn.Inlines) _, _ = v.b.Write(kind) - v.visitAttributes(fn.Attrs) + v.visitAttributesAST(fn.Attrs) } -func (v *zmkVisitor) visitLiteral(ln *ast.LiteralNode) { +func (v *zmkVisitorAST) visitLiteralAST(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralCode: v.writeLiteral('`', ln.Attrs, ln.Content) case ast.LiteralMath: v.b.WriteStrings("$$", string(ln.Content), "$$") - v.visitAttributes(ln.Attrs) + v.visitAttributesAST(ln.Attrs) case ast.LiteralInput: v.writeLiteral('\'', ln.Attrs, ln.Content) case ast.LiteralOutput: v.writeLiteral('=', ln.Attrs, ln.Content) case ast.LiteralComment: v.b.WriteString("%%") - v.visitAttributes(ln.Attrs) + v.visitAttributesAST(ln.Attrs) v.b.WriteSpace() _, _ = v.b.Write(ln.Content) default: panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind)) } } -func (v *zmkVisitor) writeLiteral(code byte, a zsx.Attributes, content []byte) { +func (v *zmkVisitorAST) writeLiteral(code byte, a zsx.Attributes, content []byte) { v.b.WriteBytes(code, code) v.writeEscaped(string(content), code) v.b.WriteBytes(code, code) - v.visitAttributes(a) + v.visitAttributesAST(a) } -// visitAttributes write HTML attributes -func (v *zmkVisitor) visitAttributes(a zsx.Attributes) { +// visitAttributesAST write HTML attributes +func (v *zmkVisitorAST) visitAttributesAST(a zsx.Attributes) { if a.IsEmpty() { return } _ = v.b.WriteByte('{') for i, k := range a.Keys() { @@ -479,18 +713,19 @@ _ = v.b.WriteByte('-') continue } v.b.WriteString(k) if vl := a[k]; len(vl) > 0 { - v.b.WriteStrings("=\"", vl) + v.b.WriteString("=\"") + v.writeEscaped(vl, '"') _ = v.b.WriteByte('"') } } _ = v.b.WriteByte('}') } -func (v *zmkVisitor) writeEscaped(s string, toEscape byte) { +func (v *zmkVisitorAST) writeEscaped(s string, toEscape byte) { last := 0 for i := range len(s) { if b := s[i]; b == toEscape || b == '\\' { v.b.WriteString(s[last:i]) v.b.WriteBytes('\\', b) Index: internal/evaluator/evaluator.go ================================================================== --- internal/evaluator/evaluator.go +++ internal/evaluator/evaluator.go @@ -13,26 +13,25 @@ // 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/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" @@ -44,109 +43,172 @@ QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // EvaluateZettel evaluates the given zettel in the given context, with the // given ports, and the given environment. -func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.ZettelNode) { +func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.Zettel) { switch zn.Syntax { case meta.ValueSyntaxNone: // AST is empty, evaluate to a description list of metadata. - zn.BlocksAST = evaluateMetadata(zn.Meta) + zn.Blocks = evaluateMetadata(zn.Meta) case meta.ValueSyntaxSxn: - zn.BlocksAST = evaluateSxn(zn.BlocksAST) + zn.Blocks = evaluateSxn(zn.Blocks) default: - EvaluateBlock(ctx, port, rtConfig, &zn.BlocksAST) - } -} - -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 + 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, bns *ast.BlockSlice) { +func EvaluateBlock(ctx context.Context, port Port, rtConfig config.Config, block *sx.Pair) ast.BlockSlice { e := evaluator{ ctx: ctx, port: port, rtConfig: rtConfig, transcludeMax: rtConfig.GetMaxTransclusions(), transcludeCount: 0, costMap: map[id.Zid]transcludeCost{}, - embedMap: map[string]ast.InlineSlice{}, - marker: &ast.ZettelNode{}, + embedMap: map[string]*sx.Pair{}, + embedMapAST: map[string]ast.InlineSlice{}, + marker: &ast.Zettel{}, + } + + obj := zsx.Walk(&e, block, nil) + evalBlock, isPair := sx.GetPair(obj) + if !isPair { + panic(fmt.Sprintf("not a pair after evaluate: %T/%v", obj, obj)) + } + bns, err := sztrans.GetBlockSlice(evalBlock) + if err != nil { + panic(err) } - ast.Walk(&e, bns) - parser.Clean(bns, true) + + // 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.ZettelNode - embedMap map[string]ast.InlineSlice + marker *ast.Zettel + embedMap map[string]*sx.Pair + embedMapAST map[string]ast.InlineSlice } type transcludeCost struct { - zn *ast.ZettelNode + zn *ast.Zettel ec int } + +func (e *evaluator) VisitBefore(_ *sx.Pair, _ *sx.Pair) (sx.Object, bool) { + return sx.Nil(), false +} +func (e *evaluator) VisitAfter(node *sx.Pair, _ *sx.Pair) sx.Object { + if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol { + switch sym { + case zsx.SymLink: + return e.evalLink(node) + case zsx.SymEmbed: + case zsx.SymVerbatimEval: + return e.evalVerbatimEval(node) + } + } + return node +} + +func (e *evaluator) evalLink(node *sx.Pair) *sx.Pair { + attrs, ref, inlines := zsx.GetLink(node) + refState, refVal := zsx.GetReference(ref) + newInlines := inlines + if inlines == nil { + newInlines = sx.MakeList(zsx.MakeText(refVal)) + } + if !sz.SymRefStateZettel.IsEqualSymbol(refState) { + if newInlines != inlines { + return zsx.MakeLink(attrs, ref, newInlines) + } + return node + } + + zid := mustParseZid(ref, refVal) + _, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid) + if errors.Is(err, &box.ErrNotAllowed{}) { + return zsx.MakeFormat(zsx.SymFormatSpan, attrs, newInlines) + } + if err != nil { + return zsx.MakeLink(attrs, zsx.MakeReference(sz.SymRefStateBroken, refVal), newInlines) + } + + if newInlines != inlines { + return zsx.MakeLink(attrs, ref, newInlines) + } + return node +} + +func (e *evaluator) evalVerbatimEval(node *sx.Pair) *sx.Pair { + _, attrs, content := zsx.GetVerbatim(node) + if p := attrs.Assoc(sx.MakeString("")); p != nil { + if s, isString := sx.GetString(p.Cdr()); isString && s.GetValue() == meta.ValueSyntaxDraw { + return parser.ParseDrawBlock(attrs, []byte(content)) + } + } + return node +} + +func mustParseZid(ref *sx.Pair, refVal string) id.Zid { + zid, err := id.Parse(refVal) + if err != nil { + refState, _ := zsx.GetReference(ref) + panic(fmt.Sprintf("%v: %q (state %v) -> %v", err, refVal, refState, ref)) + } + return zid +} + +// AST-based code, deprecated. func (e *evaluator) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: - e.visitBlockSlice(n) + e.visitBlockSliceAST(n) case *ast.InlineSlice: - e.visitInlineSlice(n) + e.visitInlineSliceAST(n) default: return e } return nil } -func (e *evaluator) visitBlockSlice(bs *ast.BlockSlice) { +func (e *evaluator) visitBlockSliceAST(bs *ast.BlockSlice) { for i := 0; i < len(*bs); i++ { bn := (*bs)[i] ast.Walk(e, bn) switch n := bn.(type) { case *ast.VerbatimNode: - i += transcludeNode(bs, i, e.evalVerbatimNode(n)) + if n.Kind == ast.VerbatimZettel { + i += transcludeNodeAST(bs, i, e.evalVerbatimZettelNodeAST(n)) + } case *ast.TranscludeNode: - i += transcludeNode(bs, i, e.evalTransclusionNode(n)) + i += transcludeNodeAST(bs, i, e.evalTransclusionNodeAST(n)) } } } -func transcludeNode(bln *ast.BlockSlice, i int, bn ast.BlockNode) int { +func transcludeNodeAST(bln *ast.BlockSlice, i int, bn ast.BlockNode) int { if ln, ok := bn.(*ast.BlockSlice); ok { - *bln = replaceWithBlockNodes(*bln, i, *ln) + *bln = replaceWithBlockNodesAST(*bln, i, *ln) return len(*ln) - 1 } if bn == nil { (*bln) = (*bln)[:i+copy((*bln)[i:], (*bln)[i+1:])] return -1 @@ -153,11 +215,11 @@ } (*bln)[i] = bn return 0 } -func replaceWithBlockNodes(bns []ast.BlockNode, i int, replaceBns []ast.BlockNode) []ast.BlockNode { +func replaceWithBlockNodesAST(bns []ast.BlockNode, i int, replaceBns []ast.BlockNode) []ast.BlockNode { if len(replaceBns) == 1 { bns[i] = replaceBns[0] return bns } newIns := make([]ast.BlockNode, 0, len(bns)+len(replaceBns)-1) @@ -171,35 +233,23 @@ newIns = append(newIns, bns[i+1:]...) } return newIns } -func (e *evaluator) evalVerbatimNode(vn *ast.VerbatimNode) ast.BlockNode { - switch vn.Kind { - case ast.VerbatimZettel: - return e.evalVerbatimZettel(vn) - case ast.VerbatimEval: - if syntax, found := vn.Attrs.Get(""); found && syntax == meta.ValueSyntaxDraw { - return parser.ParseDrawBlock(vn) - } - } - return vn -} - -func (e *evaluator) evalVerbatimZettel(vn *ast.VerbatimNode) ast.BlockNode { +func (e *evaluator) evalVerbatimZettelNodeAST(vn *ast.VerbatimNode) ast.BlockNode { m := meta.New(id.Invalid) - m.Set(meta.KeySyntax, getSyntax(vn.Attrs, meta.ValueSyntaxText)) + m.Set(meta.KeySyntax, getSyntaxAST(vn.Attrs, meta.ValueSyntaxText)) zettel := zettel.Zettel{ Meta: m, Content: zettel.NewContent(vn.Content), } e.transcludeCount++ - zn := e.evaluateEmbeddedZettel(zettel) + zn := e.evaluateEmbeddedZettelAST(zettel) return &zn.BlocksAST } -func getSyntax(a zsx.Attributes, defSyntax meta.Value) meta.Value { +func getSyntaxAST(a zsx.Attributes, defSyntax meta.Value) meta.Value { if a != nil { if val, ok := a.Get(meta.KeySyntax); ok { return meta.Value(val) } if val, ok := a.Get(""); ok { @@ -207,39 +257,39 @@ } } return defSyntax } -func (e *evaluator) evalTransclusionNode(tn *ast.TranscludeNode) ast.BlockNode { +func (e *evaluator) evalTransclusionNodeAST(tn *ast.TranscludeNode) ast.BlockNode { ref := tn.Ref // To prevent e.embedCount from counting - if errText := e.checkMaxTransclusions(ref); errText != nil { - return makeBlockNode(errText) + if errText := e.checkMaxTransclusionsAST(ref); errText != nil { + return makeBlockNodeAST(errText) } switch ref.State { case ast.RefStateZettel: // Only zettel references will be evaluated. case ast.RefStateInvalid, ast.RefStateBroken: e.transcludeCount++ - return makeBlockNode(createInlineErrorText(ref, "Invalid", "or", "broken", "transclusion", "reference")) + return makeBlockNodeAST(createInlineErrorTextAST(ref, "Invalid or broken transclusion reference")) case ast.RefStateSelf: e.transcludeCount++ - return makeBlockNode(createInlineErrorText(ref, "Self", "transclusion", "reference")) + return makeBlockNodeAST(createInlineErrorTextAST(ref, "Self transclusion reference")) case ast.RefStateFound, ast.RefStateExternal: return tn case ast.RefStateHosted, ast.RefStateBased: - if n := createEmbeddedNodeLocal(ref); n != nil { + if n := createEmbeddedNodeLocalAST(ref); n != nil { n.Attrs = tn.Attrs - return makeBlockNode(n) + return makeBlockNodeAST(n) } return tn case ast.RefStateQuery: e.transcludeCount++ - return e.evalQueryTransclusion(tn.Ref.Value) + return e.evalQueryTransclusionAST(tn.Ref.Value) default: - return makeBlockNode(createInlineErrorText(ref, "Illegal", "block", "state", strconv.Itoa(int(ref.State)))) + return makeBlockNodeAST(createInlineErrorTextAST(ref, "Illegal block state "+strconv.Itoa(int(ref.State)))) } zid, err := id.Parse(ref.URL.Path) if err != nil { panic(err) @@ -247,25 +297,25 @@ cost, ok := e.costMap[zid] zn := cost.zn if zn == e.marker { e.transcludeCount++ - return makeBlockNode(createInlineErrorText(ref, "Recursive", "transclusion")) + return makeBlockNodeAST(createInlineErrorTextAST(ref, "Recursive transclusion")) } if !ok { zettel, err1 := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid) if err1 != nil { if errors.Is(err1, &box.ErrNotAllowed{}) { return nil } e.transcludeCount++ - return makeBlockNode(createInlineErrorText(ref, "Unable", "to", "get", "zettel")) + return makeBlockNodeAST(createInlineErrorTextAST(ref, "Unable to get zettel")) } - setMetadataFromAttributes(zettel.Meta, tn.Attrs) + setMetadataFromAttributesAST(zettel.Meta, tn.Attrs) ec := e.transcludeCount e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec} - zn = e.evaluateEmbeddedZettel(zettel) + zn = e.evaluateEmbeddedZettelAST(zettel) e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec} e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first. } e.transcludeCount++ if ec := cost.ec; ec > 0 { @@ -272,62 +322,60 @@ e.transcludeCount += cost.ec } return &zn.BlocksAST } -func (e *evaluator) evalQueryTransclusion(expr string) ast.BlockNode { +func (e *evaluator) evalQueryTransclusionAST(expr string) ast.BlockNode { q := query.Parse(expr) ml, err := e.port.QueryMeta(e.ctx, q) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return nil } - return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel")) + return makeBlockNodeAST(createInlineErrorTextAST(nil, "Unable to search zettel")) } - result, _ := QueryAction(e.ctx, q, ml) + result := QueryActionAST(e.ctx, q, ml) if result != nil { ast.Walk(e, result) } return result } -func (e *evaluator) checkMaxTransclusions(ref *ast.Reference) ast.InlineNode { +func (e *evaluator) checkMaxTransclusionsAST(ref *ast.Reference) ast.InlineNode { if maxTrans := e.transcludeMax; e.transcludeCount > maxTrans { e.transcludeCount = maxTrans + 1 - return createInlineErrorText(ref, - "Too", "many", "transclusions", "(must", "be", "at", "most", strconv.Itoa(maxTrans)+",", - "see", "runtime", "configuration", "key", "max-transclusions)") + return createInlineErrorTextAST(ref, + "Too many transclusions (must be at most "+strconv.Itoa(maxTrans)+ + ", see runtime configuration key max-transclusions)") } return nil } -func makeBlockNode(in ast.InlineNode) ast.BlockNode { return ast.CreateParaNode(in) } +func makeBlockNodeAST(in ast.InlineNode) ast.BlockNode { return ast.CreateParaNode(in) } -func setMetadataFromAttributes(m *meta.Meta, a zsx.Attributes) { +func setMetadataFromAttributesAST(m *meta.Meta, a zsx.Attributes) { for aKey, aVal := range a { if meta.KeyIsValid(aKey) { m.Set(aKey, meta.Value(aVal)) } } } -func (e *evaluator) visitInlineSlice(is *ast.InlineSlice) { +func (e *evaluator) visitInlineSliceAST(is *ast.InlineSlice) { for i := 0; i < len(*is); i++ { in := (*is)[i] ast.Walk(e, in) switch n := in.(type) { - case *ast.LinkNode: - (*is)[i] = e.evalLinkNode(n) case *ast.EmbedRefNode: - i += embedNode(is, i, e.evalEmbedRefNode(n)) + i += embedNodeAST(is, i, e.evalEmbedRefNodeAST(n)) } } } -func embedNode(is *ast.InlineSlice, i int, in ast.InlineNode) int { +func embedNodeAST(is *ast.InlineSlice, i int, in ast.InlineNode) int { if ln, ok := in.(*ast.InlineSlice); ok { - *is = replaceWithInlineNodes(*is, i, *ln) + *is = replaceWithInlineNodesAST(*is, i, *ln) return len(*ln) - 1 } if in == nil { (*is) = (*is)[:i+copy((*is)[i:], (*is)[i+1:])] return -1 @@ -334,11 +382,11 @@ } (*is)[i] = in return 0 } -func replaceWithInlineNodes(ins ast.InlineSlice, i int, replaceIns ast.InlineSlice) ast.InlineSlice { +func replaceWithInlineNodesAST(ins ast.InlineSlice, i int, replaceIns ast.InlineSlice) ast.InlineSlice { if len(replaceIns) == 1 { ins[i] = replaceIns[0] return ins } newIns := make(ast.InlineSlice, 0, len(ins)+len(replaceIns)-1) @@ -352,112 +400,79 @@ newIns = append(newIns, ins[i+1:]...) } return newIns } -func (e *evaluator) evalLinkNode(ln *ast.LinkNode) ast.InlineNode { - if len(ln.Inlines) == 0 { - ln.Inlines = ast.InlineSlice{&ast.TextNode{Text: ln.Ref.Value}} - } - ref := ln.Ref - if ref == nil || ref.State != ast.RefStateZettel { - return ln - } - - zid := mustParseZid(ref) - _, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid) - if errors.Is(err, &box.ErrNotAllowed{}) { - return &ast.FormatNode{ - Kind: ast.FormatSpan, - Attrs: ln.Attrs, - Inlines: getLinkInline(ln), - } - } else if err != nil { - ln.Ref.State = ast.RefStateBroken - return ln - } - - ln.Ref.State = ast.RefStateZettel - return ln -} - -func getLinkInline(ln *ast.LinkNode) ast.InlineSlice { - if ln.Inlines != nil { - return ln.Inlines - } - return ast.InlineSlice{&ast.TextNode{Text: ln.Ref.Value}} -} - -func (e *evaluator) evalEmbedRefNode(en *ast.EmbedRefNode) ast.InlineNode { +func (e *evaluator) evalEmbedRefNodeAST(en *ast.EmbedRefNode) ast.InlineNode { ref := en.Ref // To prevent e.embedCount from counting - if errText := e.checkMaxTransclusions(ref); errText != nil { + if errText := e.checkMaxTransclusionsAST(ref); errText != nil { return errText } switch ref.State { case ast.RefStateZettel: // Only zettel references will be evaluated. case ast.RefStateInvalid, ast.RefStateBroken: e.transcludeCount++ - return createInlineErrorImage(en) + return createInlineErrorImageAST(en) case ast.RefStateSelf: e.transcludeCount++ - return createInlineErrorText(ref, "Self", "embed", "reference") + return createInlineErrorTextAST(ref, "Self embed reference") case ast.RefStateFound, ast.RefStateExternal: return en case ast.RefStateHosted, ast.RefStateBased: - if n := createEmbeddedNodeLocal(ref); n != nil { + if n := createEmbeddedNodeLocalAST(ref); n != nil { n.Attrs = en.Attrs n.Inlines = en.Inlines return n } return en default: - return createInlineErrorText(ref, "Illegal", "inline", "state", strconv.Itoa(int(ref.State))) + return createInlineErrorTextAST(ref, "Illegal inline state"+strconv.Itoa(int(ref.State))) } - zid := mustParseZid(ref) + zid := mustParseZidAST(ref) zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return nil } e.transcludeCount++ - return createInlineErrorImage(en) + return createInlineErrorImageAST(en) } if syntax := string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)); parser.IsImageFormat(syntax) { - e.updateImageRefNode(en, zettel.Meta, syntax) + e.updateImageRefNodeAST(en, zettel.Meta, syntax) return en } else if !parser.IsASTParser(syntax) { // Not embeddable. e.transcludeCount++ - return createInlineErrorText(ref, "Not", "embeddable (syntax="+syntax+")") + return createInlineErrorTextAST(ref, "Not embeddable (syntax="+syntax+")") } cost, ok := e.costMap[zid] zn := cost.zn if zn == e.marker { e.transcludeCount++ - return createInlineErrorText(ref, "Recursive", "transclusion") + return createInlineErrorTextAST(ref, "Recursive transclusion") } if !ok { ec := e.transcludeCount e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec} - zn = e.evaluateEmbeddedZettel(zettel) + zn = e.evaluateEmbeddedZettelAST(zettel) e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec} e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first. } e.transcludeCount++ - result, ok := e.embedMap[ref.Value] + result, ok := e.embedMapAST[ref.Value] if !ok { // Search for text to be embedded. - result = findInlineSlice(&zn.BlocksAST, ref.URL.Fragment) - e.embedMap[ref.Value] = result + result = findInlineSliceAST(&zn.BlocksAST, ref.URL.Fragment) + e.embedMapAST[ref.Value] = result } if len(result) == 0 { return &ast.LiteralNode{ Kind: ast.LiteralComment, Attrs: map[string]string{"-": ""}, @@ -469,42 +484,55 @@ e.transcludeCount += cost.ec } return &result } -func mustParseZid(ref *ast.Reference) id.Zid { +func mustParseZidAST(ref *ast.Reference) id.Zid { zid, err := id.Parse(ref.URL.Path) if err != nil { panic(fmt.Sprintf("%v: %q (state %v) -> %v", err, ref.URL.Path, ref.State, ref)) } return zid } -func (e *evaluator) updateImageRefNode(en *ast.EmbedRefNode, m *meta.Meta, syntax string) { +func (e *evaluator) updateImageRefNodeAST(en *ast.EmbedRefNode, m *meta.Meta, syntax string) { en.Syntax = syntax if len(en.Inlines) == 0 { - is := parser.ParseDescriptionAST(m) + is := parseDescriptionAST(m) if len(is) > 0 { ast.Walk(e, &is) if len(is) > 0 { en.Inlines = is } } } } +func parseDescriptionAST(m *meta.Meta) ast.InlineSlice { + // Non-AST function in package parser. + if m == nil { + return nil + } + 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 createInlineErrorImage(en *ast.EmbedRefNode) *ast.EmbedRefNode { +func createInlineErrorImageAST(en *ast.EmbedRefNode) *ast.EmbedRefNode { errorZid := id.ZidEmoji en.Ref = ast.ParseReference(errorZid.String()) if len(en.Inlines) == 0 { en.Inlines = ast.InlineSlice{&ast.TextNode{Text: "Error placeholder"}} } return en } -func createInlineErrorText(ref *ast.Reference, msgWords ...string) ast.InlineNode { - text := strings.Join(msgWords, " ") +func createInlineErrorTextAST(ref *ast.Reference, message string) ast.InlineNode { + text := message if ref != nil { text += ": " + ref.String() + "." } ln := &ast.LiteralNode{ Kind: ast.LiteralInput, @@ -516,11 +544,11 @@ } fn.Attrs = fn.Attrs.AddClass("error") return fn } -func createEmbeddedNodeLocal(ref *ast.Reference) *ast.EmbedRefNode { +func createEmbeddedNodeLocalAST(ref *ast.Reference) *ast.EmbedRefNode { ext := path.Ext(ref.Value) if ext != "" && ext[0] == '.' { ext = ext[1:] } pinfo := parser.Get(ext) @@ -531,26 +559,26 @@ Ref: ref, Syntax: ext, } } -func (e *evaluator) evaluateEmbeddedZettel(zettel zettel.Zettel) *ast.ZettelNode { +func (e *evaluator) evaluateEmbeddedZettelAST(zettel zettel.Zettel) *ast.Zettel { zn := parser.ParseZettel(e.ctx, zettel, string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)), e.rtConfig) ast.Walk(e, &zn.BlocksAST) return zn } -func findInlineSlice(bs *ast.BlockSlice, fragment string) ast.InlineSlice { +func findInlineSliceAST(bs *ast.BlockSlice, fragment string) ast.InlineSlice { if fragment == "" { - return firstInlinesToEmbed(*bs) + return firstInlinesToEmbedAST(*bs) } - fs := fragmentSearcher{fragment: fragment} + fs := fragmentSearcherAST{fragment: fragment} ast.Walk(&fs, bs) return fs.result } -func firstInlinesToEmbed(bs ast.BlockSlice) ast.InlineSlice { +func firstInlinesToEmbedAST(bs ast.BlockSlice) ast.InlineSlice { if ins := bs.FirstParagraphInlines(); ins != nil { return ins } if len(bs) == 0 { return nil @@ -563,44 +591,44 @@ }} } return nil } -type fragmentSearcher struct { +type fragmentSearcherAST struct { fragment string result ast.InlineSlice } -func (fs *fragmentSearcher) Visit(node ast.Node) ast.Visitor { +func (fs *fragmentSearcherAST) Visit(node ast.Node) ast.Visitor { if len(fs.result) > 0 { return nil } switch n := node.(type) { case *ast.BlockSlice: - fs.visitBlockSlice(n) + fs.visitBlockSliceAST(n) case *ast.InlineSlice: - fs.visitInlineSlice(n) + fs.visitInlineSliceAST(n) default: return fs } return nil } -func (fs *fragmentSearcher) visitBlockSlice(bs *ast.BlockSlice) { +func (fs *fragmentSearcherAST) visitBlockSliceAST(bs *ast.BlockSlice) { for i, bn := range *bs { if hn, ok := bn.(*ast.HeadingNode); ok && hn.Fragment == fs.fragment { fs.result = (*bs)[i+1:].FirstParagraphInlines() return } ast.Walk(fs, bn) } } -func (fs *fragmentSearcher) visitInlineSlice(is *ast.InlineSlice) { +func (fs *fragmentSearcherAST) visitInlineSliceAST(is *ast.InlineSlice) { for i, in := range *is { if mn, ok := in.(*ast.MarkNode); ok && mn.Fragment == fs.fragment { - ris := skipBreakeNodes((*is)[i+1:]) + ris := skipBreakeNodesAST((*is)[i+1:]) if len(mn.Inlines) > 0 { fs.result = slices.Clone(mn.Inlines) fs.result = append(fs.result, &ast.TextNode{Text: " "}) fs.result = append(fs.result, ris...) } else { @@ -610,15 +638,15 @@ } ast.Walk(fs, in) } } -func skipBreakeNodes(ins ast.InlineSlice) ast.InlineSlice { +func skipBreakeNodesAST(ins ast.InlineSlice) ast.InlineSlice { for i, in := range ins { switch in.(type) { case *ast.BreakNode: default: return ins[i:] } } return nil } Index: internal/evaluator/list.go ================================================================== --- internal/evaluator/list.go +++ internal/evaluator/list.go @@ -19,25 +19,34 @@ "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" ) -// 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) { +// 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: ast.NestedListUnordered, + kind: zsx.SymListUnordered, minVal: -1, maxVal: -1, } actions := q.Actions() if len(actions) == 0 { @@ -45,11 +54,11 @@ } acts := make([]string, 0, len(actions)) for _, act := range actions { if strings.HasPrefix(act, api.NumberedAction[0:1]) { - ap.kind = ast.NestedListOrdered + ap.kind = zsx.SymListOrdered continue } if strings.HasPrefix(act, api.MinAction) { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { ap.minVal = num @@ -82,10 +91,11 @@ } 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 @@ -93,73 +103,69 @@ type actionPara struct { ctx context.Context q *query.Query ml []*meta.Meta - kind ast.NestedListKind + kind *sx.Symbol minVal int maxVal int } -func (ap *actionPara) createBlockNodeWord(key string) (ast.BlockNode, int) { +func (ap *actionPara) createBlockNodeWord(key string) (*sx.Pair, int) { var buf bytes.Buffer ccs, bufLen := ap.prepareCatAction(key, &buf) if len(ccs) == 0 { return nil, 0 } - items := make([]ast.ItemSlice, 0, len(ccs)) + ccs.SortByName() + var items sx.ListBuilder + count := 0 for _, cat := range ccs { buf.WriteString(string(cat.Name)) - items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{ - Attrs: nil, - Ref: ast.ParseReference(buf.String()), - Inlines: ast.InlineSlice{&ast.TextNode{Text: string(cat.Name)}}, - })}) + items.Add(zsx.MakeBlock(zsx.MakePara( + zsx.MakeLink(nil, + sz.ScanReference(buf.String()), + sx.MakeList(zsx.MakeText(string(cat.Name)))), + ))) + count++ buf.Truncate(bufLen) } - return &ast.NestedListNode{ - Kind: ap.kind, - Items: items, - Attrs: nil, - }, len(items) + return zsx.MakeList(ap.kind, nil, items.List()), count } -func (ap *actionPara) createBlockNodeTagSet(key string) (ast.BlockNode, int) { +func (ap *actionPara) createBlockNodeTagSet(key string) (*sx.Pair, int) { var buf bytes.Buffer ccs, bufLen := ap.prepareCatAction(key, &buf) if len(ccs) == 0 { return nil, 0 } ccs.SortByCount() ccs = ap.limitTags(ccs) - countMap := ap.calcFontSizes(ccs) + countClassAttrs := ap.calcFontSizes(ccs) - para := make(ast.InlineSlice, 0, len(ccs)) ccs.SortByName() + var tags sx.ListBuilder + count := 0 for i, cat := range ccs { if i > 0 { - para = append(para, &ast.TextNode{Text: " "}) + tags.Add(zsx.MakeText(" ")) } buf.WriteString(string(cat.Name)) - para = append(para, - &ast.LinkNode{ - Attrs: countMap[cat.Count], - Ref: ast.ParseReference(buf.String()), - 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)}}, - }, - ) + 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 &ast.ParaNode{Inlines: para}, len(ccs) + return zsx.MakeParaList(tags.List()), count } func (ap *actionPara) limitTags(ccs meta.CountedCategories) meta.CountedCategories { if minVal, maxVal := ap.minVal, ap.maxVal; minVal > 0 || maxVal > 0 { if minVal < 0 { @@ -179,11 +185,11 @@ } } return ccs } -func (ap *actionPara) createBlockNodeMetaKeys() (ast.BlockNode, int) { +func (ap *actionPara) createBlockNodeMetaKeys() (*sx.Pair, int) { arr := make(meta.Arrangement, 128) for _, m := range ap.ml { for k := range m.Map() { arr[k] = append(arr[k], m) } @@ -194,11 +200,12 @@ ccs := arr.Counted() ccs.SortByName() var buf bytes.Buffer bufLen := ap.prepareSimpleQuery(&buf) - items := make([]ast.ItemSlice, 0, len(ccs)) + var items sx.ListBuilder + count := 0 for _, cat := range ccs { buf.WriteString(string(cat.Name)) buf.WriteString(api.ExistOperator) q1 := buf.String() buf.Truncate(bufLen) @@ -205,55 +212,46 @@ 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) { + 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 } - items := make([]ast.ItemSlice, 0, len(ap.ml)) + var items sx.ListBuilder + count := 0 for _, m := range ap.ml { if key != "" { if _, found := m.Get(key); !found { continue } } - items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{ - Attrs: nil, - Ref: ast.ParseReference(m.Zid.String()), - Inlines: ast.ParseSpacedText(m.GetTitle()), - })}) - } - return &ast.NestedListNode{ - Kind: ap.kind, - Items: items, - Attrs: nil, - }, len(items) + 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 @@ -283,15 +281,14 @@ } 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 +func (*actionPara) calcFontSizes(ccs meta.CountedCategories) map[int]*sx.Pair { + var fsAttrs [fontSizes]*sx.Pair for i := range fontSizes { - fsAttrs[i] = a.AddClass("zs-font-size-" + strconv.Itoa(i)) + fsAttrs[i] = sx.MakeList(sx.Cons(sx.MakeString("class"), sx.MakeString("zs-font-size-"+strconv.Itoa(i)))) } countMap := make(map[int]int, len(ccs)) for _, cat := range ccs { countMap[cat.Count]++ @@ -301,11 +298,11 @@ for count := range countMap { countList = append(countList, count) } slices.Sort(countList) - result := make(map[int]zsx.Attributes, len(countList)) + result := make(map[int]*sx.Pair, len(countList)) if len(countList) <= fontSizes { // If we have less different counts, center them inside the fsAttrs vector. curSize := (fontSizes - len(countList)) / 2 for _, count := range countList { result[count] = fsAttrs[curSize] Index: internal/evaluator/metadata.go ================================================================== --- internal/evaluator/metadata.go +++ internal/evaluator/metadata.go @@ -12,56 +12,52 @@ //----------------------------------------------------------------------------- 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)} + "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 - 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 + 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 Index: internal/evaluator/sxn.go ================================================================== --- /dev/null +++ internal/evaluator/sxn.go @@ -0,0 +1,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 +} Index: internal/parser/blob.go ================================================================== --- internal/parser/blob.go +++ internal/parser/blob.go @@ -59,7 +59,7 @@ func parseBlob(inp *input.Input, m *meta.Meta, syntax string) *sx.Pair { if p := Get(syntax); p != nil { syntax = p.Name } - return zsx.MakeBlock(zsx.MakeBLOB(nil, ParseDescription(m), syntax, string(inp.Src))) + return zsx.MakeBlock(zsx.MakeBLOB(nil, syntax, inp.Src, ParseDescription(m))) } Index: internal/parser/cleaner.go ================================================================== --- internal/parser/cleaner.go +++ internal/parser/cleaner.go @@ -17,19 +17,124 @@ import ( "strconv" "strings" + "t73f.de/r/sx" zerostrings "t73f.de/r/zero/strings" + "t73f.de/r/zsx" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/encoder" ) -// Clean cleans the given block list. -func Clean(bs *ast.BlockSlice, allowHTML bool) { - cv := cleanVisitor{ +// Clean the given SZ syntax tree. +func Clean(node *sx.Pair, 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) @@ -37,19 +142,18 @@ cv.doMark = true ast.Walk(&cv, bs) } } -type cleanVisitor struct { - textEnc encoder.TextEncoder +type cleanASTVisitor struct { ids map[string]ast.Node allowHTML bool hasMark bool doMark bool } -func (cv *cleanVisitor) Visit(node ast.Node) ast.Visitor { +func (cv *cleanASTVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: if !cv.allowHTML { cv.visitBlockSlice(n) return nil @@ -67,11 +171,11 @@ return nil } return cv } -func (cv *cleanVisitor) visitBlockSlice(bs *ast.BlockSlice) { +func (cv *cleanASTVisitor) visitBlockSlice(bs *ast.BlockSlice) { if bs == nil { return } if len(*bs) == 0 { *bs = nil @@ -98,11 +202,11 @@ (*bs)[pos] = nil // Allow excess nodes to be garbage collected. } *bs = (*bs)[:toPos:toPos] } -func (cv *cleanVisitor) visitInlineSlice(is *ast.InlineSlice) { +func (cv *cleanASTVisitor) visitInlineSlice(is *ast.InlineSlice) { if is == nil { return } if len(*is) == 0 { *is = nil @@ -111,28 +215,28 @@ for _, bn := range *is { ast.Walk(cv, bn) } } -func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) { +func (cv *cleanASTVisitor) visitHeading(hn *ast.HeadingNode) { if cv.doMark || hn == nil || len(hn.Inlines) == 0 { return } if hn.Slug == "" { var sb strings.Builder - _, err := cv.textEnc.WriteInlines(&sb, &hn.Inlines) - if err != nil { + 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 *cleanVisitor) visitMark(mn *ast.MarkNode) { +func (cv *cleanASTVisitor) visitMark(mn *ast.MarkNode) { if !cv.doMark { cv.hasMark = true return } if mn.Mark == "" { @@ -144,11 +248,11 @@ mn.Slug = zerostrings.Slugify(mn.Mark) } mn.Fragment = cv.addIdentifier(mn.Slug, mn) } -func (cv *cleanVisitor) addIdentifier(id string, node ast.Node) string { +func (cv *cleanASTVisitor) addIdentifier(id string, node ast.Node) string { if cv.ids == nil { cv.ids = map[string]ast.Node{id: node} return id } if n, ok := cv.ids[id]; ok && n != node { ADDED internal/parser/cleaner_test.go Index: internal/parser/cleaner_test.go ================================================================== --- /dev/null +++ internal/parser/cleaner_test.go @@ -0,0 +1,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 () \"

Heading

\"))", + exp: "(BLOCK)"}, + {name: "remove-html-0-1", + src: "(BLOCK (VERBATIM-HTML () \"

Heading

\") (PARA (TEXT \"ABC\")))", + exp: "(BLOCK (PARA (TEXT \"ABC\")))"}, + {name: "remove-html-1-0", + src: "(BLOCK (PARA (TEXT \"ABC\")) (VERBATIM-HTML () \"

Heading

\"))", + exp: "(BLOCK (PARA (TEXT \"ABC\")))"}, + {name: "remove-html-1-2", + src: "(BLOCK (PARA (TEXT \"ABC\")) (VERBATIM-HTML () \"

Heading

\") (VERBATIM-HTML () \"

Head

\"))", + exp: "(BLOCK (PARA (TEXT \"ABC\")))"}, + + {name: "allow HTML", allowHTML: true, + src: "(BLOCK (VERBATIM-HTML () \"

Heading

\"))", + exp: "(BLOCK (VERBATIM-HTML () \"

Heading

\"))"}, + {name: "allow-html-1-2", allowHTML: true, + src: "(BLOCK (PARA (TEXT \"ABC\")) (VERBATIM-HTML () \"

Heading

\") (VERBATIM-HTML () \"

Head

\"))", + exp: "(BLOCK (PARA (TEXT \"ABC\")) (VERBATIM-HTML () \"

Heading

\") (VERBATIM-HTML () \"

Head

\"))"}, + } + + 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) + } + }) + } +} Index: internal/parser/draw.go ================================================================== --- internal/parser/draw.go +++ internal/parser/draw.go @@ -21,12 +21,10 @@ "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, @@ -61,44 +59,46 @@ } svg := aasvg.CanvasToSVG(canvas, string(font), int(scaleX), int(scaleY)) if len(svg) == 0 { return zsx.MakeBlock(zsx.MakeParaList(noSVGErrMsg())) } - return zsx.MakeBlock(zsx.MakeBLOB(nil, ParseDescription(m), meta.ValueSyntaxSVG, string(svg))) + return zsx.MakeBlock(zsx.MakeBLOB(nil, meta.ValueSyntaxSVG, svg, ParseDescription(m))) } // ParseDrawBlock parses the content of an eval verbatim node into an SVG image BLOB. -func ParseDrawBlock(vn *ast.VerbatimNode) ast.BlockNode { +func ParseDrawBlock(attrs *sx.Pair, content []byte) *sx.Pair { + a := zsx.GetAttributes(attrs) font := defaultFont - if val, found := vn.Attrs.Get("font"); found { + if val, found := a.Get("font"); found { font = val } - scaleX := getScale(vn.Attrs, "x-scale", defaultScaleX) - scaleY := getScale(vn.Attrs, "y-scale", defaultScaleY) + scaleX := getScaleAST(a, "x-scale", defaultScaleX) + scaleY := getScaleAST(a, "y-scale", defaultScaleY) - canvas, err := aasvg.NewCanvas(vn.Content) + canvas, err := aasvg.NewCanvas(content) if err != nil { - return ast.CreateParaNode(ast.InlineSlice{&ast.TextNode{Text: "Error: " + err.Error()}}...) + return zsx.MakePara(zsx.MakeText("Error: " + err.Error())) } if scaleX < 1 || 1000000 < scaleX { scaleX = defaultScaleX } if scaleY < 1 || 1000000 < scaleY { scaleY = defaultScaleY } svg := aasvg.CanvasToSVG(canvas, font, scaleX, scaleY) if len(svg) == 0 { - return ast.CreateParaNode(ast.InlineSlice{&ast.TextNode{Text: "NO IMAGE"}}...) + return zsx.MakePara(zsx.MakeText("NO IMAGE")) } - return &ast.BLOBNode{ - Description: nil, // TODO: look for attribute "summary" / "title" - Syntax: meta.ValueSyntaxSVG, - Blob: svg, - } + return zsx.MakeBLOB( + nil, + meta.ValueSyntaxSVG, + svg, + nil, // TODO: look for attribute "summary" / "title" for a description. + ) } -func getScale(a zsx.Attributes, key string, defVal int) int { +func getScaleAST(a zsx.Attributes, key string, defVal int) int { if val, found := a.Get(key); found { if n, err := strconv.Atoi(val); err == nil && 0 < n && n < 100000 { return n } } Index: internal/parser/draw_test.go ================================================================== --- internal/parser/draw_test.go +++ internal/parser/draw_test.go @@ -17,16 +17,15 @@ "testing" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" - "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/parser" ) func FuzzParseDraw(f *testing.F) { f.Fuzz(func(t *testing.T, src []byte) { t.Parallel() inp := input.NewInput(src) - parser.Parse(inp, nil, meta.ValueSyntaxDraw, config.NoHTML) + parser.Parse(inp, nil, meta.ValueSyntaxDraw) }) } Index: internal/parser/none.go ================================================================== --- internal/parser/none.go +++ internal/parser/none.go @@ -14,10 +14,11 @@ 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. @@ -26,8 +27,10 @@ Name: meta.ValueSyntaxNone, AltNames: []string{}, IsASTParser: false, IsTextFormat: false, IsImageFormat: false, - Parse: func(*input.Input, *meta.Meta, string) *sx.Pair { return nil }, + Parse: func(inp *input.Input, _ *meta.Meta, _ string) *sx.Pair { + return sz.ParseNoneBlocks(inp) + }, }) } Index: internal/parser/parser.go ================================================================== --- internal/parser/parser.go +++ internal/parser/parser.go @@ -19,10 +19,11 @@ "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" @@ -95,55 +96,39 @@ return false } return pi.IsImageFormat } -// Parse parses some input and returns a slice of block nodes. -func Parse(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice { +// Parse parses some input and returns both a Sx.Object and a slice of block nodes. +func Parse(inp *input.Input, m *meta.Meta, syntax string) (*sx.Pair, ast.BlockSlice) { if obj := Get(syntax).Parse(inp, m, syntax); obj != nil { bs, err := sztrans.GetBlockSlice(obj) if err == nil { - Clean(&bs, hi.AllowHTML(syntax)) - return bs + return obj, bs } log.Printf("sztrans error: %v, for %v\n", err, obj) } - return nil -} - -// 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()}} + 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(ast.NormalizedSpacedText(string(summary))), sx.Nil()) + return sx.Cons(zsx.MakeText(sz.NormalizedSpacedText(string(summary))), sx.Nil()) } if title, found := m.Get(meta.KeyTitle); found { - return sx.Cons(zsx.MakeText(ast.NormalizedSpacedText(string(title))), sx.Nil()) + return sx.Cons(zsx.MakeText(sz.NormalizedSpacedText(string(title))), sx.Nil()) } return sx.Cons(zsx.MakeText("Zettel without title/summary: "+m.Zid.String()), sx.Nil()) } // ParseZettel parses the zettel based on the syntax. -func ParseZettel(ctx context.Context, zettel zettel.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode { +func ParseZettel(ctx context.Context, zettel zettel.Zettel, syntax string, rtConfig config.Config) *ast.Zettel { m := zettel.Meta inhMeta := m if rtConfig != nil { inhMeta = rtConfig.AddDefaultValues(ctx, inhMeta) } @@ -153,19 +138,16 @@ parseMeta := inhMeta if syntax == meta.ValueSyntaxNone { parseMeta = m } - hi := config.NoHTML - if rtConfig != nil { - hi = rtConfig.GetHTMLInsecurity() - } - bs := Parse(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax, hi) - return &ast.ZettelNode{ + rootNode, bs := Parse(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax) + return &ast.Zettel{ Meta: m, Content: zettel.Content, Zid: m.Zid, InhMeta: inhMeta, + Blocks: rootNode, BlocksAST: bs, Syntax: syntax, } } Index: internal/parser/plain.go ================================================================== --- internal/parser/plain.go +++ internal/parser/plain.go @@ -19,10 +19,11 @@ "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() { @@ -38,11 +39,11 @@ Name: meta.ValueSyntaxHTML, AltNames: []string{}, IsASTParser: false, IsTextFormat: true, IsImageFormat: false, - Parse: parsePlainHTML, + Parse: parsePlain, }) register(&Info{ Name: meta.ValueSyntaxCSS, AltNames: []string{}, IsASTParser: false, @@ -67,37 +68,27 @@ 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()), - )) + return sz.ParsePlainBlocks(inp, syntax) } func parsePlainSVG(inp *input.Input, _ *meta.Meta, syntax string) *sx.Pair { is := parseSVGInlines(inp, syntax) if is == nil { - return nil + return zsx.MakeBlock() } return zsx.MakeBlock(zsx.MakeParaList(is)) } func parseSVGInlines(inp *input.Input, syntax string) *sx.Pair { svgSrc := scanSVG(inp) if svgSrc == "" { return nil } - return sx.Cons(zsx.MakeEmbedBLOB(nil, syntax, svgSrc, nil), sx.Nil()) + return sx.Cons(zsx.MakeEmbedBLOBuncode(nil, syntax, svgSrc, nil), sx.Nil()) } func scanSVG(inp *input.Input) string { inp.SkipSpace() pos := inp.Pos Index: internal/parser/plain_test.go ================================================================== --- internal/parser/plain_test.go +++ internal/parser/plain_test.go @@ -17,12 +17,11 @@ "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/ast/sztrans" "zettelstore.de/z/internal/parser" ) func TestParseSVG(t *testing.T) { testCases := []struct { @@ -38,14 +37,17 @@ {"error#", "', 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{ - {"``