Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce fmt reset (working title prune) and fix JSON frontmatter handling #681

Merged
merged 8 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 69 additions & 45 deletions internal/cmd/fmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,71 +6,95 @@ import (

"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/stateful/runme/v3/pkg/document/identity"
idt "github.com/stateful/runme/v3/pkg/document/identity"
"github.com/stateful/runme/v3/pkg/project"
)

func fmtCmd() *cobra.Command {
var (
flatten bool
formatJSON bool
identityStr string
write bool
)
var (
flatten bool
formatJSON bool
identityStr string
write bool
)

cmd := cobra.Command{
Use: "fmt",
Short: "Format a Markdown file into canonical format",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if formatJSON {
if write {
return errors.New("invalid usage of --json with --write")
}
if !flatten {
return errors.New("invalid usage of --json without --flatten")
}
func buildFmtCmd(cmd *cobra.Command, reset bool) *cobra.Command {
cmd.RunE = func(cmd *cobra.Command, args []string) error {
if formatJSON {
if write {
return errors.New("invalid usage of --json with --write")
}
if !flatten {
return errors.New("invalid usage of --json without --flatten")
}
}

files := args
files := args

if len(args) == 0 {
var err error
files, err = getProjectFiles(cmd)
if err != nil {
return err
}
if len(args) == 0 {
var err error
files, err = getProjectFiles(cmd)
if err != nil {
return err
}
}

var identityResolver *identity.IdentityResolver
switch strings.ToLower(identityStr) {
case "all":
identityResolver = identity.NewResolver(identity.AllLifecycleIdentity)
case "doc", "document":
identityResolver = identity.NewResolver(identity.DocumentLifecycleIdentity)
case "cell":
identityResolver = identity.NewResolver(identity.CellLifecycleIdentity)
default:
identityResolver = identity.NewResolver(identity.DefaultLifecycleIdentity)
}
identityResolver := idt.NewResolver(idt.UnspecifiedLifecycleIdentity)
if reset {
identityResolver = strToIdentityResolver(identityStr)
}

return project.FormatFiles(files, identityResolver, formatJSON, write, func(file string, formatted []byte) error {
return project.FormatFiles(files, &project.FormatOptions{
FormatJSON: formatJSON,
IdentityResolver: identityResolver,
Outputter: func(file string, formatted []byte) error {
out := cmd.OutOrStdout()
_, _ = fmt.Fprintf(out, "===== %s =====\n", file)
_, _ = out.Write(formatted)
_, _ = fmt.Fprint(out, "\n")
return nil
})
},
},
Reset: reset,
Write: write,
})
}

setDefaultFlags(&cmd)
setDefaultFlags(cmd)

cmd.Flags().BoolVar(&flatten, "flatten", true, "Flatten nested blocks in the output. WARNING: This can currently break frontmatter if turned off.")
cmd.Flags().BoolVar(&formatJSON, "json", false, "Print out data as JSON. Only possible with --flatten and not allowed with --write.")
cmd.Flags().BoolVarP(&write, "write", "w", false, "Write result to the source file instead of stdout.")
cmd.Flags().StringVar(&identityStr, "identity", "", "Set the lifecycle identity. Overrides the default.")
cmd.Flags().StringVar(&identityStr, "identity", "", "Set the lifecycle identity, \"doc\", \"cell\", \"all\", or \"\" (default).")
_ = cmd.Flags().MarkDeprecated("flatten", "This flag is now default and no longer has any other effect.")

return &cmd
return cmd
}

func fmtCmd() *cobra.Command {
cmd := buildFmtCmd(&cobra.Command{
Use: "fmt",
Short: "Format a Markdown file into canonical format",
Args: cobra.MaximumNArgs(1),
}, false)

cmd.AddCommand(buildFmtCmd(&cobra.Command{
Use: "reset",
Short: "Format a Markdown file and reset all lifecycle metadata",
Args: cobra.MaximumNArgs(1),
}, true))

return cmd
}

func strToIdentityResolver(identity string) *idt.IdentityResolver {
var identityResolver *idt.IdentityResolver
switch strings.ToLower(identity) {
case "all":
identityResolver = idt.NewResolver(idt.AllLifecycleIdentity)
case "doc", "document":
identityResolver = idt.NewResolver(idt.DocumentLifecycleIdentity)
case "cell":
identityResolver = idt.NewResolver(idt.CellLifecycleIdentity)
default:
identityResolver = idt.NewResolver(idt.DefaultLifecycleIdentity)
}
return identityResolver
}
4 changes: 2 additions & 2 deletions pkg/document/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func (d *Document) FrontmatterRaw() []byte {
return d.frontmatterRaw
}

// splitSource splits source into FrontMatter and content.
// splitSource splits source into Frontmatter and content.
// TODO(adamb): replace it with an extension to goldmark.
// Example: https://github.com/abhinav/goldmark-frontmatter
func (d *Document) splitSource() {
Expand All @@ -131,7 +131,7 @@ func (d *Document) splitSource() {

for _, item := range l.items {
switch item.Type() {
case parsedItemFrontMatter:
case parsedItemFrontmatter:
d.frontmatterRaw = item.Value(d.source)
case parsedItemContent:
d.content = item.Value(d.source)
Expand Down
50 changes: 34 additions & 16 deletions pkg/document/editor/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const (
type Options struct {
IdentityResolver *identity.IdentityResolver
LoggerInstance *zap.Logger
Reset bool
}

func (o Options) Logger() *zap.Logger {
Expand All @@ -41,8 +42,16 @@ func Deserialize(data []byte, opts Options) (*Notebook, error) {

frontmatter, fmErr := doc.FrontmatterWithError()
// non-fatal error
if fmErr != nil {
switch {
case fmErr != nil:
opts.Logger().Warn("failed to parse frontmatter", zap.Error(fmErr))
case opts.Reset:
// reset Runme part of frontmatter if required
f, err := frontmatter.ResetRunme(opts.IdentityResolver.DocumentEnabled())
if err != nil {
return nil, err
}
frontmatter = f
}

cacheID := opts.IdentityResolver.CacheID()
Expand All @@ -69,36 +78,45 @@ func Deserialize(data []byte, opts Options) (*Notebook, error) {
// todo(sebastian): faux document-level `runme.dev/id` to not break existing clients; revert in near future
notebook.Metadata[PrefixAttributeName(InternalAttributePrefix, "id")] = cacheID

// Make ephemeral cell IDs permanent if the cell lifecycle ID is enabled.
if opts.IdentityResolver.CellEnabled() {
err := applyCellLifecycleIdentity(notebook)
if err != nil {
return nil, err
}
// apply lifecycle identity to cells; reset first if required
if err := applyCellLifecycleIdentity(notebook, &opts); err != nil {
return nil, err
}

return notebook, nil
}

func applyCellLifecycleIdentity(notebook *Notebook) error {
func applyCellLifecycleIdentity(notebook *Notebook, opts *Options) error {
if opts == nil {
return nil
}

ephCellIDKey := PrefixAttributeName(InternalAttributePrefix, CellID)

for _, cell := range notebook.Cells {
if cell.Kind != CodeKind {
continue
}

// don't overwrite existing cell ID
if _, ok := cell.Metadata["id"]; ok {
continue
if opts.Reset {
delete(cell.Metadata, CellID)
}

// make sure we have an ephemeral cell ID
if _, ok := cell.Metadata[ephCellIDKey]; !ok {
return errors.Errorf("missing ephemeral cell ID")
}
if opts.IdentityResolver.CellEnabled() {
// don't overwrite existing cell ID
if _, ok := cell.Metadata["id"]; ok {
continue
}

cell.Metadata[CellID] = cell.Metadata[ephCellIDKey]
// make sure we have an ephemeral cell ID
if _, ok := cell.Metadata[ephCellIDKey]; !ok {
return errors.Errorf("missing ephemeral cell ID")
}

cell.Metadata[CellID] = cell.Metadata[ephCellIDKey]
}
}

return nil
}

Expand Down
Loading
Loading