// SiYuan - Refactor your thinking // Copyright (c) 2020-present, b3log.org // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "encoding/json" "fmt" "io" "os" "strings" "text/tabwriter" "github.com/siyuan-note/siyuan/kernel/filesys" "github.com/siyuan-note/siyuan/kernel/model" "github.com/siyuan-note/siyuan/kernel/treenode" "github.com/siyuan-note/siyuan/kernel/util" "github.com/spf13/cobra" ) var blockCmd = &cobra.Command{ Use: "block", Short: "Block operations", } // ─── Read ────────────────────────────────────────────────────────────────────── var blockGetCmd = &cobra.Command{ Use: "get --id ", Short: "Get block info", RunE: func(cmd *cobra.Command, args []string) error { id, _ := cmd.Flags().GetString("id") if id == "" { return fmt.Errorf("--id is required") } block, err := model.GetBlock(id, nil) if err != nil { return err } switch outputFormat { case "json": data, _ := json.MarshalIndent(block, "", " ") fmt.Println(string(data)) default: fmt.Printf("ID: %s\n", block.ID) fmt.Printf("Type: %s\n", block.Type) fmt.Printf("Name: %s\n", block.Name) fmt.Printf("Box: %s\n", block.Box) fmt.Printf("HPath: %s\n", block.HPath) if block.Content != "" { fmt.Printf("Content: %s\n", truncate(block.Content, 200)) } if block.Markdown != "" { fmt.Printf("Markdown: %s\n", truncate(block.Markdown, 200)) } } return nil }, } var blockChildrenCmd = &cobra.Command{ Use: "children --id ", Short: "Get child blocks", RunE: func(cmd *cobra.Command, args []string) error { id, _ := cmd.Flags().GetString("id") if id == "" { return fmt.Errorf("--id is required") } children := model.GetChildBlocks(id) switch outputFormat { case "json": data, _ := json.MarshalIndent(children, "", " ") fmt.Println(string(data)) default: w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "ID\tTYPE\tCONTENT") for _, c := range children { fmt.Fprintf(w, "%s\t%s\t%s\n", c.ID, c.Type, truncate(c.Content, 80)) } w.Flush() } return nil }, } var blockBreadcrumbCmd = &cobra.Command{ Use: "breadcrumb --id ", Short: "Get block breadcrumb", RunE: func(cmd *cobra.Command, args []string) error { id, _ := cmd.Flags().GetString("id") if id == "" { return fmt.Errorf("--id is required") } paths, err := model.BuildBlockBreadcrumb(id, nil) if err != nil { return err } switch outputFormat { case "json": data, _ := json.MarshalIndent(paths, "", " ") fmt.Println(string(data)) default: w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "ID\tNAME\tTYPE") for _, p := range paths { fmt.Fprintf(w, "%s\t%s\t%s\n", p.ID, p.Name, p.Type) } w.Flush() } return nil }, } var blockDomCmd = &cobra.Command{ Use: "dom --id ", Short: "Get block DOM", RunE: func(cmd *cobra.Command, args []string) error { id, _ := cmd.Flags().GetString("id") if id == "" { return fmt.Errorf("--id is required") } fmt.Print(model.GetBlockDOM(id)) return nil }, } var blockKramdownCmd = &cobra.Command{ Use: "kramdown --id ", Short: "Get block kramdown", RunE: func(cmd *cobra.Command, args []string) error { id, _ := cmd.Flags().GetString("id") if id == "" { return fmt.Errorf("--id is required") } mode, _ := cmd.Flags().GetString("mode") if mode == "" { mode = "md" } fmt.Print(model.GetBlockKramdown(id, mode)) return nil }, } var blockStatCmd = &cobra.Command{ Use: "stat --id ", Short: "Get block content statistics", RunE: func(cmd *cobra.Command, args []string) error { id, _ := cmd.Flags().GetString("id") if id == "" { return fmt.Errorf("--id is required") } stat := filesys.StatTree(id) if stat == nil { return fmt.Errorf("document not found or empty") } switch outputFormat { case "json": data, _ := json.MarshalIndent(stat, "", " ") fmt.Println(string(data)) default: fmt.Printf("Characters: %d\n", stat.RuneCount) fmt.Printf("Words: %d\n", stat.WordCount) fmt.Printf("Blocks: %d\n", stat.BlockCount) fmt.Printf("Links: %d\n", stat.LinkCount) fmt.Printf("Images: %d\n", stat.ImageCount) fmt.Printf("Refs: %d\n", stat.RefCount) } return nil }, } // ─── Write ───────────────────────────────────────────────────────────────────── var blockInsertCmd = &cobra.Command{ Use: "insert --parent [--data | --file ]", Short: "Insert block", RunE: func(cmd *cobra.Command, args []string) error { parentID, _ := cmd.Flags().GetString("parent") previousID, _ := cmd.Flags().GetString("previous") if parentID == "" { return fmt.Errorf("--parent is required") } if dryRun { fmt.Printf("[dry-run] Would insert block under parent %s\n", parentID) if previousID != "" { fmt.Printf(" after previous sibling %s\n", previousID) } return nil } // 仅靠 parentID 定位目标时(无 previousID),目标必须是容器块,否则非法嵌套 if previousID == "" { if err := treenode.CheckContainerParent(parentID); err != nil { return err } } data, err := resolveData(cmd) if err != nil { return err } dom := markdownToBlockDOM(data) transactions := []*model.Transaction{{ DoOperations: []*model.Operation{{ Action: "insert", Data: dom, ParentID: parentID, PreviousID: previousID, }}, }} model.PerformTransactions(&transactions) model.FlushTxQueue() if bt := treenode.GetBlockTree(parentID); bt != nil { model.AppendPushReloadProtyleEntry(bt.RootID) } fmt.Println("ok") return nil }, } var blockAppendCmd = &cobra.Command{ Use: "append --parent [--data | --file ]", Short: "Append block", RunE: func(cmd *cobra.Command, args []string) error { parentID, _ := cmd.Flags().GetString("parent") if parentID == "" { return fmt.Errorf("--parent is required") } if dryRun { fmt.Printf("[dry-run] Would append block to parent %s\n", parentID) return nil } // append 只用 parentID 定位目标,目标必须是容器块,否则非法嵌套 if err := treenode.CheckContainerParent(parentID); err != nil { return err } data, err := resolveData(cmd) if err != nil { return err } dom := markdownToBlockDOM(data) transactions := []*model.Transaction{{ DoOperations: []*model.Operation{{ Action: "appendInsert", Data: dom, ParentID: parentID, }}, }} model.PerformTransactions(&transactions) model.FlushTxQueue() if bt := treenode.GetBlockTree(parentID); bt != nil { model.AppendPushReloadProtyleEntry(bt.RootID) } fmt.Println("ok") return nil }, } var blockPrependCmd = &cobra.Command{ Use: "prepend --parent [--data | --file ]", Short: "Prepend block", RunE: func(cmd *cobra.Command, args []string) error { parentID, _ := cmd.Flags().GetString("parent") if parentID == "" { return fmt.Errorf("--parent is required") } if dryRun { fmt.Printf("[dry-run] Would prepend block to parent %s\n", parentID) return nil } // prepend 只用 parentID 定位目标,目标必须是容器块,否则非法嵌套 if err := treenode.CheckContainerParent(parentID); err != nil { return err } data, err := resolveData(cmd) if err != nil { return err } dom := markdownToBlockDOM(data) transactions := []*model.Transaction{{ DoOperations: []*model.Operation{{ Action: "prependInsert", Data: dom, ParentID: parentID, }}, }} model.PerformTransactions(&transactions) model.FlushTxQueue() if bt := treenode.GetBlockTree(parentID); bt != nil { model.AppendPushReloadProtyleEntry(bt.RootID) } fmt.Println("ok") return nil }, } var blockUpdateCmd = &cobra.Command{ Use: "update --id [--data | --file ]", Short: "Update block", RunE: func(cmd *cobra.Command, args []string) error { id, _ := cmd.Flags().GetString("id") if id == "" { return fmt.Errorf("--id is required") } if dryRun { fmt.Printf("[dry-run] Would update block %s\n", id) return nil } data, err := resolveData(cmd) if err != nil { return err } dom := markdownToBlockDOM(data) transactions := []*model.Transaction{{ DoOperations: []*model.Operation{{ Action: "update", Data: dom, ID: id, }}, }} model.PerformTransactions(&transactions) model.FlushTxQueue() if bt := treenode.GetBlockTree(id); bt != nil { model.AppendPushReloadProtyleEntry(bt.RootID) } fmt.Println("ok") return nil }, } var blockDeleteCmd = &cobra.Command{ Use: "delete --id ", Short: "Delete block", RunE: func(cmd *cobra.Command, args []string) error { id, _ := cmd.Flags().GetString("id") if id == "" { return fmt.Errorf("--id is required") } if dryRun { fmt.Printf("[dry-run] Would delete block %s\n", id) return nil } bt := treenode.GetBlockTree(id) transactions := []*model.Transaction{{ DoOperations: []*model.Operation{{ Action: "delete", ID: id, }}, }} model.PerformTransactions(&transactions) model.FlushTxQueue() if bt != nil { model.AppendPushReloadProtyleEntry(bt.RootID) } fmt.Println(id) return nil }, } var blockMoveCmd = &cobra.Command{ Use: "move --id --parent ", Short: "Move block", RunE: func(cmd *cobra.Command, args []string) error { id, _ := cmd.Flags().GetString("id") parentID, _ := cmd.Flags().GetString("parent") previousID, _ := cmd.Flags().GetString("previous") if id == "" || parentID == "" { return fmt.Errorf("--id and --parent are required") } if dryRun { fmt.Printf("[dry-run] Would move block %s to parent %s\n", id, parentID) if previousID != "" { fmt.Printf(" after previous sibling %s\n", previousID) } return nil } // 仅靠 parentID 定位目标时(无 previousID),目标必须是容器块,否则非法嵌套 if previousID == "" { if err := treenode.CheckListItemNesting(parentID, id); err != nil { return err } if err := treenode.CheckContainerParent(parentID); err != nil { return err } } transactions := []*model.Transaction{{ DoOperations: []*model.Operation{{ Action: "move", ID: id, ParentID: parentID, PreviousID: previousID, }}, }} model.PerformTransactions(&transactions) model.FlushTxQueue() if bt := treenode.GetBlockTree(id); bt != nil { model.AppendPushReloadProtyleEntry(bt.RootID) } fmt.Println("ok") return nil }, } var blockBatchGetCmd = &cobra.Command{ Use: "batch-get --ids id1,id2,...", Short: "Batch get block info", RunE: func(cmd *cobra.Command, args []string) error { idsStr, _ := cmd.Flags().GetString("ids") if idsStr == "" { return fmt.Errorf("--ids is required") } ids := splitIDs(idsStr) if len(ids) == 0 { return fmt.Errorf("no valid IDs provided") } infos := model.GetDocsInfo(ids, false, false) switch outputFormat { case "json": data, _ := json.MarshalIndent(infos, "", " ") fmt.Println(string(data)) default: w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "ID\tNAME\tROOTID\tREFCOUNT") for _, info := range infos { fmt.Fprintf(w, "%s\t%s\t%s\t%d\n", info.ID, info.Name, info.RootID, info.RefCount) } for _, id := range ids { found := false for _, info := range infos { if info.ID == id { found = true break } } if !found { fmt.Fprintf(w, "%s\tnot found\t\t\n", id) } } w.Flush() } return nil }, } var blockBatchKramdownCmd = &cobra.Command{ Use: "batch-kramdown --ids id1,id2,...", Short: "Batch get block kramdown", RunE: func(cmd *cobra.Command, args []string) error { idsStr, _ := cmd.Flags().GetString("ids") if idsStr == "" { return fmt.Errorf("--ids is required") } ids := splitIDs(idsStr) if len(ids) == 0 { return fmt.Errorf("no valid IDs provided") } kramdowns := model.GetBlockKramdowns(ids, "md") switch outputFormat { case "json": data, _ := json.MarshalIndent(kramdowns, "", " ") fmt.Println(string(data)) default: var sb strings.Builder for _, id := range ids { if kd, ok := kramdowns[id]; ok { sb.WriteString(fmt.Sprintf("--- %s ---\n%s\n\n", id, kd)) } else { sb.WriteString(fmt.Sprintf("--- %s ---\n(not found)\n\n", id)) } } fmt.Print(sb.String()) } return nil }, } func splitIDs(s string) []string { parts := strings.Split(s, ",") var ids []string for _, p := range parts { p = strings.TrimSpace(p) if p != "" { ids = append(ids, p) } } return ids } func resolveData(cmd *cobra.Command) (string, error) { data, _ := cmd.Flags().GetString("data") if data != "" { return data, nil } filePath, _ := cmd.Flags().GetString("file") if filePath == "-" { stdinData, err := io.ReadAll(os.Stdin) if err != nil { return "", err } return string(stdinData), nil } if filePath != "" { fileData, err := os.ReadFile(filePath) if err != nil { return "", err } return string(fileData), nil } stdinData, err := io.ReadAll(os.Stdin) if err != nil { return "", err } return string(stdinData), nil } func markdownToBlockDOM(md string) string { luteEngine := util.NewLute() luteEngine.SetHTMLTag2TextMark(true) dom, _ := luteEngine.Md2BlockDOMTree(md, true) if dom == "" { dom = "
" } return dom } func init() { blockGetCmd.Flags().String("id", "", "block ID") blockChildrenCmd.Flags().String("id", "", "parent block ID") blockBreadcrumbCmd.Flags().String("id", "", "block ID") blockDomCmd.Flags().String("id", "", "block ID") blockKramdownCmd.Flags().String("id", "", "block ID") blockKramdownCmd.Flags().String("mode", "md", "export mode: md | textmark") blockStatCmd.Flags().String("id", "", "block ID") blockInsertCmd.Flags().String("parent", "", "parent block ID") blockInsertCmd.Flags().String("data", "", "markdown content") blockInsertCmd.Flags().String("file", "", "read content from file path (- for stdin)") blockInsertCmd.Flags().String("previous", "", "previous sibling block ID") blockAppendCmd.Flags().String("parent", "", "parent block ID") blockAppendCmd.Flags().String("data", "", "markdown content") blockAppendCmd.Flags().String("file", "", "read content from file path (- for stdin)") blockPrependCmd.Flags().String("parent", "", "parent block ID") blockPrependCmd.Flags().String("data", "", "markdown content") blockPrependCmd.Flags().String("file", "", "read content from file path (- for stdin)") blockUpdateCmd.Flags().String("id", "", "block ID") blockUpdateCmd.Flags().String("data", "", "markdown content") blockUpdateCmd.Flags().String("file", "", "read content from file path (- for stdin)") blockDeleteCmd.Flags().String("id", "", "block ID") blockMoveCmd.Flags().String("id", "", "block ID") blockMoveCmd.Flags().String("parent", "", "target parent block ID") blockMoveCmd.Flags().String("previous", "", "target previous sibling block ID") blockBatchGetCmd.Flags().String("ids", "", "comma-separated block IDs") blockBatchKramdownCmd.Flags().String("ids", "", "comma-separated block IDs") rootCmd.AddCommand(blockCmd) blockCmd.AddCommand(blockGetCmd) blockCmd.AddCommand(blockChildrenCmd) blockCmd.AddCommand(blockBreadcrumbCmd) blockCmd.AddCommand(blockDomCmd) blockCmd.AddCommand(blockKramdownCmd) blockCmd.AddCommand(blockStatCmd) blockCmd.AddCommand(blockInsertCmd) blockCmd.AddCommand(blockAppendCmd) blockCmd.AddCommand(blockPrependCmd) blockCmd.AddCommand(blockUpdateCmd) blockCmd.AddCommand(blockDeleteCmd) blockCmd.AddCommand(blockMoveCmd) blockCmd.AddCommand(blockBatchGetCmd) blockCmd.AddCommand(blockBatchKramdownCmd) }