Files
siyuan/kernel/cli/cmd/block.go
2026-06-10 16:54:02 +08:00

495 lines
13 KiB
Go

// 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 <https://www.gnu.org/licenses/>.
package cmd
import (
"encoding/json"
"fmt"
"io"
"os"
"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 <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 <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 <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 <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 <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 <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 <id> [--data <markdown> | --file <path>]",
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
}
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 <id> [--data <markdown> | --file <path>]",
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
}
data, err := resolveData(cmd)
if err != nil {
return err
}
dom := markdownToBlockDOM(data)
transactions := []*model.Transaction{{
DoOperations: []*model.Operation{{
Action: "append",
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 <id> [--data <markdown> | --file <path>]",
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
}
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 <id> [--data <markdown> | --file <path>]",
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 <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 <id> --parent <id>",
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
}
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
},
}
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 = "<div data-type=\"NodeParagraph\" data-node-id=\"\"><div data-type=\"NodeText\"></div></div>"
}
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")
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)
}