diff --git a/README.md b/README.md index 16ebd21..036d730 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ # What is markdown package The Package markdown is a simple markdown builder in golang. The markdown package assembles Markdown using method chaining, not uses a template engine like [html/template](https://pkg.go.dev/html/template). The syntax of Markdown follows **GitHub Markdown**. -The markdown package was initially developed to save test results in [nao1215/spectest](https://github.com/nao1215/spectest). Therefore, the markdown package implements the features required by spectest. For example, the markdown package supports **mermaid sequence diagrams (entity relationship diagram, sequence diagram, pie chart)**, which was a necessary feature in spectest. +The markdown package was initially developed to save test results in [nao1215/spectest](https://github.com/nao1215/spectest). Therefore, the markdown package implements the features required by spectest. For example, the markdown package supports **mermaid sequence diagrams (entity relationship diagram, sequence diagram, flowchart, pie chart)**, which was a necessary feature in spectest. Additionally, complex code that increases the complexity of the library, such as generating nested lists, will not be added. I want to keep this library as simple as possible. @@ -34,6 +34,7 @@ Additionally, complex code that increases the complexity of the library, such as - [x] Alerts; NOTE, TIP, IMPORTANT, CAUTION, WARNING - [x] mermaid sequence diagram - [x] mermaid entity relationship diagram +- [x] mermaid flowchart - [x] mermaid pie chart ### Features not in Markdown syntax @@ -165,6 +166,7 @@ func main() { if err != nil { panic(err) } + defer f.Close() md.NewMarkdown(f). H1("go generate example"). @@ -366,6 +368,7 @@ func main() { if err != nil { panic(err) } + defer f.Close() teachers := er.NewEntity( "teachers", @@ -523,6 +526,86 @@ erDiagram } ``` +### Flowchart syntax + +```go +package main + +import ( + "io" + "os" + + "github.com/nao1215/markdown" + "github.com/nao1215/markdown/mermaid/flowchart" +) + +//go:generate go run main.go + +func main() { + f, err := os.Create("generated.md") + if err != nil { + panic(err) + } + defer f.Close() + + fc := flowchart.NewFlowchart( + io.Discard, + flowchart.WithTitle("mermaid flowchart builder"), + flowchart.WithOrientalTopToBottom(), + ). + NodeWithText("A", "Node A"). + StadiumNode("B", "Node B"). + SubroutineNode("C", "Node C"). + DatabaseNode("D", "Database"). + LinkWithArrowHead("A", "B"). + LinkWithArrowHeadAndText("B", "D", "send original data"). + LinkWithArrowHead("B", "C"). + DottedLinkWithText("C", "D", "send filtered data"). + String() + + err = markdown.NewMarkdown(f). + H2("Flowchart"). + CodeBlocks(markdown.SyntaxHighlightMermaid, fc). + Build() + + if err != nil { + panic(err) + } +} +``` + +Plain text output: [markdown is here](./doc/flowchart/generated.md) +```` +## Flowchart +```mermaid +--- +title: mermaid flowchart builder +--- +flowchart TB + A["Node A"] + B(["Node B"]) + C[["Node C"]] + D[("Database")] + A-->B + B-->|"send original data"|D + B-->C + C-. "send filtered data" .-> D +``` +```` + +Mermaid output: +```mermaid +flowchart TB + A["Node A"] + B(["Node B"]) + C[["Node C"]] + D[("Database")] + A-->B + B-->|"send original data"|D + B-->C + C-. "send filtered data" .-> D +``` + ### Pie chart syntax ```go @@ -543,6 +626,7 @@ func main() { if err != nil { panic(err) } + defer f.Close() chart := piechart.NewPieChart( io.Discard, diff --git a/doc/alert/main.go b/doc/alert/main.go index abfc534..154d410 100644 --- a/doc/alert/main.go +++ b/doc/alert/main.go @@ -16,6 +16,7 @@ func main() { if err != nil { panic(err) } + defer f.Close() if err := md.NewMarkdown(f). H1("Alert example"). diff --git a/doc/badge/main.go b/doc/badge/main.go index daea9ea..7939449 100644 --- a/doc/badge/main.go +++ b/doc/badge/main.go @@ -16,6 +16,7 @@ func main() { if err != nil { panic(err) } + defer f.Close() if err := md.NewMarkdown(f). H1("badge example"). diff --git a/doc/er/main.go b/doc/er/main.go index a7ac35b..6619f73 100644 --- a/doc/er/main.go +++ b/doc/er/main.go @@ -17,6 +17,7 @@ func main() { if err != nil { panic(err) } + defer f.Close() teachers := er.NewEntity( "teachers", diff --git a/doc/flowchart/generated.md b/doc/flowchart/generated.md new file mode 100644 index 0000000..e4c1ca2 --- /dev/null +++ b/doc/flowchart/generated.md @@ -0,0 +1,15 @@ +## Flowchart +```mermaid +--- +title: mermaid flowchart builder +--- +flowchart TB + A["Node A"] + B(["Node B"]) + C[["Node C"]] + D[("Database")] + A-->B + B-->|"send original data"|D + B-->C + C-. "send filtered data" .-> D +``` \ No newline at end of file diff --git a/doc/flowchart/main.go b/doc/flowchart/main.go new file mode 100644 index 0000000..64a3f1e --- /dev/null +++ b/doc/flowchart/main.go @@ -0,0 +1,46 @@ +//go:build linux || darwin + +// Package main is generating flowchart. +package main + +import ( + "io" + "os" + + "github.com/nao1215/markdown" + "github.com/nao1215/markdown/mermaid/flowchart" +) + +//go:generate go run main.go + +func main() { + f, err := os.Create("generated.md") + if err != nil { + panic(err) + } + defer f.Close() + + fc := flowchart.NewFlowchart( + io.Discard, + flowchart.WithTitle("mermaid flowchart builder"), + flowchart.WithOrientalTopToBottom(), + ). + NodeWithText("A", "Node A"). + StadiumNode("B", "Node B"). + SubroutineNode("C", "Node C"). + DatabaseNode("D", "Database"). + LinkWithArrowHead("A", "B"). + LinkWithArrowHeadAndText("B", "D", "send original data"). + LinkWithArrowHead("B", "C"). + DottedLinkWithText("C", "D", "send filtered data"). + String() + + err = markdown.NewMarkdown(f). + H2("Flowchart"). + CodeBlocks(markdown.SyntaxHighlightMermaid, fc). + Build() + + if err != nil { + panic(err) + } +} diff --git a/doc/generate/main.go b/doc/generate/main.go index a1e5d64..57a9b4c 100644 --- a/doc/generate/main.go +++ b/doc/generate/main.go @@ -16,6 +16,7 @@ func main() { if err != nil { panic(err) } + defer f.Close() if err := md.NewMarkdown(f). H1("go generate example"). diff --git a/doc/piechart/main.go b/doc/piechart/main.go index 6e5ae67..f61cede 100644 --- a/doc/piechart/main.go +++ b/doc/piechart/main.go @@ -18,6 +18,7 @@ func main() { if err != nil { panic(err) } + defer f.Close() chart := piechart.NewPieChart( io.Discard, diff --git a/doc/sequence/main.go b/doc/sequence/main.go index 740fb1d..d76ef63 100644 --- a/doc/sequence/main.go +++ b/doc/sequence/main.go @@ -18,6 +18,7 @@ func main() { if err != nil { panic(err) } + defer f.Close() diagram := sequence.NewDiagram(io.Discard). Participant("Sophia"). diff --git a/internal/lf.go b/internal/lf.go new file mode 100644 index 0000000..935870d --- /dev/null +++ b/internal/lf.go @@ -0,0 +1,12 @@ +// Package internal package is used to store the internal implementation of the mermaid package. +package internal + +import "runtime" + +// LineFeed return line feed for current OS. +func LineFeed() string { + if runtime.GOOS == "windows" { + return "\r\n" + } + return "\n" +} diff --git a/internal/lf_test.go b/internal/lf_test.go new file mode 100644 index 0000000..122dea4 --- /dev/null +++ b/internal/lf_test.go @@ -0,0 +1,28 @@ +// Package internal package is used to store the internal implementation of the mermaid package. +package internal + +import ( + "runtime" + "testing" +) + +func TestLineFeed(t *testing.T) { + t.Parallel() + + t.Run("should return line feed for current OS", func(t *testing.T) { + t.Parallel() + + got := LineFeed() + + switch runtime.GOOS { + case "windows": + if got != "\r\n" { + t.Errorf("expected \\r\\n, but got %s", got) + } + default: + if got != "\n" { + t.Errorf("expected \\n, but got %s", got) + } + } + }) +} diff --git a/markdown.go b/markdown.go index 592438d..76570c2 100644 --- a/markdown.go +++ b/markdown.go @@ -4,9 +4,9 @@ package markdown import ( "fmt" "io" - "runtime" "strings" + "github.com/nao1215/markdown/internal" "github.com/olekukonko/tablewriter" ) @@ -126,7 +126,7 @@ func NewMarkdown(w io.Writer) *Markdown { // String returns markdown text. func (m *Markdown) String() string { - return strings.Join(m.body, lineFeed()) + return strings.Join(m.body, internal.LineFeed()) } // Error returns error. @@ -238,7 +238,8 @@ func (m *Markdown) H6f(format string, args ...interface{}) *Markdown { func (m *Markdown) Details(summary, text string) *Markdown { m.body = append( m.body, - fmt.Sprintf("
%s%s%s%s
", summary, lineFeed(), text, lineFeed())) + fmt.Sprintf("
%s%s%s%s
", + summary, internal.LineFeed(), text, internal.LineFeed())) return m } @@ -288,7 +289,7 @@ func (m *Markdown) CheckBox(set []CheckBoxSet) *Markdown { // Blockquote is markdown blockquote. // If you set text "Hello", it will be converted to "> Hello". func (m *Markdown) Blockquote(text string) *Markdown { - lines := strings.Split(text, lineFeed()) + lines := strings.Split(text, internal.LineFeed()) for _, line := range lines { m.body = append(m.body, fmt.Sprintf("> %s", line)) } @@ -302,7 +303,7 @@ func (m *Markdown) Blockquote(text string) *Markdown { // ```". func (m *Markdown) CodeBlocks(lang SyntaxHighlight, text string) *Markdown { m.body = append(m.body, - fmt.Sprintf("```%s%s%s%s```", lang, lineFeed(), text, lineFeed())) + fmt.Sprintf("```%s%s%s%s```", lang, internal.LineFeed(), text, internal.LineFeed())) return m } @@ -345,7 +346,7 @@ func (m *Markdown) Table(t TableSet) *Markdown { buf := &strings.Builder{} table := tablewriter.NewWriter(buf) - table.SetNewLine(lineFeed()) + table.SetNewLine(internal.LineFeed()) table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) table.SetCenterSeparator("|") table.SetHeader(t.Header) @@ -379,7 +380,7 @@ func (m *Markdown) CustomTable(t TableSet, options TableOptions) *Markdown { buf := &strings.Builder{} table := tablewriter.NewWriter(buf) - table.SetNewLine(lineFeed()) + table.SetNewLine(internal.LineFeed()) table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) table.SetCenterSeparator("|") table.SetAutoWrapText(options.AutoWrapText) @@ -401,11 +402,3 @@ func (m *Markdown) LF() *Markdown { m.body = append(m.body, " ") return m } - -// lineFeed return line feed for current OS. -func lineFeed() string { - if runtime.GOOS == "windows" { - return "\r\n" - } - return "\n" -} diff --git a/markdown_test.go b/markdown_test.go index 77eeda2..7c3fca6 100644 --- a/markdown_test.go +++ b/markdown_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/nao1215/markdown/internal" ) func TestPlainText(t *testing.T) { @@ -117,7 +118,7 @@ func TestMarkdownDetailsf(t *testing.T) { m := NewMarkdown(os.Stdout) m.Detailsf("Hello", "Good %s", "World") - want := fmt.Sprintf("
Hello%sGood World%s
", lineFeed(), lineFeed()) + want := fmt.Sprintf("
Hello%sGood World%s
", internal.LineFeed(), internal.LineFeed()) got := m.body[0] if diff := cmp.Diff(want, got); diff != "" { @@ -189,7 +190,7 @@ func TestMarkdownBlockquote(t *testing.T) { t.Parallel() m := NewMarkdown(os.Stdout) - m.Blockquote(fmt.Sprintf("%s%s%s%s%s", "Hello", lineFeed(), "Good", lineFeed(), "World")) + m.Blockquote(fmt.Sprintf("%s%s%s%s%s", "Hello", internal.LineFeed(), "Good", internal.LineFeed(), "World")) want := []string{ "> Hello", "> Good", @@ -209,7 +210,7 @@ func TestMarkdownCodeBlocks(t *testing.T) { m := NewMarkdown(os.Stdout) m.CodeBlocks(SyntaxHighlightGo, "Hello") - want := []string{fmt.Sprintf("```go%sHello%s```", lineFeed(), lineFeed())} + want := []string{fmt.Sprintf("```go%sHello%s```", internal.LineFeed(), internal.LineFeed())} got := m.body if diff := cmp.Diff(want, got); diff != "" { @@ -279,7 +280,7 @@ func TestMarkdownTable(t *testing.T) { m.Table(set) want := []string{ fmt.Sprintf("| NAME | AGE |%s|-------|-----|%s| David | 23 |%s", - lineFeed(), lineFeed(), lineFeed()), + internal.LineFeed(), internal.LineFeed(), internal.LineFeed()), } got := m.body @@ -388,7 +389,7 @@ func TestMarkdownCustomTable(t *testing.T) { }) want := []string{ fmt.Sprintf("| Name | Age |%s|-------|-----|%s| David | 23 |%s", - lineFeed(), lineFeed(), lineFeed()), + internal.LineFeed(), internal.LineFeed(), internal.LineFeed()), } got := m.body diff --git a/mermaid/er/entity.go b/mermaid/er/entity.go index ae2b642..1637e81 100644 --- a/mermaid/er/entity.go +++ b/mermaid/er/entity.go @@ -3,6 +3,8 @@ package er import ( "fmt" "strings" + + "github.com/nao1215/markdown/internal" ) // Entity is a entity of entity relationship. @@ -24,9 +26,9 @@ func (e *Entity) string() string { "%s%s {%s%s%s%s}", " ", // indent e.Name, - lineFeed(), - strings.Join(attrs, lineFeed()), - lineFeed(), + internal.LineFeed(), + strings.Join(attrs, internal.LineFeed()), + internal.LineFeed(), " ", // indent ) } diff --git a/mermaid/er/entity_relationship.go b/mermaid/er/entity_relationship.go index b6976f1..dc1ee91 100644 --- a/mermaid/er/entity_relationship.go +++ b/mermaid/er/entity_relationship.go @@ -4,10 +4,11 @@ package er import ( "fmt" "io" - "runtime" "sort" "strings" "sync" + + "github.com/nao1215/markdown/internal" ) // Diagram is a entity relationship diagram builder. @@ -42,8 +43,8 @@ func NewDiagram(w io.Writer, opts ...Option) *Diagram { // String returns the entity relationship diagram body. func (d *Diagram) String() string { - s := strings.Join(d.body, lineFeed()) - s += lineFeed() + s := strings.Join(d.body, internal.LineFeed()) + s += internal.LineFeed() entities := make([]Entity, 0) d.entities.Range(func(_, value interface{}) bool { @@ -60,7 +61,7 @@ func (d *Diagram) String() string { }) for _, e := range entities { - s += e.string() + lineFeed() + s += e.string() + internal.LineFeed() } return s } @@ -75,11 +76,3 @@ func (d *Diagram) Build() error { } return nil } - -// lineFeed return line feed for current OS. -func lineFeed() string { - if runtime.GOOS == "windows" { - return "\r\n" - } - return "\n" -} diff --git a/mermaid/flowchart/config.go b/mermaid/flowchart/config.go new file mode 100644 index 0000000..0f19e89 --- /dev/null +++ b/mermaid/flowchart/config.go @@ -0,0 +1,68 @@ +package flowchart + +const ( + // noTitle is a constant for no title. + noTitle string = "" +) + +// config is a flowchart configuration. +type config struct { + // title is the title of the flowchart. + title string + // oriental is the oriental of the flowchart. + // Default is top to bottom. + oriental oriental +} + +// newConfig returns a new Config with default values. +func newConfig() *config { + return &config{ + oriental: tb, + } +} + +// Option sets the options for the Flowchart struct. +type Option func(*config) + +// WithTitle sets the title configuration. +func WithTitle(title string) Option { + return func(c *config) { + c.title = title + } +} + +// WithOrientalTopToBottom sets the oriental configuration to top to bottom. +func WithOrientalTopToBottom() Option { + return func(c *config) { + c.oriental = tb + } +} + +// WithOrientalTopDown sets the oriental configuration to top down. +// Same as top to bottom. +func WithOrientalTopDown() Option { + return func(c *config) { + c.oriental = td + } +} + +// WithOrientalBottomToTop sets the oriental configuration to bottom to top. +func WithOrientalBottomToTop() Option { + return func(c *config) { + c.oriental = bt + } +} + +// WithOrientalRightToLeft sets the oriental configuration to right to left. +func WithOrientalRightToLeft() Option { + return func(c *config) { + c.oriental = rl + } +} + +// WithOrientalLeftToRight sets the oriental configuration to left to right. +func WithOrientalLeftToRight() Option { + return func(c *config) { + c.oriental = lr + } +} diff --git a/mermaid/flowchart/flowchart.go b/mermaid/flowchart/flowchart.go new file mode 100644 index 0000000..e745b1b --- /dev/null +++ b/mermaid/flowchart/flowchart.go @@ -0,0 +1,61 @@ +// Package flowchart provides a simple way to create flowcharts in mermaid syntax. +package flowchart + +import ( + "fmt" + "io" + "strings" + + "github.com/nao1215/markdown/internal" +) + +// Flowchart is a flowchart builder. +type Flowchart struct { + // body is flowchart body. + body []string + // dest is output destination for flowchart body. + dest io.Writer + // err manages errors that occur in all parts of the flowchart building. + err error + // config is the configuration for the flowchart. + config *config +} + +// NewFlowchart returns a new Flowchart. +func NewFlowchart(w io.Writer, opts ...Option) *Flowchart { + c := newConfig() + + for _, opt := range opts { + opt(c) + } + + lines := []string{} + if strings.TrimSpace(c.title) != noTitle { + lines = append(lines, "---") + lines = append(lines, fmt.Sprintf("title: %s", c.title)) + lines = append(lines, "---") + } + lines = append(lines, fmt.Sprintf("flowchart %s", c.oriental.string())) + + return &Flowchart{ + body: lines, + dest: w, + config: c, + } +} + +// String returns the flowchart body. +func (f *Flowchart) String() string { + return strings.Join(f.body, internal.LineFeed()) +} + +// Build writes the flowchart body to the output destination. +func (f *Flowchart) Build() error { + if _, err := fmt.Fprint(f.dest, f.String()); err != nil { + if f.err != nil { + return fmt.Errorf("failed to write: %w: %s", err, f.err.Error()) //nolint:wrapcheck + } + return fmt.Errorf("failed to write: %w", err) + } + return nil +} diff --git a/mermaid/flowchart/flowchart_test.go b/mermaid/flowchart/flowchart_test.go new file mode 100644 index 0000000..1483d36 --- /dev/null +++ b/mermaid/flowchart/flowchart_test.go @@ -0,0 +1,252 @@ +// Package flowchart provides a simple way to create flowcharts in mermaid syntax. +package flowchart + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestFlowchart_Build(t *testing.T) { + t.Parallel() + + t.Run("Build a flowchart with title", func(t *testing.T) { + t.Parallel() + + b := new(bytes.Buffer) + + f := NewFlowchart( + b, + WithTitle("mermaid flowchart builder"), + WithOrientalTopToBottom(), + ). + NodeWithText("A", "Node A"). + StadiumNode("B", "Node B"). + SubroutineNode("C", "Node C"). + DatabaseNode("D", "Database"). + LinkWithArrowHead("A", "B"). + LinkWithArrowHeadAndText("B", "D", "send original data"). + LinkWithArrowHead("B", "C"). + DottedLinkWithText("C", "D", "send filtered data") + + if err := f.Build(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := `--- +title: mermaid flowchart builder +--- +flowchart TB + A["Node A"] + B(["Node B"]) + C[["Node C"]] + D[("Database")] + A-->B + B-->|"send original data"|D + B-->C + C-. "send filtered data" .-> D` + + got := strings.ReplaceAll(b.String(), "\r\n", "\n") + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("value is mismatch (-want +got):%s", diff) + } + }) + + t.Run("Build a flowchart, top to bottom", func(t *testing.T) { + t.Parallel() + + b := new(bytes.Buffer) + + f := NewFlowchart( + b, + WithOrientalTopToBottom(), + ).NodeWithText("A", "Node A") + + if err := f.Build(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := `flowchart TB + A["Node A"]` + got := strings.ReplaceAll(b.String(), "\r\n", "\n") + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("value is mismatch (-want +got):%s", diff) + } + }) + + t.Run("Build a flowchart, top down", func(t *testing.T) { + t.Parallel() + b := new(bytes.Buffer) + + f := NewFlowchart( + b, + WithOrientalTopDown(), + ).NodeWithText("A", "Node A") + + if err := f.Build(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := `flowchart TD + A["Node A"]` + got := strings.ReplaceAll(b.String(), "\r\n", "\n") + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("value is mismatch (-want +got):%s", diff) + } + }) + + t.Run("Build a flowchart, bottom to top", func(t *testing.T) { + t.Parallel() + + b := new(bytes.Buffer) + + f := NewFlowchart( + b, + WithOrientalBottomToTop(), + ).NodeWithText("A", "Node A") + + if err := f.Build(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := `flowchart BT + A["Node A"]` + got := strings.ReplaceAll(b.String(), "\r\n", "\n") + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("value is mismatch (-want +got):%s", diff) + } + }) + + t.Run("Build a flowchart, right to left", func(t *testing.T) { + t.Parallel() + + b := new(bytes.Buffer) + + f := NewFlowchart( + b, + WithOrientalRightToLeft(), + ).NodeWithText("A", "Node A") + + if err := f.Build(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := `flowchart RL + A["Node A"]` + got := strings.ReplaceAll(b.String(), "\r\n", "\n") + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("value is mismatch (-want +got):%s", diff) + } + }) + + t.Run("Build a flowchart, left to right", func(t *testing.T) { + t.Parallel() + + b := new(bytes.Buffer) + + f := NewFlowchart( + b, + WithOrientalLeftToRight(), + ).NodeWithText("A", "Node A") + + if err := f.Build(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := `flowchart LR + A["Node A"]` + got := strings.ReplaceAll(b.String(), "\r\n", "\n") + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("value is mismatch (-want +got):%s", diff) + } + }) + + t.Run("Build a flowchart with all node and link", func(t *testing.T) { + t.Parallel() + + b := new(bytes.Buffer) + + f := NewFlowchart( + b, + WithOrientalTopToBottom(), + ). + Node("A"). + NodeWithText("B", "Node B"). + NodeWithMarkdown("C", "**Node C**"). + NodeWithNewLines("D", `Node +D`).RoundEdgesNode("E", "Node E"). + StadiumNode("F", "Node F"). + SubroutineNode("G", "Node G"). + CylindricalNode("H", "Node H"). + DatabaseNode("I", "Database"). + CircleNode("J", "Node J"). + AsymmetricNode("K", "Node K"). + RhombusNode("L", "Node L"). + HexagonNode("M", "Node M"). + ParallelogramNode("N", "Node N"). + ParallelogramAltNode("O", "Node O"). + TrapezoidNode("P", "Node P"). + TrapezoidAltNode("Q", "Node Q"). + DoubleCircleNode("R", "Node R"). + LinkWithArrowHead("A", "B"). + LinkWithArrowHeadAndText("B", "C", "send"). + OpenLink("C", "D"). + OpenLinkWithText("D", "E", "send"). + DottedLink("E", "F"). + DottedLinkWithText("F", "G", "send"). + ThickLink("G", "H"). + ThickLinkWithText("H", "I", "send"). + InvisibleLink("I", "J") + + if err := f.Build(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := `flowchart TB + A + B["Node B"] + ` + want += fmt.Sprintf("C[\"`**Node C**`\"]\n") //nolint:gosimple + want += fmt.Sprintf("\tD[\"`Node\n") //nolint:gosimple + want += "D`\"]" + want += ` + E("Node E") + F(["Node F"]) + G[["Node G"]] + H[("Node H")] + I[("Database")] + J(("Node J")) + K>"Node K"] + L{"Node L"} + M{{"Node M"}} + N[/"Node N"/] + O[\"Node O"\] + P[/"Node P"\] + Q[\"Node Q"/] + R((("Node R"))) + A-->B + B-->|"send"|C + C --- D + D---|"send"|E + E-.->F + F-. "send" .-> G + G ==> H + H == "send" ==> I + I ~~~ J` + + got := strings.ReplaceAll(b.String(), "\r\n", "\n") + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("value is mismatch (-want +got):%s", diff) + } + }) +} diff --git a/mermaid/flowchart/link.go b/mermaid/flowchart/link.go new file mode 100644 index 0000000..337668b --- /dev/null +++ b/mermaid/flowchart/link.go @@ -0,0 +1,57 @@ +package flowchart + +import "fmt" + +// LinkWithArrowHead adds a link with an arrow head to the flowchart. +func (f *Flowchart) LinkWithArrowHead(from, to string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s-->%s", from, to)) + return f +} + +// LinkWithArrowHeadAndText adds a link with an arrow head and text to the flowchart. +func (f *Flowchart) LinkWithArrowHeadAndText(from, to, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s-->|\"%s\"|%s", from, text, to)) + return f +} + +// OpenLink adds an open link to the flowchart. +func (f *Flowchart) OpenLink(from, to string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s --- %s", from, to)) + return f +} + +// OpenLinkWithText adds an open link with text to the flowchart. +func (f *Flowchart) OpenLinkWithText(from, to, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s---|\"%s\"|%s", from, text, to)) + return f +} + +// DottedLink adds a dotted link to the flowchart. +func (f *Flowchart) DottedLink(from, to string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s-.->%s", from, to)) + return f +} + +// DottedLinkWithText adds a dotted link with text to the flowchart. +func (f *Flowchart) DottedLinkWithText(from, to, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s-. \"%s\" .-> %s", from, text, to)) + return f +} + +// ThickLink adds a thick link to the flowchart. +func (f *Flowchart) ThickLink(from, to string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s ==> %s", from, to)) + return f +} + +// ThickLinkWithText adds a thick link with text to the flowchart. +func (f *Flowchart) ThickLinkWithText(from, to, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s == \"%s\" ==> %s", from, text, to)) + return f +} + +// InvisibleLink adds an invisible link to the flowchart. +func (f *Flowchart) InvisibleLink(from, to string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s ~~~ %s", from, to)) + return f +} diff --git a/mermaid/flowchart/node.go b/mermaid/flowchart/node.go new file mode 100644 index 0000000..dc0fc80 --- /dev/null +++ b/mermaid/flowchart/node.go @@ -0,0 +1,112 @@ +package flowchart + +import "fmt" + +// Node adds a node to the flowchart. +func (f *Flowchart) Node(name string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s", name)) + return f +} + +// NodeWithText adds a node with text to the flowchart. +// Unicode characters are supported. +func (f *Flowchart) NodeWithText(name, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s[\"%s\"]", name, text)) + return f +} + +// NodeWithMarkdown adds a node with markdown text to the flowchart. +func (f *Flowchart) NodeWithMarkdown(name, markdownText string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s[\"`%s`\"]", name, markdownText)) + return f +} + +// NodeWithNewLines adds a node with new lines to the flowchart. +func (f *Flowchart) NodeWithNewLines(name, textWithNewLines string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s[\"`%s`\"]", name, textWithNewLines)) + return f +} + +// RoundEdgesNode adds a node with round edges to the flowchart. +func (f *Flowchart) RoundEdgesNode(name, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s(\"%s\")", name, text)) + return f +} + +// StadiumNode adds a node with stadium shape to the flowchart. +func (f *Flowchart) StadiumNode(name, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s([\"%s\"])", name, text)) + return f +} + +// SubroutineNode adds a node with subroutine shape to the flowchart. +func (f *Flowchart) SubroutineNode(name, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s[[\"%s\"]]", name, text)) + return f +} + +// CylindricalNode adds a node with cylindrical shape to the flowchart. +func (f *Flowchart) CylindricalNode(name, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s[(\"%s\")]", name, text)) + return f +} + +// DatabaseNode adds a node with database shape to the flowchart. +// This method is same as CylindricalShapeNode() +func (f *Flowchart) DatabaseNode(name, text string) *Flowchart { + return f.CylindricalNode(name, text) +} + +// CircleNode adds a node with circle shape to the flowchart. +func (f *Flowchart) CircleNode(name, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s((\"%s\"))", name, text)) + return f +} + +// AsymmetricNode adds a node with asymmetric shape to the flowchart. +func (f *Flowchart) AsymmetricNode(name, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s>\"%s\"]", name, text)) + return f +} + +// RhombusNode adds a node with rhombus shape to the flowchart. +func (f *Flowchart) RhombusNode(name, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s{\"%s\"}", name, text)) + return f +} + +// HexagonNode adds a node with hexagon shape to the flowchart. +func (f *Flowchart) HexagonNode(name, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s{{\"%s\"}}", name, text)) + return f +} + +// ParallelogramNode adds a node with parallelogram shape to the flowchart. +func (f *Flowchart) ParallelogramNode(name, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s[/\"%s\"/]", name, text)) + return f +} + +// ParallelogramAltNode adds a node with parallelogram shape to the flowchart. +func (f *Flowchart) ParallelogramAltNode(name, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s[\\\"%s\"\\]", name, text)) + return f +} + +// TrapezoidNode adds a node with trapezoid shape to the flowchart. +func (f *Flowchart) TrapezoidNode(name, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s[/\"%s\"\\]", name, text)) + return f +} + +// TrapezoidAltNode adds a node with trapezoid shape to the flowchart. +func (f *Flowchart) TrapezoidAltNode(name, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s[\\\"%s\"/]", name, text)) + return f +} + +// DoubleCircleNode adds a node with double circle shape to the flowchart. +func (f *Flowchart) DoubleCircleNode(name, text string) *Flowchart { + f.body = append(f.body, fmt.Sprintf(" %s(((\"%s\")))", name, text)) + return f +} diff --git a/mermaid/flowchart/oriental.go b/mermaid/flowchart/oriental.go new file mode 100644 index 0000000..fb50fa9 --- /dev/null +++ b/mermaid/flowchart/oriental.go @@ -0,0 +1,22 @@ +package flowchart + +// oriental is a flowchart oriental. +type oriental string + +const ( + // tb is a top to bottom oriental. + tb oriental = "TB" + // td is a top down oriental, same as top to Bottom. + td oriental = "TD" + // bt is a bottom to top oriental. + bt oriental = "BT" + // rl is a right to left oriental. + rl oriental = "RL" + // lr is a left to right oriental. + lr oriental = "LR" +) + +// string returns the oriental as a string. +func (o oriental) string() string { + return string(o) +} diff --git a/mermaid/piechart/pie_chart.go b/mermaid/piechart/pie_chart.go index 6192c59..457c4c5 100644 --- a/mermaid/piechart/pie_chart.go +++ b/mermaid/piechart/pie_chart.go @@ -4,8 +4,9 @@ package piechart import ( "fmt" "io" - "runtime" "strings" + + "github.com/nao1215/markdown/internal" ) // PieChart is a pie chart builder. @@ -57,7 +58,7 @@ func NewPieChart(w io.Writer, opts ...Option) *PieChart { // String returns the pie chart body. func (p *PieChart) String() string { - return strings.Join(p.body, lineFeed()) + return strings.Join(p.body, internal.LineFeed()) } // Build writes the pie chart body to the output destination. @@ -83,11 +84,3 @@ func (p *PieChart) LabelAndFloatValue(label string, value float64) *PieChart { p.body = append(p.body, fmt.Sprintf(" \"%s\" : %f", label, value)) return p } - -// lineFeed return line feed for current OS. -func lineFeed() string { - if runtime.GOOS == "windows" { - return "\r\n" - } - return "\n" -} diff --git a/mermaid/sequence/sequence.go b/mermaid/sequence/sequence.go index 0e659c5..70275c2 100644 --- a/mermaid/sequence/sequence.go +++ b/mermaid/sequence/sequence.go @@ -4,8 +4,9 @@ package sequence import ( "fmt" "io" - "runtime" "strings" + + "github.com/nao1215/markdown/internal" ) // Diagram is a sequence diagram builder. @@ -38,7 +39,7 @@ func NewDiagram(w io.Writer, opts ...Option) *Diagram { // String returns the sequence diagram body. func (d *Diagram) String() string { - return strings.Join(d.body, lineFeed()) + return strings.Join(d.body, internal.LineFeed()) } // Error returns the error that occurred during the sequence diagram building. @@ -128,11 +129,3 @@ func (d *Diagram) LF() *Diagram { d.body = append(d.body, "") return d } - -// lineFeed return line feed for current OS. -func lineFeed() string { - if runtime.GOOS == "windows" { - return "\r\n" - } - return "\n" -} diff --git a/mermaid/sequence/sequence_test.go b/mermaid/sequence/sequence_test.go index 609a54f..561142f 100644 --- a/mermaid/sequence/sequence_test.go +++ b/mermaid/sequence/sequence_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/nao1215/markdown/internal" ) func TestString(t *testing.T) { @@ -19,7 +20,7 @@ func TestString(t *testing.T) { d := NewDiagram(io.Discard) d.Participant("Alice") - want := fmt.Sprintf("sequenceDiagram%s participant Alice", lineFeed()) + want := fmt.Sprintf("sequenceDiagram%s participant Alice", internal.LineFeed()) got := d.String() if diff := cmp.Diff(want, got); diff != "" {