Index: api/const.go ================================================================== --- api/const.go +++ api/const.go @@ -53,13 +53,20 @@ ZidEmoji = ZettelID("00000000040001") // Other sxn code zettel are in the range 50000..59999 ZidSxnPrelude = ZettelID("00000000059900") + // Predefined Zettelmarkup zettel are in the range 60000..69999 + ZidRoleZettelZettel = ZettelID("00000000060010") + ZidRoleConfigurationZettel = ZettelID("00000000060020") + ZidRoleRoleZettel = ZettelID("00000000060030") + ZidRoleTagZettel = ZettelID("00000000060040") + // Range 90000...99999 is reserved for zettel templates ZidTOCNewTemplate = ZettelID("00000000090000") ZidTemplateNewZettel = ZettelID("00000000090001") + ZidTemplateNewRole = ZettelID("00000000090004") ZidTemplateNewTag = ZettelID("00000000090003") ZidTemplateNewUser = ZettelID("00000000090002") ZidDefaultHome = ZettelID("00010000000000") ) @@ -126,10 +133,11 @@ ValueFalse = "false" ValueTrue = "true" ValueLangEN = "en" ValueRoleConfiguration = "configuration" ValueRoleTag = "tag" + ValueRoleRole = "role" ValueRoleZettel = "zettel" ValueSyntaxCSS = "css" ValueSyntaxDraw = "draw" ValueSyntaxGif = "gif" ValueSyntaxHTML = "html" @@ -167,10 +175,11 @@ QueryKeyEncoding = "enc" QueryKeyParseOnly = "parseonly" QueryKeyPart = "part" QueryKeyPhrase = "phrase" QueryKeyQuery = "q" + QueryKeyRole = "role" QueryKeySeed = "_seed" QueryKeyTag = "tag" ) // Supported encoding values. Index: client/client.go ================================================================== --- client/client.go +++ client/client.go @@ -433,14 +433,25 @@ // TagZettel returns the tag zettel of a given tag. // // This method only works if c.AllowRedirect(true) was called. func (c *Client) TagZettel(ctx context.Context, tag string) (api.ZettelID, error) { + return c.fetchTagOrRoleZettel(ctx, api.QueryKeyTag, tag) +} + +// RoleZettel returns the tag zettel of a given tag. +// +// This method only works if c.AllowRedirect(true) was called. +func (c *Client) RoleZettel(ctx context.Context, role string) (api.ZettelID, error) { + return c.fetchTagOrRoleZettel(ctx, api.QueryKeyRole, role) +} + +func (c *Client) fetchTagOrRoleZettel(ctx context.Context, key, val string) (api.ZettelID, error) { if c.client.CheckRedirect == nil { panic("client does not allow to track redirect") } - ub := c.newURLBuilder('z').AppendKVQuery(api.QueryKeyTag, tag) + ub := c.newURLBuilder('z').AppendKVQuery(key, val) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return api.InvalidZID, err } defer resp.Body.Close() Index: go.mod ================================================================== --- go.mod +++ go.mod @@ -1,5 +1,5 @@ module zettelstore.de/client.fossil go 1.21 -require zettelstore.de/sx.fossil v0.0.0-20231026154942-e6a183740a4f +require zettelstore.de/sx.fossil v0.0.0-20231130150648-05ef116ba207 Index: go.sum ================================================================== --- go.sum +++ go.sum @@ -1,2 +1,2 @@ -zettelstore.de/sx.fossil v0.0.0-20231026154942-e6a183740a4f h1:NFblKWyhNnXDDcF7C6zx7cDyIJ/7GGAusIwR6uZrfkM= -zettelstore.de/sx.fossil v0.0.0-20231026154942-e6a183740a4f/go.mod h1:Uw3OLM1ufOM4Xe0G51mvkTDUv2okd+HyDBMx+0ZG7ME= +zettelstore.de/sx.fossil v0.0.0-20231130150648-05ef116ba207 h1:8ch54z0w53bps6a00NDofEqo3AJ1l7ITXyC3XyLmlY4= +zettelstore.de/sx.fossil v0.0.0-20231130150648-05ef116ba207/go.mod h1:Uw3OLM1ufOM4Xe0G51mvkTDUv2okd+HyDBMx+0ZG7ME= Index: shtml/shtml.go ================================================================== --- shtml/shtml.go +++ shtml/shtml.go @@ -20,74 +20,79 @@ "zettelstore.de/client.fossil/api" "zettelstore.de/client.fossil/attrs" "zettelstore.de/client.fossil/sz" "zettelstore.de/client.fossil/text" "zettelstore.de/sx.fossil" - "zettelstore.de/sx.fossil/sxbuiltins" - "zettelstore.de/sx.fossil/sxeval" "zettelstore.de/sx.fossil/sxhtml" ) -// Transformer will transform a s-expression that encodes the zettel AST into an s-expression +// Evaluator will transform a s-expression that encodes the zettel AST into an s-expression // that represents HTML. -type Transformer struct { +type Evaluator struct { sf sx.SymbolFactory - rebinder RebindProc headingOffset int64 unique string - endnotes []endnoteInfo noLinks bool // true iff output must not include links - symAttr *sx.Symbol - symClass *sx.Symbol - symMeta *sx.Symbol - symA *sx.Symbol - symSpan *sx.Symbol -} - -type endnoteInfo struct { - noteAST *sx.Pair // Endnote as AST - noteHx *sx.Pair // Endnote as SxHTML - attrs *sx.Pair // attrs a-list -} - -// NewTransformer creates a new transformer object. -func NewTransformer(headingOffset int, sf sx.SymbolFactory) *Transformer { + + symAttr *sx.Symbol + symList *sx.Symbol + symNoEscape *sx.Symbol + symClass *sx.Symbol + symMeta *sx.Symbol + symP *sx.Symbol + symLI *sx.Symbol + symA *sx.Symbol + symHREF *sx.Symbol + symSpan *sx.Symbol + + fns map[string]EvalFn + minArgs map[string]int +} + +// NewEvaluator creates a new Evaluator object. +func NewEvaluator(headingOffset int, sf sx.SymbolFactory) *Evaluator { if sf == nil { sf = sx.MakeMappedFactory(128) } - return &Transformer{ + ev := &Evaluator{ sf: sf, - rebinder: nil, headingOffset: int64(headingOffset), symAttr: sf.MustMake(sxhtml.NameSymAttr), + symList: sf.MustMake(sxhtml.NameSymList), + symNoEscape: sf.MustMake(sxhtml.NameSymNoEscape), symClass: sf.MustMake("class"), symMeta: sf.MustMake("meta"), + symP: sf.MustMake("p"), + symLI: sf.MustMake("li"), symA: sf.MustMake("a"), + symHREF: sf.MustMake("href"), symSpan: sf.MustMake("span"), + + fns: make(map[string]EvalFn, 128), + minArgs: make(map[string]int, 128), } + ev.bindCommon() + ev.bindMetadata() + ev.bindBlocks() + ev.bindInlines() + return ev } -// SymbolFactory returns the symbol factory to create HTML symbols. -func (tr *Transformer) SymbolFactory() sx.SymbolFactory { return tr.sf } - // SetUnique sets a prefix to make several HTML ids unique. -func (tr *Transformer) SetUnique(s string) { tr.unique = s } +func (tr *Evaluator) SetUnique(s string) { tr.unique = s } + +// SymbolFactory returns the symbol factory of this evaluator. +func (ev *Evaluator) SymbolFactory() sx.SymbolFactory { return ev.sf } // IsValidName returns true, if name is a valid symbol name. -func (tr *Transformer) IsValidName(s string) bool { return tr.sf.IsValidName(s) } +func (tr *Evaluator) IsValidName(s string) bool { return tr.sf.IsValidName(s) } // Make a new HTML symbol. -func (tr *Transformer) Make(s string) *sx.Symbol { return tr.sf.MustMake(s) } - -// RebindProc is a procedure which is called every time before a tranformation takes place. -type RebindProc func(*TransformEnv) - -// SetRebinder sets the rebinder procedure. -func (tr *Transformer) SetRebinder(rb RebindProc) { tr.rebinder = rb } - -// TransformAttrbute transforms the given attributes into a HTML s-expression. -func (tr *Transformer) TransformAttrbute(a attrs.Attributes) *sx.Pair { +func (tr *Evaluator) Make(s string) *sx.Symbol { return tr.sf.MustMake(s) } + +// EvaluateAttrbute transforms the given attributes into a HTML s-expression. +func (tr *Evaluator) EvaluateAttrbute(a attrs.Attributes) *sx.Pair { if len(a) == 0 { return nil } plist := sx.Nil() keys := a.Keys() @@ -101,264 +106,302 @@ return nil } return plist.Cons(tr.symAttr) } -// TransformMeta creates a HTML meta s-expression -func (tr *Transformer) TransformMeta(a attrs.Attributes) *sx.Pair { - return sx.Nil().Cons(tr.TransformAttrbute(a)).Cons(tr.symMeta) -} - -// Transform an AST s-expression into a list of HTML s-expressions. -func (tr *Transformer) Transform(lst *sx.Pair) (*sx.Pair, error) { - astSF := sx.FindSymbolFactory(lst) - if astSF != nil { - if astSF == tr.sf { - panic("Invalid AST SymbolFactory") - } - } else { - astSF = sx.MakeMappedFactory(1024) - } - astEnv := sxeval.MakeRootEnvironment(128) // approx: number of bindings in te.initialize() - engine := sxeval.MakeEngine(astSF, astEnv) - engine.BindSyntax(&sxbuiltins.QuoteS) - te := TransformEnv{ - tr: tr, - astSF: astSF, - astEnv: astEnv, - err: nil, - textEnc: text.NewEncoder(astSF), - } - te.initialize() - if rb := tr.rebinder; rb != nil { - rb(&te) - } - - val, err := engine.Eval(te.astEnv, lst) - if err != nil { +// Evaluate a metadata s-expression into a list of HTML s-expressions. +func (ev *Evaluator) Evaluate(lst *sx.Pair, env *Environment) (*sx.Pair, error) { + result := ev.eval(lst, env) + if err := env.err; err != nil { return nil, err } - res, isPair := sx.GetPair(val) + pair, isPair := sx.GetPair(result) if !isPair { - panic("Result is not a list") + return nil, fmt.Errorf("evaluation does not result in a pair, but %T/%v", result, result) } - for i := 0; i < len(tr.endnotes); i++ { + + for i := 0; i < len(env.endnotes); i++ { // May extend tr.endnotes - val, err = engine.Eval(te.astEnv, tr.endnotes[i].noteAST) - if err != nil { - return res, err - } - en, ok := sx.GetPair(val) - if !ok { - panic("Endnote is not a list") - } - tr.endnotes[i].noteHx = en - } - return res, err - + + if env.endnotes[i].noteHx != nil { + continue + } + + objHx := ev.eval(env.endnotes[i].noteAST, env) + if env.err != nil { + break + } + noteHx, isHx := sx.GetPair(objHx) + if !isHx { + return nil, fmt.Errorf("endnote evaluation does not result in pair, but %T/%v", objHx, objHx) + } + env.endnotes[i].noteHx = noteHx + } + + return pair, nil } // Endnotes returns a SHTML object with all collected endnotes. -func (tr *Transformer) Endnotes() *sx.Pair { - if len(tr.endnotes) == 0 { +func (ev *Evaluator) Endnotes(env *Environment) *sx.Pair { + if env.err != nil || len(env.endnotes) == 0 { return nil } - result := sx.Nil().Cons(tr.Make("ol")) - currResult := result.AppendBang(sx.Nil().Cons(sx.Cons(tr.symClass, sx.String("zs-endnotes"))).Cons(tr.symAttr)) - for i, fni := range tr.endnotes { + + result := sx.Nil().Cons(ev.Make("ol")) + symValue, symId, symRole := ev.Make("value"), ev.Make("id"), ev.Make("role") + + currResult := result.AppendBang(sx.Nil().Cons(sx.Cons(ev.symClass, sx.String("zs-endnotes"))).Cons(ev.symAttr)) + for i, fni := range env.endnotes { noteNum := strconv.Itoa(i + 1) - noteID := tr.unique + noteNum - - attrs := fni.attrs.Cons(sx.Cons(tr.symClass, sx.String("zs-endnote"))). - Cons(sx.Cons(tr.Make("value"), sx.String(noteNum))). - Cons(sx.Cons(tr.Make("id"), sx.String("fn:"+noteID))). - Cons(sx.Cons(tr.Make("role"), sx.String("doc-endnote"))). - Cons(tr.symAttr) + attrs := fni.attrs.Cons(sx.Cons(ev.symClass, sx.String("zs-endnote"))). + Cons(sx.Cons(symValue, sx.String(noteNum))). + Cons(sx.Cons(symId, sx.String("fn:"+fni.noteID))). + Cons(sx.Cons(symRole, sx.String("doc-endnote"))). + Cons(ev.symAttr) backref := sx.Nil().Cons(sx.String("\u21a9\ufe0e")). Cons(sx.Nil(). - Cons(sx.Cons(tr.symClass, sx.String("zs-endnote-backref"))). - Cons(sx.Cons(tr.Make("href"), sx.String("#fnref:"+noteID))). - Cons(sx.Cons(tr.Make("role"), sx.String("doc-backlink"))). - Cons(tr.symAttr)). - Cons(tr.symA) + Cons(sx.Cons(ev.symClass, sx.String("zs-endnote-backref"))). + Cons(sx.Cons(ev.symHREF, sx.String("#fnref:"+fni.noteID))). + Cons(sx.Cons(symRole, sx.String("doc-backlink"))). + Cons(ev.symAttr)). + Cons(ev.symA) - li := sx.Nil().Cons(tr.Make("li")) + li := sx.Nil().Cons(ev.symLI) li.AppendBang(attrs). ExtendBang(fni.noteHx). AppendBang(sx.String(" ")).AppendBang(backref) currResult = currResult.AppendBang(li) } - tr.endnotes = nil return result } -// TransformEnv is the environment where the actual transformation takes places. -type TransformEnv struct { - tr *Transformer - astSF sx.SymbolFactory - astEnv sxeval.Environment - err error - textEnc *text.Encoder - symNoEscape *sx.Symbol - symAttr *sx.Symbol - symA *sx.Symbol - symSpan *sx.Symbol - symP *sx.Symbol -} - -func (te *TransformEnv) initialize() { - te.symNoEscape = te.Make(sxhtml.NameSymNoEscape) - te.symAttr = te.tr.symAttr - te.symA = te.tr.symA - te.symSpan = te.tr.symSpan - te.symP = te.Make("p") - - te.bind(sx.ListName, 0, listArgs) - te.bindMetadata() - te.bindBlocks() - te.bindInlines() -} - -func listArgs(args []sx.Object) sx.Object { return sx.MakeList(args...) } - -func (te *TransformEnv) bindMetadata() { - te.bind(sz.NameSymMeta, 0, listArgs) - te.bind(sz.NameSymTypeZettelmarkup, 2, func(args []sx.Object) sx.Object { - a := make(attrs.Attributes, 2). - Set("name", te.getSymbol(args[0]).String()). - Set("content", te.textEnc.Encode(te.getList(args[1]))) - return te.transformMeta(a) - }) - metaString := func(args []sx.Object) sx.Object { - a := make(attrs.Attributes, 2). - Set("name", te.getSymbol(args[0]).Name()). - Set("content", te.getString(args[1]).String()) - return te.transformMeta(a) - } - te.bind(sz.NameSymTypeCredential, 2, metaString) - te.bind(sz.NameSymTypeEmpty, 2, metaString) - te.bind(sz.NameSymTypeID, 2, metaString) - te.bind(sz.NameSymTypeNumber, 2, metaString) - te.bind(sz.NameSymTypeString, 2, metaString) - te.bind(sz.NameSymTypeTimestamp, 2, metaString) - te.bind(sz.NameSymTypeURL, 2, metaString) - te.bind(sz.NameSymTypeWord, 2, metaString) - metaSet := func(args []sx.Object) sx.Object { - var sb strings.Builder - for elem := te.getList(args[1]); elem != nil; elem = elem.Tail() { - sb.WriteByte(' ') - sb.WriteString(te.getString(elem.Car()).String()) +// Environment where sz objects are evaluated to shtml objects +type Environment struct { + err error + langStack []string + endnotes []endnoteInfo + quoteNesting uint +} +type endnoteInfo struct { + noteID string // link id + noteAST sx.Object // Endnote as AST + attrs *sx.Pair // attrs a-list + noteHx *sx.Pair // Endnote as SxHTML +} + +// MakeEnvironment builds a new evaluation environment. +func MakeEnvironment(lang string) Environment { + langStack := make([]string, 1, 16) + langStack[0] = lang + return Environment{ + err: nil, + langStack: langStack, + endnotes: nil, + quoteNesting: 0, + } +} + +// GetError returns the last error found. +func (env *Environment) GetError() error { return env.err } + +// Reset the environment. +func (env *Environment) Reset() { + env.langStack = env.langStack[0:1] + env.endnotes = nil + env.quoteNesting = 0 +} + +// PushAttribute adds the current attributes to the environment. +func (env *Environment) pushAttributes(a attrs.Attributes) { + if value, ok := a.Get("lang"); ok { + env.langStack = append(env.langStack, value) + } else { + env.langStack = append(env.langStack, env.getLanguage()) + } +} + +// popAttributes removes the current attributes from the envrionment +func (env *Environment) popAttributes() { + env.langStack = env.langStack[0 : len(env.langStack)-1] +} + +// getLanguage returns the current language +func (env *Environment) getLanguage() string { + return env.langStack[len(env.langStack)-1] +} + +// EvalFn is a function to be called for evaluation. +type EvalFn func([]sx.Object, *Environment) sx.Object + +func (ev *Evaluator) bind(name string, minArgs int, fn EvalFn) { + ev.fns[name] = fn + if minArgs > 0 { + ev.minArgs[name] = minArgs + } +} + +// ResolveBinding returns the function bound to the given name. +func (ev *Evaluator) ResolveBinding(name string) EvalFn { + if fn, found := ev.fns[name]; found { + return fn + } + return nil +} + +// Rebind overwrites a binding, but leaves the minimum number of arguments intact. +func (ev *Evaluator) Rebind(name string, fn EvalFn) { + if _, found := ev.fns[name]; !found { + panic(name) + } + ev.fns[name] = fn +} + +func (ev *Evaluator) bindCommon() { + ev.bind(sx.ListName, 0, ev.evalList) + ev.bind("quote", 1, func(args []sx.Object, _ *Environment) sx.Object { return args[0] }) +} + +func (ev *Evaluator) bindMetadata() { + ev.bind(sz.NameSymMeta, 0, ev.evalList) + evalMetaString := func(args []sx.Object, env *Environment) sx.Object { + a := make(attrs.Attributes, 2). + Set("name", ev.getSymbol(ev.eval(args[0], env), env).Name()). + Set("content", getString(args[1], env).String()) + return ev.EvaluateMeta(a) + } + ev.bind(sz.NameSymTypeCredential, 2, evalMetaString) + ev.bind(sz.NameSymTypeEmpty, 2, evalMetaString) + ev.bind(sz.NameSymTypeID, 2, evalMetaString) + ev.bind(sz.NameSymTypeNumber, 2, evalMetaString) + ev.bind(sz.NameSymTypeString, 2, evalMetaString) + ev.bind(sz.NameSymTypeTimestamp, 2, evalMetaString) + ev.bind(sz.NameSymTypeURL, 2, evalMetaString) + ev.bind(sz.NameSymTypeWord, 2, evalMetaString) + + evalMetaSet := func(args []sx.Object, env *Environment) sx.Object { + var sb strings.Builder + lst := ev.eval(args[1], env) + for elem := getList(lst, env); elem != nil; elem = elem.Tail() { + sb.WriteByte(' ') + sb.WriteString(getString(elem.Car(), env).String()) } s := sb.String() if len(s) > 0 { s = s[1:] } a := make(attrs.Attributes, 2). - Set("name", te.getSymbol(args[0]).Name()). + Set("name", ev.getSymbol(ev.eval(args[0], env), env).Name()). Set("content", s) - return te.transformMeta(a) - } - te.bind(sz.NameSymTypeIDSet, 2, metaSet) - te.bind(sz.NameSymTypeTagSet, 2, metaSet) - te.bind(sz.NameSymTypeWordSet, 2, metaSet) -} - -func (te *TransformEnv) bindBlocks() { - te.bind(sz.NameSymBlock, 0, listArgs) - te.bind(sz.NameSymPara, 0, func(args []sx.Object) sx.Object { - // for ; args != nil; args = args.Tail() { - // lst, ok := sx.GetList(args.Car()) - // if !ok || lst != nil { - // break - // } - // } - return sx.MakeList(args...).Cons(te.symP) - }) - te.bind(sz.NameSymHeading, 5, func(args []sx.Object) sx.Object { - nLevel := te.getInt64(args[0]) + return ev.EvaluateMeta(a) + } + ev.bind(sz.NameSymTypeIDSet, 2, evalMetaSet) + ev.bind(sz.NameSymTypeTagSet, 2, evalMetaSet) + ev.bind(sz.NameSymTypeWordSet, 2, evalMetaSet) + ev.bind(sz.NameSymTypeZettelmarkup, 2, func(args []sx.Object, env *Environment) sx.Object { + a := make(attrs.Attributes, 2). + Set("name", ev.getSymbol(ev.eval(args[0], env), env).String()). + Set("content", text.EvaluateInlineString(getList(args[1], env))) + return ev.EvaluateMeta(a) + }) +} + +// EvaluateMeta returns HTML meta object for an attribute. +func (ev *Evaluator) EvaluateMeta(a attrs.Attributes) *sx.Pair { + return sx.Nil().Cons(ev.EvaluateAttrbute(a)).Cons(ev.symMeta) +} + +func (ev *Evaluator) bindBlocks() { + ev.bind(sz.NameSymBlock, 0, ev.evalList) + ev.bind(sz.NameSymPara, 0, func(args []sx.Object, env *Environment) sx.Object { + return ev.evalSlice(args, env).Cons(ev.symP) + }) + ev.bind(sz.NameSymHeading, 5, func(args []sx.Object, env *Environment) sx.Object { + nLevel := getInt64(args[0], env) if nLevel <= 0 { - te.err = fmt.Errorf("%v is a negative level", nLevel) + env.err = fmt.Errorf("%v is a negative level", nLevel) return sx.Nil() } - level := strconv.FormatInt(nLevel+te.tr.headingOffset, 10) + level := strconv.FormatInt(nLevel+ev.headingOffset, 10) - a := te.getAttributes(args[1]) - if fragment := te.getString(args[3]).String(); fragment != "" { - a = a.Set("id", te.tr.unique+fragment) + a := ev.getAttributes(args[1], env) + env.pushAttributes(a) + defer env.popAttributes() + if fragment := getString(args[3], env).String(); fragment != "" { + a = a.Set("id", ev.unique+fragment) } - if result, isPair := sx.GetPair(args[4]); isPair && result != nil { + if result, isPair := sx.GetPair(ev.eval(args[4], env)); isPair && result != nil { if len(a) > 0 { - result = result.Cons(te.transformAttribute(a)) + result = result.Cons(ev.EvaluateAttrbute(a)) } - return result.Cons(te.Make("h" + level)) + return result.Cons(ev.Make("h" + level)) } - return sx.MakeList(te.Make("h"+level), sx.String("")) + return sx.MakeList(ev.Make("h"+level), sx.String("")) }) - te.bind(sz.NameSymThematic, 0, func(args []sx.Object) sx.Object { + ev.bind(sz.NameSymThematic, 0, func(args []sx.Object, env *Environment) sx.Object { result := sx.Nil() if len(args) > 0 { - if attrList := te.getList(args[0]); attrList != nil { - result = result.Cons(te.transformAttribute(sz.GetAttributes(attrList))) + if attrList := getList(ev.eval(args[0], env), env); attrList != nil { + result = result.Cons(ev.EvaluateAttrbute(sz.GetAttributes(attrList))) } } - return result.Cons(te.Make("hr")) + return result.Cons(ev.Make("hr")) }) - te.bind(sz.NameSymListOrdered, 0, te.makeListFn("ol")) - te.bind(sz.NameSymListUnordered, 0, te.makeListFn("ul")) - te.bind(sz.NameSymDescription, 0, func(args []sx.Object) sx.Object { + + ev.bind(sz.NameSymListOrdered, 0, ev.makeListFn("ol")) + ev.bind(sz.NameSymListUnordered, 0, ev.makeListFn("ul")) + ev.bind(sz.NameSymDescription, 0, func(args []sx.Object, env *Environment) sx.Object { if len(args) == 0 { return sx.Nil() } - items := sx.Nil().Cons(te.Make("dl")) + items := sx.Nil().Cons(ev.Make("dl")) curItem := items for pos := 0; pos < len(args); pos++ { - term := te.getList(args[pos]) - curItem = curItem.AppendBang(term.Cons(te.Make("dt"))) + term := getList(ev.eval(args[pos], env), env) + curItem = curItem.AppendBang(term.Cons(ev.Make("dt"))) pos++ if pos >= len(args) { break } - ddBlock := te.getList(args[pos]) + ddBlock := getList(ev.eval(args[pos], env), env) if ddBlock == nil { continue } for ddlst := ddBlock; ddlst != nil; ddlst = ddlst.Tail() { - dditem := te.getList(ddlst.Car()) - curItem = curItem.AppendBang(dditem.Cons(te.Make("dd"))) + dditem := getList(ddlst.Car(), env) + curItem = curItem.AppendBang(dditem.Cons(ev.Make("dd"))) } } return items }) - - te.bind(sz.NameSymListQuote, 0, func(args []sx.Object) sx.Object { + ev.bind(sz.NameSymListQuote, 0, func(args []sx.Object, env *Environment) sx.Object { if args == nil { return sx.Nil() } - result := sx.Nil().Cons(te.Make("blockquote")) + result := sx.Nil().Cons(ev.Make("blockquote")) currResult := result for _, elem := range args { - if quote, isPair := sx.GetPair(elem); isPair { - currResult = currResult.AppendBang(quote.Cons(te.symP)) + if quote, isPair := sx.GetPair(ev.eval(elem, env)); isPair { + currResult = currResult.AppendBang(quote.Cons(ev.symP)) } } return result }) - te.bind(sz.NameSymTable, 1, func(args []sx.Object) sx.Object { + ev.bind(sz.NameSymTable, 1, func(args []sx.Object, env *Environment) sx.Object { thead := sx.Nil() - if header := te.getList(args[0]); !sx.IsNil(header) { - thead = sx.Nil().Cons(te.transformTableRow(header)).Cons(te.Make("thead")) + if header := getList(ev.eval(args[0], env), env); !sx.IsNil(header) { + thead = sx.Nil().Cons(ev.evalTableRow(header)).Cons(ev.Make("thead")) } tbody := sx.Nil() if len(args) > 1 { - tbody = sx.Nil().Cons(te.Make("tbody")) + tbody = sx.Nil().Cons(ev.Make("tbody")) curBody := tbody for _, row := range args[1:] { - curBody = curBody.AppendBang(te.transformTableRow(te.getList(row))) + curBody = curBody.AppendBang(ev.evalTableRow(getList(ev.eval(row, env), env))) } } table := sx.Nil() if tbody != nil { @@ -368,529 +411,618 @@ table = table.Cons(thead) } if table == nil { return sx.Nil() } - return table.Cons(te.Make("table")) + return table.Cons(ev.Make("table")) }) - te.bind(sz.NameSymCell, 0, te.makeCellFn("")) - te.bind(sz.NameSymCellCenter, 0, te.makeCellFn("center")) - te.bind(sz.NameSymCellLeft, 0, te.makeCellFn("left")) - te.bind(sz.NameSymCellRight, 0, te.makeCellFn("right")) - - te.bind(sz.NameSymRegionBlock, 2, te.makeRegionFn(te.Make("div"), true)) - te.bind(sz.NameSymRegionQuote, 2, te.makeRegionFn(te.Make("blockquote"), false)) - te.bind(sz.NameSymRegionVerse, 2, te.makeRegionFn(te.Make("div"), false)) - - te.bind(sz.NameSymVerbatimComment, 1, func(args []sx.Object) sx.Object { - if te.getAttributes(args[0]).HasDefault() { + ev.bind(sz.NameSymCell, 0, ev.makeCellFn("")) + ev.bind(sz.NameSymCellCenter, 0, ev.makeCellFn("center")) + ev.bind(sz.NameSymCellLeft, 0, ev.makeCellFn("left")) + ev.bind(sz.NameSymCellRight, 0, ev.makeCellFn("right")) + + symDiv := ev.Make("div") + ev.bind(sz.NameSymRegionBlock, 2, ev.makeRegionFn(symDiv, true)) + ev.bind(sz.NameSymRegionQuote, 2, ev.makeRegionFn(ev.Make("blockquote"), false)) + ev.bind(sz.NameSymRegionVerse, 2, ev.makeRegionFn(symDiv, false)) + + ev.bind(sz.NameSymVerbatimComment, 1, func(args []sx.Object, env *Environment) sx.Object { + if ev.getAttributes(args[0], env).HasDefault() { if len(args) > 1 { - if s := te.getString(args[1]); s != "" { + if s := getString(args[1], env); s != "" { t := sx.String(s.String()) - return sx.Nil().Cons(t).Cons(te.Make(sxhtml.NameSymBlockComment)) + return sx.Nil().Cons(t).Cons(ev.Make(sxhtml.NameSymBlockComment)) } } } return nil }) - - te.bind(sz.NameSymVerbatimEval, 2, func(args []sx.Object) sx.Object { - return te.transformVerbatim(te.getAttributes(args[0]).AddClass("zs-eval"), te.getString(args[1])) - }) - te.bind(sz.NameSymVerbatimHTML, 2, te.transformHTML) - te.bind(sz.NameSymVerbatimMath, 2, func(args []sx.Object) sx.Object { - return te.transformVerbatim(te.getAttributes(args[0]).AddClass("zs-math"), te.getString(args[1])) - }) - te.bind(sz.NameSymVerbatimProg, 2, func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]) - content := te.getString(args[1]) + ev.bind(sz.NameSymVerbatimEval, 2, func(args []sx.Object, env *Environment) sx.Object { + return ev.evalVerbatim(ev.getAttributes(args[0], env).AddClass("zs-eval"), getString(args[1], env)) + }) + ev.bind(sz.NameSymVerbatimHTML, 2, ev.evalHTML) + ev.bind(sz.NameSymVerbatimMath, 2, func(args []sx.Object, env *Environment) sx.Object { + return ev.evalVerbatim(ev.getAttributes(args[0], env).AddClass("zs-math"), getString(args[1], env)) + }) + ev.bind(sz.NameSymVerbatimProg, 2, func(args []sx.Object, env *Environment) sx.Object { + a := ev.getAttributes(args[0], env) + content := getString(args[1], env) if a.HasDefault() { content = sx.String(visibleReplacer.Replace(content.String())) } - return te.transformVerbatim(a, content) - }) - te.bind(sz.NameSymVerbatimZettel, 0, func([]sx.Object) sx.Object { return sx.Nil() }) - - te.bind(sz.NameSymBLOB, 3, func(args []sx.Object) sx.Object { - return te.transformBLOB(te.getList(args[0]), te.getString(args[1]), te.getString(args[2])) - }) - - te.bind(sz.NameSymTransclude, 2, func(args []sx.Object) sx.Object { - ref, isPair := sx.GetPair(args[1]) + return ev.evalVerbatim(a, content) + }) + ev.bind(sz.NameSymVerbatimZettel, 0, nilFn) + ev.bind(sz.NameSymBLOB, 3, func(args []sx.Object, env *Environment) sx.Object { + return ev.evalBLOB(getList(args[0], env), getString(args[1], env), getString(args[2], env)) + }) + ev.bind(sz.NameSymTransclude, 2, func(args []sx.Object, env *Environment) sx.Object { + ref, isPair := sx.GetPair(ev.eval(args[1], env)) if !isPair { return sx.Nil() } refKind := ref.Car() if sx.IsNil(refKind) { return sx.Nil() } - if refValue := te.getString(ref.Tail().Car()); refValue != "" { - if te.astSF.MustMake(sz.NameSymRefStateExternal).IsEqual(refKind) { - a := te.getAttributes(args[0]).Set("src", refValue.String()).AddClass("external") - return sx.Nil().Cons(sx.Nil().Cons(te.transformAttribute(a)).Cons(te.Make("img"))).Cons(te.symP) + if refValue := getString(ref.Tail().Car(), env); refValue != "" { + if refSym, isRefSym := sx.GetSymbol(refKind); isRefSym && refSym.Name() == sz.NameSymRefStateExternal { + a := ev.getAttributes(args[0], env).Set("src", refValue.String()).AddClass("external") + return sx.Nil().Cons(sx.Nil().Cons(ev.EvaluateAttrbute(a)).Cons(ev.Make("img"))).Cons(ev.symP) } return sx.MakeList( - te.Make(sxhtml.NameSymInlineComment), + ev.Make(sxhtml.NameSymInlineComment), sx.String("transclude"), refKind, sx.String("->"), refValue, ) } - return sx.MakeList(args...) + return ev.evalSlice(args, env) }) } -func (te *TransformEnv) makeListFn(tag string) transformFn { - sym := te.Make(tag) - return func(args []sx.Object) sx.Object { +func (ev *Evaluator) makeListFn(tag string) EvalFn { + sym := ev.Make(tag) + return func(args []sx.Object, env *Environment) sx.Object { result := sx.Nil().Cons(sym) last := result for _, elem := range args { - item := sx.Nil().Cons(te.Make("li")) - if res, isPair := sx.GetPair(elem); isPair { + item := sx.Nil().Cons(ev.symLI) + if res, isPair := sx.GetPair(ev.eval(elem, env)); isPair { item.ExtendBang(res) } last = last.AppendBang(item) } return result } } -func (te *TransformEnv) transformTableRow(pairs *sx.Pair) *sx.Pair { - row := sx.Nil().Cons(te.Make("tr")) + +func (ev *Evaluator) evalTableRow(pairs *sx.Pair) *sx.Pair { + row := sx.Nil().Cons(ev.Make("tr")) if pairs == nil { return nil } curRow := row for pair := pairs; pair != nil; pair = pair.Tail() { curRow = curRow.AppendBang(pair.Car()) } return row } - -func (te *TransformEnv) makeCellFn(align string) transformFn { - return func(args []sx.Object) sx.Object { - tdata := sx.MakeList(args...) +func (ev *Evaluator) makeCellFn(align string) EvalFn { + return func(args []sx.Object, env *Environment) sx.Object { + tdata := ev.evalSlice(args, env) if align != "" { - tdata = tdata.Cons(te.transformAttribute(attrs.Attributes{"class": align})) + tdata = tdata.Cons(ev.EvaluateAttrbute(attrs.Attributes{"class": align})) } - return tdata.Cons(te.Make("td")) + return tdata.Cons(ev.Make("td")) } } -func (te *TransformEnv) makeRegionFn(sym *sx.Symbol, genericToClass bool) transformFn { - return func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]) +func (ev *Evaluator) makeRegionFn(sym *sx.Symbol, genericToClass bool) EvalFn { + return func(args []sx.Object, env *Environment) sx.Object { + a := ev.getAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() if genericToClass { if val, found := a.Get(""); found { a = a.Remove("").AddClass(val) } } result := sx.Nil() if len(a) > 0 { - result = result.Cons(te.transformAttribute(a)) + result = result.Cons(ev.EvaluateAttrbute(a)) } result = result.Cons(sym) currResult := result.LastPair() - if region, isPair := sx.GetPair(args[1]); isPair { + if region, isPair := sx.GetPair(ev.eval(args[1], env)); isPair { currResult = currResult.ExtendBang(region) } if len(args) > 2 { - if cite, isPair := sx.GetPair(args[2]); isPair && cite != nil { - currResult.AppendBang(cite.Cons(te.Make("cite"))) + if cite, isPair := sx.GetPair(ev.eval(args[2], env)); isPair && cite != nil { + currResult.AppendBang(cite.Cons(ev.Make("cite"))) } } return result } } -func (te *TransformEnv) transformVerbatim(a attrs.Attributes, s sx.String) sx.Object { +func (ev *Evaluator) evalVerbatim(a attrs.Attributes, s sx.String) sx.Object { a = setProgLang(a) code := sx.Nil().Cons(s) - if al := te.transformAttribute(a); al != nil { + if al := ev.EvaluateAttrbute(a); al != nil { code = code.Cons(al) } - code = code.Cons(te.Make("code")) - return sx.Nil().Cons(code).Cons(te.Make("pre")) + code = code.Cons(ev.Make("code")) + return sx.Nil().Cons(code).Cons(ev.Make("pre")) } -func (te *TransformEnv) bindInlines() { - te.bind(sz.NameSymInline, 0, listArgs) - te.bind(sz.NameSymText, 1, func(args []sx.Object) sx.Object { return te.getString(args[0]) }) - te.bind(sz.NameSymSpace, 0, func(args []sx.Object) sx.Object { +func (ev *Evaluator) bindInlines() { + ev.bind(sz.NameSymInline, 0, ev.evalList) + ev.bind(sz.NameSymText, 1, func(args []sx.Object, env *Environment) sx.Object { return getString(args[0], env) }) + ev.bind(sz.NameSymSpace, 0, func(args []sx.Object, env *Environment) sx.Object { if len(args) == 0 { return sx.String(" ") } - return te.getString(args[0]) + return getString(args[0], env) }) - te.bind(sz.NameSymSoft, 0, func([]sx.Object) sx.Object { return sx.String(" ") }) - brSym := te.Make("br") - te.bind(sz.NameSymHard, 0, func([]sx.Object) sx.Object { return sx.Nil().Cons(brSym) }) + ev.bind(sz.NameSymSoft, 0, func([]sx.Object, *Environment) sx.Object { return sx.String(" ") }) + symBR := ev.Make("br") + ev.bind(sz.NameSymHard, 0, func([]sx.Object, *Environment) sx.Object { return sx.Nil().Cons(symBR) }) - te.bind(sz.NameSymLinkInvalid, 2, func(args []sx.Object) sx.Object { - // a := te.getAttributes(args) + ev.bind(sz.NameSymLinkInvalid, 2, func(args []sx.Object, env *Environment) sx.Object { + a := ev.getAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() var inline *sx.Pair if len(args) > 2 { - inline = sx.MakeList(args[2:]...) + inline = ev.evalSlice(args[2:], env) } if inline == nil { - inline = sx.Nil().Cons(args[1]) - } - return inline.Cons(te.symSpan) - }) - transformHREF := func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]) - refValue := te.getString(args[1]) - return te.transformLink(a.Set("href", refValue.String()), refValue, args[2:]) - } - te.bind(sz.NameSymLinkZettel, 2, transformHREF) - te.bind(sz.NameSymLinkSelf, 2, transformHREF) - te.bind(sz.NameSymLinkFound, 2, transformHREF) - te.bind(sz.NameSymLinkBroken, 2, func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]) - refValue := te.getString(args[1]) - return te.transformLink(a.AddClass("broken"), refValue, args[2:]) - }) - te.bind(sz.NameSymLinkHosted, 2, transformHREF) - te.bind(sz.NameSymLinkBased, 2, transformHREF) - te.bind(sz.NameSymLinkQuery, 2, func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]) - refValue := te.getString(args[1]) + inline = sx.Nil().Cons(ev.eval(args[1], env)) + } + return inline.Cons(ev.symSpan) + }) + evalHREF := func(args []sx.Object, env *Environment) sx.Object { + a := ev.getAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() + refValue := getString(args[1], env) + return ev.evalLink(a.Set("href", refValue.String()), refValue, args[2:], env) + } + ev.bind(sz.NameSymLinkZettel, 2, evalHREF) + ev.bind(sz.NameSymLinkSelf, 2, evalHREF) + ev.bind(sz.NameSymLinkFound, 2, evalHREF) + ev.bind(sz.NameSymLinkBroken, 2, func(args []sx.Object, env *Environment) sx.Object { + a := ev.getAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() + refValue := getString(args[1], env) + return ev.evalLink(a.AddClass("broken"), refValue, args[2:], env) + }) + ev.bind(sz.NameSymLinkHosted, 2, evalHREF) + ev.bind(sz.NameSymLinkBased, 2, evalHREF) + ev.bind(sz.NameSymLinkQuery, 2, func(args []sx.Object, env *Environment) sx.Object { + a := ev.getAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() + refValue := getString(args[1], env) query := "?" + api.QueryKeyQuery + "=" + url.QueryEscape(refValue.String()) - return te.transformLink(a.Set("href", query), refValue, args[2:]) + return ev.evalLink(a.Set("href", query), refValue, args[2:], env) }) - te.bind(sz.NameSymLinkExternal, 2, func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]) - refValue := te.getString(args[1]) - return te.transformLink(a.Set("href", refValue.String()).AddClass("external"), refValue, args[2:]) + ev.bind(sz.NameSymLinkExternal, 2, func(args []sx.Object, env *Environment) sx.Object { + a := ev.getAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() + refValue := getString(args[1], env) + return ev.evalLink(a.Set("href", refValue.String()).AddClass("external"), refValue, args[2:], env) }) - te.bind(sz.NameSymEmbed, 3, func(args []sx.Object) sx.Object { - ref := te.getList(args[1]) - syntax := te.getString(args[2]) + ev.bind(sz.NameSymEmbed, 3, func(args []sx.Object, env *Environment) sx.Object { + ref := getList(ev.eval(args[1], env), env) + syntax := getString(args[2], env) if syntax == api.ValueSyntaxSVG { embedAttr := sx.MakeList( - te.symAttr, - sx.Cons(te.Make("type"), sx.String("image/svg+xml")), - sx.Cons(te.Make("src"), sx.String("/"+te.getString(ref.Tail()).String()+".svg")), + ev.symAttr, + sx.Cons(ev.Make("type"), sx.String("image/svg+xml")), + sx.Cons(ev.Make("src"), sx.String("/"+getString(ref.Tail(), env).String()+".svg")), ) return sx.MakeList( - te.Make("figure"), + ev.Make("figure"), sx.MakeList( - te.Make("embed"), + ev.Make("embed"), embedAttr, ), ) } - a := te.getAttributes(args[0]) - a = a.Set("src", string(te.getString(ref.Tail().Car()))) - var sb strings.Builder - te.flattenText(&sb, ref.Tail().Tail().Tail()) - if d := sb.String(); d != "" { - a = a.Set("alt", d) - } - return sx.MakeList(te.Make("img"), te.transformAttribute(a)) + a := ev.getAttributes(args[0], env) + a = a.Set("src", getString(ref.Tail().Car(), env).String()) + if len(args) > 3 { + var sb strings.Builder + flattenText(&sb, sx.MakeList(args[3:]...)) + if d := sb.String(); d != "" { + a = a.Set("alt", d) + } + } + return sx.MakeList(ev.Make("img"), ev.EvaluateAttrbute(a)) }) - te.bind(sz.NameSymEmbedBLOB, 3, func(args []sx.Object) sx.Object { - a, syntax, data := te.getAttributes(args[0]), te.getString(args[1]), te.getString(args[2]) - summary, _ := a.Get(api.KeySummary) - return te.transformBLOB( - sx.MakeList(te.astSF.MustMake(sz.NameSymInline), sx.String(summary)), + ev.bind(sz.NameSymEmbedBLOB, 3, func(args []sx.Object, env *Environment) sx.Object { + a, syntax, data := ev.getAttributes(args[0], env), getString(args[1], env), getString(args[2], env) + summary, hasSummary := a.Get(api.KeySummary) + if !hasSummary { + summary = "" + } + return ev.evalBLOB( + sx.MakeList(ev.Make(sx.ListName), sx.String(summary)), syntax, data, ) }) - te.bind(sz.NameSymCite, 2, func(args []sx.Object) sx.Object { + ev.bind(sz.NameSymCite, 2, func(args []sx.Object, env *Environment) sx.Object { + a := ev.getAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() result := sx.Nil() - if key := te.getString(args[1]); key != "" { + if key := getString(args[1], env); key != "" { if len(args) > 2 { - result = sx.MakeList(args[2:]...).Cons(sx.String(", ")) + result = ev.evalSlice(args[2:], env).Cons(sx.String(", ")) } result = result.Cons(key) } - if a := te.getAttributes(args[0]); len(a) > 0 { - result = result.Cons(te.transformAttribute(a)) + if len(a) > 0 { + result = result.Cons(ev.EvaluateAttrbute(a)) } if result == nil { return nil } - return result.Cons(te.symSpan) - }) - - te.bind(sz.NameSymMark, 3, func(args []sx.Object) sx.Object { - result := sx.MakeList(args[3:]...) - if !te.tr.noLinks { - if fragment := te.getString(args[2]); fragment != "" { - a := attrs.Attributes{"id": fragment.String() + te.tr.unique} - return result.Cons(te.transformAttribute(a)).Cons(te.symA) - } - } - return result.Cons(te.symSpan) - }) - - te.bind(sz.NameSymEndnote, 1, func(args []sx.Object) sx.Object { - attrPlist := sx.Nil() - if a := te.getAttributes(args[0]); len(a) > 0 { - if attrs := te.transformAttribute(a); attrs != nil { + return result.Cons(ev.symSpan) + }) + ev.bind(sz.NameSymMark, 3, func(args []sx.Object, env *Environment) sx.Object { + result := ev.evalSlice(args[3:], env) + if !ev.noLinks { + if fragment := getString(args[2], env); fragment != "" { + a := attrs.Attributes{"id": fragment.String() + ev.unique} + return result.Cons(ev.EvaluateAttrbute(a)).Cons(ev.symA) + } + } + return result.Cons(ev.symSpan) + }) + ev.bind(sz.NameSymEndnote, 1, func(args []sx.Object, env *Environment) sx.Object { + a := ev.getAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() + attrPlist := sx.Nil() + if len(a) > 0 { + if attrs := ev.EvaluateAttrbute(a); attrs != nil { attrPlist = attrs.Tail() } } text, isPair := sx.GetPair(args[1]) if !isPair { return sx.Nil() } - te.tr.endnotes = append(te.tr.endnotes, endnoteInfo{noteAST: text, noteHx: nil, attrs: attrPlist}) - noteNum := strconv.Itoa(len(te.tr.endnotes)) - noteID := te.tr.unique + noteNum - hrefAttr := sx.Nil().Cons(sx.Cons(te.Make("role"), sx.String("doc-noteref"))). - Cons(sx.Cons(te.Make("href"), sx.String("#fn:"+noteID))). - Cons(sx.Cons(te.tr.symClass, sx.String("zs-noteref"))). - Cons(te.symAttr) - href := sx.Nil().Cons(sx.String(noteNum)).Cons(hrefAttr).Cons(te.symA) - supAttr := sx.Nil().Cons(sx.Cons(te.Make("id"), sx.String("fnref:"+noteID))).Cons(te.symAttr) - return sx.Nil().Cons(href).Cons(supAttr).Cons(te.Make("sup")) + noteNum := strconv.Itoa(len(env.endnotes) + 1) + noteID := ev.unique + noteNum + env.endnotes = append(env.endnotes, endnoteInfo{ + noteID: noteID, noteAST: ev.eval(text, env), noteHx: nil, attrs: attrPlist}) + hrefAttr := sx.Nil().Cons(sx.Cons(ev.Make("role"), sx.String("doc-noteref"))). + Cons(sx.Cons(ev.symHREF, sx.String("#fn:"+noteID))). + Cons(sx.Cons(ev.symClass, sx.String("zs-noteref"))). + Cons(ev.symAttr) + href := sx.Nil().Cons(sx.String(noteNum)).Cons(hrefAttr).Cons(ev.symA) + supAttr := sx.Nil().Cons(sx.Cons(ev.Make("id"), sx.String("fnref:"+noteID))).Cons(ev.symAttr) + return sx.Nil().Cons(href).Cons(supAttr).Cons(ev.Make("sup")) }) - te.bind(sz.NameSymFormatDelete, 1, te.makeFormatFn("del")) - te.bind(sz.NameSymFormatEmph, 1, te.makeFormatFn("em")) - te.bind(sz.NameSymFormatInsert, 1, te.makeFormatFn("ins")) - te.bind(sz.NameSymFormatQuote, 1, te.transformQuote) - te.bind(sz.NameSymFormatSpan, 1, te.makeFormatFn("span")) - te.bind(sz.NameSymFormatStrong, 1, te.makeFormatFn("strong")) - te.bind(sz.NameSymFormatSub, 1, te.makeFormatFn("sub")) - te.bind(sz.NameSymFormatSuper, 1, te.makeFormatFn("sup")) - - te.bind(sz.NameSymLiteralComment, 1, func(args []sx.Object) sx.Object { - if te.getAttributes(args[0]).HasDefault() { + ev.bind(sz.NameSymFormatDelete, 1, ev.makeFormatFn("del")) + ev.bind(sz.NameSymFormatEmph, 1, ev.makeFormatFn("em")) + ev.bind(sz.NameSymFormatInsert, 1, ev.makeFormatFn("ins")) + ev.bind(sz.NameSymFormatMark, 1, ev.makeFormatFn("mark")) + ev.bind(sz.NameSymFormatQuote, 1, ev.evalQuote) + ev.bind(sz.NameSymFormatSpan, 1, ev.makeFormatFn("span")) + ev.bind(sz.NameSymFormatStrong, 1, ev.makeFormatFn("strong")) + ev.bind(sz.NameSymFormatSub, 1, ev.makeFormatFn("sub")) + ev.bind(sz.NameSymFormatSuper, 1, ev.makeFormatFn("sup")) + + ev.bind(sz.NameSymLiteralComment, 1, func(args []sx.Object, env *Environment) sx.Object { + if ev.getAttributes(args[0], env).HasDefault() { if len(args) > 1 { - if s := te.getString(args[1]); s != "" { - return sx.Nil().Cons(s).Cons(te.Make(sxhtml.NameSymInlineComment)) + if s := getString(ev.eval(args[1], env), env); s != "" { + return sx.Nil().Cons(s).Cons(ev.Make(sxhtml.NameSymInlineComment)) } } } return sx.Nil() }) - te.bind(sz.NameSymLiteralHTML, 2, te.transformHTML) - kbdSym := te.Make("kbd") - te.bind(sz.NameSymLiteralInput, 2, func(args []sx.Object) sx.Object { - return te.transformLiteral(args, nil, kbdSym) - }) - codeSym := te.Make("code") - te.bind(sz.NameSymLiteralMath, 2, func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]).AddClass("zs-math") - return te.transformLiteral(args, a, codeSym) - }) - sampSym := te.Make("samp") - te.bind(sz.NameSymLiteralOutput, 2, func(args []sx.Object) sx.Object { - return te.transformLiteral(args, nil, sampSym) - }) - te.bind(sz.NameSymLiteralProg, 2, func(args []sx.Object) sx.Object { - return te.transformLiteral(args, nil, codeSym) - }) - - te.bind(sz.NameSymLiteralZettel, 0, func([]sx.Object) sx.Object { return sx.Nil() }) -} - -func (te *TransformEnv) makeFormatFn(tag string) transformFn { - sym := te.Make(tag) - return func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]) - if val, found := a.Get(""); found { - a = a.Remove("").AddClass(val) - } - res := sx.MakeList(args[1:]...) - if len(a) > 0 { - res = res.Cons(te.transformAttribute(a)) + ev.bind(sz.NameSymLiteralHTML, 2, ev.evalHTML) + kbdSym := ev.Make("kbd") + ev.bind(sz.NameSymLiteralInput, 2, func(args []sx.Object, env *Environment) sx.Object { + return ev.evalLiteral(args, nil, kbdSym, env) + }) + codeSym := ev.Make("code") + ev.bind(sz.NameSymLiteralMath, 2, func(args []sx.Object, env *Environment) sx.Object { + a := ev.getAttributes(args[0], env).AddClass("zs-math") + return ev.evalLiteral(args, a, codeSym, env) + }) + sampSym := ev.Make("samp") + ev.bind(sz.NameSymLiteralOutput, 2, func(args []sx.Object, env *Environment) sx.Object { + return ev.evalLiteral(args, nil, sampSym, env) + }) + ev.bind(sz.NameSymLiteralProg, 2, func(args []sx.Object, env *Environment) sx.Object { + return ev.evalLiteral(args, nil, codeSym, env) + }) + + ev.bind(sz.NameSymLiteralZettel, 0, nilFn) +} + +func (ev *Evaluator) makeFormatFn(tag string) EvalFn { + sym := ev.Make(tag) + return func(args []sx.Object, env *Environment) sx.Object { + a := ev.getAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() + if val, hasClass := a.Get(""); hasClass { + a = a.Remove("").AddClass(val) + } + res := ev.evalSlice(args[1:], env) + if len(a) > 0 { + res = res.Cons(ev.EvaluateAttrbute(a)) } return res.Cons(sym) } } -func (te *TransformEnv) transformQuote(args []sx.Object) sx.Object { - const langAttr = "lang" - a := te.getAttributes(args[0]) - langVal, found := a.Get(langAttr) - if found { - a = a.Remove(langAttr) - } - if val, found2 := a.Get(""); found2 { + +type quoteData struct { + primLeft, primRight string + secLeft, secRight string + nbsp bool +} + +var langQuotes = map[string]quoteData{ + "": {""", """, """, """, false}, + api.ValueLangEN: {"“", "”", "‘", "’", false}, + "de": {"„", "“", "‚", "‘", false}, + "fr": {"«", "»", "‹", "›", true}, +} + +func getQuoteData(lang string) quoteData { + langFields := strings.FieldsFunc(lang, func(r rune) bool { return r == '-' || r == '_' }) + for len(langFields) > 0 { + langSup := strings.Join(langFields, "-") + quotes, ok := langQuotes[langSup] + if ok { + return quotes + } + langFields = langFields[0 : len(langFields)-1] + } + return langQuotes[""] +} + +func getQuotes(data *quoteData, env *Environment) (string, string) { + if env.quoteNesting%2 == 0 { + return data.primLeft, data.primRight + } + return data.secLeft, data.secRight +} + +func (ev *Evaluator) evalQuote(args []sx.Object, env *Environment) sx.Object { + a := ev.getAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() + + if val, hasClass := a.Get(""); hasClass { a = a.Remove("").AddClass(val) } - res := sx.MakeList(args[1:]...) + quotes := getQuoteData(env.getLanguage()) + leftQ, rightQ := getQuotes("es, env) + + env.quoteNesting++ + res := ev.evalSlice(args[1:], env) + env.quoteNesting-- + + lastPair := res.LastPair() + if lastPair.IsNil() { + res = sx.Cons(sx.MakeList(ev.symNoEscape, sx.String(leftQ), sx.String(rightQ)), sx.Nil()) + } else { + if quotes.nbsp { + lastPair.AppendBang(sx.MakeList(ev.symNoEscape, sx.String(" "), sx.String(rightQ))) + res = res.Cons(sx.MakeList(ev.symNoEscape, sx.String(leftQ), sx.String(" "))) + } else { + lastPair.AppendBang(sx.MakeList(ev.symNoEscape, sx.String(rightQ))) + res = res.Cons(sx.MakeList(ev.symNoEscape, sx.String(leftQ))) + } + } if len(a) > 0 { - res = res.Cons(te.transformAttribute(a)) + res = res.Cons(ev.EvaluateAttrbute(a)) + return res.Cons(ev.symSpan) } - res = res.Cons(te.Make("q")) - if found { - res = sx.Nil().Cons(res).Cons(te.transformAttribute(attrs.Attributes{}.Set(langAttr, langVal))).Cons(te.symSpan) - } - return res + return res.Cons(ev.symList) } var visibleReplacer = strings.NewReplacer(" ", "\u2423") -func (te *TransformEnv) transformLiteral(args []sx.Object, a attrs.Attributes, sym *sx.Symbol) sx.Object { +func (ev *Evaluator) evalLiteral(args []sx.Object, a attrs.Attributes, sym *sx.Symbol, env *Environment) sx.Object { if a == nil { - a = te.getAttributes(args[0]) + a = ev.getAttributes(args[0], env) } a = setProgLang(a) - literal := te.getString(args[1]).String() + literal := getString(args[1], env).String() if a.HasDefault() { a = a.RemoveDefault() literal = visibleReplacer.Replace(literal) } res := sx.Nil().Cons(sx.String(literal)) if len(a) > 0 { - res = res.Cons(te.transformAttribute(a)) + res = res.Cons(ev.EvaluateAttrbute(a)) } return res.Cons(sym) } - func setProgLang(a attrs.Attributes) attrs.Attributes { if val, found := a.Get(""); found { a = a.AddClass("language-" + val).Remove("") } return a } -func (te *TransformEnv) transformHTML(args []sx.Object) sx.Object { - if s := te.getString(args[1]); s != "" && IsSafe(s.String()) { - return sx.Nil().Cons(s).Cons(te.symNoEscape) +func (ev *Evaluator) evalHTML(args []sx.Object, env *Environment) sx.Object { + if s := getString(ev.eval(args[1], env), env); s != "" && IsSafe(s.String()) { + return sx.Nil().Cons(s).Cons(ev.symNoEscape) } return nil } -func (te *TransformEnv) transformBLOB(description *sx.Pair, syntax, data sx.String) sx.Object { +func (ev *Evaluator) evalBLOB(description *sx.Pair, syntax, data sx.String) sx.Object { if data == "" { return sx.Nil() } switch syntax { case "": return sx.Nil() case api.ValueSyntaxSVG: - return sx.Nil().Cons(sx.Nil().Cons(data).Cons(te.symNoEscape)).Cons(te.symP) + return sx.Nil().Cons(sx.Nil().Cons(data).Cons(ev.symNoEscape)).Cons(ev.symP) default: - imgAttr := sx.Nil().Cons(sx.Cons(te.Make("src"), sx.String("data:image/"+syntax.String()+";base64,"+data.String()))) + imgAttr := sx.Nil().Cons(sx.Cons(ev.Make("src"), sx.String("data:image/"+syntax.String()+";base64,"+data.String()))) var sb strings.Builder - te.flattenText(&sb, description) + flattenText(&sb, description) if d := sb.String(); d != "" { - imgAttr = imgAttr.Cons(sx.Cons(te.Make("alt"), sx.String(d))) + imgAttr = imgAttr.Cons(sx.Cons(ev.Make("alt"), sx.String(d))) } - return sx.Nil().Cons(sx.Nil().Cons(imgAttr.Cons(te.symAttr)).Cons(te.Make("img"))).Cons(te.symP) + return sx.Nil().Cons(sx.Nil().Cons(imgAttr.Cons(ev.symAttr)).Cons(ev.Make("img"))).Cons(ev.symP) } } -func (te *TransformEnv) flattenText(sb *strings.Builder, lst *sx.Pair) { +func flattenText(sb *strings.Builder, lst *sx.Pair) { for elem := lst; elem != nil; elem = elem.Tail() { switch obj := elem.Car().(type) { case sx.String: sb.WriteString(obj.String()) - case *sx.Pair: - te.flattenText(sb, obj) - } - } -} - -type transformFn func([]sx.Object) sx.Object - -func (te *TransformEnv) bind(name string, minArity int16, fn transformFn) { - te.astEnv.Bind(te.astSF.MustMake(name), &sxeval.Builtin{ - Name: name, - MinArity: minArity, - MaxArity: -1, - IsPure: true, - Fn: func(_ *sxeval.Frame, args []sx.Object) (sx.Object, error) { - res := fn(args) - return res, te.err - }, - }) -} - -func (te *TransformEnv) Rebind(name string, fn func([]sx.Object, sxeval.Callable) sx.Object) { - sym := te.astSF.MustMake(name) - obj, found := te.astEnv.Lookup(sym) - if !found { - panic(sym.String()) - } - preFn, ok := sxeval.GetCallable(obj) - if !ok { - panic(sym.String()) - } - te.astEnv.Bind(sym, &sxeval.Builtin{ - Name: name, - MinArity: 0, - MaxArity: -1, - IsPure: true, - Fn: func(_ *sxeval.Frame, args []sx.Object) (sx.Object, error) { - res := fn(args, preFn) - return res, te.err - }, - }) -} - -func (te *TransformEnv) Make(name string) *sx.Symbol { return te.tr.Make(name) } -func (te *TransformEnv) getSymbol(val sx.Object) *sx.Symbol { - if te.err != nil { - return nil - } - if sym, ok := sx.GetSymbol(val); ok { - return sym - } - te.err = fmt.Errorf("%v/%T is not a symbol", val, val) - return nil -} -func (te *TransformEnv) getString(val sx.Object) sx.String { - if te.err != nil { + case *sx.Symbol: + if obj.Name() == sz.NameSymSpace { + sb.WriteByte(' ') + break + } + case *sx.Pair: + flattenText(sb, obj) + } + } +} + +func (ev *Evaluator) evalList(args []sx.Object, env *Environment) sx.Object { + return ev.evalSlice(args, env) +} +func nilFn([]sx.Object, *Environment) sx.Object { return sx.Nil() } + +func (ev *Evaluator) eval(obj sx.Object, env *Environment) sx.Object { + if env.err != nil { + return sx.Nil() + } + if sx.IsNil(obj) { + return obj + } + lst, isLst := sx.GetPair(obj) + if !isLst { + return obj + } + sym, found := sx.GetSymbol(lst.Car()) + if !found { + env.err = fmt.Errorf("symbol expected, but got %T/%v", lst.Car(), lst.Car()) + return sx.Nil() + } + name := sym.Name() + fn, found := ev.fns[name] + if !found { + env.err = fmt.Errorf("symbol %q not bound", name) + return sx.Nil() + } + var args []sx.Object + for cdr := lst.Cdr(); !sx.IsNil(cdr); { + pair, isPair := sx.GetPair(cdr) + if !isPair { + break + } + args = append(args, pair.Car()) + cdr = pair.Cdr() + } + if minArgs, hasMinArgs := ev.minArgs[name]; hasMinArgs { + if minArgs > len(args) { + env.err = fmt.Errorf("%v needs at least %d arguments, but got only %d", name, minArgs, len(args)) + return sx.Nil() + } + } + result := fn(args, env) + if env.err != nil { + return sx.Nil() + } + return result +} + +func (ev *Evaluator) evalSlice(args []sx.Object, env *Environment) *sx.Pair { + result := sx.Cons(sx.Nil(), sx.Nil()) + curr := result + for _, arg := range args { + elem := ev.eval(arg, env) + if env.err != nil { + return nil + } + curr = curr.AppendBang(elem) + } + return result.Tail() +} + +func (ev *Evaluator) evalLink(a attrs.Attributes, refValue sx.String, inline []sx.Object, env *Environment) sx.Object { + result := ev.evalSlice(inline, env) + if len(inline) == 0 { + result = sx.Nil().Cons(refValue) + } + if ev.noLinks { + return result.Cons(ev.symSpan) + } + return result.Cons(ev.EvaluateAttrbute(a)).Cons(ev.symA) +} + +func (ev *Evaluator) getSymbol(val sx.Object, env *Environment) *sx.Symbol { + if env.err == nil { + if sym, ok := sx.GetSymbol(val); ok { + return sym + } + env.err = fmt.Errorf("%v/%T is not a symbol", val, val) + } + return ev.Make("???") +} +func getString(val sx.Object, env *Environment) sx.String { + if env.err != nil { return "" } if s, ok := sx.GetString(val); ok { return s } - te.err = fmt.Errorf("%v/%T is not a string", val, val) + env.err = fmt.Errorf("%v/%T is not a string", val, val) return "" } -func (te *TransformEnv) getInt64(val sx.Object) int64 { - if te.err != nil { +func getList(val sx.Object, env *Environment) *sx.Pair { + if env.err == nil { + if res, isPair := sx.GetPair(val); isPair { + return res + } + env.err = fmt.Errorf("%v/%T is not a list", val, val) + } + return nil +} +func getInt64(val sx.Object, env *Environment) int64 { + if env.err != nil { return -1017 } if num, ok := sx.GetNumber(val); ok { return int64(num.(sx.Int64)) } - te.err = fmt.Errorf("%v/%T is not a number", val, val) + env.err = fmt.Errorf("%v/%T is not a number", val, val) return -1017 } -func (te *TransformEnv) getList(val sx.Object) *sx.Pair { - if te.err == nil { - if res, isPair := sx.GetPair(val); isPair { - return res - } - te.err = fmt.Errorf("%v/%T is not a list", val, val) - } - return nil -} -func (te *TransformEnv) getAttributes(args sx.Object) attrs.Attributes { - return sz.GetAttributes(te.getList(args)) -} - -func (te *TransformEnv) transformLink(a attrs.Attributes, refValue sx.String, inline []sx.Object) sx.Object { - result := sx.MakeList(inline...) - if len(inline) == 0 { - result = sx.Nil().Cons(refValue) - } - if te.tr.noLinks { - return result.Cons(te.symSpan) - } - return result.Cons(te.transformAttribute(a)).Cons(te.symA) -} - -func (te *TransformEnv) transformAttribute(a attrs.Attributes) *sx.Pair { - return te.tr.TransformAttrbute(a) -} - -func (te *TransformEnv) transformMeta(a attrs.Attributes) *sx.Pair { - return te.tr.TransformMeta(a) +func (ev *Evaluator) getAttributes(arg sx.Object, env *Environment) attrs.Attributes { + return sz.GetAttributes(getList(ev.eval(arg, env), env)) } var unsafeSnippets = []string{ "Change Log + +

Changes for Version 0.17.0 (pending)

+ -

Changes for Version 0.16.0 (pending)

+

Changes for Version 0.16.0 (2023-11-30)

+ * Refactor shtml transformator to support evaluating the language tree of a + zettel AST. Its API has been changes, since evaluation is now top-down, + where previous transformation was bottom.up. + * Add API call to retrieve role zettel. + * Added constants for role zettel and to mark text within zettelmarkup.

Changes for Version 0.15.0 (2023-10-26)

- * Tag zettel: API, constant values - * Refactorings b/c Sx + * Tag zettel: API, constant values + * Refactorings b/c Sx

Changes for Version 0.14.0 (2023-09-23)

- * Remove support for JSON encoding + * Remove support for JSON encoding

Changes for Version 0.13.0 (2023-08-07)

* API uses plain data or sx data, but no JSON encoded data. * Dependency sx is now hosted on Fossil repository, same for this library. Index: www/index.wiki ================================================================== --- www/index.wiki +++ www/index.wiki @@ -1,12 +1,36 @@ Home This repository contains Go client software to access [https://zettelstore.de|Zettelstore] via its API. -

Latest Release: 0.15.0 (2023-10-26)

- * [./changes.wiki#0_15|Change summary] - * [/timeline?p=v0.15.0&bt=v0.13.0&y=ci|Check-ins for version 0.15.0], - [/vdiff?to=v0.15.0&from=v0.13.0|content diff] - * [/timeline?df=v0.15.0&y=ci|Check-ins derived from the 0.15.0 release], - [/vdiff?from=v0.15.0&to=trunk|content diff] +

Latest Release: 0.16.0 (2023-11-30)

+ * [./changes.wiki#0_16|Change summary] + * [/timeline?p=v0.16.0&bt=v0.15.0&y=ci|Check-ins for version 0.16.0], + [/vdiff?to=v0.16.0&from=v0.15.0|content diff] + * [/timeline?df=v0.16.0&y=ci|Check-ins derived from the 0.16.0 release], + [/vdiff?from=v0.16.0&to=trunk|content diff] * [/timeline?t=release|Timeline of all past releases] + +

Use instructions

+ +If you want to import this library into your own [https://go.dev/|Go] software, +you must execute a go get command. Since Go treats non-standard +software and non-standard platforms quite badly, you must use some non-standard +commands. + +First, you must install the version control system +[https://fossil-scm.org|Fossil], which is a superior solution compared to Git, +in too many use cases. It is just a single executable, nothing more. Make sure, +it is in your search path for commands. + +How you can execute the following Go command to retrieve a given version of +this library: + +GOVCS=zettelstore.de:fossil go get -x +zettelstore.de/client.fossil@HASH + +where HASH is the hash value of the commit you want to use. + +Go currently seems not to support software versions when the software is +managed by Fossil. This explains the need for the hash value. However, this +methods works and you can use the client software to access a Zettelstore.