Skip to content
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

Adding markdown and html output format #7

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Run
-e value
Path that will be excluded from the process
-f string
Format to use when outputting TODOs (supported formats: text, csv, json) (default "text")
Format to use when outputting TODOs (supported formats: text, csv, json, html, md) (default "text")
-o string
Destination for output (can be stdout, stderr or a file) (default "stdout")

Expand Down
132 changes: 112 additions & 20 deletions astitodo.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package astitodo

import (
"bytes"
"encoding/csv"
"encoding/json"
"fmt"
Expand All @@ -22,8 +23,11 @@ var (
todoIdentifiers = []string{"TODO", "FIXME"}
)

// TODOs represents a set of todos
type TODOs []*TODO
// TODOContainer represents a set of todos
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like the idea of creating this wrapper here.
I'd rather keep the old TODOs struct and if you need the root path in one of the Write func, you inject it as one of the method argument.

type TODOContainer struct {
Path string
TODOs []*TODO
}

// TODO represents a todo
type TODO struct {
Expand All @@ -33,13 +37,14 @@ type TODO struct {
Message []string
}

// Extract walks through an input path and extracts TODOs from all files it encounters
func Extract(path string, excludedPaths ...string) (todos TODOs, err error) {
// Extract walks through an input path and extracts TODOContainer from all files it encounters
func Extract(path string, excludedPaths ...string) (todos TODOContainer, err error) {
err = todos.extract(path, excludedPaths...)
return
}

func (todos *TODOs) extract(path string, excludedPaths ...string) error {
func (todos *TODOContainer) extract(path string, excludedPaths ...string) error {
todos.Path = path
return filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
// Process error
if err != nil {
Expand Down Expand Up @@ -76,7 +81,7 @@ func (todos *TODOs) extract(path string, excludedPaths ...string) error {
})
}

func (todos *TODOs) extractFile(filename string) (err error) {
func (todos *TODOContainer) extractFile(filename string) (err error) {
// Parse file and create the AST
var fset = token.NewFileSet()
var f *ast.File
Expand Down Expand Up @@ -120,7 +125,7 @@ func (todos *TODOs) extractFile(filename string) (err error) {

// Append text
todo.Message = append(todo.Message, t)
*todos = append(*todos, todo)
todos.TODOs = append(todos.TODOs, todo)
TODOFound = true
} else if TODOFound && len(t) > 0 {
todo.Message = append(todo.Message, t)
Expand All @@ -142,22 +147,108 @@ func isTodoIdentifier(s string) (int, bool) {
return 0, false
}

// AssignedTo returns TODOs which are assigned to the specified assignee
func (todos TODOs) AssignedTo(assignees ...string) (filteredTODOs TODOs) {
for _, t := range todos {
// AssignedTo returns TODOContainer which are assigned to the specified assignee
func (todos TODOContainer) AssignedTo(assignees ...string) (filteredTODOs TODOContainer) {
for _, t := range todos.TODOs {
for _, assignee := range assignees {
if assignee == t.Assignee {
filteredTODOs = append(filteredTODOs, t)
filteredTODOs.TODOs = append(filteredTODOs.TODOs, t)
}
}
}

return
}

// WriteText writes the TODOs as text to the specified writer
func (todos TODOs) WriteText(w io.Writer) (err error) {
for _, t := range todos {
// WriteHTML writes the TODOContainer markdown-formatted to the specified writer
func (todos TODOContainer) WriteHTML(w io.Writer) (err error) {

var tocBuffer bytes.Buffer
var contentBuffer bytes.Buffer

_, err = io.WriteString(w, fmt.Sprintf("<h1>TODOs for %s</h1>\n\n", todos.Path))

if err != nil {
return err
}

if len(todos.TODOs) == 0 {
_, err = io.WriteString(w, "<ul><li>NONE</li></ul>")
return err
}

tocBuffer.WriteString("\n<ul id=\"toc\">\n")
contentBuffer.WriteString("\n<ul id=\"content\">\n")
i := 1
for _, t := range todos.TODOs {

tocBuffer.WriteString(fmt.Sprintf("<li><a href=\"#%d\">%s:%d</a></li>\n", i, t.Filename, t.Line))

contentBuffer.WriteString("<li>")
contentBuffer.WriteString(fmt.Sprintf("<h2><a id=\"%d\">%s:%d</a></h2>\n", i, t.Filename, t.Line))
if t.Assignee != "" {
contentBuffer.WriteString(fmt.Sprintf("<div class=\"assignee\">Assignee: %s</div>\n", t.Assignee))
}
contentBuffer.WriteString("<pre class=\"todo\">\n")
for _, m := range t.Message {
contentBuffer.WriteString(fmt.Sprintf("%s\n", m))
}
contentBuffer.WriteString("</pre>\n")
contentBuffer.WriteString("</li>")
i++
}
tocBuffer.WriteString("\n</ul>\n")
contentBuffer.WriteString("\n</ul>\n")

_, err = io.WriteString(w, fmt.Sprintf("<html><head><title>Todos for %s</title><link rel=\"stylesheet\" type=\"text/css\" href=\"todos.css\" /></head><body>%s<hr>%s</body></html>",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the todo.css ?

todos.Path,
tocBuffer.String(),
contentBuffer.String()))

return err
}

// WriteMarkdown writes the TODOContainer markdown-formatted to the specified writer
func (todos TODOContainer) WriteMarkdown(w io.Writer) (err error) {

var tocBuffer bytes.Buffer
var contentBuffer bytes.Buffer

_, err = io.WriteString(w, fmt.Sprintf("# TODOs for %s\n\n", todos.Path))

if err != nil {
return err
}

if len(todos.TODOs) == 0 {
_, err = io.WriteString(w, " - NONE")
return err
}

for _, t := range todos.TODOs {
header := fmt.Sprintf("%s:%d", t.Filename, t.Line)
tocBuffer.WriteString(fmt.Sprintf(" - [%s:%d](#%s)\n", t.Filename, t.Line, header))

contentBuffer.WriteString(fmt.Sprintf("## %s\n\n", header))
if t.Assignee != "" {
contentBuffer.WriteString(fmt.Sprintf("Assignee: `%s`\n", t.Assignee))
}
contentBuffer.WriteString("```\n")
for _, m := range t.Message {
contentBuffer.WriteString(fmt.Sprintf("%s\n", m))
}
contentBuffer.WriteString("```\n")
contentBuffer.WriteString("\n---\n")
}

_, err = io.WriteString(w, fmt.Sprintf("%s\n\n---\n\n%s", tocBuffer.String(), contentBuffer.String()))

return err
}

// WriteText writes the TODOContainer as text to the specified writer
func (todos TODOContainer) WriteText(w io.Writer) (err error) {
for _, t := range todos.TODOs {
if t.Assignee != "" {
if _, err = io.WriteString(w, fmt.Sprintf("Assignee: %s\n", t.Assignee)); err != nil {
return
Expand All @@ -172,18 +263,19 @@ func (todos TODOs) WriteText(w io.Writer) (err error) {
return
}

// WriteCSV writes the TODOs as CSV to the specified writer
// WriteCSV writes the TODOContainer as CSV to the specified writer
// The columns are "Filename", "Line", "Assignee" and "Message" (which can contain newlines)
func (todos TODOs) WriteCSV(w io.Writer) (err error) {
func (todos TODOContainer) WriteCSV(w io.Writer) (err error) {
var c = csv.NewWriter(w)

// Write the headings for the document
if err = c.Write([]string{"Filename", "Line", "Assignee", "Message"}); err != nil {
if err = c.Write([]string{"Path", "Filename", "Line", "Assignee", "Message"}); err != nil {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we don't add TODOContainer, remove this column

return
}

for _, t := range todos {
for _, t := range todos.TODOs {
err = c.Write([]string{
todos.Path,
t.Filename,
strconv.Itoa(t.Line),
t.Assignee,
Expand All @@ -200,8 +292,8 @@ func (todos TODOs) WriteCSV(w io.Writer) (err error) {
return
}

// WriteJSON writes the TODOs as JSON to the specified writer
func (todos TODOs) WriteJSON(w io.Writer) (err error) {
// WriteJSON writes the TODOContainer as JSON to the specified writer
func (todos TODOContainer) WriteJSON(w io.Writer) (err error) {
enc := json.NewEncoder(w)
err = enc.Encode(todos)
return
Expand Down
14 changes: 11 additions & 3 deletions astitodo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import (

// Flags
var (
assignees = flag.String("a", "", "Only TODOs assigned to this username(s) will be displayed")
format = flag.String("f", "text", "Format to use when outputting TODOs (supported formats: text, csv, json)")
assignees = flag.String("a", "", "Only TODOContainer assigned to this username(s) will be displayed")
format = flag.String("f", "text", "Format to use when outputting TODOContainer (supported formats: text, csv, json, html, md)")
output = flag.String("o", "stdout", "Destination for output (can be stdout, stderr or a file)")
exclude = astiflag.Strings{}
)
Expand All @@ -27,7 +27,7 @@ func main() {
// Loop through paths
for _, path := range flag.Args() {
// Process path
var todos astitodo.TODOs
var todos astitodo.TODOContainer
var err error
if todos, err = astitodo.Extract(path, exclude...); err != nil {
log.Fatal(err)
Expand Down Expand Up @@ -68,6 +68,14 @@ func main() {
if err = todos.WriteJSON(writer); err != nil {
log.Fatal(err)
}
case "md":
if err = todos.WriteMarkdown(writer); err != nil {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inject *output here

log.Fatal(err)
}
case "html":
if err = todos.WriteHTML(writer); err != nil {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inject *output here

log.Fatal(err)
}
default:
log.Fatalf("unsupported format: %s", *format)
}
Expand Down
36 changes: 20 additions & 16 deletions astitodo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

func TestExtract(t *testing.T) {
expected := astitodo.TODOs{
expected := []*astitodo.TODO{
{
Line: 5,
Message: []string{"Here is a", "multi line todo"},
Expand Down Expand Up @@ -77,30 +77,34 @@ func TestExtract(t *testing.T) {

todos, err := astitodo.Extract("testdata", "testdata/excluded.go")
assert.NoError(t, err)
assert.Len(t, todos, 11)
assert.Equal(t, expected, todos)
assert.Len(t, todos.TODOs, 11)
assert.Equal(t, expected, todos.TODOs)
}

func mockTODOs() astitodo.TODOs {
return astitodo.TODOs{
func mockTODOs() astitodo.TODOContainer {
todoList := []*astitodo.TODO{
{Assignee: "1", Line: 1, Message: []string{"multi", "line"}, Filename: "filename-1"},
{Line: 2, Message: []string{"no-assignee"}, Filename: "filename-1"},
{Assignee: "2", Line: 3, Message: []string{"message-1"}, Filename: "filename-2"},
{Assignee: "asticode", Line: 4, Message: []string{"I should be false"}, Filename: "some-file"},
{Assignee: "astitodo", Line: 10, Message: []string{"Something else comes here"}, Filename: "testdata/level1.go"},
}
return astitodo.TODOContainer{
Path: "./test/folder",
TODOs: todoList,
}
}

func TestTODOs_AssignedTo(t *testing.T) {
todos := mockTODOs()
filteredTODOs := todos.AssignedTo("1")
assert.Equal(t, astitodo.TODOs{{Assignee: "1", Line: 1, Message: []string{"multi", "line"}, Filename: "filename-1"}}, filteredTODOs)
assert.Equal(t, []*astitodo.TODO{{Assignee: "1", Line: 1, Message: []string{"multi", "line"}, Filename: "filename-1"}}, filteredTODOs.TODOs)

filteredTODOs = todos.AssignedTo("asticode", "astitodo")
assert.Equal(t, astitodo.TODOs{
assert.Equal(t, []*astitodo.TODO{
{Assignee: "asticode", Line: 4, Message: []string{"I should be false"}, Filename: "some-file"},
{Assignee: "astitodo", Line: 10, Message: []string{"Something else comes here"}, Filename: "testdata/level1.go"},
}, filteredTODOs)
}, filteredTODOs.TODOs)
}

func TestTODOs_WriteCSV(t *testing.T) {
Expand All @@ -109,13 +113,13 @@ func TestTODOs_WriteCSV(t *testing.T) {
err := todos.WriteCSV(buf)
assert.NoError(t, err)
assert.NoError(t, err)
assert.Equal(t, `Filename,Line,Assignee,Message
filename-1,1,1,"multi
assert.Equal(t, `Path,Filename,Line,Assignee,Message
./test/folder,filename-1,1,1,"multi
line"
filename-1,2,,no-assignee
filename-2,3,2,message-1
some-file,4,asticode,I should be false
testdata/level1.go,10,astitodo,Something else comes here
./test/folder,filename-1,2,,no-assignee
./test/folder,filename-2,3,2,message-1
./test/folder,some-file,4,asticode,I should be false
./test/folder,testdata/level1.go,10,astitodo,Something else comes here
`, buf.String())
}

Expand All @@ -125,9 +129,9 @@ func TestTODOs_WriteJSON(t *testing.T) {
err := todos.WriteJSON(buf)
assert.NoError(t, err)
assert.NoError(t, err)
copyTodos := astitodo.TODOs{}
copyTodos := astitodo.TODOContainer{}
assert.NoError(t, json.Unmarshal(buf.Bytes(), &copyTodos))
assert.Equal(t, len(todos), len(copyTodos))
assert.Equal(t, len(todos.TODOs), len(copyTodos.TODOs))
}

func TestTODOs_WriteText(t *testing.T) {
Expand Down