Skip to main content
Glama
default.go7.03 kB
package formatter import ( "strings" "github.com/mholzen/workflowy/pkg/workflowy" ) // DefaultFormatter implements the Formatter interface with standard rules type DefaultFormatter struct { config *Config } // NewDefaultFormatter creates a new formatter with default configuration func NewDefaultFormatter() *DefaultFormatter { return &DefaultFormatter{ config: DefaultConfig(), } } // NewFormatter creates a new formatter with custom configuration func NewFormatter(config *Config) *DefaultFormatter { return &DefaultFormatter{ config: config, } } // FormatTree converts entire tree to markdown func (f *DefaultFormatter) FormatTree(items []*workflowy.Item) (string, error) { var result strings.Builder for _, item := range items { markdown, err := f.FormatNode(item, 0) if err != nil { return "", err } if markdown != "" { result.WriteString(markdown) } } return result.String(), nil } // FormatNode converts a single node and its children to markdown func (f *DefaultFormatter) FormatNode(item *workflowy.Item, depth int) (string, error) { // Check if node should be excluded if f.ShouldExclude(item) { return "", nil } // Get effective layout mode layoutMode := f.GetLayoutMode(item, depth) // Strip tags from name name := f.stripTags(item.Name) var result strings.Builder switch layoutMode { case LayoutH1, LayoutH2, LayoutH3, LayoutH4, LayoutH5, LayoutH6: result.WriteString(f.formatHeader(name, layoutMode, item)) case LayoutP: // Check if this paragraph has bullet children (list introduction) hasBulletChildren := f.hasBulletChildren(item) if hasBulletChildren && f.config.AddColonBeforeLists { name = AddColon(name) } formatted := f.formatParagraph(name) result.WriteString(formatted) result.WriteString("\n\n") // Format children (which should be bullets) for _, child := range item.Children { childMd, err := f.FormatNode(child, depth+1) if err != nil { return "", err } result.WriteString(childMd) } case LayoutBullets: // Format as bullet result.WriteString(f.formatBullet(name, depth)) // Format children for _, child := range item.Children { childMd, err := f.FormatNode(child, depth+1) if err != nil { return "", err } result.WriteString(childMd) } case LayoutTodo: result.WriteString(f.formatTodo(name, depth)) // Format children for _, child := range item.Children { childMd, err := f.FormatNode(child, depth+1) if err != nil { return "", err } result.WriteString(childMd) } } return result.String(), nil } // ShouldExclude checks if node should be excluded from output func (f *DefaultFormatter) ShouldExclude(item *workflowy.Item) bool { return HasTag(item.Name, f.config.ExcludeTag) } // GetLayoutMode determines effective layoutMode considering tags, depth, config func (f *DefaultFormatter) GetLayoutMode(item *workflowy.Item, depth int) LayoutMode { // Check for tag overrides first if HasTag(item.Name, f.config.H1Tag) { return LayoutH1 } if HasTag(item.Name, f.config.H2Tag) { return LayoutH2 } if HasTag(item.Name, f.config.H3Tag) { return LayoutH3 } if HasTag(item.Name, f.config.H4Tag) { return LayoutH4 } if HasTag(item.Name, f.config.H5Tag) { return LayoutH5 } if HasTag(item.Name, f.config.H6Tag) { return LayoutH6 } // Check if item has layoutMode in Data if item.Data != nil { if mode, ok := item.Data["layoutMode"].(string); ok && mode != "" { return LayoutMode(mode) } } // Fallback: use depth for headers if configured if f.config.UseDepthForHeaders { switch depth { case 0: return LayoutH1 case 1: return LayoutH2 case 2: return LayoutH3 case 3: return LayoutH4 case 4: return LayoutH5 case 5: return LayoutH6 default: // Beyond h6, treat as paragraphs return LayoutP } } // Default to bullets return LayoutBullets } // formatHeader formats a header with appropriate casing func (f *DefaultFormatter) formatHeader(name string, mode LayoutMode, item *workflowy.Item) string { var level int var uppercase bool switch mode { case LayoutH1: level = 1 uppercase = f.config.H1Uppercase case LayoutH2: level = 2 uppercase = f.config.H2Uppercase case LayoutH3: level = 3 uppercase = f.config.H3Uppercase case LayoutH4: level = 4 uppercase = f.config.H4Uppercase case LayoutH5: level = 5 uppercase = f.config.H5Uppercase case LayoutH6: level = 6 uppercase = f.config.H6Uppercase } if uppercase { name = Uppercase(name) } var result strings.Builder result.WriteString(HeaderPrefix(level)) result.WriteString(name) result.WriteString("\n\n") // Format children (which should be paragraphs or nested structure) for _, child := range item.Children { childMd, _ := f.FormatNode(child, level) // Use level as depth for children result.WriteString(childMd) } return result.String() } // formatParagraph formats text as a paragraph with capitalization and punctuation func (f *DefaultFormatter) formatParagraph(text string) string { text = strings.TrimSpace(text) if text == "" { return "" } if f.config.ParagraphCapitalize { text = Capitalize(text) } if f.config.ParagraphPunctuate { text = Punctuate(text) } return text } // formatBullet formats text as a bullet point func (f *DefaultFormatter) formatBullet(text string, depth int) string { return IndentBullet(depth) + text + "\n" } // formatTodo formats text as a todo item func (f *DefaultFormatter) formatTodo(text string, depth int) string { return IndentBullet(depth) + "[ ] " + text + "\n" } // stripTags removes all configured tags from the text func (f *DefaultFormatter) stripTags(text string) string { text = StripTag(text, f.config.ExcludeTag) text = StripTag(text, f.config.H1Tag) text = StripTag(text, f.config.H2Tag) text = StripTag(text, f.config.H3Tag) text = StripTag(text, f.config.H4Tag) text = StripTag(text, f.config.H5Tag) text = StripTag(text, f.config.H6Tag) return text } // hasBulletChildren checks if any immediate children are bullets func (f *DefaultFormatter) hasBulletChildren(item *workflowy.Item) bool { for _, child := range item.Children { mode := f.GetLayoutMode(child, 0) // Depth doesn't matter for checking if mode == LayoutBullets { return true } } return false } // collectParagraphBullets collects consecutive bullets to join as paragraph // Stops at empty bullet or non-paragraph-like content func (f *DefaultFormatter) collectParagraphBullets(item *workflowy.Item) []string { // This is for a single bullet item - check if it should be treated as paragraph // For now, just return the item's name if it looks like paragraph text // More sophisticated logic could check siblings, etc. // If item has children, it's not a simple paragraph bullet if len(item.Children) > 0 { return nil } // If item name is empty, stop collection if IsEmpty(item.Name) { return nil } // Return this item as a paragraph component return []string{item.Name} }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mholzen/workflowy'

If you have feedback or need assistance with the MCP directory API, please join our Discord server