Skip to main content
Glama
markdown.go10 kB
package formatter import ( "strings" "github.com/mholzen/workflowy/pkg/workflowy" ) type MarkdownFormatter struct { config *MarkdownConfig } type MarkdownConfig struct { ExcludeTag string H1Tag string H2Tag string H3Tag string PTag string ListTag string } func DefaultMarkdownConfig() *MarkdownConfig { return &MarkdownConfig{ ExcludeTag: "#exclude", H1Tag: "#h1", H2Tag: "#h2", H3Tag: "#h3", PTag: "#p", ListTag: "#list", } } func NewMarkdownFormatter() *MarkdownFormatter { return &MarkdownFormatter{ config: DefaultMarkdownConfig(), } } func NewMarkdownFormatterWithConfig(config *MarkdownConfig) *MarkdownFormatter { return &MarkdownFormatter{ config: config, } } func (f *MarkdownFormatter) FormatTree(items []*workflowy.Item) (string, error) { var result strings.Builder for _, item := range items { output := f.formatNode(item, 1) if output != "" { result.WriteString(output) } } return strings.TrimRight(result.String(), "\n") + "\n", nil } func (f *MarkdownFormatter) formatNode(item *workflowy.Item, headerLevel int) string { if f.shouldExclude(item) { return "" } layoutMode := f.getLayoutMode(item) name := f.stripAllTags(item.Name) switch layoutMode { case "h1": return f.formatAsHeader(item, name, 1) case "h2": return f.formatAsHeader(item, name, 2) case "h3": return f.formatAsHeader(item, name, 3) case "p": return f.formatAsParagraphWithChildren(item, name) case "ol": return f.formatAsOrderedList(item, name) case "quote": return f.formatAsQuote(item, name) case "code": return f.formatAsCode(item, name) case "divider": return "---\n\n" default: return f.formatBulletsNode(item, name, headerLevel) } } func (f *MarkdownFormatter) formatBulletsNode(item *workflowy.Item, name string, headerLevel int) string { if len(item.Children) == 0 { return "" } if IsListPattern(item) { return f.formatWithListChildren(item, name, headerLevel) } if f.childrenAreAllSubheaders(item.Children) { return f.formatAsHeaderWithSubheaders(item, name, headerLevel) } return f.formatAsHeaderWithParagraph(item, name, headerLevel) } func (f *MarkdownFormatter) childrenAreAllSubheaders(children []*workflowy.Item) bool { hasAnyWithGrandchildren := false for _, child := range children { if f.shouldExclude(child) { continue } if IsEmpty(child.Name) { continue } if len(child.Children) > 0 { if IsListPattern(child) { return false } hasAnyWithGrandchildren = true } else { if hasAnyWithGrandchildren { return false } } } return hasAnyWithGrandchildren } func (f *MarkdownFormatter) formatAsHeader(item *workflowy.Item, name string, level int) string { var result strings.Builder result.WriteString(HeaderPrefix(level)) result.WriteString(Capitalize(name)) result.WriteString("\n\n") for _, child := range item.Children { childOutput := f.formatNode(child, level+1) result.WriteString(childOutput) } return result.String() } func (f *MarkdownFormatter) formatAsHeaderWithParagraph(item *workflowy.Item, name string, headerLevel int) string { var result strings.Builder result.WriteString(HeaderPrefix(headerLevel)) result.WriteString(Capitalize(name)) result.WriteString("\n") paragraphs := f.collectParagraphs(item.Children) for _, para := range paragraphs { if para == "" { result.WriteString("\n") continue } result.WriteString(para) result.WriteString("\n") } result.WriteString("\n") return result.String() } func (f *MarkdownFormatter) collectParagraphs(children []*workflowy.Item) []string { var paragraphs []string var currentSentences []string needsBlankBefore := false for _, child := range children { if f.shouldExclude(child) { continue } childName := f.stripAllTags(child.Name) if IsEmptyBullet(child) || IsEmpty(childName) { if len(currentSentences) > 0 { paragraphs = append(paragraphs, strings.Join(currentSentences, " ")) currentSentences = nil } needsBlankBefore = true continue } if needsBlankBefore && len(paragraphs) > 0 { paragraphs = append(paragraphs, "") needsBlankBefore = false } if IsListPattern(child) { currentSentences = append(currentSentences, FormatAsSentence(childName)) intro := strings.Join(currentSentences, " ") currentSentences = nil listOutput := f.formatInlineListWithIntro(child, intro) paragraphs = append(paragraphs, listOutput) needsBlankBefore = true continue } currentSentences = append(currentSentences, FormatAsSentence(childName)) } if len(currentSentences) > 0 { if needsBlankBefore && len(paragraphs) > 0 { paragraphs = append(paragraphs, "") } paragraphs = append(paragraphs, strings.Join(currentSentences, " ")) } return paragraphs } func (f *MarkdownFormatter) formatInlineList(item *workflowy.Item, name string) string { return f.formatInlineListWithIntro(item, FormatAsSentence(name)) } func (f *MarkdownFormatter) formatInlineListWithIntro(item *workflowy.Item, intro string) string { var result strings.Builder result.WriteString(intro) result.WriteString("\n") for _, child := range item.Children { if f.shouldExclude(child) { continue } childName := f.stripAllTags(child.Name) if !IsEmpty(childName) { result.WriteString("- ") result.WriteString(childName) result.WriteString("\n") } } return strings.TrimRight(result.String(), "\n") } func (f *MarkdownFormatter) formatAsHeaderWithSubheaders(item *workflowy.Item, name string, headerLevel int) string { var result strings.Builder result.WriteString(HeaderPrefix(headerLevel)) result.WriteString(Capitalize(name)) result.WriteString("\n\n") for _, child := range item.Children { if f.shouldExclude(child) { continue } childName := f.stripAllTags(child.Name) if len(child.Children) > 0 { childOutput := f.formatBulletsNode(child, childName, headerLevel+1) result.WriteString(childOutput) } else if !IsEmpty(childName) { result.WriteString(FormatAsSentence(childName)) result.WriteString("\n\n") } } return result.String() } func (f *MarkdownFormatter) formatWithListChildren(item *workflowy.Item, name string, headerLevel int) string { var result strings.Builder result.WriteString(HeaderPrefix(headerLevel)) result.WriteString(Capitalize(name)) result.WriteString("\n") for _, child := range item.Children { if f.shouldExclude(child) { continue } childName := f.stripAllTags(child.Name) if !IsEmpty(childName) { result.WriteString("- ") result.WriteString(childName) result.WriteString("\n") } } result.WriteString("\n") return result.String() } func (f *MarkdownFormatter) formatAsParagraph(text string) string { return FormatAsSentence(text) } func (f *MarkdownFormatter) formatAsParagraphWithChildren(item *workflowy.Item, name string) string { var result strings.Builder result.WriteString(f.formatAsParagraph(name)) result.WriteString("\n\n") if len(item.Children) > 0 { for _, child := range item.Children { if f.shouldExclude(child) { continue } childName := f.stripAllTags(child.Name) if !IsEmpty(childName) { result.WriteString("- ") result.WriteString(childName) result.WriteString("\n") } } result.WriteString("\n") } return result.String() } func (f *MarkdownFormatter) formatAsOrderedList(item *workflowy.Item, name string) string { var result strings.Builder if name != "" { result.WriteString(name) result.WriteString("\n") } for i, child := range item.Children { if f.shouldExclude(child) { continue } childName := f.stripAllTags(child.Name) if !IsEmpty(childName) { result.WriteString(strings.Repeat(" ", 0)) result.WriteString(string(rune('1'+i)) + ". ") result.WriteString(childName) result.WriteString("\n") } } result.WriteString("\n") return result.String() } func (f *MarkdownFormatter) formatAsQuote(item *workflowy.Item, name string) string { var result strings.Builder if name != "" { result.WriteString("> ") result.WriteString(name) result.WriteString("\n") } for _, child := range item.Children { if f.shouldExclude(child) { continue } childName := f.stripAllTags(child.Name) if !IsEmpty(childName) { result.WriteString("> ") result.WriteString(childName) result.WriteString("\n") } } result.WriteString("\n") return result.String() } func (f *MarkdownFormatter) formatAsCode(item *workflowy.Item, name string) string { var result strings.Builder result.WriteString("```\n") if name != "" { result.WriteString(name) result.WriteString("\n") } for _, child := range item.Children { if f.shouldExclude(child) { continue } childName := f.stripAllTags(child.Name) result.WriteString(childName) result.WriteString("\n") } result.WriteString("```\n\n") return result.String() } func (f *MarkdownFormatter) shouldExclude(item *workflowy.Item) bool { return HasTag(item.Name, f.config.ExcludeTag) } func (f *MarkdownFormatter) getLayoutMode(item *workflowy.Item) string { if HasTag(item.Name, f.config.H1Tag) { return "h1" } if HasTag(item.Name, f.config.H2Tag) { return "h2" } if HasTag(item.Name, f.config.H3Tag) { return "h3" } if HasTag(item.Name, f.config.PTag) { return "p" } if HasTag(item.Name, f.config.ListTag) { return "list" } if item.Data != nil { if mode, ok := item.Data["layoutMode"].(string); ok && mode != "" { return mode } } return "bullets" } func (f *MarkdownFormatter) stripAllTags(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.PTag) text = StripTag(text, f.config.ListTag) return strings.TrimSpace(text) } func FormatItemsAsMarkdown(items []*workflowy.Item) (string, error) { formatter := NewMarkdownFormatter() return formatter.FormatTree(items) }

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