package projecttxt import ( "fmt" "sort" "strings" ) type Project struct { Id int `json:"id"` // Internal project id Original string `json:"original"` // Original raw project text Name string `json:"name"` // The name of the project Directory string `json:"directory"` // The directory part of the project text Notes string `json:"notes"` // The filename of the notes file, defaults to Directory+/+notes.md Repo string `json:"repo"` // The directory to the git repository, defaults to Directory+/+Name Contexts []string `json:"contexts"` ProjectTags []string `json:"projectTags"` AdditionalTags map[string]string `json:"additionalTags"` } // ParseProject parses the input text string into a Project struct func ParseProject(text string) (*Project, error) { var err error project := Project{ AdditionalTags: make(map[string]string), } project.Original = strings.Trim(text, "\t\n\r ") parts := getParts(project.Original) //parts := strings.Fields(project.Original) var pIdx int for pIdx = range parts { if partIsSpecial(parts[pIdx]) { break } project.Name = project.Name + " " + parts[pIdx] } project.Name = strings.TrimSpace(project.Name) parts = parts[pIdx:] var notesAbs, repoAbs bool project.Notes = "notes.md" project.Repo = project.Name for _, v := range parts { switch v[0] { case '^': // Project Directory project.Directory = v[1:] if strings.HasSuffix(project.Directory, "/") { project.Directory = strings.TrimSuffix(project.Directory, "/") } case '~': // Project Repo project.Repo = v[1:] if strings.HasPrefix(project.Repo, "/") { repoAbs = true } case '#': project.Notes = v[1:] if strings.HasPrefix(project.Notes, "/") { notesAbs = true } case '@': // Contexts project.Contexts = append(project.Contexts, v[1:]) case '+': // ProjectTags project.ProjectTags = append(project.ProjectTags, v[1:]) default: if strings.Contains(v, ":") { // Additional Tags tagPts := strings.Split(v, ":") if tagPts[0] != "" && tagPts[1] != "" { project.AdditionalTags[tagPts[0]] = tagPts[1] } } } } if !notesAbs { project.Notes = fmt.Sprintf("%s/%s", project.Directory, project.Notes) } if !repoAbs { project.Repo = fmt.Sprintf("%s/%s", project.Directory, project.Repo) } return &project, err } func partIsSpecial(pt string) bool { if len(pt) == 0 { return false } switch pt[0] { case '^', '~', '#', '@', '+': return true default: return false } } // getParts parses the text from text pulling out each part. // By default it splits on spaces, the only exception is if we find a double-quote func getParts(text string) []string { var ret []string var wrk string quoteStart := -1 for i := range text { if quoteStart >= 0 { if text[i] == '"' { quoteStart = -1 ret = append(ret, wrk) } else { wrk = wrk + string(text[i]) } } else { switch text[i] { case '"': quoteStart = i case ' ': if len(wrk) > 0 { ret = append(ret, wrk) } wrk = "" default: wrk = wrk + string(text[i]) } } } if len(wrk) > 0 { ret = append(ret, wrk) } return ret } // String returns a complete project string in project.txt format. // // Contexts, ProjectTags, and Additional Tags are alphabetically sorted, // and appended at the end in the following order: // Contexts, ProjectTags, Tags // // For example: // Project.txt ^/home/user/projects/projecttxt @personal +projectxt contact:"Frank Workman" "Address 1":"1234 Work Place" func (project Project) String() string { text := fmt.Sprintf("%s ", project.Name) text += fmt.Sprintf("^%s ", project.Directory) if project.Repo != "" { text += fmt.Sprintf("~%s ", project.Repo) } if project.Notes != "" { text += fmt.Sprintf("#%s ", project.Notes) } if len(project.Contexts) > 0 { sort.Strings(project.Contexts) for _, context := range project.Contexts { text += fmt.Sprintf("@%s ", context) } } if len(project.ProjectTags) > 0 { sort.Strings(project.ProjectTags) for _, pTag := range project.ProjectTags { text += fmt.Sprintf("+%s ", pTag) } } if len(project.AdditionalTags) > 0 { keys := make([]string, 0, len(project.AdditionalTags)) for key := range project.AdditionalTags { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { text += fmt.Sprintf("%s:%s ", key, project.AdditionalTags[key]) } } return text } func (project *Project) HasContext(context string) bool { for _, v := range project.Contexts { if v == context { return true } } return false } func (project *Project) GetContextsString() string { var text string sort.Strings(project.Contexts) for _, context := range project.Contexts { text += fmt.Sprintf("%s ", context) } return text } func (project *Project) HasProjectTag(projectTag string) bool { for _, v := range project.ProjectTags { if v == projectTag { return true } } return false } func (project *Project) GetProjectsString() string { var text string sort.Strings(project.ProjectTags) for _, pTag := range project.ProjectTags { text += fmt.Sprintf("%s ", pTag) } return text } func (project *Project) SetTag(name, val string) { project.AdditionalTags[name] = val } func (project *Project) HasTag(name string) bool { _, ok := project.AdditionalTags[name] return ok } func (project *Project) GetTag(name string) string { return project.AdditionalTags[name] } func (project *Project) GetTagsString() string { var text string keys := make([]string, 0, len(project.AdditionalTags)) for key := range project.AdditionalTags { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { text += fmt.Sprintf("%s:%s ; ", key, project.AdditionalTags[key]) } return text }