diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index 8108be1bd6..ef90c00dd2 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -38,8 +38,9 @@ type AtmosConfiguration struct { } type AtmosSettings struct { - ListMergeStrategy string `yaml:"list_merge_strategy" json:"list_merge_strategy" mapstructure:"list_merge_strategy"` - Docs Docs `yaml:"docs,omitempty" json:"docs,omitempty" mapstructure:"docs"` + ListMergeStrategy string `yaml:"list_merge_strategy" json:"list_merge_strategy" mapstructure:"list_merge_strategy"` + Docs Docs `yaml:"docs,omitempty" json:"docs,omitempty" mapstructure:"docs"` + Markdown MarkdownSettings `yaml:"markdown,omitempty" json:"markdown,omitempty" mapstructure:"markdown"` } type Docs struct { @@ -588,3 +589,57 @@ type Vendor struct { // If a directory is specified, all .yaml files in the directory will be processed in lexicographical order BasePath string `yaml:"base_path" json:"base_path" mapstructure:"base_path"` } + +type MarkdownSettings struct { + Document MarkdownStyle `yaml:"document,omitempty" json:"document,omitempty" mapstructure:"document"` + BlockQuote MarkdownStyle `yaml:"block_quote,omitempty" json:"block_quote,omitempty" mapstructure:"block_quote"` + Paragraph MarkdownStyle `yaml:"paragraph,omitempty" json:"paragraph,omitempty" mapstructure:"paragraph"` + List MarkdownStyle `yaml:"list,omitempty" json:"list,omitempty" mapstructure:"list"` + ListItem MarkdownStyle `yaml:"list_item,omitempty" json:"list_item,omitempty" mapstructure:"list_item"` + Heading MarkdownStyle `yaml:"heading,omitempty" json:"heading,omitempty" mapstructure:"heading"` + H1 MarkdownStyle `yaml:"h1,omitempty" json:"h1,omitempty" mapstructure:"h1"` + H2 MarkdownStyle `yaml:"h2,omitempty" json:"h2,omitempty" mapstructure:"h2"` + H3 MarkdownStyle `yaml:"h3,omitempty" json:"h3,omitempty" mapstructure:"h3"` + H4 MarkdownStyle `yaml:"h4,omitempty" json:"h4,omitempty" mapstructure:"h4"` + H5 MarkdownStyle `yaml:"h5,omitempty" json:"h5,omitempty" mapstructure:"h5"` + H6 MarkdownStyle `yaml:"h6,omitempty" json:"h6,omitempty" mapstructure:"h6"` + Text MarkdownStyle `yaml:"text,omitempty" json:"text,omitempty" mapstructure:"text"` + Strong MarkdownStyle `yaml:"strong,omitempty" json:"strong,omitempty" mapstructure:"strong"` + Emph MarkdownStyle `yaml:"emph,omitempty" json:"emph,omitempty" mapstructure:"emph"` + Hr MarkdownStyle `yaml:"hr,omitempty" json:"hr,omitempty" mapstructure:"hr"` + Item MarkdownStyle `yaml:"item,omitempty" json:"item,omitempty" mapstructure:"item"` + Enumeration MarkdownStyle `yaml:"enumeration,omitempty" json:"enumeration,omitempty" mapstructure:"enumeration"` + Code MarkdownStyle `yaml:"code,omitempty" json:"code,omitempty" mapstructure:"code"` + CodeBlock MarkdownStyle `yaml:"code_block,omitempty" json:"code_block,omitempty" mapstructure:"code_block"` + Table MarkdownStyle `yaml:"table,omitempty" json:"table,omitempty" mapstructure:"table"` + DefinitionList MarkdownStyle `yaml:"definition_list,omitempty" json:"definition_list,omitempty" mapstructure:"definition_list"` + DefinitionTerm MarkdownStyle `yaml:"definition_term,omitempty" json:"definition_term,omitempty" mapstructure:"definition_term"` + DefinitionDescription MarkdownStyle `yaml:"definition_description,omitempty" json:"definition_description,omitempty" mapstructure:"definition_description"` + HtmlBlock MarkdownStyle `yaml:"html_block,omitempty" json:"html_block,omitempty" mapstructure:"html_block"` + HtmlSpan MarkdownStyle `yaml:"html_span,omitempty" json:"html_span,omitempty" mapstructure:"html_span"` + Link MarkdownStyle `yaml:"link,omitempty" json:"link,omitempty" mapstructure:"link"` + LinkText MarkdownStyle `yaml:"link_text,omitempty" json:"link_text,omitempty" mapstructure:"link_text"` +} + +type MarkdownStyle struct { + BlockPrefix string `yaml:"block_prefix,omitempty" json:"block_prefix,omitempty" mapstructure:"block_prefix"` + BlockSuffix string `yaml:"block_suffix,omitempty" json:"block_suffix,omitempty" mapstructure:"block_suffix"` + Color string `yaml:"color,omitempty" json:"color,omitempty" mapstructure:"color"` + BackgroundColor string `yaml:"background_color,omitempty" json:"background_color,omitempty" mapstructure:"background_color"` + Bold bool `yaml:"bold,omitempty" json:"bold,omitempty" mapstructure:"bold"` + Italic bool `yaml:"italic,omitempty" json:"italic,omitempty" mapstructure:"italic"` + Underline bool `yaml:"underline,omitempty" json:"underline,omitempty" mapstructure:"underline"` + Margin int `yaml:"margin,omitempty" json:"margin,omitempty" mapstructure:"margin"` + Padding int `yaml:"padding,omitempty" json:"padding,omitempty" mapstructure:"padding"` + Indent int `yaml:"indent,omitempty" json:"indent,omitempty" mapstructure:"indent"` + IndentToken string `yaml:"indent_token,omitempty" json:"indent_token,omitempty" mapstructure:"indent_token"` + LevelIndent int `yaml:"level_indent,omitempty" json:"level_indent,omitempty" mapstructure:"level_indent"` + Format string `yaml:"format,omitempty" json:"format,omitempty" mapstructure:"format"` + Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty" mapstructure:"prefix"` + StyleOverride bool `yaml:"style_override,omitempty" json:"style_override,omitempty" mapstructure:"style_override"` + Chroma map[string]ChromaStyle `yaml:"chroma,omitempty" json:"chroma,omitempty" mapstructure:"chroma"` +} + +type ChromaStyle struct { + Color string `yaml:"color,omitempty" json:"color,omitempty" mapstructure:"color"` +} diff --git a/pkg/ui/markdown/renderer.go b/pkg/ui/markdown/renderer.go index f00e4059e1..1b86e70708 100644 --- a/pkg/ui/markdown/renderer.go +++ b/pkg/ui/markdown/renderer.go @@ -27,11 +27,17 @@ func NewRenderer(opts ...Option) (*Renderer, error) { opt(r) } + // Get default style + style, err := GetDefaultStyle() + if err != nil { + return nil, err + } + // Initialize glamour renderer renderer, err := glamour.NewTermRenderer( glamour.WithAutoStyle(), glamour.WithWordWrap(int(r.width)), - glamour.WithStylesFromJSONBytes(DefaultStyle), + glamour.WithStylesFromJSONBytes(style), glamour.WithColorProfile(r.profile), glamour.WithEmoji(), ) diff --git a/pkg/ui/markdown/styles.go b/pkg/ui/markdown/styles.go index ddb9056077..26750da2a7 100644 --- a/pkg/ui/markdown/styles.go +++ b/pkg/ui/markdown/styles.go @@ -1,149 +1,269 @@ package markdown -// DefaultStyle defines the default Atmos markdown style -var DefaultStyle = []byte(`{ - "document": { - "block_prefix": "", - "block_suffix": "\n", - "color": "#FFFFFF", - "margin": 0 - }, - "block_quote": { - "indent": 1, - "indent_token": "│ ", - "color": "#9B51E0" - }, - "paragraph": { - "block_prefix": "", - "block_suffix": "", - "color": "#FFFFFF" - }, - "list": { - "level_indent": 4, - "color": "#FFFFFF", - "margin": 0, - "block_suffix": "" - }, - "list_item": { - "block_prefix": "– ", - "color": "#FFFFFF", - "margin": 0, - "block_suffix": "" - }, - "heading": { - "block_prefix": "", - "block_suffix": "\n", - "color": "#00A3E0", - "bold": true, - "margin": 0, - "style_override": true - }, - "h1": { - "prefix": "", - "color": "#FFFFFF", - "background_color": "#9B51E0", - "bold": true, - "margin": 2, - "block_prefix": "\n", - "block_suffix": "\n", - "padding": 1 - }, - "h2": { - "prefix": "## ", - "color": "#9B51E0", - "bold": true, - "margin": 1 - }, - "h3": { - "prefix": "### ", - "color": "#00A3E0", - "bold": true - }, - "h4": { - "prefix": "#### ", - "color": "#00A3E0", - "bold": true - }, - "h5": { - "prefix": "##### ", - "color": "#00A3E0", - "bold": true - }, - "h6": { - "prefix": "###### ", - "color": "#00A3E0", - "bold": true - }, - "text": { - "color": "#FFFFFF" - }, - "strong": { - "color": "#9B51E0", - "bold": true - }, - "emph": { - "color": "#9B51E0", - "italic": true - }, - "hr": { - "color": "#9B51E0", - "format": "\n--------\n" - }, - "item": { - "block_prefix": "• " - }, - "enumeration": { - "block_prefix": ". " - }, - "code": { - "color": "#9B51E0" - }, - "code_block": { - "margin": 1, - "indent": 2, - "block_suffix": "", - "chroma": { - "text": { - "color": "#00A3E0" - }, - "keyword": { - "color": "#9B51E0" - }, - "literal": { - "color": "#00A3E0" - }, - "string": { - "color": "#00A3E0" - }, - "name": { - "color": "#00A3E0" - }, - "number": { - "color": "#00A3E0" - }, - "comment": { - "color": "#9B51E0" - } - } - }, - "table": { - "center_separator": "┼", - "column_separator": "│", - "row_separator": "─" - }, - "definition_list": {}, - "definition_term": {}, - "definition_description": { - "block_prefix": "\n" - }, - "html_block": {}, - "html_span": {}, - "link": { - "color": "#00A3E0", - "underline": true - }, - "link_text": { - "color": "#9B51E0", - "bold": true - } -}`) +import ( + "encoding/json" + + "github.com/charmbracelet/glamour/ansi" + "github.com/cloudposse/atmos/pkg/config" + "github.com/cloudposse/atmos/pkg/schema" +) + +// GetDefaultStyle returns the markdown style configuration from atmos.yaml settings +// or falls back to built-in defaults if not configured +func GetDefaultStyle() ([]byte, error) { + atmosConfig, err := config.InitCliConfig(schema.ConfigAndStacksInfo{}, false) + if err != nil { + return getBuiltinDefaultStyle() + } + + // Get the built-in default style + defaultBytes, err := getBuiltinDefaultStyle() + if err != nil { + return nil, err + } + + if atmosConfig.Settings.Markdown.Document.Color == "" && + atmosConfig.Settings.Markdown.Heading.Color == "" && + atmosConfig.Settings.Markdown.H1.Color == "" && + atmosConfig.Settings.Markdown.H2.Color == "" && + atmosConfig.Settings.Markdown.H3.Color == "" { + return defaultBytes, nil + } + + var style ansi.StyleConfig + if err := json.Unmarshal(defaultBytes, &style); err != nil { + return nil, err + } + + // Apply custom styles on top of defaults + if atmosConfig.Settings.Markdown.Document.Color != "" { + style.Document.Color = &atmosConfig.Settings.Markdown.Document.Color + } + + if atmosConfig.Settings.Markdown.Heading.Color != "" { + style.Heading.Color = &atmosConfig.Settings.Markdown.Heading.Color + style.Heading.Bold = &atmosConfig.Settings.Markdown.Heading.Bold + } + + if atmosConfig.Settings.Markdown.H1.Color != "" { + style.H1.Color = &atmosConfig.Settings.Markdown.H1.Color + if atmosConfig.Settings.Markdown.H1.BackgroundColor != "" { + style.H1.BackgroundColor = &atmosConfig.Settings.Markdown.H1.BackgroundColor + } + style.H1.Bold = &atmosConfig.Settings.Markdown.H1.Bold + style.H1.Margin = uintPtr(uint(atmosConfig.Settings.Markdown.H1.Margin)) + } + + if atmosConfig.Settings.Markdown.H2.Color != "" { + style.H2.Color = &atmosConfig.Settings.Markdown.H2.Color + style.H2.Bold = &atmosConfig.Settings.Markdown.H2.Bold + } + + if atmosConfig.Settings.Markdown.H3.Color != "" { + style.H3.Color = &atmosConfig.Settings.Markdown.H3.Color + style.H3.Bold = &atmosConfig.Settings.Markdown.H3.Bold + } + + if atmosConfig.Settings.Markdown.CodeBlock.Color != "" { + if style.CodeBlock.StyleBlock.StylePrimitive.Color == nil { + style.CodeBlock.StyleBlock.StylePrimitive.Color = &atmosConfig.Settings.Markdown.CodeBlock.Color + } else { + *style.CodeBlock.StyleBlock.StylePrimitive.Color = atmosConfig.Settings.Markdown.CodeBlock.Color + } + style.CodeBlock.Margin = uintPtr(uint(atmosConfig.Settings.Markdown.CodeBlock.Margin)) + } + + if atmosConfig.Settings.Markdown.Link.Color != "" { + style.Link.Color = &atmosConfig.Settings.Markdown.Link.Color + style.Link.Underline = &atmosConfig.Settings.Markdown.Link.Underline + } + + if atmosConfig.Settings.Markdown.Strong.Color != "" { + style.Strong.Color = &atmosConfig.Settings.Markdown.Strong.Color + style.Strong.Bold = &atmosConfig.Settings.Markdown.Strong.Bold + } + + if atmosConfig.Settings.Markdown.Emph.Color != "" { + style.Emph.Color = &atmosConfig.Settings.Markdown.Emph.Color + style.Emph.Italic = &atmosConfig.Settings.Markdown.Emph.Italic + } + + return json.Marshal(style) +} + +// this only returns the built-in default style configuration +func getBuiltinDefaultStyle() ([]byte, error) { + style := ansi.StyleConfig{ + Document: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + BlockPrefix: "", + BlockSuffix: "\n", + Color: stringPtr("#FFFFFF"), + }, + Margin: uintPtr(0), + }, + BlockQuote: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Color: stringPtr("#9B51E0"), + }, + Indent: uintPtr(1), + IndentToken: stringPtr("│ "), + }, + Paragraph: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + BlockPrefix: "", + BlockSuffix: "", + Color: stringPtr("#FFFFFF"), + }, + }, + List: ansi.StyleList{ + LevelIndent: 4, + }, + Heading: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + BlockPrefix: "", + BlockSuffix: "\n", + Color: stringPtr("#00A3E0"), + Bold: boolPtr(true), + }, + Margin: uintPtr(0), + }, + H1: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "", + Color: stringPtr("#FFFFFF"), + BackgroundColor: stringPtr("#9B51E0"), + Bold: boolPtr(true), + }, + Margin: uintPtr(2), + }, + H2: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "## ", + Color: stringPtr("#9B51E0"), + Bold: boolPtr(true), + }, + Margin: uintPtr(1), + }, + H3: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "### ", + Color: stringPtr("#00A3E0"), + Bold: boolPtr(true), + }, + }, + H4: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "#### ", + Color: stringPtr("#00A3E0"), + Bold: boolPtr(true), + }, + }, + H5: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "##### ", + Color: stringPtr("#00A3E0"), + Bold: boolPtr(true), + }, + }, + H6: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "###### ", + Color: stringPtr("#00A3E0"), + Bold: boolPtr(true), + }, + }, + Text: ansi.StylePrimitive{ + Color: stringPtr("#FFFFFF"), + }, + Strong: ansi.StylePrimitive{ + Color: stringPtr("#9B51E0"), + Bold: boolPtr(true), + }, + Emph: ansi.StylePrimitive{ + Color: stringPtr("#9B51E0"), + Italic: boolPtr(true), + }, + HorizontalRule: ansi.StylePrimitive{ + Color: stringPtr("#9B51E0"), + Format: "\n--------\n", + }, + Item: ansi.StylePrimitive{ + BlockPrefix: "• ", + }, + Enumeration: ansi.StylePrimitive{ + BlockPrefix: ". ", + }, + Code: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Color: stringPtr("#9B51E0"), + }, + }, + CodeBlock: ansi.StyleCodeBlock{ + StyleBlock: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Color: stringPtr("#00A3E0"), + }, + Margin: uintPtr(1), + }, + Chroma: &ansi.Chroma{ + Text: ansi.StylePrimitive{ + Color: stringPtr("#00A3E0"), + }, + Keyword: ansi.StylePrimitive{ + Color: stringPtr("#9B51E0"), + }, + Literal: ansi.StylePrimitive{ + Color: stringPtr("#00A3E0"), + }, + LiteralString: ansi.StylePrimitive{ + Color: stringPtr("#00A3E0"), + }, + Name: ansi.StylePrimitive{ + Color: stringPtr("#00A3E0"), + }, + LiteralNumber: ansi.StylePrimitive{ + Color: stringPtr("#00A3E0"), + }, + Comment: ansi.StylePrimitive{ + Color: stringPtr("#9B51E0"), + }, + }, + }, + Table: ansi.StyleTable{ + StyleBlock: ansi.StyleBlock{}, + CenterSeparator: stringPtr("┼"), + ColumnSeparator: stringPtr("│"), + RowSeparator: stringPtr("─"), + }, + DefinitionList: ansi.StyleBlock{}, + DefinitionTerm: ansi.StylePrimitive{}, + DefinitionDescription: ansi.StylePrimitive{ + BlockPrefix: "\n", + }, + HTMLBlock: ansi.StyleBlock{}, + HTMLSpan: ansi.StyleBlock{}, + Link: ansi.StylePrimitive{ + Color: stringPtr("#00A3E0"), + Underline: boolPtr(true), + }, + LinkText: ansi.StylePrimitive{ + Color: stringPtr("#9B51E0"), + Bold: boolPtr(true), + }, + } + + return json.Marshal(style) +} + +func stringPtr(s string) *string { + return &s +} + +func boolPtr(b bool) *bool { + return &b +} + +func uintPtr(u uint) *uint { + return &u +}