From 2304921feefff56d34d4b673cba8a491f399f2a6 Mon Sep 17 00:00:00 2001 From: Daniel <845765@qq.com> Date: Fri, 13 Oct 2023 10:42:51 +0800 Subject: [PATCH 1/2] :art: Update flashcard user guide --- .../20221223221636-ms2b4w9.sy | 38 ++++++++++++++++--- .../20221223215557-o6gfsoy.sy | 38 +++++++++++++++---- .../20221223221501-mops33i.sy | 32 +++++++++++++++- 3 files changed, 94 insertions(+), 14 deletions(-) diff --git a/app/guide/20210808180117-6v0mkxr/20200923234011-ieuun1p/20210808180303-xaduj2o/20221223221636-ms2b4w9.sy b/app/guide/20210808180117-6v0mkxr/20200923234011-ieuun1p/20210808180303-xaduj2o/20221223221636-ms2b4w9.sy index 4f16b5621..26a841453 100644 --- a/app/guide/20210808180117-6v0mkxr/20200923234011-ieuun1p/20210808180303-xaduj2o/20221223221636-ms2b4w9.sy +++ b/app/guide/20210808180117-6v0mkxr/20200923234011-ieuun1p/20210808180303-xaduj2o/20221223221636-ms2b4w9.sy @@ -5,7 +5,7 @@ "Properties": { "id": "20221223221636-ms2b4w9", "title": "Flashcards", - "updated": "20230820185231" + "updated": "20231013104018" }, "Children": [ { @@ -87,7 +87,7 @@ "ListData": {}, "Properties": { "id": "20230219092249-yzjjb1o", - "updated": "20230219092854" + "updated": "20231013104018" }, "Children": [ { @@ -132,7 +132,7 @@ }, "Properties": { "id": "20230219092249-xdxmusm", - "updated": "20230219092407" + "updated": "20231013103949" }, "Children": [ { @@ -140,12 +140,12 @@ "Type": "NodeParagraph", "Properties": { "id": "20230219092249-04pxvxk", - "updated": "20230219092407" + "updated": "20231013103949" }, "Children": [ { "Type": "NodeText", - "Data": "If the super block is set as a flashcard, the first sub-block of the super block will be regarded as a question, and the rest of the sub-blocks will be regarded as an answer" + "Data": "If a super block is set as a flashcard, the first sub-block of the super block will be regarded as a question, and the rest of the sub-blocks will be regarded as an answer" } ] } @@ -178,6 +178,34 @@ ] } ] + }, + { + "ID": "20231013103926-u4hxd38", + "Type": "NodeListItem", + "ListData": { + "BulletChar": 42, + "Marker": "Kg==" + }, + "Properties": { + "id": "20231013103926-u4hxd38", + "updated": "20231013104018" + }, + "Children": [ + { + "ID": "20231013103926-o3y1n5a", + "Type": "NodeParagraph", + "Properties": { + "id": "20231013103926-o3y1n5a", + "updated": "20231013104018" + }, + "Children": [ + { + "Type": "NodeText", + "Data": "If a heading block is set as an flashcard, the heading block will be treated as a question and the blocks below it will be treated answers" + } + ] + } + ] } ] }, diff --git a/app/guide/20210808180117-czj9bvb/20200812220555-lj3enxa/20210808180321-hbvl5c2/20221223215557-o6gfsoy.sy b/app/guide/20210808180117-czj9bvb/20200812220555-lj3enxa/20210808180321-hbvl5c2/20221223215557-o6gfsoy.sy index 7bc23d986..8e532a5e9 100644 --- a/app/guide/20210808180117-czj9bvb/20200812220555-lj3enxa/20210808180321-hbvl5c2/20221223215557-o6gfsoy.sy +++ b/app/guide/20210808180117-czj9bvb/20200812220555-lj3enxa/20210808180321-hbvl5c2/20221223215557-o6gfsoy.sy @@ -5,7 +5,7 @@ "Properties": { "id": "20221223215557-o6gfsoy", "title": "闪卡", - "updated": "20230820185147" + "updated": "20231013103850" }, "Children": [ { @@ -87,7 +87,7 @@ "ListData": {}, "Properties": { "id": "20230219085658-xnrf7rf", - "updated": "20230219092140" + "updated": "20231013103850" }, "Children": [ { @@ -112,11 +112,7 @@ "Children": [ { "Type": "NodeText", - "Data": "内容块" - }, - { - "Type": "NodeText", - "Data": "中的 " + "Data": "内容块中的 " }, { "Type": "NodeTextMark", @@ -186,6 +182,34 @@ ] } ] + }, + { + "ID": "20231013103816-b0k440b", + "Type": "NodeListItem", + "ListData": { + "BulletChar": 42, + "Marker": "Kg==" + }, + "Properties": { + "id": "20231013103816-b0k440b", + "updated": "20231013103850" + }, + "Children": [ + { + "ID": "20231013103816-sx34w1r", + "Type": "NodeParagraph", + "Properties": { + "id": "20231013103816-sx34w1r", + "updated": "20231013103850" + }, + "Children": [ + { + "Type": "NodeText", + "Data": "如果将标题块设置为上卡,则该标题块会被视作问题,其下方块会被视作答案" + } + ] + } + ] } ] }, diff --git a/app/guide/20211226090932-5lcq56f/20211226115423-d5z1joq/20211226121203-rjjngpz/20221223221501-mops33i.sy b/app/guide/20211226090932-5lcq56f/20211226115423-d5z1joq/20211226121203-rjjngpz/20221223221501-mops33i.sy index b289fb8f5..f0e504181 100644 --- a/app/guide/20211226090932-5lcq56f/20211226115423-d5z1joq/20211226121203-rjjngpz/20221223221501-mops33i.sy +++ b/app/guide/20211226090932-5lcq56f/20211226115423-d5z1joq/20211226121203-rjjngpz/20221223221501-mops33i.sy @@ -5,7 +5,7 @@ "Properties": { "id": "20221223221501-mops33i", "title": "閃卡", - "updated": "20230820185212" + "updated": "20231013104052" }, "Children": [ { @@ -87,7 +87,7 @@ "ListData": {}, "Properties": { "id": "20230219092911-8074ovh", - "updated": "20230219092911" + "updated": "20231013104052" }, "Children": [ { @@ -182,6 +182,34 @@ ] } ] + }, + { + "ID": "20231013104042-7impulf", + "Type": "NodeListItem", + "ListData": { + "BulletChar": 42, + "Marker": "Kg==" + }, + "Properties": { + "id": "20231013104042-7impulf", + "updated": "20231013104052" + }, + "Children": [ + { + "ID": "20231013104042-wzwhxmr", + "Type": "NodeParagraph", + "Properties": { + "id": "20231013104042-wzwhxmr", + "updated": "20231013104052" + }, + "Children": [ + { + "Type": "NodeText", + "Data": "如果將標題塊設為上卡,則該標題塊會被視為問題,其下方塊會被視為答案" + } + ] + } + ] } ] }, From 3de7781b1c78fb35ba3e90b729015bd0d9cfaa45 Mon Sep 17 00:00:00 2001 From: Daniel <845765@qq.com> Date: Fri, 13 Oct 2023 10:44:29 +0800 Subject: [PATCH 2/2] :art: Supports searching database view content https://github.com/siyuan-note/siyuan/issues/9419 --- kernel/av/table.go | 16 +- kernel/model/attribute_view.go | 118 ++++++------ kernel/treenode/node.go | 318 +++++++++++++++++++++++++++++++-- 3 files changed, 371 insertions(+), 81 deletions(-) diff --git a/kernel/av/table.go b/kernel/av/table.go index 4ca323915..90153d1b4 100644 --- a/kernel/av/table.go +++ b/kernel/av/table.go @@ -73,14 +73,6 @@ const ( CalcOperatorLatest CalcOperator = "Latest" ) -type TableCell struct { - ID string `json:"id"` - Value *Value `json:"value"` - ValueType KeyType `json:"valueType"` - Color string `json:"color"` - BgColor string `json:"bgColor"` -} - func (value *Value) Compare(other *Value) int { if nil == value { return -1 @@ -566,6 +558,14 @@ type TableColumn struct { Template string `json:"template"` // 模板内容 } +type TableCell struct { + ID string `json:"id"` + Value *Value `json:"value"` + ValueType KeyType `json:"valueType"` + Color string `json:"color"` + BgColor string `json:"bgColor"` +} + type TableRow struct { ID string `json:"id"` Cells []*TableCell `json:"cells"` diff --git a/kernel/model/attribute_view.go b/kernel/model/attribute_view.go index ae180d554..de3004e65 100644 --- a/kernel/model/attribute_view.go +++ b/kernel/model/attribute_view.go @@ -39,65 +39,6 @@ type BlockAttributeViewKeys struct { KeyValues []*av.KeyValues `json:"keyValues"` } -func renderTemplateCol(ial map[string]string, tplContent string, rowValues []*av.KeyValues) string { - if "" == ial["id"] { - block := getRowBlockValue(rowValues) - ial["id"] = block.Block.ID - } - if "" == ial["updated"] { - block := getRowBlockValue(rowValues) - ial["updated"] = time.UnixMilli(block.Block.Updated).Format("20060102150405") - } - - funcMap := sprig.TxtFuncMap() - goTpl := template.New("").Delims(".action{", "}") - tpl, tplErr := goTpl.Funcs(funcMap).Parse(tplContent) - if nil != tplErr { - logging.LogWarnf("parse template [%s] failed: %s", tplContent, tplErr) - return "" - } - - buf := &bytes.Buffer{} - dataModel := map[string]interface{}{} // 复制一份 IAL 以避免修改原始数据 - for k, v := range ial { - dataModel[k] = v - - // Database template column supports `created` and `updated` built-in variables https://github.com/siyuan-note/siyuan/issues/9364 - createdStr := ial["id"] - if "" != createdStr { - createdStr = createdStr[:len("20060102150405")] - } - created, parseErr := time.ParseInLocation("20060102150405", createdStr, time.Local) - if nil == parseErr { - dataModel["created"] = created - } else { - logging.LogWarnf("parse created [%s] failed: %s", createdStr, parseErr) - dataModel["created"] = time.Now() - } - updatedStr := ial["updated"] - updated, parseErr := time.ParseInLocation("20060102150405", updatedStr, time.Local) - if nil == parseErr { - dataModel["updated"] = updated - } else { - dataModel["updated"] = time.Now() - } - } - for _, rowValue := range rowValues { - if 0 < len(rowValue.Values) { - v := rowValue.Values[0] - if av.KeyTypeNumber == v.Type { - dataModel[rowValue.Key.Name] = v.Number.Content - } else { - dataModel[rowValue.Key.Name] = v.String() - } - } - } - if err := tpl.Execute(buf, dataModel); nil != err { - logging.LogWarnf("execute template [%s] failed: %s", tplContent, err) - } - return buf.String() -} - func GetBlockAttributeViewKeys(blockID string) (ret []*BlockAttributeViewKeys) { waitForSyncingStorages() @@ -292,6 +233,65 @@ func RenderAttributeView(avID string) (viewable av.Viewable, attrView *av.Attrib return } +func renderTemplateCol(ial map[string]string, tplContent string, rowValues []*av.KeyValues) string { + if "" == ial["id"] { + block := getRowBlockValue(rowValues) + ial["id"] = block.Block.ID + } + if "" == ial["updated"] { + block := getRowBlockValue(rowValues) + ial["updated"] = time.UnixMilli(block.Block.Updated).Format("20060102150405") + } + + funcMap := sprig.TxtFuncMap() + goTpl := template.New("").Delims(".action{", "}") + tpl, tplErr := goTpl.Funcs(funcMap).Parse(tplContent) + if nil != tplErr { + logging.LogWarnf("parse template [%s] failed: %s", tplContent, tplErr) + return "" + } + + buf := &bytes.Buffer{} + dataModel := map[string]interface{}{} // 复制一份 IAL 以避免修改原始数据 + for k, v := range ial { + dataModel[k] = v + + // Database template column supports `created` and `updated` built-in variables https://github.com/siyuan-note/siyuan/issues/9364 + createdStr := ial["id"] + if "" != createdStr { + createdStr = createdStr[:len("20060102150405")] + } + created, parseErr := time.ParseInLocation("20060102150405", createdStr, time.Local) + if nil == parseErr { + dataModel["created"] = created + } else { + logging.LogWarnf("parse created [%s] failed: %s", createdStr, parseErr) + dataModel["created"] = time.Now() + } + updatedStr := ial["updated"] + updated, parseErr := time.ParseInLocation("20060102150405", updatedStr, time.Local) + if nil == parseErr { + dataModel["updated"] = updated + } else { + dataModel["updated"] = time.Now() + } + } + for _, rowValue := range rowValues { + if 0 < len(rowValue.Values) { + v := rowValue.Values[0] + if av.KeyTypeNumber == v.Type { + dataModel[rowValue.Key.Name] = v.Number.Content + } else { + dataModel[rowValue.Key.Name] = v.String() + } + } + } + if err := tpl.Execute(buf, dataModel); nil != err { + logging.LogWarnf("execute template [%s] failed: %s", tplContent, err) + } + return buf.String() +} + func renderAttributeViewTable(attrView *av.AttributeView, view *av.View) (ret *av.Table, err error) { ret = &av.Table{ ID: view.ID, diff --git a/kernel/treenode/node.go b/kernel/treenode/node.go index 90211fe0f..3110a7657 100644 --- a/kernel/treenode/node.go +++ b/kernel/treenode/node.go @@ -18,9 +18,13 @@ package treenode import ( "bytes" + "github.com/Masterminds/sprig/v3" "github.com/siyuan-note/siyuan/kernel/av" + "github.com/siyuan-note/siyuan/kernel/cache" "strings" "sync" + "text/template" + "time" "github.com/88250/gulu" "github.com/88250/lute" @@ -140,19 +144,10 @@ func NodeStaticContent(node *ast.Node, excludeTypes []string, includeTextMarkATi if ast.NodeDocument == node.Type { return node.IALAttr("title") - } else if ast.NodeAttributeView == node.Type { - if "" != node.AttributeViewID { - attrView, err := av.ParseAttributeView(node.AttributeViewID) - if nil == err { - buf := bytes.Buffer{} - for _, v := range attrView.Views { - buf.WriteString(v.Name) - buf.WriteString(" ") - } - return strings.TrimSpace(buf.String()) - } - } - return "" + } + + if ast.NodeAttributeView == node.Type { + return getAttributeViewContent(node.AttributeViewID) } buf := bytes.Buffer{} @@ -165,7 +160,7 @@ func NodeStaticContent(node *ast.Node, excludeTypes []string, includeTextMarkATi if n.IsContainerBlock() { if !lastSpace { - buf.WriteString(" ") + buf.WriteByte(' ') lastSpace = true } return ast.WalkContinue @@ -479,3 +474,298 @@ func IsChartCodeBlockCode(code *ast.Node) bool { language = strings.ReplaceAll(language, editor.Caret, "") return render.NoHighlight(language) } + +func getAttributeViewContent(avID string) (content string) { + if "" == avID { + return + } + + attrView, err := av.ParseAttributeView(avID) + if nil != err { + logging.LogErrorf("parse attribute view [%s] failed: %s", avID, err) + return + } + + buf := bytes.Buffer{} + for _, v := range attrView.Views { + buf.WriteString(v.Name) + buf.WriteByte(' ') + } + + if 1 > len(attrView.Views) { + content = strings.TrimSpace(buf.String()) + return + } + + var view *av.View + for _, v := range attrView.Views { + if av.LayoutTypeTable == v.LayoutType { + view = v + break + } + } + if nil == view { + content = buf.String() + return + } + + table, err := renderAttributeViewTable(attrView, view) + if nil != err { + content = strings.TrimSpace(buf.String()) + return + } + + for _, col := range table.Columns { + buf.WriteString(col.Name) + buf.WriteByte(' ') + } + + for _, row := range table.Rows { + for _, cell := range row.Cells { + if nil == cell.Value { + continue + } + buf.WriteString(cell.Value.String()) + buf.WriteByte(' ') + } + } + + content = strings.TrimSpace(buf.String()) + return +} + +func renderAttributeViewTable(attrView *av.AttributeView, view *av.View) (ret *av.Table, err error) { + ret = &av.Table{ + ID: view.ID, + Name: view.Name, + Columns: []*av.TableColumn{}, + Rows: []*av.TableRow{}, + } + + // 组装列 + for _, col := range view.Table.Columns { + key, getErr := attrView.GetKey(col.ID) + if nil != getErr { + err = getErr + return + } + + ret.Columns = append(ret.Columns, &av.TableColumn{ + ID: key.ID, + Name: key.Name, + Type: key.Type, + Icon: key.Icon, + Options: key.Options, + NumberFormat: key.NumberFormat, + Template: key.Template, + Wrap: col.Wrap, + Hidden: col.Hidden, + Width: col.Width, + Calc: col.Calc, + }) + } + + // 生成行 + rows := map[string][]*av.KeyValues{} + for _, keyValues := range attrView.KeyValues { + for _, val := range keyValues.Values { + values := rows[val.BlockID] + if nil == values { + values = []*av.KeyValues{{Key: keyValues.Key, Values: []*av.Value{val}}} + } else { + values = append(values, &av.KeyValues{Key: keyValues.Key, Values: []*av.Value{val}}) + } + rows[val.BlockID] = values + } + } + + // 过滤掉不存在的行 + var notFound []string + for blockID, keyValues := range rows { + blockValue := getRowBlockValue(keyValues) + if nil == blockValue { + notFound = append(notFound, blockID) + continue + } + + if blockValue.IsDetached { + continue + } + + if nil != blockValue.Block && "" == blockValue.Block.ID { + notFound = append(notFound, blockID) + continue + } + + if GetBlockTree(blockID) == nil { + notFound = append(notFound, blockID) + } + } + for _, blockID := range notFound { + delete(rows, blockID) + } + + // 生成行单元格 + for rowID, row := range rows { + var tableRow av.TableRow + for _, col := range ret.Columns { + var tableCell *av.TableCell + for _, keyValues := range row { + if keyValues.Key.ID == col.ID { + tableCell = &av.TableCell{ + ID: keyValues.Values[0].ID, + Value: keyValues.Values[0], + ValueType: col.Type, + } + break + } + } + if nil == tableCell { + tableCell = &av.TableCell{ + ID: ast.NewNodeID(), + ValueType: col.Type, + } + } + tableRow.ID = rowID + + switch tableCell.ValueType { + case av.KeyTypeNumber: // 格式化数字 + if nil != tableCell.Value && nil != tableCell.Value.Number && tableCell.Value.Number.IsNotEmpty { + tableCell.Value.Number.Format = col.NumberFormat + tableCell.Value.Number.FormatNumber() + } + case av.KeyTypeTemplate: // 渲染模板列 + tableCell.Value = &av.Value{ID: tableCell.ID, KeyID: col.ID, BlockID: rowID, Type: av.KeyTypeTemplate, Template: &av.ValueTemplate{Content: col.Template}} + case av.KeyTypeCreated: // 填充创建时间列值,后面再渲染 + tableCell.Value = &av.Value{ID: tableCell.ID, KeyID: col.ID, BlockID: rowID, Type: av.KeyTypeCreated} + case av.KeyTypeUpdated: // 填充更新时间列值,后面再渲染 + tableCell.Value = &av.Value{ID: tableCell.ID, KeyID: col.ID, BlockID: rowID, Type: av.KeyTypeUpdated} + } + + tableRow.Cells = append(tableRow.Cells, tableCell) + } + ret.Rows = append(ret.Rows, &tableRow) + } + + // 渲染自动生成的列值,比如模板列、创建时间列和更新时间列 + for _, row := range ret.Rows { + for _, cell := range row.Cells { + switch cell.ValueType { + case av.KeyTypeTemplate: // 渲染模板列 + keyValues := rows[row.ID] + ial := map[string]string{} + block := row.GetBlockValue() + if !block.IsDetached { + ial = cache.GetBlockIAL(row.ID) + if nil == ial { + ial = map[string]string{} + } + } + content := renderTemplateCol(ial, cell.Value.Template.Content, keyValues) + cell.Value.Template.Content = content + case av.KeyTypeCreated: // 渲染创建时间 + createdStr := row.ID[:len("20060102150405")] + created, parseErr := time.ParseInLocation("20060102150405", createdStr, time.Local) + if nil == parseErr { + cell.Value.Created = av.NewFormattedValueCreated(created.UnixMilli(), 0, av.CreatedFormatNone) + cell.Value.Created.IsNotEmpty = true + } else { + cell.Value.Created = av.NewFormattedValueCreated(time.Now().UnixMilli(), 0, av.CreatedFormatNone) + } + case av.KeyTypeUpdated: // 渲染更新时间 + ial := map[string]string{} + block := row.GetBlockValue() + if !block.IsDetached { + ial = cache.GetBlockIAL(row.ID) + if nil == ial { + ial = map[string]string{} + } + } + updatedStr := ial["updated"] + if "" == updatedStr { + block := row.GetBlockValue() + cell.Value.Updated = av.NewFormattedValueUpdated(block.Block.Updated, 0, av.UpdatedFormatNone) + cell.Value.Updated.IsNotEmpty = true + } else { + updated, parseErr := time.ParseInLocation("20060102150405", updatedStr, time.Local) + if nil == parseErr { + cell.Value.Updated = av.NewFormattedValueUpdated(updated.UnixMilli(), 0, av.UpdatedFormatNone) + cell.Value.Updated.IsNotEmpty = true + } else { + cell.Value.Updated = av.NewFormattedValueUpdated(time.Now().UnixMilli(), 0, av.UpdatedFormatNone) + } + } + } + } + } + return +} + +func renderTemplateCol(ial map[string]string, tplContent string, rowValues []*av.KeyValues) string { + if "" == ial["id"] { + block := getRowBlockValue(rowValues) + ial["id"] = block.Block.ID + } + if "" == ial["updated"] { + block := getRowBlockValue(rowValues) + ial["updated"] = time.UnixMilli(block.Block.Updated).Format("20060102150405") + } + + funcMap := sprig.TxtFuncMap() + goTpl := template.New("").Delims(".action{", "}") + tpl, tplErr := goTpl.Funcs(funcMap).Parse(tplContent) + if nil != tplErr { + logging.LogWarnf("parse template [%s] failed: %s", tplContent, tplErr) + return "" + } + + buf := &bytes.Buffer{} + dataModel := map[string]interface{}{} // 复制一份 IAL 以避免修改原始数据 + for k, v := range ial { + dataModel[k] = v + + // Database template column supports `created` and `updated` built-in variables https://github.com/siyuan-note/siyuan/issues/9364 + createdStr := ial["id"] + if "" != createdStr { + createdStr = createdStr[:len("20060102150405")] + } + created, parseErr := time.ParseInLocation("20060102150405", createdStr, time.Local) + if nil == parseErr { + dataModel["created"] = created + } else { + logging.LogWarnf("parse created [%s] failed: %s", createdStr, parseErr) + dataModel["created"] = time.Now() + } + updatedStr := ial["updated"] + updated, parseErr := time.ParseInLocation("20060102150405", updatedStr, time.Local) + if nil == parseErr { + dataModel["updated"] = updated + } else { + dataModel["updated"] = time.Now() + } + } + for _, rowValue := range rowValues { + if 0 < len(rowValue.Values) { + v := rowValue.Values[0] + if av.KeyTypeNumber == v.Type { + dataModel[rowValue.Key.Name] = v.Number.Content + } else { + dataModel[rowValue.Key.Name] = v.String() + } + } + } + if err := tpl.Execute(buf, dataModel); nil != err { + logging.LogWarnf("execute template [%s] failed: %s", tplContent, err) + } + return buf.String() +} + +func getRowBlockValue(keyValues []*av.KeyValues) (ret *av.Value) { + for _, kv := range keyValues { + if av.KeyTypeBlock == kv.Key.Type && 0 < len(kv.Values) { + ret = kv.Values[0] + break + } + } + return +}