├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .goreleaser.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── images ├── til_build.png ├── til_save.png └── till_header.png ├── main.go ├── pages ├── page.go ├── tag.go └── tag_map.go ├── src ├── colours.go ├── configuration.go ├── logging.go ├── page_elements.go └── target_directory.go └── til_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: senorprogrammer 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: senorprogrammer 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | bench.txt 3 | bin/ 4 | dist/ 5 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | - GOPROXY="https://proxy.golang.org,direct" 4 | 5 | archives: 6 | - id: default 7 | wrap_in_directory: true 8 | 9 | builds: 10 | - binary: til 11 | goos: 12 | - darwin 13 | - linux 14 | goarch: 15 | - 386 16 | - amd64 17 | - arm 18 | - arm64 19 | 20 | before: 21 | hooks: 22 | - make build 23 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at chriscummer+til@me.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Chris Cummer 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build help install lint test uninstall 2 | 3 | # Set go modules to on and use GoCenter for immutable modules 4 | export GO111MODULE = on 5 | export GOPROXY = https://proxy.golang.org,direct 6 | 7 | # Determines the path to this Makefile 8 | THIS_FILE := $(lastword $(MAKEFILE_LIST)) 9 | 10 | APP=til 11 | 12 | ## build: builds a local version 13 | build: 14 | go build -o bin/${APP} 15 | @echo "Done building" 16 | 17 | ## help: prints this help message 18 | help: 19 | @echo "Usage: \n" 20 | @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' 21 | 22 | ## install: installs a local version of the app 23 | install: 24 | @echo "Installing ${APP}..." 25 | @go clean 26 | @go install -ldflags="-s -w" 27 | $(eval INSTALLPATH = $(shell which ${APP})) 28 | @echo "${APP} installed into ${INSTALLPATH}" 29 | 30 | ## lint: runs a number of code quality checks against the source code 31 | lint: 32 | golangci-lint run 33 | 34 | ## test: runs the test suite 35 | test: build 36 | go test ./... 37 | 38 | ## uninstall: uninstals a locally-installed version 39 | uninstall: 40 | @rm ~/go/bin/${APP} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
%s
[%s](%s)",
110 | page.PrettyDate(),
111 | page.Title,
112 | filepath.Base(page.FilePath),
113 | )
114 | }
115 |
116 | // Open tll the OS to open the newly-created page in the editor (as specified in the config)
117 | // If there's no editor explicitly defined by the user, tell the OS to try and open it
118 | func (page *Page) Open(defaultEditor string) error {
119 | editor := src.GlobalConfig.UString("editor", defaultEditor)
120 | if editor == "" {
121 | editor = defaultEditor
122 | }
123 |
124 | cmd := exec.Command(editor, page.FilePath)
125 | err := cmd.Run()
126 |
127 | return err
128 | }
129 |
130 | // PrettyDate returns a human-friendly representation of the CreatedAt date
131 | func (page *Page) PrettyDate() string {
132 | return page.CreatedAt().Format("Jan 02, 2006")
133 | }
134 |
135 | // Save writes the content of the page to file
136 | func (page *Page) Save() {
137 | pageSrc := page.FrontMatter()
138 | pageSrc += fmt.Sprintf("# %s\n\n", page.Title)
139 |
140 | err := ioutil.WriteFile(page.FilePath, []byte(pageSrc), 0644)
141 | if err != nil {
142 | src.Defeat(err)
143 | }
144 | }
145 |
146 | // Tags returns a slice of tags assigned to this page
147 | func (page *Page) Tags() []*Tag {
148 | tags := []*Tag{}
149 |
150 | names := strings.Split(page.TagsStr, ",")
151 | for _, name := range names {
152 | tags = append(tags, NewTag(name, page))
153 | }
154 |
155 | return tags
156 | }
157 |
--------------------------------------------------------------------------------
/pages/tag.go:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | // Tag represents a page tag (e.g.: linux, zombies)
9 | type Tag struct {
10 | Name string
11 | Pages []*Page
12 | }
13 |
14 | // NewTag creates and returns an instance of Tag
15 | func NewTag(name string, page *Page) *Tag {
16 | tag := &Tag{
17 | Name: strings.TrimSpace(name),
18 | Pages: []*Page{page},
19 | }
20 |
21 | return tag
22 | }
23 |
24 | // AddPage adds a page to the list of pages
25 | func (tag *Tag) AddPage(page *Page) {
26 | tag.Pages = append(tag.Pages, page)
27 | }
28 |
29 | // IsValid returns true if this is a valid tag, false if it is not
30 | func (tag *Tag) IsValid() bool {
31 | return tag.Name != ""
32 | }
33 |
34 | // Link returns a link string suitable for embedding in a Markdown page
35 | func (tag *Tag) Link() string {
36 | if tag.Name == "" {
37 | return ""
38 | }
39 |
40 | return fmt.Sprintf(
41 | "[%s](%s)",
42 | tag.Name,
43 | fmt.Sprintf("./%s", tag.Name),
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/pages/tag_map.go:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import (
4 | "sort"
5 | )
6 |
7 | // TagMap is a map of tag name to Tag instance
8 | type TagMap struct {
9 | Tags map[string][]*Tag
10 | }
11 |
12 | // NewTagMap creates and returns an instance of TagMap
13 | func NewTagMap(pageSet []*Page) *TagMap {
14 | tm := &TagMap{
15 | Tags: make(map[string][]*Tag),
16 | }
17 |
18 | tm.BuildFromPages(pageSet)
19 |
20 | return tm
21 | }
22 |
23 | // Add adds a Tag instance to the map
24 | func (tm *TagMap) Add(tag *Tag) {
25 | if !tag.IsValid() {
26 | return
27 | }
28 |
29 | tm.Tags[tag.Name] = append(tm.Tags[tag.Name], tag)
30 | }
31 |
32 | // BuildFromPages populates the tag map from a slice of Page instances
33 | func (tm *TagMap) BuildFromPages(pages []*Page) {
34 | for _, page := range pages {
35 | for _, tag := range page.Tags() {
36 | tm.Add(tag)
37 | }
38 | }
39 | }
40 |
41 | // Get returns the tags for a given tag name
42 | func (tm *TagMap) Get(name string) []*Tag {
43 | return tm.Tags[name]
44 | }
45 |
46 | // Len returns the number of tags in the map
47 | func (tm *TagMap) Len() int {
48 | return len(tm.Tags)
49 | }
50 |
51 | // PagesFor returns a flattened slice of pages for a given tag name, sorted
52 | // in reverse-chronological order
53 | func (tm *TagMap) PagesFor(tagName string) []*Page {
54 | pages := []*Page{}
55 | tags := tm.Get(tagName)
56 |
57 | for _, tag := range tags {
58 | pages = append(pages, tag.Pages...)
59 |
60 | sort.Slice(pages, func(i, j int) bool {
61 | return pages[i].CreatedAt().After(pages[j].CreatedAt())
62 | })
63 | }
64 |
65 | return pages
66 | }
67 |
68 | // SortedTagNames returns the tag names in alphabetical order
69 | func (tm *TagMap) SortedTagNames() []string {
70 | tagArr := make([]string, tm.Len())
71 | i := 0
72 |
73 | for tag := range tm.Tags {
74 | tagArr[i] = tag
75 | i++
76 | }
77 |
78 | sort.Strings(tagArr)
79 |
80 | return tagArr
81 | }
82 |
--------------------------------------------------------------------------------
/src/colours.go:
--------------------------------------------------------------------------------
1 | package src
2 |
3 | import "fmt"
4 |
5 | var (
6 | // Blue writes blue text
7 | Blue = Colour("\033[1;36m%s\033[0m")
8 |
9 | // Green writes green text
10 | Green = Colour("\033[1;32m%s\033[0m")
11 |
12 | // Red writes red text
13 | Red = Colour("\033[1;31m%s\033[0m")
14 | )
15 |
16 | // Colour returns a function that defines a printable colour string
17 | func Colour(colorString string) func(...interface{}) string {
18 | sprint := func(args ...interface{}) string {
19 | return fmt.Sprintf(colorString,
20 | fmt.Sprint(args...))
21 | }
22 | return sprint
23 | }
24 |
--------------------------------------------------------------------------------
/src/configuration.go:
--------------------------------------------------------------------------------
1 | package src
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "path/filepath"
9 |
10 | "github.com/olebedev/config"
11 | )
12 |
13 | const (
14 | defaultConfig = `---
15 | commitMessage: "build, save, push"
16 | committerEmail: test@example.com
17 | committerName: "TIL Autobot"
18 | editor: ""
19 | targetDirectory:
20 | a: "~/Documents/tilblog"
21 | `
22 |
23 | tilConfigDir = "~/.config/til/"
24 | tilConfigFile = "config.yml"
25 | )
26 |
27 | const (
28 | errConfigDirCreate = "could not create the configuration directory"
29 | errConfigExpandPath = "could not expand the config directory"
30 | errConfigFileAssert = "could not assert the configuration file exists"
31 | errConfigFileCreate = "could not create the configuration file"
32 | errConfigFileWrite = "could not write the configuration file"
33 | errConfigPathEmpty = "config path cannot be empty"
34 | )
35 |
36 | // GlobalConfig holds and makes available all the user-configurable
37 | // settings that are stored in the config file.
38 | // (I know! Friends don't let friends use globals, but since I have
39 | // no friends working on this, there's no one around to stop me)
40 | var GlobalConfig *config.Config
41 |
42 | // Config handles all things to do with configuration
43 | type Config struct{}
44 |
45 | // Load reads the configuration file
46 | func (c *Config) Load() {
47 | makeConfigDir()
48 | makeConfigFile()
49 |
50 | GlobalConfig = readConfigFile()
51 | }
52 |
53 | // getConfigDir returns the string path to the directory that should
54 | // contain the configuration file.
55 | // It tries to be XDG-compatible
56 | func getConfigDir() (string, error) {
57 | cDir := os.Getenv("XDG_CONFIG_HOME")
58 | if cDir == "" {
59 | cDir = tilConfigDir
60 | }
61 |
62 | // If the user hasn't changed the default path then we expect it to start
63 | // with a tilde (the user's home), and we need to turn that into an
64 | // absolute path. If it does not start with a '~' then we assume the
65 | // user has set their $XDG_CONFIG_HOME to something specific, and we
66 | // do not mess with it (because doing so makes the archlinux people
67 | // very cranky)
68 | if cDir[0] != '~' {
69 | return cDir, nil
70 | }
71 |
72 | dir, err := os.UserHomeDir()
73 | if err != nil {
74 | return "", errors.New(errConfigExpandPath)
75 | }
76 |
77 | cDir = filepath.Join(dir, cDir[1:])
78 |
79 | if cDir == "" {
80 | return "", errors.New(errConfigPathEmpty)
81 | }
82 |
83 | return cDir, nil
84 | }
85 |
86 | // GetConfigFilePath returns the string path to the configuration file
87 | func GetConfigFilePath() (string, error) {
88 | cDir, err := getConfigDir()
89 | if err != nil {
90 | return "", err
91 | }
92 |
93 | if cDir == "" {
94 | return "", errors.New(errConfigPathEmpty)
95 | }
96 |
97 | return fmt.Sprintf("%s/%s", cDir, tilConfigFile), nil
98 | }
99 |
100 | func makeConfigDir() {
101 | cDir, err := getConfigDir()
102 | if err != nil {
103 | Defeat(err)
104 | }
105 |
106 | if cDir == "" {
107 | Defeat(errors.New(errConfigPathEmpty))
108 | }
109 |
110 | if _, err := os.Stat(cDir); os.IsNotExist(err) {
111 | err := os.MkdirAll(cDir, os.ModePerm)
112 | if err != nil {
113 | Defeat(errors.New(errConfigDirCreate))
114 | }
115 |
116 | Progress(fmt.Sprintf("created %s", cDir))
117 | }
118 | }
119 |
120 | func makeConfigFile() {
121 | cPath, err := GetConfigFilePath()
122 | if err != nil {
123 | Defeat(err)
124 | }
125 |
126 | if cPath == "" {
127 | Defeat(errors.New(errConfigPathEmpty))
128 | }
129 |
130 | _, err = os.Stat(cPath)
131 |
132 | if err != nil {
133 | // Something went wrong trying to find the config file.
134 | // Let's see if we can figure out what happened
135 | if os.IsNotExist(err) {
136 | // Ah, the config file does not exist, which is probably fine
137 | _, err = os.Create(cPath)
138 | if err != nil {
139 | // That was not fine
140 | Defeat(errors.New(errConfigFileCreate))
141 | }
142 |
143 | } else {
144 | // But wait, it's some kind of other error. What kind?
145 | // I dunno, but it's probably bad so die
146 | Defeat(err)
147 | }
148 | }
149 |
150 | // Let's double-check that the file's there now
151 | fileInfo, err := os.Stat(cPath)
152 | if err != nil {
153 | Defeat(errors.New(errConfigFileAssert))
154 | }
155 |
156 | // Write the default config, but only if the file is empty.
157 | // Don't want to stop on any non-default values the user has written in there
158 | if fileInfo.Size() == 0 {
159 | if ioutil.WriteFile(cPath, []byte(defaultConfig), 0600) != nil {
160 | Defeat(errors.New(errConfigFileWrite))
161 | }
162 |
163 | Progress(fmt.Sprintf("created %s", cPath))
164 | }
165 | }
166 |
167 | // readConfigFile reads the contents of the config file and jams them
168 | // into the global config variable
169 | func readConfigFile() *config.Config {
170 | cPath, err := GetConfigFilePath()
171 | if err != nil {
172 | Defeat(err)
173 | }
174 |
175 | if cPath == "" {
176 | Defeat(errors.New(errConfigPathEmpty))
177 | }
178 |
179 | cfg, err := config.ParseYamlFile(cPath)
180 | if err != nil {
181 | Defeat(err)
182 | }
183 |
184 | return cfg
185 | }
186 |
--------------------------------------------------------------------------------
/src/logging.go:
--------------------------------------------------------------------------------
1 | package src
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | )
8 |
9 | // LL is a go routine-safe implementation of Logger
10 | // (More globals! This is getting crazy)
11 | var LL *log.Logger
12 |
13 | // Defeat writes out an error message
14 | func Defeat(err error) {
15 | LL.Fatal(fmt.Sprintf("%s %s", Red("✘"), err.Error()))
16 | }
17 |
18 | // Info writes out an informative message
19 | func Info(msg string) {
20 | LL.Print(fmt.Sprintf("%s %s", Green("->"), msg))
21 | }
22 |
23 | // Progress writes out a progress status message
24 | func Progress(msg string) {
25 | LL.Print(fmt.Sprintf("\t%s %s\n", Blue("->"), msg))
26 | }
27 |
28 | // Victory writes out a victorious final message and then expires dramatically
29 | func Victory(msg string) {
30 | LL.Print(fmt.Sprintf("%s %s", Green("✓"), msg))
31 | os.Exit(0)
32 | }
33 |
--------------------------------------------------------------------------------
/src/page_elements.go:
--------------------------------------------------------------------------------
1 | package src
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | )
7 |
8 | func Footer() string {
9 | return fmt.Sprintf(
10 | "generated %s by til\n",
11 | time.Now().Format("2 Jan 2006 15:04:05"),
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/src/target_directory.go:
--------------------------------------------------------------------------------
1 | package src
2 |
3 | import (
4 | "errors"
5 | "log"
6 | "os"
7 | "path/filepath"
8 |
9 | "github.com/olebedev/config"
10 | )
11 |
12 | const (
13 | errTargetDirCreate = "could not create the target directories"
14 | errTargetDirFlag = "multiple target directories defined, no -t value provided"
15 | errTargetDirUndefined = "target directory is undefined or misconfigured in config"
16 | )
17 |
18 | // BuildTargetDirectory verifies that the target directory, as specified in
19 | // the config file, exists and contains a /docs folder for writing pages to.
20 | // If these directories don't exist, it tries to create them
21 | func BuildTargetDirectory() {
22 | tDir, err := GetTargetDir(GlobalConfig, "", true)
23 | if err != nil {
24 | log.Printf("BuildTargetDirectory got error: %v", err)
25 | return
26 | }
27 |
28 | if _, err := os.Stat(tDir); os.IsNotExist(err) {
29 | err := os.MkdirAll(tDir, os.ModePerm)
30 | if err != nil {
31 | Defeat(errors.New(errTargetDirCreate))
32 | }
33 | }
34 | }
35 |
36 | // GetTargetDir returns the absolute string path to the directory that the
37 | // content will be written to
38 | func GetTargetDir(cfg *config.Config, targetDirFlag string, withDocsDir bool) (string, error) {
39 | docsBit := ""
40 | if withDocsDir {
41 | docsBit = "/docs"
42 | }
43 |
44 | // Target directories are defined in the config file as a map of
45 | // identifier : target directory
46 | // Example:
47 | // targetDirectories:
48 | // a: ~/Documents/blog
49 | // b: ~/Documents/notes
50 | uDirs, err := cfg.Map("targetDirectories")
51 | if err != nil {
52 | return "", err
53 | }
54 |
55 | // config returns a map of [string]interface{} which is helpful on the
56 | // left side, not so much on the right side. Convert the right to strings
57 | tDirs := make(map[string]string, len(uDirs))
58 | for k, dir := range uDirs {
59 | tDirs[k] = dir.(string)
60 | }
61 |
62 | // Extracts the dir we want operate against by using the value of the
63 | // -target flag passed in. If no value was passed in, AND we only have one
64 | // entry in the map, use that entry. If no value was passed in and there
65 | // are multiple entries in the map, raise an error because ¯\_(ツ)_/¯
66 | tDir := ""
67 |
68 | if len(tDirs) == 1 {
69 | for _, dir := range tDirs {
70 | tDir = dir
71 | }
72 | } else {
73 | if targetDirFlag == "" {
74 | return "", errors.New(errTargetDirFlag)
75 | }
76 |
77 | tDir = tDirs[targetDirFlag]
78 | }
79 |
80 | if tDir == "" {
81 | return "", errors.New(errTargetDirUndefined)
82 | }
83 |
84 | // If we're not using a path relative to the user's home directory,
85 | // take the config value as a fully-qualified path and just append the
86 | // name of the write dir to it
87 | if tDir[0] != '~' {
88 | return tDir + docsBit, nil
89 | }
90 |
91 | // We are pathing relative to the home directory, so figure out the
92 | // absolute path for that
93 | dir, err := os.UserHomeDir()
94 | if err != nil {
95 | return "", errors.New(errConfigExpandPath)
96 | }
97 |
98 | return filepath.Join(dir, tDir[1:], docsBit), nil
99 | }
100 |
--------------------------------------------------------------------------------
/til_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "os"
7 | "testing"
8 |
9 | "github.com/olebedev/config"
10 | "github.com/senorprogrammer/til/pages"
11 | "github.com/senorprogrammer/til/src"
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | func Test_determineCommitMessage(t *testing.T) {
16 | tests := []struct {
17 | name string
18 | cfgMessage string
19 | args []string
20 | expected string
21 | }{
22 | {
23 | name: "passed in via -s",
24 | cfgMessage: "from the config",
25 | args: []string{"test", "-t", "b", "-s", "this", "is", "test"},
26 | expected: "this is test",
27 | },
28 | {
29 | name: "from config file",
30 | cfgMessage: "from the config",
31 | args: []string{"test", "-t", "b", "-s"},
32 | expected: "from the config",
33 | },
34 | {
35 | name: "from default const",
36 | cfgMessage: "",
37 | args: []string{"test", "-t", "b", "-s"},
38 | expected: "build, save, push",
39 | },
40 | }
41 |
42 | for _, tt := range tests {
43 | t.Run(tt.name, func(t *testing.T) {
44 | os.Args = tt.args
45 | flag.Parse()
46 |
47 | msg := fmt.Sprintf("commitMessage: %s", tt.cfgMessage)
48 | cfg, _ := config.ParseYamlBytes([]byte(msg))
49 |
50 | actual := determineCommitMessage(cfg, os.Args)
51 |
52 | assert.Equal(t, tt.expected, actual)
53 | })
54 | }
55 | }
56 |
57 | /* -------------------- Configuration -------------------- */
58 |
59 | func Test_getConfigPath(t *testing.T) {
60 | actual, err := src.GetConfigFilePath()
61 |
62 | assert.Contains(t, actual, "config.yml")
63 | assert.NoError(t, err)
64 | }
65 |
66 | /* -------------------- More Helper Functions -------------------- */
67 |
68 | func Test_Colour(t *testing.T) {
69 | x := src.Colour("yo%soy")
70 | actual := x("cat")
71 |
72 | assert.Equal(t, "yocatoy", actual)
73 | }
74 |
75 | /* -------------------- Page -------------------- */
76 |
77 | func Test_Page_CreatedAt(t *testing.T) {
78 | page := &pages.Page{Date: "2020-05-07T13:13:08-07:00"}
79 |
80 | actual := page.CreatedAt()
81 |
82 | assert.Equal(t, 2020, actual.Year())
83 | assert.Equal(t, 5, int(actual.Month()))
84 | assert.Equal(t, 7, actual.Day())
85 | }
86 |
87 | func Test_Page_CreatedMonth(t *testing.T) {
88 | page := &pages.Page{Date: "2020-05-07T13:13:08-07:00"}
89 |
90 | actual := page.CreatedMonth()
91 |
92 | assert.Equal(t, 5, int(actual))
93 | }
94 |
95 | func Test_IsContentPage(t *testing.T) {
96 | tests := []struct {
97 | name string
98 | title string
99 | expected bool
100 | }{
101 | {
102 | name: "when is not content page",
103 | title: "",
104 | expected: false,
105 | },
106 | {
107 | name: "when is content page",
108 | title: "test",
109 | expected: true,
110 | },
111 | }
112 |
113 | for _, tt := range tests {
114 | t.Run(tt.name, func(t *testing.T) {
115 | page := &pages.Page{Title: tt.title}
116 |
117 | actual := page.IsContentPage()
118 |
119 | assert.Equal(t, tt.expected, actual)
120 | })
121 | }
122 | }
123 |
124 | func Test_Page_Link(t *testing.T) {
125 | page := &pages.Page{
126 | Date: "2020-05-07T13:13:08-07:00",
127 | FilePath: "docs/zombies.md",
128 | Title: "Zombies",
129 | }
130 |
131 | actual := page.Link()
132 |
133 | assert.Equal(t, "May 07, 2020
[Zombies](zombies.md)", actual)
134 | }
135 |
136 | func Test_Page_PrettDate(t *testing.T) {
137 | page := &pages.Page{Date: "2020-05-07T13:13:08-07:00"}
138 |
139 | actual := page.PrettyDate()
140 |
141 | assert.Equal(t, "May 07, 2020", actual)
142 | }
143 |
144 | /* -------------------- Tag -------------------- */
145 |
146 | func Test_Tag_NewTag(t *testing.T) {
147 | actual := pages.NewTag("ada", &pages.Page{Title: "test"})
148 |
149 | assert.IsType(t, &pages.Tag{}, actual)
150 | assert.Equal(t, "ada", actual.Name)
151 | assert.Equal(t, "test", actual.Pages[0].Title)
152 | }
153 |
154 | func Test_Tag_AddPage(t *testing.T) {
155 | tag := pages.NewTag("ada", &pages.Page{Title: "test"})
156 | tag.AddPage(&pages.Page{Title: "zombies"})
157 |
158 | assert.Equal(t, 2, len(tag.Pages))
159 | assert.Equal(t, "zombies", tag.Pages[1].Title)
160 | }
161 |
162 | func Test_Tag_IsValid(t *testing.T) {
163 | tests := []struct {
164 | name string
165 | title string
166 | expected bool
167 | }{
168 | {
169 | name: "when invalid",
170 | title: "",
171 | expected: false,
172 | },
173 | {
174 | name: "when valid",
175 | title: "test",
176 | expected: true,
177 | },
178 | }
179 |
180 | for _, tt := range tests {
181 | t.Run(tt.name, func(t *testing.T) {
182 | tag := pages.NewTag(tt.title, &pages.Page{})
183 |
184 | actual := tag.IsValid()
185 |
186 | assert.Equal(t, tt.expected, actual)
187 | })
188 | }
189 | }
190 |
191 | /* -------------------- TagMap -------------------- */
192 |
193 | func Test_NewTagMap(t *testing.T) {
194 | tests := []struct {
195 | name string
196 | pages []*pages.Page
197 | expectedLen int
198 | }{
199 | {
200 | name: "with no pages",
201 | pages: []*pages.Page{},
202 | expectedLen: 0,
203 | },
204 | {
205 | name: "with pages",
206 | pages: []*pages.Page{
207 | {TagsStr: "go, ada"},
208 | },
209 | expectedLen: 2,
210 | },
211 | }
212 |
213 | for _, tt := range tests {
214 | t.Run(tt.name, func(t *testing.T) {
215 | actual := pages.NewTagMap(tt.pages).Tags
216 |
217 | assert.Equal(t, tt.expectedLen, len(actual))
218 | })
219 | }
220 | }
221 |
222 | func Test_TagMap_Add(t *testing.T) {
223 | tests := []struct {
224 | name string
225 | tag *pages.Tag
226 | expectedLen int
227 | }{
228 | {
229 | name: "with an invalid tag",
230 | tag: &pages.Tag{},
231 | expectedLen: 0,
232 | },
233 | {
234 | name: "with a new tag",
235 | tag: &pages.Tag{Name: "go"},
236 | expectedLen: 1,
237 | },
238 | }
239 |
240 | for _, tt := range tests {
241 | t.Run(tt.name, func(t *testing.T) {
242 | tMap := pages.NewTagMap([]*pages.Page{})
243 | tMap.Add(tt.tag)
244 |
245 | actual := tMap.Tags
246 |
247 | assert.Equal(t, tt.expectedLen, len(actual))
248 | })
249 | }
250 | }
251 |
252 | func Test_TagMap_BuildFromPages(t *testing.T) {
253 | tests := []struct {
254 | name string
255 | pages []*pages.Page
256 | expectedLen int
257 | }{
258 | {
259 | name: "with no pages",
260 | pages: []*pages.Page{},
261 | expectedLen: 0,
262 | },
263 | {
264 | name: "with pages",
265 | pages: []*pages.Page{
266 | {TagsStr: "go"},
267 | {TagsStr: "ada"},
268 | },
269 | expectedLen: 2,
270 | },
271 | }
272 |
273 | for _, tt := range tests {
274 | t.Run(tt.name, func(t *testing.T) {
275 | tMap := pages.NewTagMap([]*pages.Page{})
276 | tMap.BuildFromPages(tt.pages)
277 |
278 | actual := tMap.Tags
279 |
280 | assert.Equal(t, tt.expectedLen, len(actual))
281 | })
282 | }
283 | }
284 |
285 | func Test_TagMap_Get(t *testing.T) {
286 | tests := []struct {
287 | name string
288 | input string
289 | expectedLen int
290 | }{
291 | {
292 | name: "with missing tag",
293 | input: "ada",
294 | expectedLen: 0,
295 | },
296 | {
297 | name: "with valid tag",
298 | input: "go",
299 | expectedLen: 1,
300 | },
301 | }
302 |
303 | for _, tt := range tests {
304 | pageSet := []*pages.Page{{TagsStr: "go"}}
305 | tMap := pages.NewTagMap(pageSet)
306 |
307 | actual := tMap.Get(tt.input)
308 |
309 | t.Run(tt.name, func(t *testing.T) {
310 | assert.Equal(t, tt.expectedLen, len(actual))
311 | })
312 | }
313 | }
314 |
315 | func Test_TagMap_Len(t *testing.T) {
316 | tests := []struct {
317 | name string
318 | page *pages.Page
319 | expectedLen int
320 | }{
321 | {
322 | name: "with missing tag",
323 | page: &pages.Page{},
324 | expectedLen: 0,
325 | },
326 | {
327 | name: "with valid tag",
328 | page: &pages.Page{TagsStr: "go"},
329 | expectedLen: 1,
330 | },
331 | }
332 |
333 | for _, tt := range tests {
334 | pageSet := []*pages.Page{tt.page}
335 | tMap := pages.NewTagMap(pageSet)
336 |
337 | actual := tMap.Len()
338 |
339 | t.Run(tt.name, func(t *testing.T) {
340 | assert.Equal(t, tt.expectedLen, actual)
341 | })
342 | }
343 | }
344 |
345 | func Test_TagMap_SortedTagNames(t *testing.T) {
346 | pageSet := []*pages.Page{{TagsStr: "go, ada, lua"}}
347 | tMap := pages.NewTagMap(pageSet)
348 |
349 | expected := []string{"ada", "go", "lua"}
350 | actual := tMap.SortedTagNames()
351 |
352 | assert.Equal(t, expected, actual)
353 | }
354 |
--------------------------------------------------------------------------------