-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Document Builder type #13
Open
aThorp96
wants to merge
8
commits into
a-h:master
Choose a base branch
from
aThorp96:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
3620697
Add DocumentBuilder type
aThorp96 3e9feb1
Remove broken example comment
aThorp96 2af1205
Add support for document headers and footers
aThorp96 42d2ee7
Address feedback
aThorp96 a0990ef
first commit showing unexpected behaviour
a-h 8943cee
fix broken behaviour of header, added benchmark
a-h 4f57996
think about a DocumentWriter
a-h 15b9004
suggest way to have more than one header and footer etc.
a-h File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
package gemini | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"strings" | ||
|
||
"github.com/pkg/errors" | ||
) | ||
|
||
type DocumentSections []func(dw *DocumentWriter) error | ||
|
||
func (s DocumentSections) Write(dw *DocumentWriter) (err error) { | ||
for i := 0; i < len(s); i++ { | ||
if err = s[i](dw); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// NewDocumentWriter creates a DocumentWriter. | ||
func NewDocumentWriter(w io.Writer) *DocumentWriter { | ||
return &DocumentWriter{ | ||
w: w, | ||
} | ||
} | ||
|
||
type DocumentWriter struct { | ||
w io.Writer | ||
} | ||
|
||
func (dw *DocumentWriter) NewLine() (err error) { | ||
_, err = io.WriteString(dw.w, "\n") | ||
return fmt.Errorf("error writing newline: %w", err) | ||
} | ||
|
||
func (dw *DocumentWriter) Header1(text string) (err error) { | ||
if _, err = io.WriteString(dw.w, "# "); err != nil { | ||
return fmt.Errorf("error writing header1: %w", err) | ||
} | ||
if _, err = io.WriteString(dw.w, text); err != nil { | ||
return fmt.Errorf("error writing header1: %w", err) | ||
} | ||
if _, err = io.WriteString(dw.w, "\n"); err != nil { | ||
return fmt.Errorf("error writing header1: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
func (dw *DocumentWriter) Header2(text string) (err error) { | ||
if _, err = io.WriteString(dw.w, "## "); err != nil { | ||
return fmt.Errorf("error writing header2: %w", err) | ||
} | ||
if _, err = io.WriteString(dw.w, text); err != nil { | ||
return fmt.Errorf("error writing header2: %w", err) | ||
} | ||
if _, err = io.WriteString(dw.w, "\n"); err != nil { | ||
return fmt.Errorf("error writing header2: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
func (dw *DocumentWriter) Quote(text string) (err error) { | ||
if _, err = io.WriteString(dw.w, "> "); err != nil { | ||
return fmt.Errorf("error writing quote: %w", err) | ||
} | ||
if _, err = io.WriteString(dw.w, text); err != nil { | ||
return fmt.Errorf("error writing quote: %w", err) | ||
} | ||
if _, err = io.WriteString(dw.w, "\n"); err != nil { | ||
return fmt.Errorf("error writing quote: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
func (dw *DocumentWriter) Line(text string) (err error) { | ||
if _, err = io.WriteString(dw.w, text); err != nil { | ||
return err | ||
} | ||
if _, err = io.WriteString(dw.w, "\n"); err != nil { | ||
return fmt.Errorf("error writing line: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
// DocumentBuilder allows programmatic document creation using the builder pattern. | ||
// DocumentBuilder supports the use of headers and footers, which are combined with the body at build time. | ||
type DocumentBuilder struct { | ||
header string | ||
body *strings.Builder | ||
footer string | ||
} | ||
|
||
// NewDocumentBuilder creates a DocumentBuilder. | ||
func NewDocumentBuilder() *DocumentBuilder { | ||
builder := new(strings.Builder) | ||
return &DocumentBuilder{"", builder, ""} | ||
} | ||
|
||
// SetHeader sets a document header. The header is written before the document body during `Build()`. | ||
func (doc *DocumentBuilder) SetHeader(header string) { | ||
doc.header = header | ||
} | ||
|
||
// SetFooter sets a document footer. The footer is written after the document body during `Build()`. | ||
func (doc *DocumentBuilder) SetFooter(footer string) { | ||
doc.footer = footer | ||
} | ||
|
||
// AddLine appends a new line to the document. Adds a newline to the end of the line if none is present. | ||
func (doc *DocumentBuilder) AddLine(line string) error { | ||
_, err := doc.body.WriteString(line) | ||
if err != nil { | ||
return errors.Wrap(err, "Error writing to document") | ||
} | ||
|
||
if !strings.HasSuffix(line, "\n") { | ||
_, err = doc.body.WriteString("\n") | ||
if err != nil { | ||
return errors.Wrap(err, "Error writing to document") | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// AddH1Header appends an H1 (#) header line to the document. | ||
func (doc *DocumentBuilder) AddH1Header(header string) error { | ||
_, err := doc.body.WriteString("# ") | ||
if err != nil { | ||
return errors.Wrap(err, "Error writing to document") | ||
} | ||
return doc.AddLine(header) | ||
} | ||
|
||
// AddH2Header appends an H2 (##) header line to the document. | ||
func (doc *DocumentBuilder) AddH2Header(header string) error { | ||
_, err := doc.body.WriteString("## ") | ||
if err != nil { | ||
return errors.Wrap(err, "Error writing to document") | ||
} | ||
return doc.AddLine(header) | ||
} | ||
|
||
// AddH3Header appends an H3 (###) header line to the document. | ||
func (doc *DocumentBuilder) AddH3Header(header string) error { | ||
_, err := doc.body.WriteString("### ") | ||
if err != nil { | ||
return errors.Wrap(err, "Error writing header line to document") | ||
} | ||
return doc.AddLine(header) | ||
} | ||
|
||
// AddQuote appends a quote line to the document. | ||
func (doc *DocumentBuilder) AddQuote(header string) error { | ||
_, err := doc.body.WriteString("> ") | ||
if err != nil { | ||
return errors.Wrap(err, "Error writing quote to document") | ||
} | ||
return doc.AddLine(header) | ||
} | ||
|
||
// AddBullet appends an unordered list item to the document. | ||
func (doc *DocumentBuilder) AddBullet(header string) error { | ||
_, err := doc.body.WriteString("* ") | ||
if err != nil { | ||
return errors.Wrap(err, "Error writing bullet to document") | ||
} | ||
return doc.AddLine(header) | ||
} | ||
|
||
// ToggleFormatting appends a toggle formatting line to the document. | ||
func (doc *DocumentBuilder) ToggleFormatting() error { | ||
return doc.AddLine("```") | ||
} | ||
|
||
// AddLink appends an aliased link line to the document. | ||
func (doc *DocumentBuilder) AddLink(url string, title string) error { | ||
_, err := doc.body.WriteString("=> ") | ||
if err != nil { | ||
return errors.Wrap(err, "Error writing link to document") | ||
} | ||
_, err = doc.body.WriteString(url) | ||
if err != nil { | ||
return errors.Wrap(err, "Error writing link to document") | ||
} | ||
_, err = doc.body.WriteString("\t") | ||
if err != nil { | ||
return errors.Wrap(err, "Error writing link to document") | ||
} | ||
// AddLine to ensure there is a newline | ||
return doc.AddLine(title) | ||
} | ||
|
||
// AddRawLink appends a link line to the document. | ||
func (doc *DocumentBuilder) AddRawLink(url string) error { | ||
_, err := doc.body.WriteString("=> ") | ||
if err != nil { | ||
return errors.Wrap(err, "Error writing raw link to document") | ||
} | ||
err = doc.AddLine(url) | ||
return err | ||
} | ||
|
||
// Build builds the document into a serialized byte slice. | ||
func (doc *DocumentBuilder) Build() ([]byte, error) { | ||
buf := bytes.Buffer{} | ||
|
||
// Write header | ||
_, err := buf.WriteString(doc.header) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "Error building document header") | ||
} | ||
|
||
// Write body | ||
_, err = buf.WriteString(doc.body.String()) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "Error building document body") | ||
} | ||
|
||
// Write footer | ||
_, err = buf.WriteString(doc.footer) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "Error building document footer") | ||
} | ||
|
||
return buf.Bytes(), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package gemini | ||
|
||
import ( | ||
"bytes" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
) | ||
|
||
func TestDocumentBuilder(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
f func() *DocumentBuilder | ||
expected string | ||
expectedErr error | ||
}{ | ||
{ | ||
name: "an empty builder produces no output", | ||
f: func() *DocumentBuilder { | ||
return NewDocumentBuilder() | ||
}, | ||
expected: "", | ||
expectedErr: nil, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
tt := tt | ||
result, err := tt.f().Build() | ||
if err != tt.expectedErr { | ||
t.Errorf("expected err %q, got %q", tt.expectedErr, err) | ||
continue | ||
} | ||
actual := string(result) | ||
if diff := cmp.Diff(tt.expected, actual); diff != "" { | ||
t.Error(diff) | ||
} | ||
} | ||
} | ||
|
||
func BenchmarkDocumentBuilder(b *testing.B) { | ||
// Version 1 = 223 ns per operation, with 5 allocations. | ||
b.ReportAllocs() | ||
for i := 0; i < b.N; i++ { | ||
db := NewDocumentBuilder() | ||
var err error | ||
if err = db.AddH1Header("heading 1"); err != nil { | ||
b.Error(err) | ||
} | ||
if err = db.AddH2Header("heading 2"); err != nil { | ||
b.Error(err) | ||
} | ||
if err = db.AddLine("normal text"); err != nil { | ||
b.Error(err) | ||
} | ||
if err = db.AddQuote("quote"); err != nil { | ||
b.Error(err) | ||
} | ||
result, err := db.Build() | ||
if err != nil { | ||
b.Error(err) | ||
} | ||
if len(result) == 0 { | ||
b.Error("expected output, but didn't get any") | ||
} | ||
} | ||
} | ||
|
||
func BenchmarkDocumentWriter(b *testing.B) { | ||
b.ReportAllocs() | ||
// By moving the creation of the io.Writer out of the type, the | ||
// buffer can be reused, resulting in lower execution speeds. | ||
// This comes in at 167.0 ns per operation, with zero allocations. | ||
// In practical terms, the io.Writer is most likely a file, or the | ||
// Gemini output stream. | ||
w := new(bytes.Buffer) | ||
for i := 0; i < b.N; i++ { | ||
db := NewDocumentWriter(w) | ||
var err error | ||
if err = db.Header1("heading 1"); err != nil { | ||
b.Error(err) | ||
} | ||
if err = db.Header2("heading 2"); err != nil { | ||
b.Error(err) | ||
} | ||
if err = db.Line("normal text"); err != nil { | ||
b.Error(err) | ||
} | ||
if err = db.Quote("quote"); err != nil { | ||
b.Error(err) | ||
} | ||
if len(w.Bytes()) == 0 { | ||
b.Error("expected output, but didn't get any") | ||
} | ||
w.Reset() | ||
} | ||
} | ||
|
||
func TestDocumentSections(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
sections []func(*DocumentWriter) error | ||
expected string | ||
}{ | ||
{ | ||
name: "headers and footers are appended", | ||
sections: []func(*DocumentWriter) error{ | ||
func(dw *DocumentWriter) error { | ||
return dw.Header1("header") | ||
}, | ||
func(dw *DocumentWriter) error { | ||
return dw.Line("middle") | ||
}, | ||
func(dw *DocumentWriter) error { | ||
return dw.Header1("footer") | ||
}, | ||
}, | ||
expected: `# header | ||
middle | ||
# footer | ||
`, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
tt := tt | ||
ds := DocumentSections(tt.sections) | ||
w := new(bytes.Buffer) | ||
dw := NewDocumentWriter(w) | ||
err := ds.Write(dw) | ||
if err != nil { | ||
t.Errorf("unexpected error: %v", err) | ||
continue | ||
} | ||
if diff := cmp.Diff(tt.expected, w.String()); diff != "" { | ||
t.Error(diff) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,8 @@ | ||
module github.com/a-h/gemini | ||
|
||
go 1.14 | ||
|
||
require ( | ||
github.com/google/go-cmp v0.5.6 // indirect | ||
github.com/pkg/errors v0.9.1 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= | ||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See commit message for dependency discussion