Skip to content
Merged
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
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: ci

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
hygiene:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
cache: false

- name: Install Licenser
run: go install github.com/liamawhite/licenser@21016ac7e79acc475e3b11f71aac7499a21790a5

- run: make hygiene
- run: make dirty

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
cache: false

- run: make test
34 changes: 34 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright 2024 Liam White
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

.PHONY: all hygiene dirty mod format test licenser

all: format mod test dirty

hygiene: format mod

dirty:
git diff --exit-code

mod:
go mod tidy

format: licenser
gofmt -w .

test:
go test -v ./...

licenser:
licenser apply -r "Liam White"
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,31 @@
# parse
A functional parser library written in Go.
# Parse

Parse is a functional parsing library written in Go. It enables the building of complex parsers from small, simple and (most importantly) unit testable functions that process the input.

The core of the library is a partial rewrite of [Adrian Hesketh's library](https://github.com/a-h/parse/) with reduced verbosity, bug fixes and then extended as required for [Notedown](https://github.com/notedownorg/notedown).

## Packages

- [`core`](./core) contains all the base parsers for parsing documents.
- [`time`](./time) contains all parsers related to time, dates and durations.
- [`test`](./test) contains helper functions for unit testing your own parsers.

The packages are designed to be composable via dot import. Dot imports are generally discouraged in Golang except in the case of reducing verbosity for DSL-like APIs which is typical here.

```go

import (
. "github.com/liamawhite/parse/core"
// . "github.com/liamawhite/parse/time" whichever other packages you may need
)

var BlankLine = Times(2, NewLine) // \n\n

```


## Implementing Your Own Parsers

To implement a parser implement the `Parser[T]` type alias, a function that takes an `Input` and returns `(T, bool, error)`. Each parser should attempt to parse the `Input` and roll back if it is unable to find what it is looking for.

You can find examples in the [`time`](./time) package.
32 changes: 32 additions & 0 deletions core/all.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2024 Liam White
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package core

// All matches all of the given parsers in order or rolls back the input.
func All[T any](parsers ...Parser[T]) Parser[[]T] {
return func(in Input) ([]T, bool, error) {
start := in.Checkpoint()
match := make([]T, 0, len(parsers))
for _, parser := range parsers {
m, ok, err := parser(in)
if err != nil || !ok {
in.Restore(start)
return nil, false, err
}
match = append(match, m)
}
return match, true, nil
}
}
42 changes: 42 additions & 0 deletions core/all_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2024 Liam White
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package core_test

import (
"testing"

"github.com/liamawhite/parse/core"
. "github.com/liamawhite/parse/test"
)

func TestAll(t *testing.T) {
tests := []ParserTest[[]string]{
{
Name: "no match",
Input: "AC",
Parser: core.All(core.Rune('A'), core.Rune('B')),
ExpectedOK: false,
RemainingInput: "AC",
},
{
Name: "match",
Input: "AB",
Parser: core.All(core.Rune('A'), core.Rune('B')),
ExpectedMatch: []string{"A", "B"},
ExpectedOK: true,
},
}
RunTests(t, tests)
}
29 changes: 29 additions & 0 deletions core/any.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2024 Liam White
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package core

// Any looks for matches in the given parsers, returning the first match or rolls back the input if no match is found.
func Any[T any](parsers ...Parser[T]) Parser[T] {
return func(in Input) (T, bool, error) {
for _, parser := range parsers {
match, ok, err := parser(in)
if err != nil || ok {
return match, true, err
}
}
var t T
return t, false, nil
}
}
43 changes: 43 additions & 0 deletions core/any_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2024 Liam White
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package core_test

import (
"testing"

"github.com/liamawhite/parse/core"
. "github.com/liamawhite/parse/test"
)

func TestAny(t *testing.T) {
tests := []ParserTest[string]{
{
Name: "no match",
Input: "C",
Parser: core.Any(core.Rune('A'), core.Rune('B')),
ExpectedOK: false,
RemainingInput: "C",
},
{
Name: "match",
Input: "B",
Parser: core.Any(core.Rune('A'), core.Rune('B')),
ExpectedMatch: "B",
ExpectedOK: true,
},
}
RunTests(t, tests)

}
24 changes: 24 additions & 0 deletions core/eof.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2024 Liam White
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package core

// EOF matches the end of the input. Use T to make it composeable with other parsers.
func EOF[T any]() Parser[T] {
return func(in Input) (T, bool, error) {
_, canAdvance := in.Peek(1)
var t T
return t, !canAdvance, nil
}
}
41 changes: 41 additions & 0 deletions core/eof_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2024 Liam White
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package core_test

import (
"testing"

"github.com/liamawhite/parse/core"
. "github.com/liamawhite/parse/test"
)

func TestEOF(t *testing.T) {
tests := []ParserTest[string]{
{
Name: "no match",
Input: "A",
Parser: core.EOF[string](),
ExpectedOK: false,
RemainingInput: "A",
},
{
Name: "match",
Input: "",
Parser: core.EOF[string](),
ExpectedOK: true,
},
}
RunTests(t, tests)
}
68 changes: 68 additions & 0 deletions core/input.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2024 Liam White
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package core

// Use unexported type alias so we control the interface and prevent people doing arbirary things e.g. use negative ints.
type checkpoint int

type Input interface {
Peek(n int) (string, bool)
Take(n int) (string, bool)
Checkpoint() checkpoint
Restore(cp checkpoint)
}

type input struct {
s string
index int
}

func NewInput(s string) Input {
return &input{
s: s,
}
}

func (i *input) Peek(n int) (s string, ok bool) {
if i.index+n > len(i.s) {
return
}
if n < 0 {
return i.s[i.index:], true
}
return i.s[i.index : i.index+n], true
}

func (i *input) Take(n int) (s string, ok bool) {
if i.index+n > len(i.s) {
return
}
if n < 0 {
return
}
from := i.index
i.index += n
return i.s[from:i.index], true
}

// Take a snapshot of the current parsing position
func (i *input) Checkpoint() checkpoint {
return checkpoint(i.index)
}

// Restore the parsing position to a previous snapshot
func (i *input) Restore(cp checkpoint) {
i.index = int(cp)
}
Loading
Loading