package entity import "fmt" import "strings" import "unicode" import "unicode/utf8" import "path/filepath" import "github.com/google/uuid" import "git.tebibyte.media/fspl/fspl/errors" // Metadata represents a module metadata file. type Metadata struct { Pos errors.Position UUID uuid.UUID Dependencies []*Dependency } func (this *Metadata) Position () errors.Position { return this.Pos } func (this *Metadata) String () string { out := fmt.Sprint(Quote(this.UUID.String())) for _, dependency := range this.Dependencies { out += fmt.Sprint("\n", dependency) } return out } // Directive is a declaration within a module metadata file. type Directive interface { fmt.Stringer // Position returns the position of the directive within its metadata // file. Position () errors.Position directive() } var _ Directive = &Dependency { } // Dependency is a metadata dependency listing. type Dependency struct { Pos errors.Position Address Address Nickname string } func (*Dependency) directive () { } func (this *Dependency) Position () errors.Position { return this.Pos } func (this *Dependency) String () string { out := fmt.Sprint("+ ", this.Address) if this.Nickname != "" { out += fmt.Sprint(" ", this.Nickname) } return out } // Address is the address of a unit. type Address string func (addr Address) String () string { return Quote(string(addr)) } // SourceFile returns the FSPL source file associated with the address. If the // address does not point to an FSPL source file, it returns "", false. func (addr Address) SourceFile () (string, bool) { ext := filepath.Ext(string(addr)) if ext == ".fspl" { return filepath.Clean(string(addr)), true } else { return "", false } } // Module returns the module associated with the address. If the address does // does not point to a module, it returns "", false. func (addr Address) Module () (string, bool) { path := string(addr) switch { case filepath.Base(path) == "fspl.mod": return filepath.Dir(path), true case filepath.Ext(path) == "" || filepath.Ext(path) == ".": return filepath.Clean(path), true default: return "", false } } // Nickname automatically generates a nickname from the address, which is a // valid Ident token. On failure "", false is returned. The nickname is // generated according to the folowing rules: // // - If the name contains at least one dot, the last dot and everything after // it is removed // - All non-letter, non-numeric characters are removed, and any letters that // were directly after them are converted to uppercase // - All numeric digits at the start of the string are removed // - The first character is converted to lowercase func (addr Address) Nickname () (string, bool) { // get the normalized basename with no extension base := filepath.Base(string(addr)) base = strings.TrimSuffix(base, filepath.Ext(base)) // remove all non-letter, non-digit characters and convert to camel case nickname := "" uppercase := false for _, char := range base { if unicode.IsLetter(char) || unicode.IsDigit(char) { if uppercase { uppercase = false char = unicode.ToUpper(char) } nickname += string(char) } else { uppercase = true } } // remove numeric digits for len(nickname) > 0 { char, size := utf8.DecodeRuneInString(nickname) nickname = nickname[size:] if unicode.IsLetter(char) { // lowercase the first letter nickname = string(unicode.ToLower(char)) + nickname break } } return nickname, nickname != "" } // UUID automatically generates a UUID from the address. It is the MD5 hash of // the basename, with a space of the zero UUID. func (addr Address) UUID () uuid.UUID { return uuid.NewMD5(uuid.UUID { }, []byte(filepath.Base(string(addr)))) }