Skip to content

Commit

Permalink
feat(parser): Add parser combinators
Browse files Browse the repository at this point in the history
A bot pf a WIP. Draft parser combinators to handle all parts of the CC standard.
  • Loading branch information
SKalt committed Oct 18, 2020
1 parent fd540c4 commit 9a64b66
Show file tree
Hide file tree
Showing 7 changed files with 568 additions and 202 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
.vscode
34 changes: 24 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,46 @@
# git-cc
> a git extension to help write [conventional commits][cc-standard]
## quickstart
## install

```sh
go install github.com/skalt/git-cc
git cc -init -y # need to configure git-cc within this repo. Use default types.

git add .
git cc feat added conventional commits
# equivalent to `git commit -m "feat: added conventional commits"`

# --> "feat: added conventional commits"
git cc fix something: an error
# equivalent to `git commit -m "fix(something): an error"`
# --> "fix(something): an error"
git cc feat
# interactively write a conventional commit
# interactively write a conventional commit from the description onward
git cc
# interactively write a conventional commit!
```
<!--
## Why Conventional Commits?
The cool kids are doing it.
Plus, `cc`s make good, `grep`pable changelogs. -->

## Why An Interactive Cli?
Figuring out what to write for an informative commit can be difficult.
The convential commits standard helps figure out what to write.
An interactive prompts helps with following the standard.

<details><summary>Some parts of the conventional commit standard require quoting to work on the command-line.</summary>

## Why?
- `git commit -m fix(something): ...` fails since `()` would be a syntactically-invalid subshell
- `git commit -m feat!: ...` fails since `!` expands to a bash history command
</details>

You want the same interface in any project, on any machine. You want to use your git log as your changelog.


Prior art:

[`committizen`][commitizen].
- [`committizen`][commitizen]
- [`commitsar`][commitsar]

[cc-standard]: https://www.conventionalcommits.org/en/v1.0.0/

[commitizen]: https://github.com/commitizen/cz-cli
[commitlint]: https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional
[commitlint]: https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional
[commitsar]: https://github.com/commitsar-app/commitsar
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
module github.com/skalt/git-cc

go 1.14

require (
github.com/charmbracelet/bubbletea v0.11.1 // indirect
github.com/go-git/go-git/v5 v5.0.0 // indirect
github.com/manifoldco/promptui v0.7.0
github.com/spf13/cobra v1.0.0 // indirect
)
216 changes: 216 additions & 0 deletions go.sum

Large diffs are not rendered by default.

256 changes: 256 additions & 0 deletions pkg/parser/combinators.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package parser

import (
"fmt"
"regexp"
)

// see https://medium.com/@armin.heller/using-parser-combinators-in-go-e63b3ad69c94,
// https://github.com/Geal/nom (v5+), and https://bodil.lol/parser-combinators/

// TODO: have parsers output a Concrete Syntax Tree (CST).
// IDEA: have parserresult interface be tree-ish:
// type ResultIdea struct {
// Children []ResultIdea
// Kind string // should be Token or symbol, might be empty in case of ignored chars
// Range [2]int // start, end. Empty if start == end.
// Original *string // pointer to the original input
// //? Parent *ResultIdea
// }

// func (r *ResultIdea) OriginalString() string {
// return (*r.Original)[r.Range[0]:r.Range[1]]
// }
// func (r *ResultIdea) Remaining() []rune {
// return []rune((*r.Original)[r.Range[1]:])
// }

// type ParserIdea func( /*input*/ *string, int /*rune start index*/) (ResultIdea, error)

type ParserResult struct {
Output interface{}
Remaining []rune
}
type ParserCallback func([]rune) interface{}
type Parser func([]rune) (ParserResult, error)
type void struct{}

func ident(input []rune) interface{} {
return []rune(input)
}

func LastRuneOf(parser Parser) Parser {
return func(input []rune) (ParserResult, error) {
maxIndex := len(input) - 1
if maxIndex < 0 {
return parser([]rune{})
} else {
lastRune := input[maxIndex-1:]
return parser(lastRune)
}
}
}

func TakeUntil(parser Parser, callback ParserCallback) Parser {
return func(input []rune) (ParserResult, error) {
for i, char := range input {
_, err := parser([]rune{char})
if err == nil {
return ParserResult{callback(input[:i]), input[i:]}, nil
}
}
return ParserResult{nil, input}, fmt.Errorf("didn't match parser")
}
}

func Opt(parser Parser) Parser {
return func(input []rune) (ParserResult, error) {
result, _ := parser(input)
return result, nil
}
}

func LiteralRune(match rune, callback ParserCallback) Parser {
return func(input []rune) (ParserResult, error) {
if len(input) > 0 {
if input[0] == match {
return ParserResult{callback([]rune{match}), input[1:]}, nil
} else {
return ParserResult{nil, input}, fmt.Errorf("%v not matched", match)
}
} else {
return ParserResult{nil, input}, fmt.Errorf("no input")
}
}
}

func Not(parser Parser) Parser {
return func(input []rune) (ParserResult, error) {
result, err := parser(input)
if err == nil {
return result, nil
} else {
return ParserResult{nil, input}, fmt.Errorf("wasn't expecting to match %v", parser)
}
}
}

func toRunes(i interface{}) []rune {
switch i.(type) {
case string:
str, _ := i.(string)
return []rune(str)
case rune:
r, _ := i.(rune)
return []rune{r}
case []rune:
runes, _ := i.([]rune)
return runes
default:
panic(fmt.Errorf("%v is not string or []rune", i))
}
}

func Tag(tag interface{}) Parser {
toMatch := toRunes(tag)
return func(input []rune) (ParserResult, error) {
if len(toMatch) > len(input) {
return ParserResult{nil, input}, fmt.Errorf("input longer than tag")
}
for i, matching := range toMatch {
if input[i] != matching {
err := fmt.Errorf(
"\"%v\" does not match \"%v\"",
string(input[:i+1]),
string(toMatch),
)
return ParserResult{nil, input}, err
}
}
return ParserResult{string(toMatch), input[len(toMatch):]}, nil
}
}

func Any(parsers ...Parser) Parser {
return func(input []rune) (ParserResult, error) {
for _, parser := range parsers {
result, err := parser(input)
if err == nil {
return result, err
}
}
return ParserResult{nil, input}, fmt.Errorf("expected a parser to match")
}
}
func Empty(input []rune) (ParserResult, error) {
if len(input) == 0 {
return ParserResult{nil, input}, nil
} else {
return ParserResult{nil, input}, fmt.Errorf("Not the end")
}
}
func OneOfTheseRunes(str string) Parser {
set := make(map[rune]void)
var present void
for _, char := range []rune(str) {
set[char] = present
}
parsers := make([]Parser, len(set))
for char := range set {
parsers = append(parsers, LiteralRune(char, ident))
}
return Any(parsers...)
}

func asString(result []rune) interface{} {
return string(result)
}
func clone(input []rune) []rune {
cloned := make([]rune, len(input))
copy(cloned, input)
return cloned
}

func Sequence(parsers ...Parser) Parser {
return func(input []rune) (ParserResult, error) {
var currentInput = []rune{}
results := make([]interface{}, len(parsers))
copy(currentInput, input)
for _, parser := range parsers {
result, err := parser(currentInput)
if err != nil {
return ParserResult{nil, input}, err
} else {
currentInput = result.Remaining
results = append(results, result.Output)
}
}
return ParserResult{results, currentInput}, nil
}
}
func Delimeted(start Parser, middle Parser, end Parser) Parser {
return func(input []rune) (ParserResult, error) {
result, err := Sequence(start, middle, end)(input)
if err != nil {
return ParserResult{nil, input}, err
}
results, _ := result.Output.([]string)
return ParserResult{results[1], result.Remaining}, nil
}
}
func Many0(parser Parser) Parser {
return func(input []rune) (ParserResult, error) {
i := 0
results := make([]interface{}, len(input))
for i < len(input) { // the highest possible # of times callable
result, err := parser(input[i:])
if err != nil {
break
}
results = append(results, result.Output)
i += len(result.Remaining)
}
return ParserResult{results, input[i:]}, nil
}
}

func Many1(parser Parser) Parser {
return func(input []rune) (ParserResult, error) {
result, _ := Many0(parser)(input)
resultArr := result.Output.([]interface{})
if len(resultArr) == 0 {
return result, fmt.Errorf("no results")
} else {
return result, nil
}
}
}

func Regex(pattern string) Parser {
re := regexp.MustCompile(`^` + pattern) // should be from the start of the bytes
return func(input []rune) (ParserResult, error) {
b := []byte(string(input))
result := re.FindIndex(b) //Match(b)
if result == nil { // no match found
return ParserResult{nil, input}, fmt.Errorf("no match for /%s/", pattern)
} else {
// a rune can be multiple bytes, so convert the reult back to runes
startByte := result[0]
endByte := result[1]
endRune := len([]rune(string(b[startByte:endByte])))
return ParserResult{input[:endRune], input[endRune:]}, nil
}
}
}

// func Range(start rune, end rune) Parser {
// return Not(Empty)(
// func(input []rune) (ParserResult, error) {
// if input[0] >= start && input[0] <= end {
// // ok
// } else {
// return
// }
// },
// )
// }
Loading

0 comments on commit 9a64b66

Please # to comment.