markdown

package
v1.0.44 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 29, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var MarkdownCreate = common.Shortcut{
	Service:     "markdown",
	Command:     "+create",
	Description: "Create a Markdown file in Drive",
	Risk:        "write",
	Scopes:      []string{"drive:file:upload", "drive:drive.metadata:readonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "folder-token", Desc: "target Drive folder token (default: root folder; mutually exclusive with --wiki-token)"},
		{Name: "wiki-token", Desc: "target wiki node token (uploads under that wiki node; mutually exclusive with --folder-token)"},
		{Name: "name", Desc: "file name with .md suffix; required with --content, optional with --file"},
		{Name: "content", Desc: "Markdown content", Input: []string{common.File, common.Stdin}},
		{Name: "file", Desc: "local .md file path"},
	},
	Tips: []string{
		"Omit both --folder-token and --wiki-token to create the Markdown file in the caller's Drive root folder.",
		"Use --wiki-token <wiki_node_token> to create the Markdown file under a wiki node; the shortcut maps this to parent_type=wiki automatically.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateMarkdownSpec(runtime, markdownUploadSpec{
			FileName:    strings.TrimSpace(runtime.Str("name")),
			FolderToken: strings.TrimSpace(runtime.Str("folder-token")),
			WikiToken:   strings.TrimSpace(runtime.Str("wiki-token")),
			FilePath:    strings.TrimSpace(runtime.Str("file")),
			FileSet:     runtime.Changed("file"),
			Content:     runtime.Str("content"),
			ContentSet:  runtime.Changed("content"),
		}, true)
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		spec := markdownUploadSpec{
			FileName:    strings.TrimSpace(runtime.Str("name")),
			FolderToken: strings.TrimSpace(runtime.Str("folder-token")),
			WikiToken:   strings.TrimSpace(runtime.Str("wiki-token")),
			FilePath:    strings.TrimSpace(runtime.Str("file")),
			FileSet:     runtime.Changed("file"),
			Content:     runtime.Str("content"),
			ContentSet:  runtime.Changed("content"),
		}
		fileSize, err := markdownSourceSize(runtime, spec)
		if err != nil {
			return common.NewDryRunAPI().Set("error", err.Error())
		}
		dry := markdownUploadDryRun(spec, fileSize, fileSize > markdownSinglePartSizeLimit)
		dry.POST("/open-apis/drive/v1/metas/batch_query").
			Desc("Fetch the created Markdown file's real access URL").
			Body(map[string]interface{}{
				"request_docs": []map[string]interface{}{
					{
						"doc_token": "<file_token from upload response>",
						"doc_type":  "file",
					},
				},
				"with_url": true,
			})
		return dry
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		spec := markdownUploadSpec{
			FileName:    strings.TrimSpace(runtime.Str("name")),
			FolderToken: strings.TrimSpace(runtime.Str("folder-token")),
			WikiToken:   strings.TrimSpace(runtime.Str("wiki-token")),
			FilePath:    strings.TrimSpace(runtime.Str("file")),
			FileSet:     runtime.Changed("file"),
			Content:     runtime.Str("content"),
			ContentSet:  runtime.Changed("content"),
		}
		fileSize, err := markdownSourceSize(runtime, spec)
		if err != nil {
			return err
		}

		var result markdownUploadResult
		if spec.FileSet {
			result, err = uploadMarkdownLocalFile(runtime, spec, fileSize)
		} else {
			result, err = uploadMarkdownContent(runtime, spec, []byte(spec.Content))
		}
		if err != nil {
			return err
		}

		out := map[string]interface{}{
			"file_token": result.FileToken,
			"file_name":  finalMarkdownFileName(spec),
			"size_bytes": fileSize,
		}
		if u, metaErr := common.FetchDriveMetaURL(runtime, result.FileToken, "file"); metaErr == nil && strings.TrimSpace(u) != "" {
			out["url"] = u
		} else if metaErr != nil {
			fmt.Fprintf(runtime.IO().ErrOut, "warning: created Markdown file URL lookup failed: %v\n", metaErr)
		}
		if grant := common.AutoGrantCurrentUserDrivePermission(runtime, result.FileToken, "file"); grant != nil {
			out["permission_grant"] = grant
		}

		runtime.OutFormat(out, nil, func(w io.Writer) {
			prettyPrintMarkdownWrite(w, out)
		})
		return nil
	},
}
View Source
var MarkdownDiff = common.Shortcut{
	Service:     "markdown",
	Command:     "+diff",
	Description: "Compare remote Markdown versions or compare remote Markdown against a local file",
	Risk:        "read",
	Scopes:      []string{"drive:file:download"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "file-token", Desc: "target Markdown file token", Required: true},
		{Name: "from-version", Desc: "base remote version; when --to-version is omitted, compare this version to the latest remote version"},
		{Name: "to-version", Desc: "target remote version; requires --from-version"},
		{Name: "file", Desc: "local .md file path to compare against the remote content"},
		{Name: "context-lines", Desc: "number of unchanged context lines to include around each diff hunk", Type: "int", Default: "3"},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateMarkdownDiffSpec(runtime, markdownDiffSpec{
			FileToken:    strings.TrimSpace(runtime.Str("file-token")),
			FromVersion:  strings.TrimSpace(runtime.Str("from-version")),
			ToVersion:    strings.TrimSpace(runtime.Str("to-version")),
			FilePath:     strings.TrimSpace(runtime.Str("file")),
			ContextLines: runtime.Int("context-lines"),
			Format:       runtime.Format,
		})
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return markdownDiffDryRun(markdownDiffSpec{
			FileToken:    strings.TrimSpace(runtime.Str("file-token")),
			FromVersion:  strings.TrimSpace(runtime.Str("from-version")),
			ToVersion:    strings.TrimSpace(runtime.Str("to-version")),
			FilePath:     strings.TrimSpace(runtime.Str("file")),
			ContextLines: runtime.Int("context-lines"),
		})
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		spec := markdownDiffSpec{
			FileToken:    strings.TrimSpace(runtime.Str("file-token")),
			FromVersion:  strings.TrimSpace(runtime.Str("from-version")),
			ToVersion:    strings.TrimSpace(runtime.Str("to-version")),
			FilePath:     strings.TrimSpace(runtime.Str("file")),
			ContextLines: runtime.Int("context-lines"),
		}

		var (
			fromLabel   string
			toLabel     string
			fromContent string
			toContent   string
			err         error
		)

		switch markdownDiffMode(spec) {
		case markdownDiffModeRemoteVsLocal:
			fromLabel = "a/" + spec.FileToken
			if spec.FromVersion != "" {
				fromLabel += "@version:" + spec.FromVersion
			} else {
				fromLabel += "@latest"
			}
			_, fromContent, err = downloadMarkdownContent(ctx, runtime, spec.FileToken, spec.FromVersion)
			if err != nil {
				return err
			}

			toLabel = "b/" + spec.FilePath
			toContent, err = readMarkdownLocalFile(runtime, spec.FilePath)
			if err != nil {
				return err
			}
		default:
			fromLabel = "a/" + spec.FileToken + "@version:" + spec.FromVersion
			_, fromContent, err = downloadMarkdownContent(ctx, runtime, spec.FileToken, spec.FromVersion)
			if err != nil {
				return err
			}

			if spec.ToVersion != "" {
				toLabel = "b/" + spec.FileToken + "@version:" + spec.ToVersion
				_, toContent, err = downloadMarkdownContent(ctx, runtime, spec.FileToken, spec.ToVersion)
			} else {
				toLabel = "b/" + spec.FileToken + "@latest"
				_, toContent, err = downloadMarkdownContent(ctx, runtime, spec.FileToken, "")
			}
			if err != nil {
				return err
			}
		}

		diffText, changed, addedLines, deletedLines, hunks := summarizeMarkdownDiff(fromLabel, toLabel, fromContent, toContent, spec.ContextLines)

		out := map[string]interface{}{
			"changed":       changed,
			"mode":          markdownDiffMode(spec),
			"file_token":    spec.FileToken,
			"from_version":  spec.FromVersion,
			"to_version":    spec.ToVersion,
			"from_label":    fromLabel,
			"to_label":      toLabel,
			"added_lines":   addedLines,
			"deleted_lines": deletedLines,
			"context_lines": spec.ContextLines,
			"hunks":         hunks,
			"diff":          diffText,
		}
		if spec.FilePath != "" {
			out["local_file"] = spec.FilePath
		}

		runtime.OutFormatRaw(out, nil, func(w io.Writer) {
			prettyPrintMarkdownDiff(w, out)
		})
		return nil
	},
}
View Source
var MarkdownFetch = common.Shortcut{
	Service:     "markdown",
	Command:     "+fetch",
	Description: "Fetch a Markdown file from Drive",
	Risk:        "read",
	Scopes:      []string{"drive:file:download"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "file-token", Desc: "Markdown file token", Required: true},
		{Name: "output", Desc: "local save path or directory; omit to return content directly"},
		{Name: "overwrite", Type: "bool", Desc: "overwrite existing local output file"},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		fileToken := strings.TrimSpace(runtime.Str("file-token"))
		if err := validate.ResourceName(fileToken, "--file-token"); err != nil {
			return output.ErrValidation("%s", err)
		}
		outputPath := strings.TrimSpace(runtime.Str("output"))
		if outputPath == "" {
			return nil
		}
		if _, err := validate.SafeOutputPath(outputPath); err != nil {
			return output.ErrValidation("unsafe output path: %s", err)
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		dry := common.NewDryRunAPI().
			Desc("download markdown file bytes; when --output is omitted the CLI returns content as UTF-8 text").
			GET("/open-apis/drive/v1/files/:file_token/download").
			Set("file_token", runtime.Str("file-token"))
		if outputPath := strings.TrimSpace(runtime.Str("output")); outputPath != "" {
			dry.Set("output", outputPath)
		} else {
			dry.Set("output", "<stdout>")
		}
		return dry
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		fileToken := strings.TrimSpace(runtime.Str("file-token"))
		outputPath := strings.TrimSpace(runtime.Str("output"))

		resp, err := runtime.DoAPIStream(ctx, &larkcore.ApiReq{
			HttpMethod: http.MethodGet,
			ApiPath:    fmt.Sprintf("/open-apis/drive/v1/files/%s/download", validate.EncodePathSegment(fileToken)),
		})
		if err != nil {
			return output.ErrNetwork("download failed: %s", err)
		}
		defer resp.Body.Close()

		fileName := fileNameFromDownloadHeader(resp.Header, fileToken+".md")
		if outputPath == "" {
			payload, err := io.ReadAll(resp.Body)
			if err != nil {
				return output.ErrNetwork("download failed: %s", err)
			}
			out := map[string]interface{}{
				"file_token": fileToken,
				"file_name":  fileName,
				"content":    string(payload),
				"size_bytes": len(payload),
			}
			runtime.OutFormatRaw(out, nil, func(w io.Writer) {
				prettyPrintMarkdownContent(w, out)
			})
			return nil
		}

		if markdownFetchOutputIsDirectory(runtime, outputPath) {
			outputPath = filepath.Join(outputPath, fileName)
		}
		if _, statErr := runtime.FileIO().Stat(outputPath); statErr == nil && !runtime.Bool("overwrite") {
			return output.ErrValidation("output file already exists: %s (use --overwrite to replace)", outputPath)
		}

		result, err := runtime.FileIO().Save(outputPath, fileio.SaveOptions{
			ContentType:   resp.Header.Get("Content-Type"),
			ContentLength: resp.ContentLength,
		}, resp.Body)
		if err != nil {
			return common.WrapSaveErrorByCategory(err, "io")
		}

		savedPath, _ := runtime.ResolveSavePath(outputPath)
		if savedPath == "" {
			savedPath = outputPath
		}

		out := map[string]interface{}{
			"file_token": fileToken,
			"file_name":  fileName,
			"saved_path": savedPath,
			"size_bytes": result.Size(),
		}
		runtime.OutFormat(out, nil, func(w io.Writer) {
			prettyPrintMarkdownSavedFile(w, out)
		})
		return nil
	},
}
View Source
var MarkdownOverwrite = common.Shortcut{
	Service:     "markdown",
	Command:     "+overwrite",
	Description: "Overwrite an existing Markdown file in Drive",
	Risk:        "write",
	Scopes:      []string{"drive:file:upload", "drive:drive.metadata:readonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "file-token", Desc: "target Markdown file token", Required: true},
		{Name: "name", Desc: "optional file name with .md suffix; overrides the existing/local file name"},
		{Name: "content", Desc: "new Markdown content", Input: []string{common.File, common.Stdin}},
		{Name: "file", Desc: "local .md file path"},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		fileToken := strings.TrimSpace(runtime.Str("file-token"))
		if err := validate.ResourceName(fileToken, "--file-token"); err != nil {
			return output.ErrValidation("%s", err)
		}
		return validateMarkdownSpec(runtime, markdownUploadSpec{
			FileToken:  fileToken,
			FileName:   strings.TrimSpace(runtime.Str("name")),
			FilePath:   strings.TrimSpace(runtime.Str("file")),
			FileSet:    runtime.Changed("file"),
			Content:    runtime.Str("content"),
			ContentSet: runtime.Changed("content"),
		}, false)
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		spec := markdownUploadSpec{
			FileToken:  strings.TrimSpace(runtime.Str("file-token")),
			FileName:   strings.TrimSpace(runtime.Str("name")),
			FilePath:   strings.TrimSpace(runtime.Str("file")),
			FileSet:    runtime.Changed("file"),
			Content:    runtime.Str("content"),
			ContentSet: runtime.Changed("content"),
		}
		fileSize, err := markdownSourceSize(runtime, spec)
		if err != nil {
			return common.NewDryRunAPI().Set("error", err.Error())
		}
		return markdownOverwriteDryRun(spec, fileSize, fileSize > markdownSinglePartSizeLimit)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		fileToken := strings.TrimSpace(runtime.Str("file-token"))
		spec := markdownUploadSpec{
			FileToken:  fileToken,
			FileName:   strings.TrimSpace(runtime.Str("name")),
			FilePath:   strings.TrimSpace(runtime.Str("file")),
			FileSet:    runtime.Changed("file"),
			Content:    runtime.Str("content"),
			ContentSet: runtime.Changed("content"),
		}

		fileSize, err := markdownSourceSize(runtime, spec)
		if err != nil {
			return err
		}

		fileName, err := resolveMarkdownOverwriteFileName(runtime, spec)
		if err != nil {
			return err
		}
		spec.FileName = fileName

		var result markdownUploadResult
		if spec.FileSet {
			result, err = uploadMarkdownLocalFile(runtime, spec, fileSize)
		} else {
			result, err = uploadMarkdownContent(runtime, spec, []byte(spec.Content))
		}
		if err != nil {
			return err
		}

		out := map[string]interface{}{
			"file_token": result.FileToken,
			"file_name":  fileName,
			"version":    result.Version,
			"size_bytes": fileSize,
		}
		runtime.OutFormat(out, nil, func(w io.Writer) {
			prettyPrintMarkdownWrite(w, out)
		})
		return nil
	},
}
View Source
var MarkdownPatch = common.Shortcut{
	Service:     "markdown",
	Command:     "+patch",
	Description: "Patch a Markdown file in Drive via fetch-local-replace-overwrite",
	Risk:        "write",
	Scopes:      []string{"drive:file:download", "drive:file:upload", "drive:drive.metadata:readonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "file-token", Desc: "target Markdown file token", Required: true},
		{Name: "pattern", Desc: "literal text or RE2 regex to match", Input: []string{common.File, common.Stdin}},
		{Name: "content", Desc: "replacement Markdown content", Input: []string{common.File, common.Stdin}},
		{Name: "regex", Type: "bool", Desc: "interpret --pattern as RE2 regular expression"},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		spec := newMarkdownPatchSpec(runtime)
		if err := validateMarkdownPatchSpec(runtime, spec); err != nil {
			return err
		}
		if spec.Regex {
			if _, err := regexp.Compile(spec.Pattern); err != nil {
				return output.ErrValidation("invalid --pattern regex: %s", err)
			}
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		spec := newMarkdownPatchSpec(runtime)
		mode := markdownPatchModeLiteral
		if spec.Regex {
			mode = markdownPatchModeRegex
		}
		sizeThreshold := common.FormatSize(markdownSinglePartSizeLimit)
		return common.NewDryRunAPI().
			Desc("Download the current Markdown file, apply the replacement locally, and overwrite the file only when matches are found").
			GET("/open-apis/drive/v1/files/:file_token/download").
			Desc("[1] Download the current Markdown content").
			Set("file_token", spec.FileToken).
			POST("/open-apis/drive/v1/metas/batch_query").
			Desc("[2] Read current file metadata to preserve the existing file name before overwrite").
			Body(map[string]interface{}{
				"request_docs": []map[string]interface{}{
					{
						"doc_token": spec.FileToken,
						"doc_type":  "file",
					},
				},
			}).
			POST("/open-apis/drive/v1/files/upload_all").
			Desc("[3a] If the patched Markdown is at most "+sizeThreshold+", overwrite the file with multipart/form-data upload_all").
			Body(map[string]interface{}{
				"file_name":   "<existing_remote_name_or_" + spec.FileToken + ".md>",
				"parent_type": "explorer",
				"parent_node": "",
				"size":        "<updated_size_bytes>",
				"file":        "<patched_markdown_content>",
				"file_token":  spec.FileToken,
			}).
			POST("/open-apis/drive/v1/files/upload_prepare").
			Desc("[3b] If the patched Markdown exceeds "+sizeThreshold+", initialize multipart overwrite upload").
			Body(map[string]interface{}{
				"file_name":   "<existing_remote_name_or_" + spec.FileToken + ".md>",
				"parent_type": "explorer",
				"parent_node": "",
				"size":        "<updated_size_bytes>",
				"file_token":  spec.FileToken,
			}).
			POST("/open-apis/drive/v1/files/upload_part").
			Desc("[3c] Upload file parts (repeated) when multipart overwrite is required").
			Body(map[string]interface{}{
				"upload_id": "<upload_id>",
				"seq":       "<chunk_index>",
				"size":      "<chunk_size>",
				"file":      "<chunk_binary>",
			}).
			POST("/open-apis/drive/v1/files/upload_finish").
			Desc("[3d] Finalize multipart overwrite upload and return the new version").
			Body(map[string]interface{}{
				"upload_id": "<upload_id>",
				"block_num": "<block_num>",
			}).
			Set("mode", mode)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		spec := newMarkdownPatchSpec(runtime)

		resp, err := openMarkdownDownload(ctx, runtime, spec.FileToken)
		if err != nil {
			return err
		}
		defer resp.Body.Close()

		payload, err := io.ReadAll(resp.Body)
		if err != nil {
			return output.ErrNetwork("download failed: %s", err)
		}
		original := string(payload)
		patched, matchCount, err := applyMarkdownPatch(original, spec)
		if err != nil {
			return err
		}

		mode := markdownPatchModeLiteral
		if spec.Regex {
			mode = markdownPatchModeRegex
		}

		out := map[string]interface{}{
			"updated":           false,
			"mode":              mode,
			"match_count":       matchCount,
			"version":           "",
			"size_bytes_before": len(payload),
			"size_bytes_after":  len(payload),
		}
		if matchCount == 0 {
			runtime.OutFormat(out, nil, func(w io.Writer) {
				prettyPrintMarkdownPatch(w, out)
			})
			return nil
		}

		patchedPayload := []byte(patched)
		if err := validateNonEmptyMarkdownSize(int64(len(patchedPayload))); err != nil {
			return err
		}

		specUpload := markdownUploadSpec{
			FileToken: spec.FileToken,
		}
		fileName, err := resolveMarkdownOverwriteFileName(runtime, specUpload)
		if err != nil {
			return err
		}
		specUpload.FileName = fileName

		result, err := uploadMarkdownContent(runtime, specUpload, patchedPayload)
		if err != nil {
			return err
		}

		out["updated"] = true
		out["version"] = result.Version
		out["size_bytes_after"] = len(patchedPayload)

		runtime.OutFormat(out, nil, func(w io.Writer) {
			prettyPrintMarkdownPatch(w, out)
		})
		return nil
	},
}

Functions

func Shortcuts

func Shortcuts() []common.Shortcut

Shortcuts returns all markdown shortcuts.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL