From aa3a3665aeafe5e976f5687066340c3309bfd722 Mon Sep 17 00:00:00 2001 From: Dieter Eickstaedt Date: Wed, 23 Nov 2022 11:56:42 +0100 Subject: [PATCH 1/6] feature: Adding FrontMatterConsumer Callback --- ansi/renderer.go | 7 ++ extension/_test/frontmatter.txt | 20 +++++ extension/ast/frontmatter.go | 29 +++++++ extension/frontmatter.go | 138 ++++++++++++++++++++++++++++++++ extension/frontmatter_test.go | 20 +++++ glamour.go | 34 +++++++- go.mod | 1 + 7 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 extension/_test/frontmatter.txt create mode 100644 extension/ast/frontmatter.go create mode 100644 extension/frontmatter.go create mode 100644 extension/frontmatter_test.go diff --git a/ansi/renderer.go b/ansi/renderer.go index 55444cae..a2812c41 100644 --- a/ansi/renderer.go +++ b/ansi/renderer.go @@ -1,6 +1,7 @@ package ansi import ( + localast "github.com/charmbracelet/glamour/extension/ast" "io" "net/url" "strings" @@ -16,6 +17,7 @@ import ( // Options is used to configure an ANSIRenderer. type Options struct { BaseURL string + LinkTextOnly bool WordWrap int PreserveNewLines bool ColorProfile termenv.Profile @@ -37,6 +39,7 @@ func NewRenderer(options Options) *ANSIRenderer { // RegisterFuncs implements NodeRenderer.RegisterFuncs. func (r *ANSIRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { // blocks + reg.Register(localast.KindFrontmatter, r.handleFrontmatter) reg.Register(ast.KindDocument, r.renderNode) reg.Register(ast.KindHeading, r.renderNode) reg.Register(ast.KindBlockquote, r.renderNode) @@ -86,6 +89,10 @@ func (r *ANSIRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(east.KindEmoji, r.renderNode) } +func (r *ANSIRenderer) handleFrontmatter(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + return ast.WalkContinue, nil +} + func (r *ANSIRenderer) renderNode(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { // _, _ = w.Write([]byte(node.Type.String())) writeTo := io.Writer(w) diff --git a/extension/_test/frontmatter.txt b/extension/_test/frontmatter.txt new file mode 100644 index 00000000..6cbff2b6 --- /dev/null +++ b/extension/_test/frontmatter.txt @@ -0,0 +1,20 @@ +1 +//- - - - - - - - -// +--- +title: Ein Title +longText: | + Ein langer Text + der über mehrere + Zeilen geht +number: 120 +--- +//- - - - - - - - -// + +//= = = = = = = = = = = = = = = = = = = = = = = =// diff --git a/extension/ast/frontmatter.go b/extension/ast/frontmatter.go new file mode 100644 index 00000000..978d5498 --- /dev/null +++ b/extension/ast/frontmatter.go @@ -0,0 +1,29 @@ +package ast + +import ( + "fmt" + "github.com/yuin/goldmark/ast" +) + +// Frontmatter AST Node holding parsed YAML Frontmatter. +// Status Parse Error or empty. +type Frontmatter struct { + ast.BaseBlock + MetaData map[interface{}]interface{} + Status string +} + +var KindFrontmatter = ast.NewNodeKind("Frontmatter") + +func (f Frontmatter) Kind() ast.NodeKind { + return KindFrontmatter +} + +func (f *Frontmatter) Dump(source []byte, level int) { + m := map[string]string{} + m["_status"] = fmt.Sprintf("%v", f.Status) + for key, value := range f.MetaData { + m[fmt.Sprintf("%v", key)] = fmt.Sprintf("%v", value) + } + ast.DumpHelper(f, source, level, m, nil) +} diff --git a/extension/frontmatter.go b/extension/frontmatter.go new file mode 100644 index 00000000..b6b8d309 --- /dev/null +++ b/extension/frontmatter.go @@ -0,0 +1,138 @@ +package extension + +import ( + "bytes" + "fmt" + localast "github.com/charmbracelet/glamour/extension/ast" + "github.com/yuin/goldmark" + gast "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" + "gopkg.in/yaml.v3" + "regexp" + "sort" + "strings" +) + +type FrontmatterResultConsumer interface { + HandleFrontmatter(frontmatter map[string]interface{}) +} + +type frontMatterParser struct { + Handler FrontmatterResultConsumer +} + +func (f frontMatterParser) Trigger() []byte { + return []byte("---") +} + +func (f frontMatterParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) { + line, _ := reader.PeekLine() + line = bytes.TrimRight(line, "\r\n") + if matched, _ := regexp.Match("^-{3,}$", line); matched { + reader.AdvanceLine() + + // read all lines and interpret as yaml + var buf bytes.Buffer + var y map[interface{}]interface{} + for { + line, _ = reader.PeekLine() + if matched, _ := regexp.Match("^-{3,}$", []byte(strings.TrimRight(string(line), "\r\n"))); !matched { + + buf.Write(line) + } else { + break + } + reader.AdvanceLine() + } + if err := yaml.Unmarshal(buf.Bytes(), &y); err != nil { + fmt.Errorf("unable to parse Frontmatter as YAML %s", err.Error()) + return &localast.Frontmatter{MetaData: nil, Status: err.Error()}, parser.NoChildren + } + result := localast.Frontmatter{MetaData: y} + if f.Handler != nil { + var m = make(map[string]interface{}) + for key, value := range y { + m[fmt.Sprintf("%v", key)] = value + } + f.Handler.HandleFrontmatter(m) + } + return &result, parser.NoChildren + } + return nil, parser.NoChildren +} + +func (f frontMatterParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State { + // all parsing done in Open already + return parser.Close +} + +func (f frontMatterParser) Close(node gast.Node, reader text.Reader, pc parser.Context) { + // nothing to do +} + +func (f frontMatterParser) CanInterruptParagraph() bool { + return false +} + +func (f frontMatterParser) CanAcceptIndentedLine() bool { + return false +} + +var DefaultFrontMatterParser = &frontMatterParser{} + +func NewFrontMatterParser() parser.BlockParser { + return DefaultFrontMatterParser +} + +type FrontmatterHTMLRenderer struct { + Config html.Config +} + +// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. +func (r *FrontmatterHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(localast.KindFrontmatter, r.renderFrontmatterStart) +} + +func (r *FrontmatterHTMLRenderer) renderFrontmatterStart(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) { + if entering { + w.WriteString("\n") + } + return gast.WalkContinue, nil +} + +func NewFrontmatterHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { + r := &FrontmatterHTMLRenderer{ + Config: html.NewConfig(), + } + for _, opt := range opts { + opt.SetHTMLOption(&r.Config) + } + return r +} + +func (f frontMatterParser) Extend(m goldmark.Markdown) { + m.Parser().AddOptions( + parser.WithBlockParsers( + util.Prioritized(NewFrontMatterParser(), 99))) + m.Renderer().AddOptions(renderer.WithNodeRenderers(util.Prioritized(NewFrontmatterHTMLRenderer(), 99))) +} diff --git a/extension/frontmatter_test.go b/extension/frontmatter_test.go new file mode 100644 index 00000000..05799d7c --- /dev/null +++ b/extension/frontmatter_test.go @@ -0,0 +1,20 @@ +package extension + +import ( + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/testutil" + "testing" +) + +func TestFrontMatterParsing(t *testing.T) { + markdown := goldmark.New( + goldmark.WithRendererOptions( + html.WithUnsafe(), + ), + goldmark.WithExtensions( + DefaultFrontMatterParser, + ), + ) + testutil.DoTestCaseFile(markdown, "_test/frontmatter.txt", t, testutil.ParseCliCaseArg()...) +} diff --git a/glamour.go b/glamour.go index da31d478..f2225951 100644 --- a/glamour.go +++ b/glamour.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "os" + ext "github.com/charmbracelet/glamour/extension" "github.com/muesli/termenv" "github.com/yuin/goldmark" emoji "github.com/yuin/goldmark-emoji" @@ -24,10 +25,11 @@ type TermRendererOption func(*TermRenderer) error // TermRenderer can be used to render markdown content, posing a depth of // customization and styles to fit your needs. type TermRenderer struct { - md goldmark.Markdown - ansiOptions ansi.Options - buf bytes.Buffer - renderBuf bytes.Buffer + md goldmark.Markdown + ansiOptions ansi.Options + buf bytes.Buffer + renderBuf bytes.Buffer + frontMatterHandler ext.FrontmatterResultConsumer } // Render initializes a new TermRenderer and renders a markdown with a specific @@ -56,6 +58,12 @@ func RenderBytes(in []byte, stylePath string) ([]byte, error) { return r.RenderBytes(in) } +func (tr *TermRenderer) HandleFrontmatter(frontmatter map[string]interface{}) { + if tr.frontMatterHandler != nil { + tr.frontMatterHandler.HandleFrontmatter(frontmatter) + } +} + // NewTermRenderer returns a new TermRenderer the given options. func NewTermRenderer(options ...TermRendererOption) (*TermRenderer, error) { tr := &TermRenderer{ @@ -63,6 +71,7 @@ func NewTermRenderer(options ...TermRendererOption) (*TermRenderer, error) { goldmark.WithExtensions( extension.GFM, extension.DefinitionList, + ext.DefaultFrontMatterParser, ), goldmark.WithParserOptions( parser.WithAutoHeadingID(), @@ -71,8 +80,11 @@ func NewTermRenderer(options ...TermRendererOption) (*TermRenderer, error) { ansiOptions: ansi.Options{ WordWrap: 80, ColorProfile: termenv.TrueColor, + LinkTextOnly: false, }, } + // register Termrenderer as Callback for Frontmatter + ext.DefaultFrontMatterParser.Handler = tr for _, o := range options { if err := o(tr); err != nil { return nil, err @@ -97,6 +109,13 @@ func WithBaseURL(baseURL string) TermRendererOption { } } +func WithFrontMatterHandler(consumer ext.FrontmatterResultConsumer) TermRendererOption { + return func(termRenderer *TermRenderer) error { + termRenderer.frontMatterHandler = consumer + return nil + } +} + // WithColorProfile sets the TermRenderer's color profile // (TrueColor / ANSI256 / ANSI). func WithColorProfile(profile termenv.Profile) TermRendererOption { @@ -185,6 +204,13 @@ func WithWordWrap(wordWrap int) TermRendererOption { } } +func WithLinkTextOnly(onlyLinkText bool) TermRendererOption { + return func(tr *TermRenderer) error { + tr.ansiOptions.LinkTextOnly = onlyLinkText + return nil + } +} + // WithPreservedNewlines preserves newlines from being replaced. func WithPreservedNewLines() TermRendererOption { return func(tr *TermRenderer) error { diff --git a/go.mod b/go.mod index e674ad5d..cafde7a5 100644 --- a/go.mod +++ b/go.mod @@ -10,4 +10,5 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/yuin/goldmark v1.5.3 github.com/yuin/goldmark-emoji v1.0.1 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c ) From 18374f2d4fde60e2a0a5b9b34f5f54bc1bc5acd2 Mon Sep 17 00:00:00 2001 From: Dieter Eickstaedt Date: Wed, 23 Nov 2022 13:54:42 +0100 Subject: [PATCH 2/6] Remove LinkTextOnly code --- ansi/renderer.go | 1 - extension/_test/frontmatter.txt | 14 ++++++-------- glamour.go | 8 -------- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/ansi/renderer.go b/ansi/renderer.go index a2812c41..b6551d99 100644 --- a/ansi/renderer.go +++ b/ansi/renderer.go @@ -17,7 +17,6 @@ import ( // Options is used to configure an ANSIRenderer. type Options struct { BaseURL string - LinkTextOnly bool WordWrap int PreserveNewLines bool ColorProfile termenv.Profile diff --git a/extension/_test/frontmatter.txt b/extension/_test/frontmatter.txt index 6cbff2b6..c160a1d1 100644 --- a/extension/_test/frontmatter.txt +++ b/extension/_test/frontmatter.txt @@ -1,20 +1,18 @@ 1 //- - - - - - - - -// --- -title: Ein Title +title: A Title longText: | - Ein langer Text - der über mehrere - Zeilen geht + Long text spanning multiple + lines of input number: 120 --- //- - - - - - - - -// //= = = = = = = = = = = = = = = = = = = = = = = =// diff --git a/glamour.go b/glamour.go index f2225951..767d9aa1 100644 --- a/glamour.go +++ b/glamour.go @@ -80,7 +80,6 @@ func NewTermRenderer(options ...TermRendererOption) (*TermRenderer, error) { ansiOptions: ansi.Options{ WordWrap: 80, ColorProfile: termenv.TrueColor, - LinkTextOnly: false, }, } // register Termrenderer as Callback for Frontmatter @@ -204,13 +203,6 @@ func WithWordWrap(wordWrap int) TermRendererOption { } } -func WithLinkTextOnly(onlyLinkText bool) TermRendererOption { - return func(tr *TermRenderer) error { - tr.ansiOptions.LinkTextOnly = onlyLinkText - return nil - } -} - // WithPreservedNewlines preserves newlines from being replaced. func WithPreservedNewLines() TermRendererOption { return func(tr *TermRenderer) error { From 32712ff03fcae04f509e2c11d8fd3062ee8e5c30 Mon Sep 17 00:00:00 2001 From: Jahvon Dockery Date: Tue, 9 Jul 2024 12:51:41 -0400 Subject: [PATCH 3/6] fix: use specified indent token with codeblock (#299) --- ansi/codeblock.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ansi/codeblock.go b/ansi/codeblock.go index 9b578194..601e85ab 100644 --- a/ansi/codeblock.go +++ b/ansi/codeblock.go @@ -118,8 +118,12 @@ func (e *CodeBlockElement) Render(w io.Writer, ctx RenderContext) error { mutex.Unlock() } + ic := " " + if rules.IndentToken != nil { + ic = *rules.IndentToken + } iw := indent.NewWriterPipe(w, indentation+margin, func(wr io.Writer) { - renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, " ") + renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, ic) }) if len(theme) > 0 { From e382ae9f8b47f1c5abc849a13a58a9da420fc3f8 Mon Sep 17 00:00:00 2001 From: nervo Date: Tue, 9 Jul 2024 18:52:03 +0200 Subject: [PATCH 4/6] chore: cleanup useless comments (#302) --- ansi/elements.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/ansi/elements.go b/ansi/elements.go index bb158187..ce41eb3a 100644 --- a/ansi/elements.go +++ b/ansi/elements.go @@ -34,8 +34,6 @@ type Element struct { // NewElement returns the appropriate render Element for a given node. func (tr *ANSIRenderer) NewElement(node ast.Node, source []byte) Element { ctx := tr.context - // fmt.Print(strings.Repeat(" ", ctx.blockStack.Len()), node.Type(), node.Kind()) - // defer fmt.Println() switch node.Kind() { // Document From 5e17ca86e5d33f4a6392da9d90fbc9462651f6dc Mon Sep 17 00:00:00 2001 From: Vasyl Tyshchuk Date: Tue, 9 Jul 2024 19:52:48 +0300 Subject: [PATCH 5/6] feat: Add Tokyo Night theme (#229) --- styles/tokyo_night.json | 184 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 styles/tokyo_night.json diff --git a/styles/tokyo_night.json b/styles/tokyo_night.json new file mode 100644 index 00000000..801278c8 --- /dev/null +++ b/styles/tokyo_night.json @@ -0,0 +1,184 @@ +{ + "document": { + "block_prefix": "\n", + "block_suffix": "\n", + "color": "#a9b1d6", + "margin": 2 + }, + "block_quote": { + "indent": 1, + "indent_token": "│ " + }, + "paragraph": {}, + "list": { + "color": "#a9b1d6", + "level_indent": 2 + }, + "heading": { + "block_suffix": "\n", + "color": "#bb9af7", + "bold": true + }, + "h1": { + "prefix": "# " + }, + "h2": { + "prefix": "## " + }, + "h3": { + "prefix": "### " + }, + "h4": { + "prefix": "#### " + }, + "h5": { + "prefix": "##### " + }, + "h6": { + "prefix": "###### " + }, + "text": {}, + "strikethrough": { + "crossed_out": true + }, + "emph": { + "italic": true + }, + "strong": { + "bold": true + }, + "hr": { + "color": "#565f89", + "format": "\n--------\n" + }, + "item": { + "block_prefix": "• " + }, + "enumeration": { + "block_prefix": ". ", + "color": "#7aa2f7" + }, + "task": { + "ticked": "[✓] ", + "unticked": "[ ] " + }, + "link": { + "color": "#7aa2f7", + "underline": true + }, + "link_text": { + "color": "#2ac3de" + }, + "image": { + "color": "#7aa2f7", + "underline": true + }, + "image_text": { + "color": "#2ac3de", + "format": "Image: {{.text}} →" + }, + "code": { + "color": "#9ece6a" + }, + "code_block": { + "color": "#ff9e64", + "margin": 2, + "chroma": { + "text": { + "color": "#a9b1d6" + }, + "error": { + "color": "#a9b1d6", + "background_color": "#f7768e" + }, + "comment": { + "color": "#565f89" + }, + "comment_preproc": { + "color": "#2ac3de" + }, + "keyword": { + "color": "#2ac3de" + }, + "keyword_reserved": { + "color": "#2ac3de" + }, + "keyword_namespace": { + "color": "#2ac3de" + }, + "keyword_type": { + "color": "#7aa2f7" + }, + "operator": { + "color": "#2ac3de" + }, + "punctuation": { + "color": "#a9b1d6" + }, + "name": { + "color": "#7aa2f7" + }, + "name_builtin": { + "color": "#7aa2f7" + }, + "name_tag": { + "color": "#2ac3de" + }, + "name_attribute": { + "color": "#9ece6a" + }, + "name_class": { + "color": "#7aa2f7" + }, + "name_constant": { + "color": "#bb9af7" + }, + "name_decorator": { + "color": "#9ece6a" + }, + "name_exception": {}, + "name_function": { + "color": "#9ece6a" + }, + "name_other": {}, + "literal": {}, + "literal_date": {}, + "literal_string": { + "color": "#e0af68" + }, + "literal_string_escape": { + "color": "#2ac3de" + }, + "generic_deleted": { + "color": "#f7768e" + }, + "generic_emph": { + "italic": true + }, + "generic_inserted": { + "color": "#9ece6a" + }, + "generic_strong": { + "bold": true + }, + "generic_subheading": { + "color": "#bb9af7" + }, + "background": { + "background_color": "#1a1b26" + } + } + }, + "table": { + "center_separator": "┼", + "column_separator": "│", + "row_separator": "─" + }, + "definition_list": {}, + "definition_term": {}, + "definition_description": { + "block_prefix": "\n🠶 " + }, + "html_block": {}, + "html_span": {} +} From dc67412fbfc89eb9b13c66a741af85e0cd6be4d1 Mon Sep 17 00:00:00 2001 From: Dieter Eickstaedt Date: Tue, 9 Jul 2024 21:24:41 +0200 Subject: [PATCH 6/6] init: merge --- glamour.go | 7 +++++++ go.sum | 3 +++ 2 files changed, 10 insertions(+) diff --git a/glamour.go b/glamour.go index 742b538d..5c74d880 100644 --- a/glamour.go +++ b/glamour.go @@ -123,6 +123,13 @@ func WithBaseURL(baseURL string) TermRendererOption { } } +func WithFrontMatterRenderer(r renderer.Renderer) TermRendererOption { + return func(termRenderer *TermRenderer) error { + termRenderer.md.Renderer().AddOptions(renderer.WithNodeRenderers(util.Prioritized(r, 99))) + return nil + } +} + func WithFrontMatterHandler(consumer ext.FrontmatterResultConsumer) TermRendererOption { return func(termRenderer *TermRenderer) error { termRenderer.frontMatterHandler = consumer diff --git a/go.sum b/go.sum index 9ff84c44..ef368ee7 100644 --- a/go.sum +++ b/go.sum @@ -44,3 +44,6 @@ golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=