summaryrefslogtreecommitdiff
path: root/gopher/gophermap/extended.go
diff options
context:
space:
mode:
Diffstat (limited to 'gopher/gophermap/extended.go')
-rw-r--r--gopher/gophermap/extended.go220
1 files changed, 220 insertions, 0 deletions
diff --git a/gopher/gophermap/extended.go b/gopher/gophermap/extended.go
new file mode 100644
index 0000000..d9fedd0
--- /dev/null
+++ b/gopher/gophermap/extended.go
@@ -0,0 +1,220 @@
+package gophermap
+
+import (
+ "bufio"
+ "errors"
+ "io"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "tildegit.org/tjp/sliderule/gopher"
+ "tildegit.org/tjp/sliderule/internal/types"
+)
+
+// ExtendedMapDocument is a gophermap doc with gophernicus's extensions
+//
+// These are documented at: gopher://gopher.gophernicus.org/0/docs/README.Gophermap
+type ExtendedMapDocument struct {
+ Lines []gopher.MapItem
+ Location *url.URL
+}
+
+// ParseExtended parses a gophermap document including gophernicus extensions.
+func ParseExtended(input io.Reader, location *url.URL) (ExtendedMapDocument, error) {
+ rdr := bufio.NewReader(input)
+ doc := ExtendedMapDocument{Location: location}
+
+outer:
+ for num := 1; ; num += 1 {
+ line, err := rdr.ReadString('\n')
+ isEOF := errors.Is(err, io.EOF)
+ if err != nil && !isEOF {
+ return doc, err
+ }
+ line = strings.TrimRight(line, "\r\n")
+
+ if len(line) > 0 {
+ switch line[0] {
+
+ case '#':
+ doc.Lines = append(doc.Lines, gopher.MapItem{
+ Type: CommentType,
+ Display: strings.TrimPrefix(line[1:], " "),
+ })
+ continue outer
+ case '!':
+ doc.Lines = append(doc.Lines, gopher.MapItem{
+ Type: TitleType,
+ Display: line[1:],
+ })
+ continue outer
+ case '-':
+ doc.Lines = append(doc.Lines, gopher.MapItem{
+ Type: HiddenType,
+ Selector: line[1:],
+ })
+ continue outer
+ case ':':
+ doc.Lines = append(doc.Lines, gopher.MapItem{
+ Type: ExtensionType,
+ Display: line[1:],
+ })
+ continue outer
+ case '=':
+ doc.Lines = append(doc.Lines, gopher.MapItem{
+ Type: InclusionType,
+ Selector: line[1:],
+ })
+ continue outer
+ }
+ }
+
+ switch line {
+ case "~":
+ doc.Lines = append(doc.Lines, gopher.MapItem{Type: UserListType})
+ continue outer
+ case "%":
+ doc.Lines = append(doc.Lines, gopher.MapItem{Type: VHostListType})
+ continue outer
+ case ".":
+ doc.Lines = append(doc.Lines, gopher.MapItem{Type: EndDocType})
+ break outer
+ case "*":
+ doc.Lines = append(doc.Lines, gopher.MapItem{Type: DirListType})
+ break outer
+ }
+
+ if !strings.Contains(line, "\t") {
+ doc.Lines = append(doc.Lines, gopher.MapItem{
+ Type: gopher.InfoMessageType,
+ Display: line,
+ Selector: location.Path,
+ Hostname: location.Hostname(),
+ Port: location.Port(),
+ })
+ continue
+ }
+
+ item := gopher.MapItem{Type: types.Status(line[0])}
+
+ spl := strings.Split(line[1:], "\t")
+ if len(spl) != 4 {
+ return doc, InvalidLine(num)
+ }
+ item.Display = string(spl[0])
+ item.Selector = string(spl[1])
+ item.Hostname = string(spl[2])
+ item.Port = string(spl[3])
+ if _, err = strconv.Atoi(item.Port); err != nil {
+ return doc, InvalidLine(num)
+ }
+ doc.Lines = append(doc.Lines, item)
+
+ if isEOF {
+ break
+ }
+ }
+
+ return doc, nil
+}
+
+// Extensions to gopher types from Gophernicus.
+const (
+ // CommentType is omitted from generated compatible gophermaps.
+ CommentType types.Status = '#'
+
+ // TitleType defines the title of a gophermap document.
+ TitleType types.Status = '!'
+
+ // HiddenType hides a link from the generated compatible gophermap.
+ HiddenType types.Status = '-'
+
+ // ExtensionType defines the gopher type to use for files in the current directory with a given extension.
+ ExtensionType types.Status = ':'
+
+ // UserListType generates a list of users with valid ~/public_gopher directories.
+ UserListType types.Status = '~'
+
+ // VHostListType generates a listing of virtual hosts.
+ VHostListType types.Status = '%'
+
+ // InclusionType causes another gophermap to be included at this location.
+ InclusionType types.Status = '='
+
+ // DirListType stops parsing the current file and ends the generated gophermap with a listing of the current directory.
+ DirListType types.Status = '*'
+
+ // EndDocType ends the current gophermap file.
+ EndDocType types.Status = '.'
+)
+
+// Compatible builds a standards-compliant gophermap from the current extended menu.
+func (edoc ExtendedMapDocument) Compatible(cwd string) (gopher.MapDocument, string, error) {
+ doc := gopher.MapDocument{}
+
+ title := ""
+ hidden := map[string]struct{}{}
+ extensions := map[string]types.Status{}
+
+ for num, item := range edoc.Lines {
+ switch item.Type {
+ case CommentType:
+ case TitleType:
+ title = item.Display
+ case HiddenType:
+ hidden[item.Selector] = struct{}{}
+ case ExtensionType:
+ from, to, found := strings.Cut(item.Display, "=")
+ if !found {
+ return nil, "", InvalidLine(num)
+ }
+ extensions[from] = types.Status(to[0])
+ case UserListType: //TODO
+ return nil, "", errors.New("User listings '~' are not supported")
+ case VHostListType: //TODO
+ return nil, "", errors.New("Virtual host listings '%' are not supported")
+ case InclusionType:
+ location := filepath.Join(cwd, item.Selector)
+ subEdoc, err := openExtended(location, edoc.Location)
+ if err != nil {
+ return nil, "", err
+ }
+
+ lines, _, err := subEdoc.Compatible(filepath.Dir(location))
+ if err != nil {
+ return nil, "", err
+ }
+ doc = append(doc, lines...)
+ case DirListType:
+ dirlist, err := listDir(cwd, edoc.Location, hidden, extensions)
+ if err != nil {
+ return nil, "", err
+ }
+
+ doc = append(doc, dirlist...)
+
+ break
+ case EndDocType:
+ break
+ default:
+ doc = append(doc, item)
+ }
+ }
+
+ return doc, title, nil
+}
+
+func openExtended(path string, location *url.URL) (ExtendedMapDocument, error) {
+ file, err := os.Open(path)
+ if err != nil {
+ return ExtendedMapDocument{}, err
+ }
+ defer func() {
+ _ = file.Close()
+ }()
+
+ return ParseExtended(file, location)
+}