Compare commits
145 Commits
Author | SHA1 | Date |
---|---|---|
diamondburned | b5bb0c9bb9 | |
diamondburned | 8bfabf58ec | |
diamondburned | 410ac73469 | |
diamondburned | 4e11444f6c | |
diamondburned | 86956a65ec | |
diamondburned | f2de1cb84d | |
diamondburned | 0cb14b9819 | |
diamondburned | f24feb2002 | |
diamondburned | f8c644fa7e | |
diamondburned | c7d4473c23 | |
diamondburned | 174496bdf9 | |
diamondburned | da5c38eb2f | |
diamondburned | c2fb784dbf | |
diamondburned | d40f221221 | |
diamondburned | ee9c2cc37c | |
diamondburned | 0569261f72 | |
diamondburned | 4ea6773527 | |
diamondburned | 1ece6ea076 | |
diamondburned | 1251001e8c | |
diamondburned | 41a7dac033 | |
diamondburned | 02c686f994 | |
diamondburned | 1460ee6b4b | |
diamondburned | 06a26af5ba | |
diamondburned | 903fe9fbfd | |
diamondburned | 7cb512f8b1 | |
diamondburned | 24fc2c9bbb | |
diamondburned | f1db8e0601 | |
diamondburned | 7fe9b3ed4c | |
diamondburned | fd8106eaf1 | |
diamondburned | 9fd965d45a | |
diamondburned | 955b99c9b6 | |
diamondburned | c32c50c0e8 | |
diamondburned | 318c85ab65 | |
diamondburned | e59ab2dbf1 | |
diamondburned | ea2c12d119 | |
diamondburned | 4c835a467b | |
diamondburned | 10549e49e1 | |
diamondburned | 289eda1c25 | |
diamondburned | 05f8ec0cbf | |
diamondburned | 1dd36e0034 | |
diamondburned | 76f5201a6f | |
diamondburned | 4864d61476 | |
diamondburned | 0ebf0c3302 | |
diamondburned | d62231a4ef | |
diamondburned | cfc0e00c8a | |
diamondburned | 1b1e10a8a6 | |
diamondburned | 6140b5a131 | |
diamondburned | 285ac6403f | |
diamondburned | 819bcd3504 | |
diamondburned | 32fa6266db | |
diamondburned | 89b5ede1d8 | |
diamondburned | 5f7316cf9d | |
diamondburned | 086f987b3c | |
diamondburned | e08064021e | |
diamondburned | dd4e230e0f | |
diamondburned | 99f7224d32 | |
diamondburned | 555931f974 | |
diamondburned | aaa29f35b0 | |
diamondburned | 1588cfef9c | |
diamondburned | e2751cc260 | |
diamondburned | 7b9b4864a5 | |
diamondburned | 2d00544d67 | |
diamondburned | 59778af1dd | |
diamondburned | 9a64b50703 | |
diamondburned | 25980eb794 | |
diamondburned | 2d93bf62ea | |
diamondburned | f515470458 | |
diamondburned | 516532ee01 | |
diamondburned | 8e9321928b | |
diamondburned | 8b8c46a714 | |
diamondburned | d51668512b | |
diamondburned | 62711b89f2 | |
diamondburned | 214233cf3d | |
diamondburned | ab2b4d48fa | |
diamondburned | 40dbe21c82 | |
diamondburned | 4239dc47c4 | |
diamondburned | cd018ef8f9 | |
diamondburned | 1b70301711 | |
diamondburned | 681cc520d9 | |
diamondburned | 2f5c86aa60 | |
diamondburned | 3f4d50fa92 | |
diamondburned | 7ae629e1ca | |
diamondburned | c45d874a80 | |
diamondburned | 8827df937d | |
diamondburned | 9a7fe13cef | |
diamondburned | 9974fcc636 | |
diamondburned | 1fe254db60 | |
diamondburned | 106b543f09 | |
diamondburned | ecbd4515a2 | |
diamondburned (Forefront) | 0abbf861bc | |
diamondburned (Forefront) | c8d6c89a08 | |
diamondburned (Forefront) | 78767a3f2f | |
diamondburned (Forefront) | d29ee70d56 | |
diamondburned (Forefront) | e7aa6fb885 | |
diamondburned (Forefront) | d754f011ba | |
diamondburned (Forefront) | a8cfc54f6d | |
diamondburned (Forefront) | 88879d45f2 | |
diamondburned (Forefront) | 88834ab465 | |
diamondburned (Forefront) | 5d2cd4a57b | |
diamondburned (Forefront) | 6c7cd5feb2 | |
diamondburned (Forefront) | dfb60ac0eb | |
diamondburned (Forefront) | e953dbbcb1 | |
diamondburned (Forefront) | 05b7a6a10c | |
diamondburned (Forefront) | ba88528a7a | |
diamondburned (Forefront) | d4917d2e6d | |
diamondburned (Forefront) | 6bb1d742a2 | |
diamondburned (Forefront) | 7698aa5fc2 | |
diamondburned (Forefront) | 8768baf196 | |
diamondburned (Forefront) | 391004677b | |
diamondburned (Forefront) | 831a6ea7e6 | |
diamondburned (Forefront) | 4a7f7a7994 | |
diamondburned (Forefront) | b38dc6c6b4 | |
diamondburned (Forefront) | 8fdf82883a | |
diamondburned (Forefront) | 66dd2b1b11 | |
diamondburned (Forefront) | b6eca8eafa | |
diamondburned (Forefront) | 2e7c6b0098 | |
diamondburned (Forefront) | 557ac54a04 | |
diamondburned (Forefront) | 2eed8da97f | |
diamondburned (Forefront) | 242f2c6192 | |
diamondburned (Forefront) | 72966ad02a | |
diamondburned (Forefront) | ce009a8cba | |
diamondburned (Forefront) | b4abf67cca | |
diamondburned (Forefront) | a2235171a1 | |
diamondburned (Forefront) | 01e6e1ce31 | |
diamondburned (Forefront) | 7194d04894 | |
diamondburned (Forefront) | 231088e94d | |
diamondburned (Forefront) | fa7f2fdca4 | |
diamondburned (Forefront) | deadc66d46 | |
diamondburned (Forefront) | 3eb64db96c | |
diamondburned (Forefront) | 46debdf53b | |
diamondburned (Forefront) | 9b3e03753c | |
diamondburned (Forefront) | 5d3c3568f8 | |
diamondburned (Forefront) | f846071657 | |
diamondburned (Forefront) | c1846a7796 | |
diamondburned (Forefront) | 3d2eaae6de | |
diamondburned (Forefront) | 6df6fab132 | |
diamondburned (Forefront) | 4669362443 | |
diamondburned (Forefront) | c49bea418a | |
diamondburned (Forefront) | f13b042836 | |
diamondburned (Forefront) | c159c2c1df | |
diamondburned (Forefront) | 251f975dc7 | |
diamondburned (Forefront) | 519066e136 | |
diamondburned (Forefront) | 668497aef0 | |
diamondburned (Forefront) | f099d3f70b | |
diamondburned (Forefront) | 107d70485e |
|
@ -0,0 +1,13 @@
|
|||
Copyright 2020 diamondburned
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted, provided that the above copyright notice
|
||||
and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
102
README.md
102
README.md
|
@ -1,99 +1,27 @@
|
|||
# cchat
|
||||
# [cchat][godoc]
|
||||
|
||||
A set of stabilized interfaces for cchat implementations, joining the backend
|
||||
and frontend together.
|
||||
|
||||
## Backend
|
||||
Refer to the [GoDoc][godoc] for interfaces and documentations.
|
||||
|
||||
### Service
|
||||
[godoc]: https://godoc.org/github.com/diamondburned/cchat
|
||||
|
||||
A service is a server with extra services implemented.
|
||||
## Known implementations
|
||||
|
||||
#### Interfaces
|
||||
The following sections contain known cchat implementations. PRs are welcomed for
|
||||
more implementations to be added here.
|
||||
|
||||
- Server
|
||||
- ServerList
|
||||
- ServerIcon (optional)
|
||||
- Configurator (optional)
|
||||
- Authenticator (optional)
|
||||
### Backend
|
||||
|
||||
### Authenticator
|
||||
- [diamondburned/cchat-mock](https://github.com/diamondburned/cchat-mock)
|
||||
- A small subset of the cchat backend implementation mocked with fake data
|
||||
for testing.
|
||||
- [diamondburned/cchat-discord](https://github.com/diamondburned/cchat-discord)
|
||||
- A Discord backend implementing cchat interfaces.
|
||||
|
||||
The authenticator interface allows for a multistage initial authentication API
|
||||
that the backend could use. Multistage is done by calling `AuthenticateForm`
|
||||
then `Authenticate` again forever until no errors are returned.
|
||||
### Frontend
|
||||
|
||||
```go
|
||||
for {
|
||||
// Pseudo-function to render the form and return the results of those forms
|
||||
// when the user confirms it.
|
||||
outputs := renderAuthForm(svc.AuthenticateForm())
|
||||
- [diamondburned/cchat-gtk](https://github.com/diamondburned/cchat-gtk)
|
||||
- A GTK+3 implementation of a cchat frontend.
|
||||
|
||||
if err := svc.Authenticate(outputs); err != nil {
|
||||
renderError(errors.Wrap(err, "Error while authenticating"))
|
||||
continue // retry
|
||||
}
|
||||
|
||||
break // success
|
||||
}
|
||||
```
|
||||
|
||||
### Commander
|
||||
|
||||
The commander interface allows the backend to implement custom commands to
|
||||
easily extend the API.
|
||||
|
||||
#### Interfaces
|
||||
|
||||
- CommandCompleter (optional)
|
||||
|
||||
### Server
|
||||
|
||||
A server is any entity that is usually a channel or a guild.
|
||||
|
||||
#### Interfaces
|
||||
|
||||
- ServerList and/or ServerMessage
|
||||
- ServerIcon (optional)
|
||||
|
||||
### Messages
|
||||
|
||||
#### Interfaces
|
||||
|
||||
- MessageHeader
|
||||
- MessageCreate or MessageUpdate or MessageDelete
|
||||
- MessageNonce (optional)
|
||||
- MessageAuthorAvatar (optional)
|
||||
|
||||
## Frontend
|
||||
|
||||
### ServersContainer
|
||||
|
||||
A servers container is any type of view that displays the list of servers. It
|
||||
should implement a `SetServers([]Server)` that the backend could use to call
|
||||
anytime the server list changes (at all).
|
||||
|
||||
Typically, most frontend should implement this interface onto a tree node
|
||||
instead of a tree view, as servers can be infinitely nested.
|
||||
|
||||
This interface expects the frontend to handle its own errors.
|
||||
|
||||
### MessagesContainer
|
||||
|
||||
A messages container is a view implementation that displays a list of messages
|
||||
live. This implements the 3 most common message events: `CreateMessage`,
|
||||
`UpdateMessage` and `DeleteMessage`. The frontend must handle all 3.
|
||||
|
||||
Since this container interface extends a single Server, the frontend is allowed
|
||||
to have multiple views. This is usually done with tabs or splits, but the
|
||||
backend should update them all nonetheless.
|
||||
|
||||
### SendableMessage
|
||||
|
||||
The frontend can make its own send message data implementation to indicate extra
|
||||
capabilities that the backend may want.
|
||||
|
||||
An example of this is `MessageNonce`, which is similar to IRCv3's [labeled
|
||||
response extension](https://ircv3.net/specs/extensions/labeled-response).
|
||||
The frontend could implement this interface and check if incoming
|
||||
`MessageCreate` events implement the same interface.
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
"github.com/diamondburned/cchat/cmd/internal/cchat-generator/genutils"
|
||||
"github.com/diamondburned/cchat/repository"
|
||||
)
|
||||
|
||||
const OutputDir = "."
|
||||
|
||||
func init() {
|
||||
log.SetFlags(0)
|
||||
}
|
||||
|
||||
var comment = repository.Comment{Raw: `
|
||||
Package empty provides no-op asserter method implementations of interfaces
|
||||
in cchat's root and text packages.
|
||||
`}
|
||||
|
||||
type Package struct {
|
||||
Path string
|
||||
repository.Package
|
||||
}
|
||||
|
||||
func main() {
|
||||
gen := genutils.NewFile("empty")
|
||||
gen.PackageComment(comment.GoString(1))
|
||||
|
||||
// Sort.
|
||||
var packages = make([]Package, 0, len(repository.Main))
|
||||
|
||||
for pkgpath, pk := range repository.Main {
|
||||
packages = append(packages, Package{
|
||||
Path: pkgpath,
|
||||
Package: pk,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(packages, func(i, j int) bool {
|
||||
return packages[i].Path < packages[j].Path
|
||||
})
|
||||
|
||||
for _, pkg := range packages {
|
||||
gen.ImportName(pkg.Path, path.Base(pkg.Path))
|
||||
|
||||
for _, iface := range pkg.Interfaces {
|
||||
// Skip structs without asserter methods.
|
||||
if !hasAsserter(iface) {
|
||||
continue
|
||||
}
|
||||
|
||||
var ifaceName = newIfaceName(pkg.Path, iface)
|
||||
|
||||
gen.Commentf("%[1]s provides no-op asserters for cchat.%[1]s.", ifaceName)
|
||||
gen.Type().Id(ifaceName).Struct()
|
||||
gen.Line()
|
||||
|
||||
for _, embed := range iface.Embeds {
|
||||
if iface := pkg.Interface(embed.InterfaceName); iface != nil {
|
||||
genIfaceMethods(gen, *iface, ifaceName, pkg.Path)
|
||||
}
|
||||
}
|
||||
|
||||
genIfaceMethods(gen, iface, ifaceName, pkg.Path)
|
||||
|
||||
gen.Line()
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.Create(filepath.Join(os.Args[1], "empty.go"))
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to create output file:", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := gen.Render(f); err != nil {
|
||||
log.Fatalln("Failed to render output:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func newIfaceName(pkgpath string, iface repository.Interface) string {
|
||||
if pkgpath == repository.RootPath {
|
||||
return iface.Name
|
||||
} else {
|
||||
return strings.Title(repository.TrimRoot(pkgpath)) + iface.Name
|
||||
}
|
||||
}
|
||||
|
||||
func genIfaceMethods(gen *jen.File, iface repository.Interface, ifaceName, pkgpath string) {
|
||||
for _, method := range iface.Methods {
|
||||
am, ok := method.(repository.AsserterMethod)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("As%s", am.ChildType)
|
||||
gen.Comment(fmt.Sprintf("%s returns nil.", name))
|
||||
|
||||
stmt := jen.Func()
|
||||
stmt.Parens(jen.Id(ifaceName))
|
||||
stmt.Id(name)
|
||||
stmt.Params()
|
||||
stmt.Add(genutils.GenerateExternType(pkgpath, am))
|
||||
stmt.Values(jen.Return(jen.Nil()))
|
||||
|
||||
gen.Add(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
func hasAsserter(iface repository.Interface) bool {
|
||||
for _, method := range iface.Methods {
|
||||
if _, isA := method.(repository.AsserterMethod); isA {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
"github.com/diamondburned/cchat/cmd/internal/cchat-generator/genutils"
|
||||
"github.com/diamondburned/cchat/repository"
|
||||
)
|
||||
|
||||
func generateEnums(enums []repository.Enumeration) jen.Code {
|
||||
sort.Slice(enums, func(i, j int) bool {
|
||||
return enums[i].Name < enums[j].Name
|
||||
})
|
||||
|
||||
var stmt = new(jen.Statement)
|
||||
|
||||
for _, enum := range enums {
|
||||
if !enum.Comment.IsEmpty() {
|
||||
stmt.Comment(enum.Comment.GoString(1))
|
||||
stmt.Line()
|
||||
}
|
||||
|
||||
stmt.Type().Id(enum.Name).Id(enum.GoType())
|
||||
stmt.Line()
|
||||
stmt.Line()
|
||||
|
||||
stmt.Const().DefsFunc(func(group *jen.Group) {
|
||||
for i, value := range enum.Values {
|
||||
var c jen.Statement
|
||||
if !value.Comment.IsEmpty() {
|
||||
c.Comment(value.Comment.GoString(2))
|
||||
c.Line()
|
||||
}
|
||||
|
||||
c.Id(enum.Name + value.Name)
|
||||
|
||||
switch {
|
||||
// Regular.
|
||||
case i == 0 && !enum.Bitwise:
|
||||
c.Id(enum.Name).Op("=").Iota()
|
||||
|
||||
// Bitwise.
|
||||
case i == 0 && enum.Bitwise:
|
||||
c.Id(enum.Name).Op("=").Lit(0)
|
||||
case i == 1 && enum.Bitwise:
|
||||
c.Id(enum.Name).Op("=").Lit(1).Op("<<").Iota()
|
||||
}
|
||||
|
||||
group.Add(&c)
|
||||
}
|
||||
})
|
||||
|
||||
stmt.Line()
|
||||
stmt.Line()
|
||||
|
||||
var recv = genutils.RecvName(enum.Name)
|
||||
|
||||
if enum.Bitwise {
|
||||
fn := stmt.Func()
|
||||
fn.Params(jen.Id(recv).Id(enum.Name))
|
||||
fn.Id("Has")
|
||||
fn.Params(jen.Id("has").Id(enum.Name))
|
||||
fn.Bool()
|
||||
fn.BlockFunc(func(g *jen.Group) {
|
||||
g.Return(jen.Id(recv).Op("&").Id("has").Op("==").Id("has"))
|
||||
})
|
||||
} else {
|
||||
fn := stmt.Func()
|
||||
fn.Params(jen.Id(recv).Id(enum.Name))
|
||||
fn.Id("Is")
|
||||
fn.Params(jen.Id("is").Id(enum.Name))
|
||||
fn.Bool()
|
||||
fn.BlockFunc(func(g *jen.Group) {
|
||||
g.Return(jen.Id(recv).Id("==").Id("is"))
|
||||
})
|
||||
}
|
||||
|
||||
stmt.Line()
|
||||
stmt.Line()
|
||||
}
|
||||
|
||||
return stmt
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
"github.com/diamondburned/cchat/cmd/internal/cchat-generator/genutils"
|
||||
"github.com/diamondburned/cchat/repository"
|
||||
)
|
||||
|
||||
func generateInterfaces(ifaces []repository.Interface) jen.Code {
|
||||
sort.Slice(ifaces, func(i, j int) bool {
|
||||
return ifaces[i].Name < ifaces[j].Name
|
||||
})
|
||||
|
||||
var stmt = new(jen.Statement)
|
||||
|
||||
for _, iface := range ifaces {
|
||||
if !iface.Comment.IsEmpty() {
|
||||
stmt.Comment(iface.Comment.GoString(1))
|
||||
stmt.Line()
|
||||
}
|
||||
|
||||
stmt.Type().Id(iface.Name).InterfaceFunc(func(group *jen.Group) {
|
||||
if len(iface.Embeds) > 0 {
|
||||
for _, embed := range iface.Embeds {
|
||||
if !embed.Comment.IsEmpty() {
|
||||
group.Comment(embed.Comment.GoString(1))
|
||||
}
|
||||
group.Id(embed.InterfaceName)
|
||||
}
|
||||
|
||||
group.Line()
|
||||
}
|
||||
|
||||
// Put Asserter methods last.
|
||||
sort.SliceStable(iface.Methods, func(i, j int) bool {
|
||||
_, assert := iface.Methods[i].(repository.AsserterMethod)
|
||||
return !assert
|
||||
})
|
||||
|
||||
// Boolean to only write the Asserter comment once.
|
||||
var writtenComment bool
|
||||
|
||||
for _, method := range iface.Methods {
|
||||
if !writtenComment {
|
||||
if _, ok := method.(repository.AsserterMethod); ok {
|
||||
group.Line()
|
||||
group.Comment("// Asserters.")
|
||||
group.Line()
|
||||
writtenComment = true
|
||||
}
|
||||
}
|
||||
|
||||
var stmt = new(jen.Statement)
|
||||
if comment := method.UnderlyingComment(); !comment.IsEmpty() {
|
||||
stmt.Comment(comment.GoString(1))
|
||||
stmt.Line()
|
||||
}
|
||||
|
||||
stmt.Id(method.UnderlyingName())
|
||||
|
||||
switch method := method.(type) {
|
||||
case repository.GetterMethod:
|
||||
stmt.Params(generateFuncParams(method.Parameters, "")...)
|
||||
stmt.Params(generateFuncParams(method.Returns, method.ErrorType)...)
|
||||
case repository.SetterMethod:
|
||||
stmt.Params(generateFuncParams(method.Parameters, "")...)
|
||||
stmt.Params(generateFuncParamsErr(repository.NamedType{}, method.ErrorType)...)
|
||||
case repository.ContainerUpdaterMethod:
|
||||
stmt.Params(generateFuncParamsCtx(method.Parameters, "")...)
|
||||
stmt.Params(generateFuncParamsErr(repository.NamedType{}, method.ErrorType)...)
|
||||
case repository.IOMethod:
|
||||
stmt.Params(generateFuncParamsCtx(method.Parameters, "")...)
|
||||
stmt.Params(generateFuncParamsErr(method.ReturnValue, method.ErrorType)...)
|
||||
var comment = "Blocking"
|
||||
if method.Disposer {
|
||||
comment += ", Disposer"
|
||||
}
|
||||
stmt.Comment("// " + comment)
|
||||
case repository.ContainerMethod:
|
||||
stmt.Params(generateContainerFuncParams(method)...)
|
||||
stmt.Params(generateContainerFuncReturns(method)...)
|
||||
case repository.AsserterMethod:
|
||||
stmt.Params()
|
||||
stmt.Params(genutils.GenerateType(method))
|
||||
stmt.Comment("// Optional")
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
group.Add(stmt)
|
||||
}
|
||||
})
|
||||
|
||||
stmt.Line()
|
||||
stmt.Line()
|
||||
}
|
||||
|
||||
return stmt
|
||||
}
|
||||
|
||||
func generateFuncParamsErr(param repository.NamedType, errorType string) []jen.Code {
|
||||
stmt := make([]jen.Code, 0, 2)
|
||||
|
||||
if !param.IsZero() {
|
||||
stmt = append(stmt, generateFuncParam(param))
|
||||
}
|
||||
|
||||
if errorType != "" {
|
||||
if param.Name == "" {
|
||||
stmt = append(stmt, jen.Id(errorType))
|
||||
} else {
|
||||
stmt = append(stmt, jen.Err().Id(errorType))
|
||||
}
|
||||
}
|
||||
|
||||
return stmt
|
||||
}
|
||||
|
||||
func generateFuncParam(param repository.NamedType) jen.Code {
|
||||
if param.Name == "" {
|
||||
return genutils.GenerateType(param)
|
||||
}
|
||||
return jen.Id(param.Name).Add(genutils.GenerateType(param))
|
||||
}
|
||||
|
||||
func generateFuncParamsCtx(params []repository.NamedType, errorType string) []jen.Code {
|
||||
var name string
|
||||
if len(params) > 0 && params[0].Name != "" {
|
||||
name = "ctx"
|
||||
}
|
||||
|
||||
p := []repository.NamedType{{Name: name, Type: "context.Context"}}
|
||||
p = append(p, params...)
|
||||
|
||||
return generateFuncParams(p, errorType)
|
||||
}
|
||||
|
||||
func generateFuncParams(params []repository.NamedType, errorType string) []jen.Code {
|
||||
if len(params) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var stmt jen.Statement
|
||||
for _, param := range params {
|
||||
stmt.Add(generateFuncParam(param))
|
||||
}
|
||||
|
||||
if errorType != "" {
|
||||
if params[0].Name != "" {
|
||||
stmt.Add(jen.Err().Id(errorType))
|
||||
} else {
|
||||
stmt.Add(jen.Id(errorType))
|
||||
}
|
||||
}
|
||||
|
||||
return stmt
|
||||
}
|
||||
|
||||
func generateContainerFuncReturns(method repository.ContainerMethod) []jen.Code {
|
||||
var stmt jen.Statement
|
||||
|
||||
stmt.Add(jen.Id("stop").Func().Params())
|
||||
stmt.Add(jen.Err().Error())
|
||||
|
||||
return stmt
|
||||
}
|
||||
|
||||
func generateContainerFuncParams(method repository.ContainerMethod) []jen.Code {
|
||||
var stmt jen.Statement
|
||||
|
||||
if method.HasContext {
|
||||
stmt.Qual("context", "Context")
|
||||
}
|
||||
stmt.Add(genutils.GenerateType(method))
|
||||
|
||||
return stmt
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
"github.com/diamondburned/cchat/cmd/internal/cchat-generator/genutils"
|
||||
"github.com/diamondburned/cchat/repository"
|
||||
)
|
||||
|
||||
func generateStructs(structs []repository.Struct) jen.Code {
|
||||
sort.Slice(structs, func(i, j int) bool {
|
||||
return structs[i].Name < structs[j].Name
|
||||
})
|
||||
|
||||
var stmt = new(jen.Statement)
|
||||
|
||||
for _, s := range structs {
|
||||
stmt.Add(generateStruct(s))
|
||||
stmt.Line()
|
||||
stmt.Line()
|
||||
}
|
||||
|
||||
return stmt
|
||||
}
|
||||
|
||||
func generateErrorStructs(errStructs []repository.ErrorStruct) jen.Code {
|
||||
sort.Slice(errStructs, func(i, j int) bool {
|
||||
return errStructs[i].Name < errStructs[j].Name
|
||||
})
|
||||
|
||||
var stmt = new(jen.Statement)
|
||||
|
||||
for _, errStruct := range errStructs {
|
||||
stmt.Add(generateStruct(errStruct.Struct))
|
||||
stmt.Line()
|
||||
stmt.Line()
|
||||
|
||||
var recv = genutils.RecvName(errStruct.Name)
|
||||
|
||||
stmt.Func()
|
||||
stmt.Params(jen.Id(recv).Id(errStruct.Name))
|
||||
stmt.Id("Error").Params().String()
|
||||
stmt.Block(jen.Return(generateTmplString(errStruct.ErrorString, recv)))
|
||||
|
||||
stmt.Line()
|
||||
stmt.Line()
|
||||
|
||||
if wrap := errStruct.Wrapped(); wrap != "" {
|
||||
stmt.Func()
|
||||
stmt.Params(jen.Id(recv).Id(errStruct.Name))
|
||||
stmt.Id("Unwrap").Params().Error()
|
||||
stmt.Block(jen.Return(jen.Id(recv).Dot(wrap)))
|
||||
stmt.Line()
|
||||
stmt.Line()
|
||||
}
|
||||
}
|
||||
|
||||
return stmt
|
||||
}
|
||||
|
||||
func generateStruct(s repository.Struct) jen.Code {
|
||||
var stmt = new(jen.Statement)
|
||||
if !s.Comment.IsEmpty() {
|
||||
stmt.Comment(s.Comment.GoString(1))
|
||||
stmt.Line()
|
||||
}
|
||||
|
||||
stmt.Type().Id(s.Name).StructFunc(func(group *jen.Group) {
|
||||
for _, field := range s.Fields {
|
||||
var stmt = new(jen.Statement)
|
||||
if field.Name != "" {
|
||||
stmt.Id(field.Name)
|
||||
}
|
||||
stmt.Add(genutils.GenerateType(field))
|
||||
group.Add(stmt)
|
||||
}
|
||||
})
|
||||
|
||||
if !s.Stringer.IsEmpty() {
|
||||
stmt.Line()
|
||||
stmt.Line()
|
||||
|
||||
var recv = genutils.RecvName(s.Name)
|
||||
|
||||
if !s.Stringer.Comment.IsEmpty() {
|
||||
stmt.Comment(s.Stringer.Comment.GoString(1))
|
||||
stmt.Line()
|
||||
}
|
||||
|
||||
stmt.Func()
|
||||
stmt.Params(jen.Id(recv).Id(s.Name))
|
||||
stmt.Id("String").Params().String()
|
||||
stmt.BlockFunc(func(g *jen.Group) {
|
||||
if s.Stringer.Format == "%s" {
|
||||
g.Return(jen.Id(recv).Dot(s.Stringer.Fields[0]))
|
||||
return
|
||||
}
|
||||
|
||||
g.Return(generateTmplString(s.Stringer.TmplString, recv))
|
||||
})
|
||||
}
|
||||
|
||||
return stmt
|
||||
}
|
||||
|
||||
func generateTmplString(tmpl repository.TmplString, recv string) jen.Code {
|
||||
return jen.Qual("fmt", "Sprintf").CallFunc(func(g *jen.Group) {
|
||||
g.Lit(tmpl.Format)
|
||||
|
||||
for _, field := range tmpl.Fields {
|
||||
g.Add(jen.Id(recv).Dot(field))
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
"github.com/diamondburned/cchat/cmd/internal/cchat-generator/genutils"
|
||||
"github.com/diamondburned/cchat/repository"
|
||||
)
|
||||
|
||||
func generateTypeAlises(aliases []repository.TypeAlias) jen.Code {
|
||||
sort.Slice(aliases, func(i, j int) bool {
|
||||
return aliases[i].Name < aliases[j].Name
|
||||
})
|
||||
|
||||
var stmt = new(jen.Statement)
|
||||
|
||||
for _, alias := range aliases {
|
||||
if !alias.Comment.IsEmpty() {
|
||||
stmt.Comment(alias.Comment.GoString(1))
|
||||
stmt.Line()
|
||||
}
|
||||
|
||||
stmt.Type().Id(alias.Name).Op("=").Add(genutils.GenerateType(alias))
|
||||
stmt.Line()
|
||||
stmt.Line()
|
||||
}
|
||||
|
||||
return stmt
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package genutils
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
)
|
||||
|
||||
// NewFile creates a new file with the appropriate header comments.
|
||||
func NewFile(pkgname string) *jen.File {
|
||||
gen := jen.NewFile(pkgname)
|
||||
gen.HeaderComment("Code generated by ./cmd/internal. DO NOT EDIT.")
|
||||
return gen
|
||||
}
|
||||
|
||||
// NewFilePath creates a new file with the appropriate header comments.
|
||||
func NewFilePath(pkgpath string) *jen.File {
|
||||
gen := jen.NewFilePath(pkgpath)
|
||||
gen.HeaderComment("Code generated by ./cmd/internal. DO NOT EDIT.")
|
||||
return gen
|
||||
}
|
||||
|
||||
type Qualer interface {
|
||||
Qual() (path, name string)
|
||||
}
|
||||
|
||||
// GenerateType generates a jen.Qual from the given Qualer.
|
||||
func GenerateType(t Qualer) jen.Code {
|
||||
path, name := t.Qual()
|
||||
if path == "" {
|
||||
return jen.Id(name)
|
||||
}
|
||||
return jen.Qual(path, name)
|
||||
}
|
||||
|
||||
// GenerateExternType generates a jen.Qual from the given Qualer, except if
|
||||
// the path is empty, root is used instead.
|
||||
func GenerateExternType(root string, t Qualer) jen.Code {
|
||||
path, name := t.Qual()
|
||||
if path == "" {
|
||||
return jen.Qual(root, name)
|
||||
}
|
||||
return jen.Qual(path, name)
|
||||
}
|
||||
|
||||
// RecvName is used to get the receiver variable name. It returns the first
|
||||
// letter lower-cased. It does NOT do length checking. It only works with ASCII.
|
||||
func RecvName(name string) string {
|
||||
return string(unicode.ToLower(rune(name[0])))
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
"github.com/diamondburned/cchat/cmd/internal/cchat-generator/genutils"
|
||||
"github.com/diamondburned/cchat/repository"
|
||||
)
|
||||
|
||||
const OutputDir = "."
|
||||
|
||||
func init() {
|
||||
log.SetFlags(0)
|
||||
}
|
||||
|
||||
func main() {
|
||||
for pkgPath, pkg := range repository.Main {
|
||||
g := generate(pkgPath, pkg)
|
||||
|
||||
destDir := filepath.Join(
|
||||
os.Args[1],
|
||||
filepath.FromSlash(trimPrefix(repository.RootPath, pkgPath)),
|
||||
)
|
||||
destFile := filepath.Base(pkgPath) + ".go"
|
||||
|
||||
// Guarantee that the directory exists.
|
||||
if destDir != "" {
|
||||
if err := os.MkdirAll(destDir, os.ModePerm); err != nil {
|
||||
log.Fatalln("Failed to mkdir -p:", err)
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.Create(filepath.Join(destDir, destFile))
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to create output file:", err)
|
||||
}
|
||||
|
||||
if err := g.Render(f); err != nil {
|
||||
log.Fatalln("Failed to render output:", err)
|
||||
}
|
||||
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func trimPrefix(rootPrefix, path string) string {
|
||||
return strings.Trim(strings.TrimPrefix(path, rootPrefix), "/")
|
||||
}
|
||||
|
||||
func generate(pkgPath string, repo repository.Package) *jen.File {
|
||||
gen := genutils.NewFilePath(pkgPath)
|
||||
gen.PackageComment(repo.Comment.GoString(1))
|
||||
gen.Add(generateTypeAlises(repo.TypeAliases))
|
||||
gen.Line()
|
||||
gen.Add(generateEnums(repo.Enums))
|
||||
gen.Line()
|
||||
gen.Add(generateStructs(repo.Structs))
|
||||
gen.Line()
|
||||
gen.Add(generateErrorStructs(repo.ErrorStructs))
|
||||
gen.Line()
|
||||
gen.Add(generateInterfaces(repo.Interfaces))
|
||||
gen.Line()
|
||||
|
||||
return gen
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/diamondburned/cchat/repository"
|
||||
)
|
||||
|
||||
const output = "repository.gob"
|
||||
|
||||
func main() {
|
||||
f, err := os.Create(output)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to create file:", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := gob.NewEncoder(f).Encode(repository.Main); err != nil {
|
||||
os.Remove(output)
|
||||
log.Fatalln("Failed to gob encode:", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package cchat
|
||||
|
||||
//go:generate go run ./cmd/internal/cchat-generator ./
|
||||
//go:generate go run ./cmd/internal/cchat-empty-gen ./utils/empty/
|
||||
|
||||
type authenticateError struct{ error }
|
||||
|
||||
func (authenticateError) NextStage() []Authenticator { return nil }
|
||||
|
||||
// WrapAuthenticateError wraps the given error to become an AuthenticateError.
|
||||
// Its NextStage method returns nil. If the given err is nil, then nil is
|
||||
// returned.
|
||||
func WrapAuthenticateError(err error) AuthenticateError {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return authenticateError{err}
|
||||
}
|
6
go.mod
6
go.mod
|
@ -1,3 +1,9 @@
|
|||
module github.com/diamondburned/cchat
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/dave/jennifer v1.4.1
|
||||
github.com/go-test/deep v1.0.7
|
||||
github.com/pkg/errors v0.9.1
|
||||
)
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
github.com/dave/jennifer v1.4.1 h1:XyqG6cn5RQsTj3qlWQTKlRGAyrTcsk1kUmWdZBzRjDw=
|
||||
github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA=
|
||||
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
|
||||
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
@ -0,0 +1,112 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/doc"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// commentTrimSurrounding is a regex to trim surrounding new line and tabs.
|
||||
// This is needed to find the correct level of indentation.
|
||||
commentTrimSurrounding = regexp.MustCompile(`(^\n)|(\n\t+$)`)
|
||||
)
|
||||
|
||||
// TabWidth is used to format comments.
|
||||
var TabWidth = 4
|
||||
|
||||
// Comment represents a raw comment string. Most use cases should use GoString()
|
||||
// to get the comment's content.
|
||||
type Comment struct {
|
||||
Raw string
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the comment is empty.
|
||||
func (c Comment) IsEmpty() bool {
|
||||
return c.Raw == ""
|
||||
}
|
||||
|
||||
// GoString formats the documentation string in 80 columns wide paragraphs and
|
||||
// prefix each line with "// ". The ident argument controls the nested level. If
|
||||
// less than or equal to zero, then it is changed to 1, which is the top level.
|
||||
func (c Comment) GoString(ident int) string {
|
||||
if c.Raw == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if ident < 1 {
|
||||
ident = 1
|
||||
}
|
||||
|
||||
ident-- // 0th-indexed
|
||||
ident *= TabWidth
|
||||
|
||||
var lines = strings.Split(c.WrapText(80-len("// ")-ident), "\n")
|
||||
for i, line := range lines {
|
||||
if line != "" {
|
||||
line = "// " + line
|
||||
} else {
|
||||
line = "//"
|
||||
}
|
||||
lines[i] = line
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// WrapText wraps the raw text in n columns wide paragraphs.
|
||||
func (c Comment) WrapText(column int) string {
|
||||
var txt = c.Unindent()
|
||||
if txt == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
doc.ToText(&buf, txt, "", strings.Repeat(" ", TabWidth-1), column)
|
||||
|
||||
text := strings.TrimRight(buf.String(), "\n")
|
||||
text = strings.Replace(text, "\t", strings.Repeat(" ", TabWidth), -1)
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// Unindent removes the indentations that were there for the sake of syntax in
|
||||
// RawText. It gets the lowest indentation level from each line and trim it.
|
||||
func (c Comment) Unindent() string {
|
||||
if c.IsEmpty() {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Trim new lines.
|
||||
txt := commentTrimSurrounding.ReplaceAllString(c.Raw, "")
|
||||
|
||||
// Split the lines and rejoin them later to trim the indentation.
|
||||
var lines = strings.Split(txt, "\n")
|
||||
var indent = 0
|
||||
|
||||
// Get the minimum indentation count.
|
||||
for _, line := range lines {
|
||||
linedent := strings.Count(line, "\t")
|
||||
if linedent < 0 {
|
||||
continue
|
||||
}
|
||||
if linedent < indent || indent == 0 {
|
||||
indent = linedent
|
||||
}
|
||||
}
|
||||
|
||||
// Trim the indentation.
|
||||
if indent > 0 {
|
||||
for i, line := range lines {
|
||||
if len(line) > 0 {
|
||||
lines[i] = line[indent-1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rejoin.
|
||||
txt = strings.Join(lines, "\n")
|
||||
|
||||
return txt
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
const _goComment = `
|
||||
// The authenticator interface allows for a multistage initial authentication
|
||||
// API that the backend could use. Multistage is done by calling Authenticate
|
||||
// and check for AuthenticateError's NextStage method.`
|
||||
|
||||
// Trim away the prefix new line.
|
||||
var goComment = _goComment[1:]
|
||||
|
||||
func TestComment(t *testing.T) {
|
||||
var authenticator = Main[RootPath].Interface("Authenticator")
|
||||
|
||||
t.Run("godoc", func(t *testing.T) {
|
||||
godoc := authenticator.Comment.GoString(0)
|
||||
|
||||
if eq := deep.Equal(goComment, godoc); eq != nil {
|
||||
t.Fatal("go comment inequality:", eq)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package repository
|
||||
|
||||
// Enumeration returns a Go enumeration.
|
||||
type Enumeration struct {
|
||||
Comment Comment
|
||||
Name string
|
||||
Values []EnumValue
|
||||
|
||||
// Bitwise is true if the enumeration is a bitwise one. The type would then
|
||||
// be uint32 instead of uint8, allowing for 32 constants. As usual, the
|
||||
// first value of enum must be 0.
|
||||
Bitwise bool
|
||||
}
|
||||
|
||||
// GoType returns uint8 for a normal enum and uint32 for a bitwise enum. It
|
||||
// returns an empty string if the length of values is overbound.
|
||||
//
|
||||
// The maximum number of values in a normal enum is math.MaxUint8 or 255. The
|
||||
// maximum number of values in a bitwise enum is 32 for 32 bits in a uint32.
|
||||
func (e Enumeration) GoType() string {
|
||||
if !e.Bitwise {
|
||||
if len(e.Values) > 255 {
|
||||
return ""
|
||||
}
|
||||
return "uint8"
|
||||
}
|
||||
|
||||
if len(e.Values) > 32 {
|
||||
return ""
|
||||
}
|
||||
return "uint32"
|
||||
}
|
||||
|
||||
type EnumValue struct {
|
||||
Comment Comment
|
||||
Name string // also return value from String()
|
||||
}
|
||||
|
||||
// IsPlaceholder returns true if the enumeration value is meant to be a
|
||||
// placeholder. In Go, it would look like this:
|
||||
//
|
||||
// const (
|
||||
// _ EnumType = iota // IsPlaceholder() == true
|
||||
// V1
|
||||
// )
|
||||
//
|
||||
func (v EnumValue) IsPlaceholder() bool {
|
||||
return v.Name == ""
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package gob
|
||||
|
||||
//go:generate go run ../../cmd/internal/cchat-gob-gen
|
Binary file not shown.
|
@ -0,0 +1,168 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register(ContainerMethod{})
|
||||
gob.Register(AsserterMethod{})
|
||||
gob.Register(GetterMethod{})
|
||||
gob.Register(SetterMethod{})
|
||||
gob.Register(ContainerUpdaterMethod{})
|
||||
gob.Register(IOMethod{})
|
||||
}
|
||||
|
||||
type Interface struct {
|
||||
Comment Comment
|
||||
Name string
|
||||
Embeds []EmbeddedInterface
|
||||
Methods []Method // actual methods
|
||||
}
|
||||
|
||||
type EmbeddedInterface struct {
|
||||
Comment Comment
|
||||
InterfaceName string
|
||||
}
|
||||
|
||||
// IsContainer returns true if the interface is a frontend container interface,
|
||||
// that is when its name has "Container" at the end.
|
||||
func (i Interface) IsContainer() bool {
|
||||
return strings.HasSuffix(i.Name, "Container")
|
||||
}
|
||||
|
||||
type Method interface {
|
||||
UnderlyingName() string
|
||||
UnderlyingComment() Comment
|
||||
internalMethod()
|
||||
}
|
||||
|
||||
type method struct {
|
||||
Comment Comment
|
||||
Name string
|
||||
}
|
||||
|
||||
func (m method) UnderlyingName() string { return m.Name }
|
||||
func (m method) UnderlyingComment() Comment { return m.Comment }
|
||||
func (m method) internalMethod() {}
|
||||
|
||||
// GetterMethod is a method that returns a regular value. These methods must not
|
||||
// do any IO. An example of one would be ID() returning ID.
|
||||
type GetterMethod struct {
|
||||
method
|
||||
|
||||
// Parameters is the list of parameters in the function.
|
||||
Parameters []NamedType
|
||||
// Returns is the list of named types returned from the function.
|
||||
Returns []NamedType
|
||||
// ErrorType is non-empty if the function returns an error at the end of
|
||||
// returns. For the most part, this field should be "error" if that is the
|
||||
// case, but some methods may choose to extend the error base type.
|
||||
ErrorType string
|
||||
}
|
||||
|
||||
// ReturnError returns true if the method can error out.
|
||||
func (m GetterMethod) ReturnError() bool {
|
||||
return m.ErrorType != ""
|
||||
}
|
||||
|
||||
// SetterMethod is a method that sets values. These methods must not do IO, and
|
||||
// they have to be non-blocking.
|
||||
type SetterMethod struct {
|
||||
method
|
||||
|
||||
// Parameters is the list of parameters in the function. These parameters
|
||||
// should be the parameters to set.
|
||||
Parameters []NamedType
|
||||
// ErrorType is non-empty if the function returns an error at the end of
|
||||
// returns. An error may be returned from the backend if the input is
|
||||
// invalid, but it must not do IO. Frontend setters must never error.
|
||||
ErrorType string
|
||||
}
|
||||
|
||||
// ContainerUpdaterMethod is a SetterMethod that passes to the container the
|
||||
// current context to prevent race conditions when synchronizing.
|
||||
// The rule of thumb is that any setter method done inside a method with a
|
||||
// context is usually this type of method.
|
||||
type ContainerUpdaterMethod struct {
|
||||
method
|
||||
|
||||
// Parameters is the list of parameters in the function. These parameters
|
||||
// should be the parameters to set.
|
||||
Parameters []NamedType
|
||||
// ErrorType is non-empty if the function returns an error at the end of
|
||||
// returns. An error may be returned from the backend if the input is
|
||||
// invalid, but it must not do IO. Frontend setters must never error.
|
||||
ErrorType string
|
||||
}
|
||||
|
||||
// IOMethod is a regular method that can do IO and thus is blocking. These
|
||||
// methods usually always return errors. IOMethods must always have means of
|
||||
// cancelling them in the API, but implementations don't have to use it; as
|
||||
// such, the user should always have a timeout to gracefully wait.
|
||||
type IOMethod struct {
|
||||
method
|
||||
|
||||
// Parameters is the list of parameters in the function.
|
||||
Parameters []NamedType
|
||||
// ReturnValue is the return value in the function.
|
||||
ReturnValue NamedType
|
||||
// ErrorType is non-empty if the function returns an error at the end of
|
||||
// returns. For the most part, this field should be "error" if that is the
|
||||
// case, but some methods may choose to extend the error base type.
|
||||
ErrorType string
|
||||
// Disposer indicates that this method signals the disposal of the interface
|
||||
// that implements it. This is used similarly to stop functions, except all
|
||||
// disposer functions can be synchronous, and the frontend should handle
|
||||
// indicating such. The frontend can also ignore the result and run the
|
||||
// method in a dangling goroutine, but it must gracefully wait for it to be
|
||||
// done on exit.
|
||||
//
|
||||
// Similarly to the stop function, the instance that the disposer method belongs
|
||||
// to will also be considered invalid and should be freed once the function
|
||||
// returns regardless of the error.
|
||||
Disposer bool
|
||||
}
|
||||
|
||||
// ReturnError returns true if the method can error out.
|
||||
func (m IOMethod) ReturnError() bool {
|
||||
return m.ErrorType != ""
|
||||
}
|
||||
|
||||
// ContainerMethod is a method that uses a Container. These methods can do IO
|
||||
// and always return a stop callback and an error.
|
||||
type ContainerMethod struct {
|
||||
method
|
||||
|
||||
// HasContext is true if the method accepts a context as its first argument.
|
||||
HasContext bool
|
||||
// ContainerType is the name of the container interface. The name will
|
||||
// almost always have "Container" as its suffix.
|
||||
ContainerType string
|
||||
}
|
||||
|
||||
// Qual returns what TypeQual returns with m.ContainerType.
|
||||
func (m ContainerMethod) Qual() (path, name string) {
|
||||
return TypeQual(m.ContainerType)
|
||||
}
|
||||
|
||||
// AsserterMethod is a method that allows the parent interface to extend itself
|
||||
// into children interfaces. These methods must not do IO.
|
||||
type AsserterMethod struct {
|
||||
// ChildType is the children type that is returned.
|
||||
ChildType string
|
||||
}
|
||||
|
||||
func (m AsserterMethod) internalMethod() {}
|
||||
func (m AsserterMethod) UnderlyingComment() Comment { return Comment{} }
|
||||
|
||||
// UnderlyingName returns the name of the method.
|
||||
func (m AsserterMethod) UnderlyingName() string {
|
||||
return "As" + m.ChildType
|
||||
}
|
||||
|
||||
// Qual returns what TypeQual returns with m.ChildType.
|
||||
func (m AsserterMethod) Qual() (path, name string) {
|
||||
return TypeQual(m.ChildType)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,29 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
func TestGob(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
if err := gob.NewEncoder(&buf).Encode(Main); err != nil {
|
||||
t.Fatal("Failed to gob encode:", err)
|
||||
}
|
||||
|
||||
t.Log("Marshaled; total bytes:", buf.Len())
|
||||
|
||||
var unmarshaled Packages
|
||||
|
||||
if err := gob.NewDecoder(&buf).Decode(&unmarshaled); err != nil {
|
||||
t.Fatal("Failed to gob decode:", err)
|
||||
}
|
||||
|
||||
if eq := deep.Equal(Main, unmarshaled); eq != nil {
|
||||
t.Fatal("Inequalities after unmarshaling:", eq)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MakePath returns RootPath joined with relPath.
|
||||
func MakePath(relPath string) string {
|
||||
return path.Join(RootPath, relPath)
|
||||
}
|
||||
|
||||
// MakeQual returns a qualified identifier that is the full path and name of
|
||||
// something.
|
||||
func MakeQual(relPath, name string) string {
|
||||
return fmt.Sprintf("(%s).%s", MakePath(relPath), name)
|
||||
}
|
||||
|
||||
// TrimRoot trims the root path and returns the path relative to root.
|
||||
func TrimRoot(fullPath string) string {
|
||||
return strings.TrimPrefix(strings.TrimPrefix(fullPath, RootPath), "/")
|
||||
}
|
||||
|
||||
// Packages maps Go module paths to packages.
|
||||
type Packages map[string]Package
|
||||
|
||||
type Package struct {
|
||||
Comment Comment
|
||||
|
||||
Enums []Enumeration
|
||||
TypeAliases []TypeAlias
|
||||
Structs []Struct
|
||||
ErrorStructs []ErrorStruct
|
||||
Interfaces []Interface
|
||||
}
|
||||
|
||||
// Interface finds an interface. Nil is returned if none is found.
|
||||
func (p Package) Interface(name string) *Interface {
|
||||
for i, iface := range p.Interfaces {
|
||||
if iface.Name == name {
|
||||
return &p.Interfaces[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Struct finds a struct or an error struct. Nil is returned if none is found.
|
||||
func (p Package) Struct(name string) *Struct {
|
||||
for i, sstruct := range p.Structs {
|
||||
if sstruct.Name == name {
|
||||
return &p.Structs[i]
|
||||
}
|
||||
}
|
||||
for i, estruct := range p.ErrorStructs {
|
||||
if estruct.Name == name {
|
||||
return &p.ErrorStructs[i].Struct
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enum finds an enumeration. Nil is returned if none is found.
|
||||
func (p Package) Enum(name string) *Enumeration {
|
||||
for i, enum := range p.Enums {
|
||||
if enum.Name == name {
|
||||
return &p.Enums[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TypeAlias finds a type alias. Nil is returned if none is found.
|
||||
func (p Package) TypeAlias(name string) *TypeAlias {
|
||||
for i, alias := range p.TypeAliases {
|
||||
if alias.Name == name {
|
||||
return &p.TypeAliases[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindType finds any type. Nil is returned if nothing is found; a pointer to
|
||||
// any of the type is returned if name is found.
|
||||
func (p Package) FindType(name string) interface{} {
|
||||
if iface := p.Interface(name); iface != nil {
|
||||
return iface
|
||||
}
|
||||
if sstr := p.Struct(name); sstr != nil {
|
||||
return sstr
|
||||
}
|
||||
if enum := p.Enum(name); enum != nil {
|
||||
return enum
|
||||
}
|
||||
if alias := p.TypeAlias(name); alias != nil {
|
||||
return alias
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NamedType is an optionally named value with a type.
|
||||
type NamedType struct {
|
||||
Name string // optional
|
||||
Type string // import/path.Type OR (import/path).Type
|
||||
}
|
||||
|
||||
// Qual splits the type name into the path and type name. Refer to TypeQual.
|
||||
func (t NamedType) Qual() (path, typeName string) {
|
||||
return TypeQual(t.Type)
|
||||
}
|
||||
|
||||
// IsZero is true if t.Type is empty.
|
||||
func (t NamedType) IsZero() bool {
|
||||
return t.Type == ""
|
||||
}
|
||||
|
||||
// TypeQual splits the type name into path and type name. It accepts inputs that
|
||||
// are similar to the example below:
|
||||
//
|
||||
// string
|
||||
// context.Context
|
||||
// github.com/diamondburned/cchat/text.Rich
|
||||
// (github.com/diamondburned/cchat/text).Rich
|
||||
//
|
||||
func TypeQual(typePath string) (path, typeName string) {
|
||||
parts := strings.Split(typePath, ".")
|
||||
if len(parts) > 1 {
|
||||
path = strings.Join(parts[:len(parts)-1], ".")
|
||||
path = strings.TrimPrefix(path, "(")
|
||||
path = strings.TrimSuffix(path, ")")
|
||||
typeName = parts[len(parts)-1]
|
||||
return
|
||||
}
|
||||
|
||||
typeName = typePath
|
||||
return
|
||||
}
|
||||
|
||||
// TmplString is a generation-time templated string. It is used for string
|
||||
// concatenation.
|
||||
//
|
||||
// Given the following TmplString:
|
||||
//
|
||||
// TmplString{Format: "Hello, %s", Fields: []string{"Foo()"}}
|
||||
//
|
||||
// The output should be the same as the output of
|
||||
//
|
||||
// fmt.Sprintf("Hello, %s", v.Foo())
|
||||
//
|
||||
type TmplString struct {
|
||||
Format string // printf format syntax
|
||||
Fields []string // list of struct fields
|
||||
}
|
||||
|
||||
// IsEmpty returns true if TmplString is zero.
|
||||
func (s TmplString) IsEmpty() bool {
|
||||
return s.Format == ""
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package repository
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTypeQual(t *testing.T) {
|
||||
type test struct {
|
||||
typePath string
|
||||
path string
|
||||
typ string
|
||||
}
|
||||
|
||||
var tests = []test{
|
||||
{"string", "", "string"},
|
||||
{"context.Context", "context", "Context"},
|
||||
{
|
||||
"github.com/diamondburned/cchat/text.Rich",
|
||||
"github.com/diamondburned/cchat/text", "Rich",
|
||||
},
|
||||
{
|
||||
"(github.com/diamondburned/cchat/text).Rich",
|
||||
"github.com/diamondburned/cchat/text", "Rich",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
path, typ := TypeQual(test.typePath)
|
||||
if path != test.path {
|
||||
t.Errorf("Unexpected path %q != %q", path, test.path)
|
||||
}
|
||||
if typ != test.typ {
|
||||
t.Errorf("Unexpected type %q != %q", typ, test.typ)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package repository
|
||||
|
||||
type Struct struct {
|
||||
Comment Comment
|
||||
Name string
|
||||
Fields []StructField
|
||||
Stringer Stringer // used for String()
|
||||
}
|
||||
|
||||
type StructField struct {
|
||||
Comment
|
||||
NamedType
|
||||
}
|
||||
|
||||
type Stringer struct {
|
||||
Comment
|
||||
TmplString
|
||||
}
|
||||
|
||||
func (s Stringer) IsEmpty() bool { return s.TmplString.IsEmpty() }
|
||||
|
||||
// ErrorStruct are structs that implement the "error" interface and starts with
|
||||
// "Err".
|
||||
type ErrorStruct struct {
|
||||
Struct
|
||||
ErrorString TmplString // used for Error()
|
||||
}
|
||||
|
||||
// Wrapped returns true if the error struct contains a field with the error
|
||||
// type.
|
||||
func (t ErrorStruct) Wrapped() (fieldName string) {
|
||||
for _, ret := range t.Struct.Fields {
|
||||
if ret.Type == "error" {
|
||||
return ret.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package repository
|
||||
|
||||
// TypeAlias represents a Go type alias.
|
||||
type TypeAlias struct {
|
||||
Comment Comment
|
||||
NamedType
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
// Package plugins provides a source for cchat services as Go plugins. This
|
||||
// package looks in UserConfigDir()/cchat/plugins/ by default.
|
||||
//
|
||||
// Usage
|
||||
//
|
||||
// The package can easily be used by just dash importing it:
|
||||
//
|
||||
// _ "github.com/diamondburned/cchat/services/plugins"
|
||||
//
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"plugin"
|
||||
|
||||
"github.com/diamondburned/cchat/services"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var pluginPath string
|
||||
|
||||
// SetPluginPath sets the plugin path before loading plugins. This only works
|
||||
// until LoadPlugins is called.
|
||||
func SetPluginPath(path string) {
|
||||
pluginPath = path
|
||||
}
|
||||
|
||||
// TryConfigPath returns a path to $cfgDir/suffix. cfgDir is from
|
||||
// os.UserConfigDir.
|
||||
func TryConfigPath(suffix ...string) (string, error) {
|
||||
d, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(append([]string{d}, suffix...)...), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
services.RegisterSource(loadPlugins)
|
||||
}
|
||||
|
||||
func loadPlugins() (errs []error) {
|
||||
if pluginPath == "" {
|
||||
p, err := TryConfigPath("cchat", "plugins")
|
||||
if err != nil {
|
||||
errs = []error{errors.Wrap(err, "Failed to get config path")}
|
||||
return
|
||||
}
|
||||
pluginPath = p
|
||||
}
|
||||
|
||||
d, err := ioutil.ReadDir(pluginPath)
|
||||
if err != nil {
|
||||
// If the directory does not exist, then make one and exit.
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(pluginPath, 0755); err != nil {
|
||||
errs = []error{errors.Wrap(err, "Failed to make plugins dir")}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
errs = []error{errors.Wrap(err, "Failed to read plugin path")}
|
||||
return
|
||||
}
|
||||
|
||||
for _, f := range d {
|
||||
// We only need the plugin to call its init() function.
|
||||
_, err := plugin.Open(filepath.Join(pluginPath, f.Name()))
|
||||
if err != nil {
|
||||
errs = append(errs, errors.Wrap(err, "Failed to open plugin"))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
// Package services provides a global repository of cchat services. It also
|
||||
// supports additional sources.
|
||||
//
|
||||
// Registering services
|
||||
//
|
||||
// To register a service, it's best to call RegisterService() in the package's
|
||||
// init(). This allows for dash imports:
|
||||
//
|
||||
// _ "git.sr.ht/~user/cchat-abc"
|
||||
//
|
||||
// Registering sources
|
||||
//
|
||||
// Sources are simply functions that manage other services. An example of this
|
||||
// would be the plugins package. Note that only packages that can error out on
|
||||
// load should do this. A package can call RegisterService() multiple times.
|
||||
//
|
||||
// For examples on using RegisterSource(), check the plugins package.
|
||||
package services
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
)
|
||||
|
||||
var services []cchat.Service
|
||||
|
||||
// RegisterService adds a service.
|
||||
func RegisterService(service ...cchat.Service) {
|
||||
services = append(services, service...)
|
||||
}
|
||||
|
||||
var sources []func() []error
|
||||
var sourceErrs []error
|
||||
var sourceOnce sync.Once
|
||||
|
||||
// RegisterSource adds a service source. Services are expected to call
|
||||
// RegisterService() on source().
|
||||
func RegisterSource(source func() []error) {
|
||||
sources = append(sources, source)
|
||||
}
|
||||
|
||||
// Get returns all services. It will also fetch the plugins from all sources.
|
||||
// Future calls will not fetch the plugins again.
|
||||
func Get() ([]cchat.Service, []error) {
|
||||
sourceOnce.Do(func() {
|
||||
sourceErrs = []error{} // mark as non-nil
|
||||
for _, src := range sources {
|
||||
sourceErrs = append(sourceErrs, src()...)
|
||||
}
|
||||
})
|
||||
|
||||
// why are we here, just to suffer
|
||||
return services, sourceErrs
|
||||
}
|
208
text/text.go
208
text/text.go
|
@ -1,41 +1,53 @@
|
|||
// Code generated by ./cmd/internal. DO NOT EDIT.
|
||||
|
||||
// Package text provides a rich text API for cchat interfaces to use.
|
||||
//
|
||||
//
|
||||
// Asserting
|
||||
//
|
||||
// Although interfaces here contain asserter methods similarly to cchat, the
|
||||
// backend should take care to not implement multiple interfaces that may seem
|
||||
// conflicting. For example, if Avatarer is already implemented, then Imager
|
||||
// shouldn't be.
|
||||
package text
|
||||
|
||||
// Attribute is the type for basic rich text markup attributes.
|
||||
type Attribute uint32
|
||||
|
||||
const (
|
||||
// Normal is a zero-value attribute.
|
||||
AttributeNormal Attribute = 0
|
||||
// Bold represents bold text.
|
||||
AttributeBold Attribute = 1 << iota
|
||||
// Italics represents italicized text.
|
||||
AttributeItalics
|
||||
// Underline represents underlined text.
|
||||
AttributeUnderline
|
||||
// Strikethrough represents struckthrough text.
|
||||
AttributeStrikethrough
|
||||
// Spoiler represents spoiler text, which usually looks blacked out until
|
||||
// hovered or clicked on.
|
||||
AttributeSpoiler
|
||||
// Monospace represents monospaced text, typically for inline code.
|
||||
AttributeMonospace
|
||||
// Dimmed represents dimmed text, typically slightly less visible than other
|
||||
// text.
|
||||
AttributeDimmed
|
||||
)
|
||||
|
||||
func (a Attribute) Has(has Attribute) bool {
|
||||
return a&has == has
|
||||
}
|
||||
|
||||
// Rich is a normal text wrapped with optional format segments.
|
||||
type Rich struct {
|
||||
Content string
|
||||
// Segments are optional rich-text segment markers.
|
||||
Content string
|
||||
Segments []Segment
|
||||
}
|
||||
|
||||
func (r Rich) Empty() bool {
|
||||
return r.Content == ""
|
||||
}
|
||||
|
||||
// Segment is the minimum requirement for a format segment. Frontends will use
|
||||
// this to determine when the format starts and ends. They will also assert this
|
||||
// interface to any other formatting interface, including Linker, Colorer and
|
||||
// Attributor.
|
||||
type Segment interface {
|
||||
Bounds() (start, end int)
|
||||
}
|
||||
|
||||
// Linker is a hyperlink format that a segment could implement. This implies
|
||||
// that the segment should be replaced with a hyperlink, similarly to the anchor
|
||||
// tag with href being the URL and the inner text being the text string.
|
||||
type Linker interface {
|
||||
Link() (text, url string)
|
||||
}
|
||||
|
||||
// Imager implies the segment should be replaced with a (possibly inlined)
|
||||
// image.
|
||||
type Imager interface {
|
||||
Image() (url string)
|
||||
}
|
||||
|
||||
// Colorer is a text color format that a segment could implement. This is to be
|
||||
// applied directly onto the text.
|
||||
type Colorer interface {
|
||||
Color() uint16
|
||||
// String returns the Content in plain text.
|
||||
func (r Rich) String() string {
|
||||
return r.Content
|
||||
}
|
||||
|
||||
// Attributor is a rich text markup format that a segment could implement. This
|
||||
|
@ -44,15 +56,127 @@ type Attributor interface {
|
|||
Attribute() Attribute
|
||||
}
|
||||
|
||||
// Attribute is the type for basic rich text markup attributes.
|
||||
type Attribute uint16
|
||||
// Avatarer implies the segment should be replaced with a rounded-corners image.
|
||||
// This works similarly to Imager.
|
||||
//
|
||||
// For segments that also implement mentioner, the image should be treated as a
|
||||
// round avatar.
|
||||
type Avatarer interface {
|
||||
// AvatarText returns the underlying text of the image. Frontends could use this
|
||||
// for hovering or displaying the text instead of the image.
|
||||
AvatarText() string
|
||||
// AvatarSize returns the requested dimension for the image. This function could
|
||||
// return (0, 0), which the frontend should use the avatar's dimensions.
|
||||
AvatarSize() (size int)
|
||||
// Avatar returns the URL for the image.
|
||||
Avatar() (url string)
|
||||
}
|
||||
|
||||
const (
|
||||
AttrBold Attribute = 1 << iota
|
||||
AttrItalics
|
||||
AttrUnderline
|
||||
AttrStrikethrough
|
||||
AttrSpoiler
|
||||
AttrMonospace
|
||||
AttrQuoted
|
||||
)
|
||||
// Codeblocker is a codeblock that supports optional syntax highlighting using
|
||||
// the language given. Note that as this is a block, it will appear separately
|
||||
// from the rest of the paragraph.
|
||||
//
|
||||
// This interface is equivalent to Markdown's codeblock syntax.
|
||||
type Codeblocker interface {
|
||||
CodeblockLanguage() (language string)
|
||||
}
|
||||
|
||||
// Colorer is a text color format that a segment could implement. This is to be
|
||||
// applied directly onto the text.
|
||||
//
|
||||
// The Color method must return a valid 32-bit RGBA color. That is, if the text
|
||||
// color is solid, then the alpha value must be 0xFF. Frontends that support
|
||||
// 32-bit colors must render alpha accordingly without any edge cases.
|
||||
type Colorer interface {
|
||||
// Color returns a 32-bit RGBA color.
|
||||
Color() uint32
|
||||
}
|
||||
|
||||
// Imager implies the segment should be replaced with a (possibly inlined)
|
||||
// image.
|
||||
//
|
||||
// The Imager segment must return a bound of length zero, that is, the start and
|
||||
// end bounds must be the same, unless the Imager segment covers something
|
||||
// meaningful, as images must not substitute texts and only complement them.
|
||||
//
|
||||
// An example of the start and end bounds being the same would be any inline
|
||||
// image, and an Imager that belongs to a Mentioner segment should have its
|
||||
// bounds overlap. Normally, implementations with separated Mentioner and Imager
|
||||
// implementations don't have to bother about this, since with Mentioner, the
|
||||
// same Bounds will be shared, and with Imager, the Bounds method can easily
|
||||
// return the same variable for start and end.
|
||||
//
|
||||
// For segments that also implement mentioner, the image should be treated as a
|
||||
// square avatar.
|
||||
type Imager interface {
|
||||
// ImageText returns the underlying text of the image. Frontends could use this
|
||||
// for hovering or displaying the text instead of the image.
|
||||
ImageText() string
|
||||
// ImageSize returns the requested dimension for the image. This function could
|
||||
// return (0, 0), which the frontend should use the image's dimensions.
|
||||
ImageSize() (w int, h int)
|
||||
// Image returns the URL for the image.
|
||||
Image() (url string)
|
||||
}
|
||||
|
||||
// Linker is a hyperlink format that a segment could implement. This implies
|
||||
// that the segment should be replaced with a hyperlink, similarly to the anchor
|
||||
// tag with href being the URL and the inner text being the text string.
|
||||
type Linker interface {
|
||||
Link() (url string)
|
||||
}
|
||||
|
||||
// Mentioner implies that the segment can be clickable, and when clicked it
|
||||
// should open up a dialog containing information from MentionInfo().
|
||||
//
|
||||
// It is worth mentioning that frontends should assume whatever segment that
|
||||
// Mentioner highlighted to be the display name of that user. This would allow
|
||||
// frontends to flexibly layout the labels.
|
||||
type Mentioner interface {
|
||||
// MentionInfo returns the popup information of the mentioned segment. This is
|
||||
// typically user information or something similar to that context.
|
||||
MentionInfo() Rich
|
||||
}
|
||||
|
||||
// MessageReferencer is similar to Linker, except it references a message
|
||||
// instead of an arbitrary URL. As such, its appearance may be formatted
|
||||
// similarly to a link, but this is up to the frontend to decide. When clicked,
|
||||
// the frontend should scroll to the message with the ID returned by MessageID()
|
||||
// and highlight it, though this is also for appearance, so the frontend may
|
||||
// decide in detail how to display it.
|
||||
type MessageReferencer interface {
|
||||
MessageID() string
|
||||
}
|
||||
|
||||
// Quoteblocker represents a quoteblock that behaves similarly to the blockquote
|
||||
// HTML tag. The quoteblock may be represented typically by an actaul quoteblock
|
||||
// or with green arrows prepended to each line.
|
||||
type Quoteblocker interface {
|
||||
// QuotePrefix returns the prefix that every line the segment covers have. This
|
||||
// is typically the greater-than sign ">" in Markdown. Frontends could use this
|
||||
// information to format the quote properly.
|
||||
QuotePrefix() (prefix string)
|
||||
}
|
||||
|
||||
// Segment is the minimum requirement for a format segment. Frontends will use
|
||||
// this to determine when the format starts and ends. They will also assert this
|
||||
// interface to any other formatting interface, including Linker, Colorer and
|
||||
// Attributor.
|
||||
//
|
||||
// Note that a segment may implement multiple interfaces. For example, a
|
||||
// Mentioner may also implement Colorer.
|
||||
type Segment interface {
|
||||
Bounds() (start int, end int)
|
||||
|
||||
// Asserters.
|
||||
|
||||
AsColorer() Colorer // Optional
|
||||
AsLinker() Linker // Optional
|
||||
AsImager() Imager // Optional
|
||||
AsAvatarer() Avatarer // Optional
|
||||
AsMentioner() Mentioner // Optional
|
||||
AsAttributor() Attributor // Optional
|
||||
AsCodeblocker() Codeblocker // Optional
|
||||
AsQuoteblocker() Quoteblocker // Optional
|
||||
AsMessageReferencer() MessageReferencer // Optional
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package text
|
||||
|
||||
// Plain creates a new text.Rich without any formatting segments.
|
||||
func Plain(text string) Rich {
|
||||
return Rich{Content: text}
|
||||
}
|
||||
|
||||
// SolidColor takes in a 24-bit RGB color and overrides the alpha bits with
|
||||
// 0xFF, making the color solid.
|
||||
func SolidColor(rgb uint32) uint32 {
|
||||
return (rgb << 8) | 0xFF
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the given rich segment's content is empty. Note that
|
||||
// a rich text is not necessarily empty if the content is empty, because there
|
||||
// may be images within the segments.
|
||||
func (r Rich) IsEmpty() bool {
|
||||
return r.Content == "" && len(r.Segments) == 0
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
// Code generated by ./cmd/internal. DO NOT EDIT.
|
||||
|
||||
// Package empty provides no-op asserter method implementations of interfaces in
|
||||
// cchat's root and text packages.
|
||||
package empty
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
// Service provides no-op asserters for cchat.Service.
|
||||
type Service struct{}
|
||||
|
||||
// AsConfigurator returns nil.
|
||||
func (Service) AsConfigurator() cchat.Configurator { return nil }
|
||||
|
||||
// AsSessionRestorer returns nil.
|
||||
func (Service) AsSessionRestorer() cchat.SessionRestorer { return nil }
|
||||
|
||||
// Session provides no-op asserters for cchat.Session.
|
||||
type Session struct{}
|
||||
|
||||
// AsCommander returns nil.
|
||||
func (Session) AsCommander() cchat.Commander { return nil }
|
||||
|
||||
// AsSessionSaver returns nil.
|
||||
func (Session) AsSessionSaver() cchat.SessionSaver { return nil }
|
||||
|
||||
// Commander provides no-op asserters for cchat.Commander.
|
||||
type Commander struct{}
|
||||
|
||||
// AsCompleter returns nil.
|
||||
func (Commander) AsCompleter() cchat.Completer { return nil }
|
||||
|
||||
// Server provides no-op asserters for cchat.Server.
|
||||
type Server struct{}
|
||||
|
||||
// AsLister returns nil.
|
||||
func (Server) AsLister() cchat.Lister { return nil }
|
||||
|
||||
// AsMessenger returns nil.
|
||||
func (Server) AsMessenger() cchat.Messenger { return nil }
|
||||
|
||||
// AsCommander returns nil.
|
||||
func (Server) AsCommander() cchat.Commander { return nil }
|
||||
|
||||
// AsConfigurator returns nil.
|
||||
func (Server) AsConfigurator() cchat.Configurator { return nil }
|
||||
|
||||
// Messenger provides no-op asserters for cchat.Messenger.
|
||||
type Messenger struct{}
|
||||
|
||||
// AsSender returns nil.
|
||||
func (Messenger) AsSender() cchat.Sender { return nil }
|
||||
|
||||
// AsEditor returns nil.
|
||||
func (Messenger) AsEditor() cchat.Editor { return nil }
|
||||
|
||||
// AsActioner returns nil.
|
||||
func (Messenger) AsActioner() cchat.Actioner { return nil }
|
||||
|
||||
// AsNicknamer returns nil.
|
||||
func (Messenger) AsNicknamer() cchat.Nicknamer { return nil }
|
||||
|
||||
// AsBacklogger returns nil.
|
||||
func (Messenger) AsBacklogger() cchat.Backlogger { return nil }
|
||||
|
||||
// AsMemberLister returns nil.
|
||||
func (Messenger) AsMemberLister() cchat.MemberLister { return nil }
|
||||
|
||||
// AsUnreadIndicator returns nil.
|
||||
func (Messenger) AsUnreadIndicator() cchat.UnreadIndicator { return nil }
|
||||
|
||||
// AsTypingIndicator returns nil.
|
||||
func (Messenger) AsTypingIndicator() cchat.TypingIndicator { return nil }
|
||||
|
||||
// Sender provides no-op asserters for cchat.Sender.
|
||||
type Sender struct{}
|
||||
|
||||
// AsCompleter returns nil.
|
||||
func (Sender) AsCompleter() cchat.Completer { return nil }
|
||||
|
||||
// MemberSection provides no-op asserters for cchat.MemberSection.
|
||||
type MemberSection struct{}
|
||||
|
||||
// AsMemberDynamicSection returns nil.
|
||||
func (MemberSection) AsMemberDynamicSection() cchat.MemberDynamicSection { return nil }
|
||||
|
||||
// SendableMessage provides no-op asserters for cchat.SendableMessage.
|
||||
type SendableMessage struct{}
|
||||
|
||||
// AsNoncer returns nil.
|
||||
func (SendableMessage) AsNoncer() cchat.Noncer { return nil }
|
||||
|
||||
// AsReplier returns nil.
|
||||
func (SendableMessage) AsReplier() cchat.Replier { return nil }
|
||||
|
||||
// AsAttacher returns nil.
|
||||
func (SendableMessage) AsAttacher() cchat.Attacher { return nil }
|
||||
|
||||
// TextSegment provides no-op asserters for cchat.TextSegment.
|
||||
type TextSegment struct{}
|
||||
|
||||
// AsColorer returns nil.
|
||||
func (TextSegment) AsColorer() text.Colorer { return nil }
|
||||
|
||||
// AsLinker returns nil.
|
||||
func (TextSegment) AsLinker() text.Linker { return nil }
|
||||
|
||||
// AsImager returns nil.
|
||||
func (TextSegment) AsImager() text.Imager { return nil }
|
||||
|
||||
// AsAvatarer returns nil.
|
||||
func (TextSegment) AsAvatarer() text.Avatarer { return nil }
|
||||
|
||||
// AsMentioner returns nil.
|
||||
func (TextSegment) AsMentioner() text.Mentioner { return nil }
|
||||
|
||||
// AsAttributor returns nil.
|
||||
func (TextSegment) AsAttributor() text.Attributor { return nil }
|
||||
|
||||
// AsCodeblocker returns nil.
|
||||
func (TextSegment) AsCodeblocker() text.Codeblocker { return nil }
|
||||
|
||||
// AsQuoteblocker returns nil.
|
||||
func (TextSegment) AsQuoteblocker() text.Quoteblocker { return nil }
|
||||
|
||||
// AsMessageReferencer returns nil.
|
||||
func (TextSegment) AsMessageReferencer() text.MessageReferencer { return nil }
|
|
@ -0,0 +1,136 @@
|
|||
package split
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// The original shellwords implementation belongs to mattn. This version alters
|
||||
// some code along with some trivial optimizations.
|
||||
|
||||
// ArgsIndexed converts text into a shellwords-split list of strings. This
|
||||
// function roughly follows shell syntax, in that it works with a single space
|
||||
// character in bytes. This means that implementations could use bytes, as long
|
||||
// as it only works with characters within the ASCII range, assuming the source
|
||||
// text is in UTF-8 (which it should, per Go specifications).
|
||||
func ArgsIndexed(text string, offset int64) (args []string, argIndex int64) {
|
||||
// Quickly loop over everything to roughly count spaces. It doesn't have to
|
||||
// be accurate. This isn't very useful, to be honest.
|
||||
args = make([]string, 0, approxArgLen(text))
|
||||
|
||||
var escaped, doubleQuoted, singleQuoted bool
|
||||
argIndex = -1 // in case
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
buf.Grow(len(text))
|
||||
|
||||
got := false
|
||||
cursor := 0
|
||||
|
||||
for i, length := int64(0), int64(len(text)); i < length; i++ {
|
||||
r := text[i]
|
||||
if offset == i {
|
||||
argIndex = int64(len(args))
|
||||
}
|
||||
|
||||
switch {
|
||||
case escaped:
|
||||
got = true
|
||||
escaped = false
|
||||
|
||||
if doubleQuoted {
|
||||
switch r {
|
||||
case 'n':
|
||||
buf.WriteByte('\n')
|
||||
continue
|
||||
case 't':
|
||||
buf.WriteByte('\t')
|
||||
continue
|
||||
}
|
||||
}
|
||||
buf.WriteByte(r)
|
||||
continue
|
||||
|
||||
case isSpace(r):
|
||||
switch {
|
||||
case singleQuoted, doubleQuoted:
|
||||
buf.WriteByte(r)
|
||||
case got:
|
||||
cursor += buf.Len()
|
||||
args = append(args, buf.String())
|
||||
buf.Reset()
|
||||
got = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch r {
|
||||
case '\\':
|
||||
if singleQuoted {
|
||||
buf.WriteByte(r)
|
||||
} else {
|
||||
escaped = true
|
||||
}
|
||||
continue
|
||||
|
||||
case '"':
|
||||
if !singleQuoted {
|
||||
if doubleQuoted {
|
||||
got = true
|
||||
}
|
||||
doubleQuoted = !doubleQuoted
|
||||
continue
|
||||
}
|
||||
|
||||
case '\'':
|
||||
if !doubleQuoted {
|
||||
if singleQuoted {
|
||||
got = true
|
||||
}
|
||||
singleQuoted = !singleQuoted
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
got = true
|
||||
buf.WriteByte(r)
|
||||
}
|
||||
|
||||
if got || escaped || singleQuoted || doubleQuoted {
|
||||
if argIndex < 0 {
|
||||
argIndex = int64(len(args))
|
||||
}
|
||||
|
||||
args = append(args, buf.String())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// this is completely optional.
|
||||
func approxArgLen(text string) int {
|
||||
var arglen int
|
||||
var inside bool
|
||||
var escape bool
|
||||
for i := 0; i < len(text); i++ {
|
||||
switch b := text[i]; b {
|
||||
case '\\':
|
||||
escape = true
|
||||
continue
|
||||
case '\'', '"':
|
||||
if !escape {
|
||||
inside = !inside
|
||||
}
|
||||
default:
|
||||
if isSpace(b) && !inside {
|
||||
arglen++
|
||||
}
|
||||
}
|
||||
|
||||
if escape {
|
||||
escape = false
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate 1 more just in case.
|
||||
return arglen + 1
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package split
|
||||
|
||||
import "testing"
|
||||
|
||||
var argsSplitTests = []testEntry{
|
||||
{
|
||||
input: "bruhemus 'momentus lorem' \"ipsum\"",
|
||||
offset: 13, // ^
|
||||
output: []string{"bruhemus", "momentus lorem", "ipsum"},
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
input: "Yoohoo! My name\\'s Astolfo! I belong to the Rider-class! And, and... uhm, nice " +
|
||||
"to meet you!",
|
||||
offset: 37, // ^
|
||||
output: []string{
|
||||
"Yoohoo!", "My", "name's", "Astolfo!", "I", "belong", "to", "the", "Rider-class!",
|
||||
"And,", "and...", "uhm,", "nice", "to", "meet", "you!"},
|
||||
index: 6,
|
||||
},
|
||||
{
|
||||
input: "sorry, what were you typing?",
|
||||
offset: int64(len("sorry, what were you typing?")) - 1,
|
||||
output: []string{"sorry,", "what", "were", "you", "typing?"},
|
||||
index: 4,
|
||||
},
|
||||
{
|
||||
input: "zeroed out input",
|
||||
offset: 0,
|
||||
output: []string{"zeroed", "out", "input"},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
input: "に ほ ん ご",
|
||||
offset: 3,
|
||||
output: []string{"に ほ ん ご"},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
input: `echo "this \"quote\" is a regular test"`,
|
||||
offset: 5,
|
||||
output: []string{"echo", `this "quote" is a regular test`},
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
input: `echo "this \"quote\" is a regular test"`,
|
||||
offset: 4,
|
||||
output: []string{"echo", `this "quote" is a regular test`},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
input: `echo "`,
|
||||
offset: 6,
|
||||
output: []string{"echo", ""},
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
input: `info \n`,
|
||||
offset: 7,
|
||||
output: []string{"info", "n"},
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
input: `info "\n"`,
|
||||
offset: 7,
|
||||
output: []string{"info", "\n"},
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
input: `info '\nnot a new line!'`,
|
||||
offset: 15,
|
||||
output: []string{"info", `\nnot a new line!`},
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
input: `info \\n`,
|
||||
offset: 7,
|
||||
output: []string{"info", "\\n"},
|
||||
index: 1,
|
||||
},
|
||||
}
|
||||
|
||||
func TestArgsIndexed(t *testing.T) {
|
||||
for _, test := range argsSplitTests {
|
||||
a, j := ArgsIndexed(test.input, test.offset)
|
||||
test.compare(t, a, j)
|
||||
|
||||
if expect, got := approxArgLen(test.input), len(test.output); expect != got {
|
||||
t.Error("Approximated arg len is off, (expected/got)", expect, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const argsbenchstr = "Alright, Master! I\\'m your blade, your edge and your arrow! You\\'ve " +
|
||||
"placed so much trust in me, despite how weak I am - I\\'ll do everything in my power to not " +
|
||||
"disappoint you!"
|
||||
|
||||
func BenchmarkArgsIndexed(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ArgsIndexed(benchstr, benchcursor)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkArgsIndexedLong(b *testing.B) {
|
||||
const benchstr = benchstr + benchstr + benchstr + benchstr + benchstr + benchstr
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
ArgsIndexed(benchstr, benchcursor)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package split
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// These code to mostly be copied from package strings.
|
||||
|
||||
// SpaceIndexed returns a splitted string with the current index that
|
||||
// CompleteMessage wants. The text is the entire input string and the offset is
|
||||
// where the cursor currently is.
|
||||
func SpaceIndexed(text string, offset int64) ([]string, int64) {
|
||||
// First count the fields.
|
||||
n, hasRunes := countSpace(text)
|
||||
if hasRunes {
|
||||
// Some runes in the input string are not ASCII.
|
||||
return spaceIndexedRunes([]rune(text), offset)
|
||||
}
|
||||
|
||||
// ASCII fast path
|
||||
a := make([]string, n)
|
||||
na := int64(0)
|
||||
fieldStart := int64(0)
|
||||
i := int64(0)
|
||||
j := n - 1 // last by default
|
||||
|
||||
// Skip spaces in the front of the input.
|
||||
for i < int64(len(text)) && asciiSpace[text[i]] != 0 {
|
||||
i++
|
||||
}
|
||||
|
||||
fieldStart = i
|
||||
|
||||
for i < int64(len(text)) {
|
||||
if asciiSpace[text[i]] == 0 {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
a[na] = text[fieldStart:i]
|
||||
if fieldStart <= offset && offset <= i {
|
||||
j = na
|
||||
}
|
||||
|
||||
na++
|
||||
i++
|
||||
|
||||
// Skip spaces in between fields.
|
||||
for i < int64(len(text)) && asciiSpace[text[i]] != 0 {
|
||||
i++
|
||||
}
|
||||
fieldStart = i
|
||||
}
|
||||
if fieldStart < int64(len(text)) { // Last field might end at EOF.
|
||||
a[na] = text[fieldStart:]
|
||||
}
|
||||
|
||||
return a, j
|
||||
}
|
||||
|
||||
func spaceIndexedRunes(runes []rune, offset int64) ([]string, int64) {
|
||||
// A span is used to record a slice of s of the form s[start:end].
|
||||
// The start index is inclusive and the end index is exclusive.
|
||||
type span struct{ start, end int64 }
|
||||
|
||||
spans := make([]span, 0, 16)
|
||||
|
||||
// Find the field start and end indices.
|
||||
wasField := false
|
||||
fromIndex := int64(0)
|
||||
for i, rune := range runes {
|
||||
if unicode.IsSpace(rune) {
|
||||
if wasField {
|
||||
spans = append(spans, span{start: fromIndex, end: int64(i)})
|
||||
wasField = false
|
||||
}
|
||||
} else {
|
||||
if !wasField {
|
||||
fromIndex = int64(i)
|
||||
wasField = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last field might end at EOF.
|
||||
if wasField {
|
||||
spans = append(spans, span{fromIndex, int64(len(runes))})
|
||||
}
|
||||
|
||||
// Create strings from recorded field indices.
|
||||
a := make([]string, 0, len(spans))
|
||||
j := int64(len(spans)) - 1 // assume last
|
||||
|
||||
for i, span := range spans {
|
||||
a = append(a, string(runes[span.start:span.end]))
|
||||
|
||||
if span.start <= offset && offset <= span.end {
|
||||
j = int64(i)
|
||||
}
|
||||
}
|
||||
|
||||
return a, j
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package split
|
||||
|
||||
import "testing"
|
||||
|
||||
var spaceSplitTests = []testEntry{
|
||||
{
|
||||
input: "bruhemus momentus lorem ipsum",
|
||||
offset: 13, // ^
|
||||
output: []string{"bruhemus", "momentus", "lorem", "ipsum"},
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
input: "Yoohoo! My name's Astolfo! I belong to the Rider-class! And, and... uhm, nice " +
|
||||
"to meet you!",
|
||||
offset: 37, // ^
|
||||
output: []string{
|
||||
"Yoohoo!", "My", "name's", "Astolfo!", "I", "belong", "to", "the", "Rider-class!",
|
||||
"And,", "and...", "uhm,", "nice", "to", "meet", "you!"},
|
||||
index: 6,
|
||||
},
|
||||
{
|
||||
input: "sorry, what were you typing?",
|
||||
offset: int64(len("sorry, what were you typing?")) - 1,
|
||||
output: []string{"sorry,", "what", "were", "you", "typing?"},
|
||||
index: 4,
|
||||
},
|
||||
{
|
||||
input: "zeroed out input",
|
||||
offset: 0,
|
||||
output: []string{"zeroed", "out", "input"},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
input: "に ほ ん ご",
|
||||
offset: 3,
|
||||
output: []string{"に", "ほ", "ん", "ご"},
|
||||
index: 1,
|
||||
},
|
||||
}
|
||||
|
||||
func TestSpaceIndexed(t *testing.T) {
|
||||
for _, test := range spaceSplitTests {
|
||||
a, j := SpaceIndexed(test.input, test.offset)
|
||||
test.compare(t, a, j)
|
||||
}
|
||||
}
|
||||
|
||||
const benchstr = "Alright, Master! I\\'m your blade, your edge and your arrow! You\\'ve placed " +
|
||||
"so much trust in me, despite how weak I am - I\\'ll do everything in my power to not " +
|
||||
"disappoint you!"
|
||||
const benchcursor = 32 // arbitrary
|
||||
|
||||
func BenchmarkSpaceIndexed(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
SpaceIndexed(benchstr, benchcursor)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSpaceIndexedLong(b *testing.B) {
|
||||
const benchstr = benchstr + benchstr + benchstr + benchstr + benchstr + benchstr
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
SpaceIndexed(benchstr, benchcursor)
|
||||
}
|
||||
}
|
||||
|
||||
// same as benchstr but w/ a horizontal line (outside ascii)
|
||||
const benchstr8 = "Alright, Master! I'm your blade, your edge and your arrow! You've placed " +
|
||||
"so much trust in me, despite how weak I am ― I'll do everything in my power to not " +
|
||||
"disappoint you!"
|
||||
|
||||
func BenchmarkSpaceIndexedUTF8(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
SpaceIndexed(benchstr8, benchcursor)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// Package split provides a simple string splitting utility for use with
|
||||
// CompleteMessage.
|
||||
package split
|
||||
|
||||
import "unicode/utf8"
|
||||
|
||||
// SplitFunc is the type that describes the two splitter functions, SpaceIndexed
|
||||
// and ArgsIndexed.
|
||||
type SplitFunc = func(text string, offset int64) ([]string, int64)
|
||||
|
||||
var (
|
||||
_ SplitFunc = SpaceIndexed
|
||||
_ SplitFunc = ArgsIndexed
|
||||
)
|
||||
|
||||
// just helper functions here
|
||||
|
||||
var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1}
|
||||
|
||||
func isSpace(b byte) bool { return asciiSpace[b] == 1 }
|
||||
|
||||
func countSpace(text string) (n int64, hasRunes bool) {
|
||||
// This implementation to also be mostly copy-pasted from package strings.
|
||||
|
||||
// This is an exact count if s is ASCII, otherwise it is an approximation.
|
||||
// var n int64
|
||||
|
||||
wasSpace := int64(1)
|
||||
// setBits is used to track which bits are set in the bytes of s.
|
||||
setBits := uint8(0)
|
||||
for i := 0; i < len(text); i++ {
|
||||
r := text[i]
|
||||
setBits |= r
|
||||
isSpace := int64(asciiSpace[r])
|
||||
n += wasSpace & ^isSpace
|
||||
wasSpace = isSpace
|
||||
}
|
||||
|
||||
return n, setBits >= utf8.RuneSelf
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package split
|
||||
|
||||
import "testing"
|
||||
|
||||
type testEntry struct {
|
||||
input string
|
||||
offset int64
|
||||
output []string
|
||||
index int64
|
||||
}
|
||||
|
||||
func (test testEntry) compare(t *testing.T, words []string, index int64) {
|
||||
if !strsleq(words, test.output) {
|
||||
t.Error("Mismatch output (input/got/expected)", test.input, words, test.output)
|
||||
}
|
||||
if index != test.index {
|
||||
t.Error("Mismatch index (input/got/expected)", test.input, index, test.index)
|
||||
}
|
||||
}
|
||||
|
||||
// string slice equal
|
||||
func strsleq(s1, s2 []string) bool {
|
||||
if len(s1) != len(s2) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(s1); i++ {
|
||||
if s1[i] != s2[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
Loading…
Reference in New Issue