Initial commit of gemini tool

This commit is contained in:
sloum 2020-05-20 11:01:49 -07:00
commit 2dae0e8ed6
4 changed files with 390 additions and 0 deletions

BIN
gemini Executable file

Binary file not shown.

336
main.go Normal file
View File

@ -0,0 +1,336 @@
package main
import (
"crypto/tls"
"flag"
"fmt"
"io/ioutil"
"net/url"
"os"
"strings"
)
const (
SPEC_URL string = "gemini://gemini.circumlunar.space:1965/docs/spec-spec.txt"
VERSION string = "0.1"
WARNING int = iota
ERROR
)
type issue struct {
message string
context string
lineNumber int
errType int
}
var inPreBlock bool = false
var lineNumber int = 0
var issues []issue
func min(a, b int) int {
if a < b {
return a
}
return b
}
func isWhiteSpace(c rune) bool {
switch c {
case ' ', '\t':
return true
default:
return false
}
}
func firstIndexNotChar(ln string, ch rune) int {
for i, c := range ln {
if c != ch {
return i
}
}
return -1
}
// Makes sure only 3 heading levels are used, adds a space after the heading declaration, before the text
func heading(ln string) string {
ln = strings.TrimSpace(ln)
if strings.HasPrefix(ln, "####") {
length := min(len(ln), 50)
issues = append(issues, issue{"Too many heading levels", ln[:length], lineNumber, ERROR})
}
endHeading := firstIndexNotChar(ln, '#')
return fmt.Sprintf("%s %s", ln[:endHeading], strings.TrimSpace(ln[endHeading:]))
}
func link(ln string) string {
ln = strings.TrimSpace(ln)
if len(ln) < 3 {
issues = append(issues, issue{"Link initiated but not followed with a url", ln, lineNumber, ERROR})
}
linkln := strings.TrimSpace(ln[2:])
linkln = strings.Replace(linkln, "\t", " ", -1)
split := strings.SplitN(linkln, " ", 2)
if len(split) == 1 {
return fmt.Sprintf("=> %s", strings.TrimSpace(split[0]))
}
return fmt.Sprintf("=> %s %s", strings.TrimSpace(split[0]), strings.TrimSpace(split[1]))
}
// Makes sure there is one space between the list declaration and the text
func list(ln string) string {
ln = strings.TrimSpace(ln)
if len(ln) < 2 {
return ln
}
return fmt.Sprintf("* %s", strings.TrimSpace(ln[1:]))
}
// preBlockToggle toggles the preBlock state as well as providing proper trimming of extra space
// if text is provided with an opening ``` a space is put in between the ``` and the text
func preBlockToggle(ln string) string {
ln = strings.TrimSpace(ln)
inPreBlock = !inPreBlock
if len(ln) > 3 {
alt := strings.TrimSpace(ln[3:])
if len(alt) == 0 {
return "```"
}
// If there is alt text and we are at a closing ```:
if !inPreBlock {
length := min(len(ln), 50)
issues = append(issues, issue{"Alt text cannot be included when closing a block, put it with the opening ```", ln[:length], lineNumber, ERROR})
return ln
}
// If there is alt text and this is opening a ```:
return fmt.Sprintf("``` %s", strings.TrimSpace(ln[3:]))
}
return ln
}
// preText just removes excess space on the right hand side
func preText(ln string) string {
return strings.TrimRight(ln, " \t")
}
// nonPreText normalizes spacing by eliminating instances of multiple spaces or tabs in a row.
func nonPreText(ln string) string {
ln = strings.TrimSpace(ln)
var output strings.Builder
ws := false
for _, c := range ln {
if !isWhiteSpace(c) {
ws = false
} else if ws {
continue
} else {
ws = true
}
output.WriteRune(c)
}
return output.String()
}
func lineRouter(ln string) string {
if len(ln) == 0 {
return ln
}
// Handle special strings
if ln[0] == '#' {
return heading(ln)
} else if ln[0] == '*' {
return list(ln)
} else if strings.HasPrefix(ln, "```") {
return preBlockToggle(ln)
} else if strings.HasPrefix(ln, "=>") {
return link(ln)
}
// Handle text
if inPreBlock {
return preText(ln)
}
return nonPreText(ln)
}
func fmtFile(path string) (string, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
lines := strings.SplitN(string(b), "\n", -1)
for i, ln := range lines {
lines[i] = lineRouter(ln)
lineNumber = i
}
return strings.Join(lines, "\n"), nil
}
func printIssues() {
for _, issue := range issues {
color := ""
kind := "Error"
if issue.errType == ERROR {
color = "\033[41;97;1m"
} else {
kind = "Warning"
color = "\033[30;103;1m"
}
fmt.Fprintf(os.Stderr, "%s%s:\033[0m %s\n%sLine %d:\033[0m %s\n\n", color, kind, issue.message, color, issue.lineNumber, issue.context)
}
}
func LoadCertificate(cert, key string) (tls.Certificate, error) {
certificate, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
return tls.Certificate{}, err
}
return certificate, nil
}
func get(resource, cert, key string, headerOnly, currentSpec bool) (string, error) {
hasScheme := strings.Contains(resource, "://")
if !hasScheme {
resource = "gemini://" + resource
}
u, err := url.Parse(resource)
if err != nil {
return "", err
}
if u.Scheme != "gemini" {
return "", fmt.Errorf("URL scheme must be 'gemini'")
}
if u.Port() == "" {
u.Host = u.Host + ":1965"
}
conf := &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: true,
}
// TODO add client cert if available
conn, err := tls.Dial("tcp", u.Host, conf)
if err != nil {
return "", fmt.Errorf("TLS Dial Error: %s", err.Error())
}
defer conn.Close()
send := u.String() + "\r\n"
_, err = conn.Write([]byte(send))
if err != nil {
return "", err
}
result, err := ioutil.ReadAll(conn)
if err != nil {
return "", err
}
text := string(result)
endHeader := strings.Index(text, "\n")
if endHeader < 1 {
return "", fmt.Errorf("Invalid response from server")
}
if text[0] == '3' {
newUrl := strings.TrimSpace(text[3:endHeader])
return get(newUrl, cert, key, headerOnly, currentSpec)
}
head := strings.TrimSpace(text[:endHeader])
body := strings.TrimSpace(text[endHeader:])
if headerOnly {
return head, nil
}
if body == "" {
return "", fmt.Errorf("Error: Body was empty. Response header:\n%s\n", head)
}
return body, nil
}
func main() {
fmtCmd := flag.NewFlagSet("fmt", flag.ExitOnError)
fmtOut := fmtCmd.String("o", "", "Path to output file")
fmtSupress := fmtCmd.Bool("s", false, "Supress error messaging")
getCmd := flag.NewFlagSet("get", flag.ExitOnError)
getOut := getCmd.String("o", "", "Path to output file")
getMime := getCmd.Bool("header", false, "Only retrieve header")
getCert := getCmd.String("cert", "", "Path to certificate file")
getKey := getCmd.String("key", "", "Path to key file")
specCmd := flag.NewFlagSet("spec", flag.ExitOnError)
specOut := specCmd.String("o", "", "Path to output file")
if len(os.Args) < 2 {
fmt.Println("expected command: fmt, get, spec, or version")
os.Exit(1)
}
switch os.Args[1] {
case "fmt":
fmtCmd.Parse(os.Args[2:])
if len(fmtCmd.Args()) != 1 {
fmt.Printf("Incorrect number of positional arguments expected 1, got %d\n", len(fmtCmd.Args()))
os.Exit(1)
}
text, err := fmtFile(fmtCmd.Args()[0])
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
os.Exit(1)
}
if *fmtOut == "" {
fmt.Print(text)
fmt.Fprint(os.Stderr, "\n") // So that the terminal input is always on a new line, but this LF doesnt get piped into a document
} else {
// TODO handle file writing
}
if !*fmtSupress {
printIssues()
}
case "get":
getCmd.Parse(os.Args[2:])
if len(getCmd.Args()) != 1 {
fmt.Printf("Incorrect number of positional arguments expected 1, got %d\n", len(fmtCmd.Args()))
os.Exit(1)
}
text, err := get(getCmd.Args()[0], *getCert, *getKey, *getMime, false)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
os.Exit(1)
}
if *getOut == "" {
fmt.Print(text)
fmt.Print("\n")
} else {
// TODO handle file writing
}
case "spec":
specCmd.Parse(os.Args[2:])
if len(getCmd.Args()) != 0 {
fmt.Printf("Incorrect number of positional arguments expected 0, got %d\n", len(fmtCmd.Args()))
os.Exit(1)
}
text, err := get(SPEC_URL, "", "", false, true)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
}
if *specOut == "" {
fmt.Print(text)
fmt.Print("\n")
} else {
// TODO handle file writing
}
case "version":
fmt.Printf("Gemini Tool v%s\n", VERSION)
}
}

27
test.gmi Normal file
View File

@ -0,0 +1,27 @@
This is a test file to see how
well this formatter will work.
=>gemini://test.com A link
* List item 1
*List item 2
* List item 3
#Header 1
## Header 2
###Header 3
#### Header 4
```
This is a regular
code fence
``` Test
```Test
This is a fence
with an alt
```

27
test.txt Normal file
View File

@ -0,0 +1,27 @@
This is a test file to see how
well this formatter will work.
=> gemini://test.com A link
* List item 1
* List item 2
* List item 3
# Header 1
## Header 2
### Header 3
#### Header 4
```
This is a regular
code fence
```
``` Test
This is a fence
with an alt
```