diff --git a/README.md b/README.md index a62346d..9d48b39 100644 --- a/README.md +++ b/README.md @@ -56,22 +56,11 @@ concards README.md ``` ## All Special Tokens -All the special tokens that are a part of concards syntax are below. Just add -"@" signs to escape them! +All the special tokens that are a part of concards syntax are below. ``` @> = Starts a concards block. Starts a question. <@ = Ends the concards block. @ = Separates answers. - -@@> = "@>" -<@@ = "<@" -@@ = "@" - -@@@> = "@@>" -<@@@ = "<@@" -@@@ = "@@" - -... ``` ## Advanced Usage @@ -85,7 +74,7 @@ Here is an example meta-data file: Here is the same file, but annotated: ``` -sha256sum | review timestamp | streak | alg | data +sha256sum cut in half | review timestamp | streak | alg | data ---------------------------------+-----------------------+--------+-----+----- 3dda75cb44ed447186834541475f32e2 | 2019-01-01T00:00:00Z | 0 | sm2 | 2.5 8525b45f883c05eec46b4f7a88e7f7ef | 2020-01-01T00:00:00Z | 0 | sm2 | 2.5 diff --git a/core/card.go b/core/card.go index 9698922..3393d01 100644 --- a/core/card.go +++ b/core/card.go @@ -2,6 +2,7 @@ package core import ( "strings" + "bufio" "fmt" "crypto/sha256" @@ -9,41 +10,59 @@ import ( // A card is a list of facts. Usually, but not limited to, Q&A format. type Card struct { - File string - Facts []string + file string + facts [][]string } -// Assumes a "cleaned" file string. -func NewCard(facts [][]string, file string) (*Card, error) { - c := Card{} - c.File = file - for _, x := range facts { - if len(x) > 0 { - c.Facts = append(c.Facts, strings.Join(x, " ")) +func NewCard(file string, sides string) (*Card, error) { + fact := []string{} + facts := [][]string{} + + scanner := bufio.NewScanner(strings.NewReader(sides)) + scanner.Split(bufio.ScanWords) + for scanner.Scan() { + t := scanner.Text() + if t == "@" { + if len(fact) > 0 { + facts = append(facts, fact) + fact = []string{} + } + } else if len(t) > 0 { + fact = append(fact, t) } - } + } + + if len(fact) > 0 { + facts = append(facts, fact) + } - if len(c.Facts) > 0 { - return &c, nil + if len(facts) > 0 { + return &Card{file, facts}, nil } else { return nil, fmt.Errorf("Question not provided.") } } -func (c *Card) HasAnswer() bool { - return len(c.Facts) > 1 +func (c *Card) GetSubCards() []*Card { + sub_cards := []*Card{} + question := c.GetQuestion() + answers := c.GetFacts()[1:] + for _, answer := range answers { + if sc, err := NewCard(c.file, answer + " @ " + question); err == nil { + sub_cards = append(sub_cards, sc) + } else { + panic("Error: Sub card was not created due to bad parent card. This is a logic error and should be fixed.") + } + } + return sub_cards } -func (c *Card) GetQuestion() string { - if len(c.Facts) > 0 { - return c.Facts[0] - } else { - return "" - } +func (c *Card) HasAnswer() bool { + return len(c.facts) > 1 } func (c *Card) String() string { - return strings.Join(c.Facts, " @ ") + return strings.Join(c.GetFacts(), " @ ") } func (c *Card) Hash() [sha256.Size]byte { @@ -53,3 +72,31 @@ func (c *Card) Hash() [sha256.Size]byte { func (c *Card) HashStr() string { return fmt.Sprintf("%x", c.Hash())[:32] } + +func (c *Card) Len() int { + return len(c.facts) +} + +func (c *Card) GetFact(i int) string { + if len(c.facts) > i { + return strings.Join(c.facts[i], " ") + } else { + return "" + } +} + +func (c *Card) GetQuestion() string { + return c.GetFact(0) +} + +func (c *Card) GetFacts() []string { + facts := []string{} + for i, _ := range c.facts { + facts = append(facts, c.GetFact(i)) + } + return facts +} + +func (c *Card) GetFile() string { + return c.file +} diff --git a/core/core_test.go b/core/core_test.go index c31f91b..082be25 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -7,24 +7,11 @@ import "fmt" const fullSum = "c6cd355e32654cb4ba506b529ff32288971420ead2e36fdc69e802e9e7510315" const halfSum = "c6cd355e32654cb4ba506b529ff32288" -var f1 = [][]string{ - {"hello", "there"}, - {"i'm", "a", "beard"}, -} - -var f2 = [][]string{ - {"hello"}, -} - -var f3 = [][]string{ - {"i'm", "um"}, - {"hello"}, -} - -var f4 = [][]string{ - {"alan", "the", "great"}, - {"sy", "shoe", "yu"}, -} +var f1 = "hello there @ i'm a beard" +var f2 = "hello" +var f3 = "i'm um @ hello" +var f4 = "alan the great @ sy shoe yu" +var f5 = "a @ b @ c @ d e @ f @ @ g" func TestMeta(t *testing.T) { a := NewMeta("2020-01-01T00:00:00Z", "0", "sm2", []string{"2.5"}) @@ -42,7 +29,7 @@ func TestParse(t *testing.T) { } func TestCard(t *testing.T) { - c, err := NewCard(f1, "") + c, err := NewCard("", f1) if err != nil { t.FailNow() } txt := c.String() @@ -57,7 +44,7 @@ func TestCard(t *testing.T) { func TestDeck(t *testing.T) { d := NewDeck() - d.AddFacts(f1, "afile") + d.AddCardFromSides("afile", f1, false) if d.GetCard(0).GetQuestion() != "hello there" { t.Fail() } if !d.GetCard(0).HasAnswer() { t.Fail() } if d.GetMeta(0) != nil { t.Fail() } @@ -73,9 +60,9 @@ func TestDeck(t *testing.T) { d.Forget(0) if d.GetMeta(0) != nil { t.Fail() } if !d.GetCard(0).HasAnswer() { t.Fail() } - d.AddFacts(f1, "nofile") + d.AddCardFromSides("nofile", f1, false) if d.Len() != 1 { t.Fail() } - if d.GetCard(0).File != "afile" { t.Fail() } + if d.GetCard(0).GetFile() != "afile" { t.Fail() } d.FilterOutFile("nofile") if d.Len() != 1 { t.Fail() } d.FilterOutFile("afile") @@ -86,10 +73,10 @@ func TestDeck(t *testing.T) { func TestDeckMove(t *testing.T) { d := NewDeck() - d.AddFacts(f1, "afile") - d.AddFacts(f2, "afile") - d.AddFacts(f3, "afile") - d.AddFacts(f4, "afile") + d.AddCardFromSides("afile", f1, false) + d.AddCardFromSides("afile", f2, false) + d.AddCardFromSides("afile", f3, false) + d.AddCardFromSides("afile", f4, false) d.TopToEnd() if d.TopCard().GetQuestion() != "hello" { panic("Bad moves") } if d.Len() != 4 { panic("Bad len") } @@ -104,3 +91,27 @@ func TestDeckMove(t *testing.T) { d.TopToEnd() if d.TopCard().GetQuestion() != "hello there" { panic("Bad moves") } } + +func TestAddSubCards(t *testing.T) { + d := NewDeck() + d.AddCardFromSides("dat-file", f5, true) + + if d.TopCard().GetQuestion() != "a" { panic("Subcards were inserted before the parent card.") } + if d.Len() != 6 { panic("Wrong number of sub cards inserted.") } + d.DelTop() + if d.TopCard().GetQuestion() != "b" { panic("Second card should be the first sub card.") } + if d.TopCard().GetFact(1) != "a" { panic("Answer isn't the parent card.") } + if d.Len() != 5 { panic("Delete didn't work.") } + + if d.GetCard(1).GetQuestion() != "c" { panic("Sub cards not inserted in the correct order.") } + if d.GetCard(1).GetFact(1) != "a" { panic("Sub card doesn't have parent as the answer.") } + + if d.GetCard(2).GetQuestion() != "d e" { panic("Sub cards not inserted in the correct order.") } + if d.GetCard(2).GetFact(1) != "a" { panic("Sub card doesn't have parent as the answer.") } + + if d.GetCard(3).GetQuestion() != "f" { panic("Sub cards not inserted in the correct order.") } + if d.GetCard(3).GetFact(1) != "a" { panic("Sub card doesn't have parent as the answer.") } + + if d.GetCard(4).GetQuestion() != "g" { panic("Sub cards not inserted in the correct order.") } + if d.GetCard(4).GetFact(1) != "a" { panic("Sub card doesn't have parent as the answer.") } +} diff --git a/core/deck.go b/core/deck.go index e693ac9..657646e 100644 --- a/core/deck.go +++ b/core/deck.go @@ -54,7 +54,6 @@ func (d *Deck) AddCard(c *Card) error { } } -// TODO: Error handling here. func (d *Deck) InsertCard(c *Card, i int) error { hash := c.HashStr() _, exists := d.Cmap[hash] @@ -71,11 +70,23 @@ func (d *Deck) InsertCard(c *Card, i int) error { } } -func (d *Deck) AddFacts(facts [][]string, file string) error { - if c, err := NewCard(facts, file); err == nil { - return d.AddCard(c) +func (d *Deck) AddCardFromSides(file string, sides string, include_sides bool) []error { + errors := []error{} + if c, create_err := NewCard(file, sides); create_err == nil { + cards := []*Card{c} + if include_sides { + cards = append(cards, c.GetSubCards()...) + } + + for _, c := range cards { + if add_err := d.AddCard(c); add_err != nil { + errors = append(errors, add_err) + } + } + } else { + errors = append(errors, create_err) } - return nil + return errors } func (d *Deck) AddMeta(h string, m *Meta) { diff --git a/core/filters.go b/core/filters.go index ab52426..6ae37fe 100644 --- a/core/filters.go +++ b/core/filters.go @@ -22,7 +22,7 @@ func (d *Deck) FilterNumber(param int) { func (d *Deck) FileIntersection(path string, other_deck *Deck) { d.filter(func(i int) bool { _, contains := other_deck.Cmap[d.refs[i]] - return d.GetCard(i).File == path && !contains + return d.GetCard(i).GetFile() == path && !contains }) } @@ -35,7 +35,7 @@ func (d *Deck) OuterLeftJoin(other_deck *Deck) { func (d *Deck) FilterOutFile(path string) { d.filter(func(i int) bool { - return d.GetCard(i).File == path + return d.GetCard(i).GetFile() == path }) } diff --git a/file/argparse.go b/file/argparse.go index adc2c27..a1923c1 100644 --- a/file/argparse.go +++ b/file/argparse.go @@ -12,6 +12,7 @@ type Config struct { IsReview bool IsMemorize bool IsDone bool + IsSides bool IsPrint bool IsStream bool @@ -57,28 +58,33 @@ func GenConfig() *Config { parser := argparse.NewParser("concards", "Concards is a simple CLI based SRS flashcard app.") // Create flags - f_version := parser.Flag("V", "version", &argparse.Options{Help: "Concards build information."}) - f_review := parser.Flag("r", "review", &argparse.Options{Help: "Show cards available to be reviewed"}) - f_memorize := parser.Flag("m", "memorize", &argparse.Options{Help: "Show cards available to be memorized"}) - f_done := parser.Flag("d", "done", &argparse.Options{Help: "Show cards not available to be reviewed or memorized"}) - f_print := parser.Flag("p", "print", &argparse.Options{Help: "Prints all cards, one line per card"}) - f_number := parser.Int("n", "number", &argparse.Options{Default: 0, Help: "Limit the number of cards in the program to \"#\""}) - f_editor := parser.String("E", "editor", &argparse.Options{Default: getDefaultEditor(), Help: "Which editor to use. Defaults to \"$EDITOR\""}) - f_meta := parser.String("M", "meta", &argparse.Options{Default: getDefaultMeta(), Help: "Path to meta file. Defaults to \"$CONCARDS_META\" or \"~/.concards-meta\""}) + f_version := parser.Flag("V", "version", &argparse.Options{Help: "Concards build information."}) + f_review := parser.Flag("r", "review", &argparse.Options{Help: "Show cards available to be reviewed."}) + f_memorize := parser.Flag("m", "memorize", &argparse.Options{Help: "Show cards available to be memorized."}) + f_done := parser.Flag("d", "done", &argparse.Options{Help: "Show cards not available to be reviewed or memorized."}) + f_sides := parser.Flag("s", "sides", &argparse.Options{Help: "Add cards for all sides."}) + f_print := parser.Flag("p", "print", &argparse.Options{Help: "Prints all cards, one line per card."}) + f_number := parser.Int("n", "number", &argparse.Options{Default: 0, Help: "How many cards to review."}) + f_editor := parser.String("E", "editor", &argparse.Options{Default: getDefaultEditor(), Help: "Which editor to use. Defaults to \"$EDITOR\""}) + f_meta := parser.String("M", "meta", &argparse.Options{Default: getDefaultMeta(), Help: "Path to meta file. Defaults to \"$CONCARDS_META\" or \"~/.concards-meta\""}) parser.HelpFunc = func(c *argparse.Command, msg interface{}) string { var help string help += fmt.Sprintf("%s\n\nUsage:\n %s [OPTIONS]... [FILE|FOLDER]...\n\nOptions:\n", c.GetDescription(), c.GetName()) for _, arg := range c.GetArgs() { - help += fmt.Sprintf(" -%s --%-9s %s.\n", arg.GetSname(), arg.GetLname(), arg.GetOpts().Help) + if arg.IsFlag() { + help += fmt.Sprintf(" -%s --%-9s %s.\n", arg.GetSname(), arg.GetLname(), arg.GetOpts().Help) + } else { + help += fmt.Sprintf(" -%s --%-9s %s.\n", arg.GetSname(), arg.GetLname() + " " + arg.GetSname(), arg.GetOpts().Help) + } } return help } // Parse input - files, err := parser.Parse(os.Args) + files, err := parser.ParseReturnArguments(os.Args) if err != nil { fmt.Print(parser.Help(nil)) os.Exit(1) @@ -94,6 +100,7 @@ func GenConfig() *Config { c.IsReview = *f_review c.IsMemorize = *f_memorize c.IsDone = *f_done + c.IsSides = *f_sides c.IsPrint = *f_print c.IsStream = false diff --git a/file/edit.go b/file/edit.go index da4559b..f7bd952 100644 --- a/file/edit.go +++ b/file/edit.go @@ -8,35 +8,51 @@ import ( "github.com/alanxoc3/concards/core" ) -// Assumes the deck is sorted how you want it to be sorted. -func EditFile(d *core.Deck, cfg *Config) error { - if d.IsEmpty() { - return fmt.Errorf("Error: The deck is empty.") - } +type DeckFunc func(string, *Config) (*core.Deck, error) - // We need to get information for the top card first. - cur_hash, cur_card, cur_meta := d.Top() - file_name := cur_card.File +func ReadCards(filename string, cfg *Config) (*core.Deck, error) { + d := core.NewDeck() + if err := ReadCardsToDeck(d, filename, cfg.IsSides); err != nil { + return nil, err + } + return d, nil +} - // Save the contents of the file now. - deck_before := core.NewDeck() - ReadCardsToDeck(deck_before, file_name) +func EditCards(filename string, cfg *Config) (*core.Deck, error) { + if cfg == nil { panic("Config was nil when passed to edit function.") } - // Then edit the file. - cmd := exec.Command(cfg.Editor, file_name) + // Load the file with your favorite editor. + cmd := exec.Command(cfg.Editor, filename) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout if err := cmd.Run(); err != nil { - return fmt.Errorf("Error: The editor returned an error code.") + return nil, fmt.Errorf("Error: The editor returned an error code.") } - // Save the contents of the file after. - deck_after := core.NewDeck() - ReadCardsToDeck(deck_after, file_name) + return ReadCards(filename, cfg) +} + +// Assumes the deck is sorted how you want it to be sorted. +func EditFile(d *core.Deck, cfg *Config, rf DeckFunc, ef DeckFunc) error { + if d.IsEmpty() { + return fmt.Errorf("Error: The deck is empty.") + } + + // We need to get information for the top card first. + cur_hash, cur_card, cur_meta := d.Top() + filename := cur_card.GetFile() + + // Deck before editing. + deck_before, e := rf(filename, cfg) + if e != nil { return e } + + // Deck after editing. + deck_after, e := ef(filename, cfg) + if e != nil { return e } // Take out any card that was removed from the file. - d.FileIntersection(file_name, deck_after) + d.FileIntersection(filename, deck_after) // Get only the cards that were created in the file. deck_after.OuterLeftJoin(deck_before) diff --git a/file/file_test.go b/file/file_test.go index a039dd3..49cf23c 100644 --- a/file/file_test.go +++ b/file/file_test.go @@ -17,7 +17,7 @@ b718c81a83d82bb83f82b0a8b18bb82b 2020-01-11T00:00:00Z 27 sm2 .05 func TestReadMetasToDeck(t *testing.T) { d := core.NewDeck() - ReadCardsToDeckHelper(strings.NewReader(f1 + f2), d, "") + ReadCardsToDeckHelper(strings.NewReader(f1 + f2), d, "", false) ReadMetasToDeckHelper(strings.NewReader(c1), d) for i := 0; i < d.Len(); i++ { @@ -34,13 +34,13 @@ func TestReadMetasToDeck(t *testing.T) { func TestReadCardsToDeck(t *testing.T) { d := core.NewDeck() - ReadCardsToDeckHelper(strings.NewReader(f2), d, "nihao") + ReadCardsToDeckHelper(strings.NewReader(f2), d, "nihao", false) for i := 0; i < d.Len(); i++ { _, c, _ := d.Get(i) switch i { case 0: if c.GetQuestion() != "hi" { t.Fail() } - if c.File != "nihao" { t.Fail() } + if c.GetFile() != "nihao" { t.Fail() } case 1: if c.GetQuestion() != "yoyo man go" { t.Fail() } } } diff --git a/file/txtfile.go b/file/txtfile.go index 05688bf..e7dae29 100644 --- a/file/txtfile.go +++ b/file/txtfile.go @@ -2,6 +2,7 @@ package file import ( "bufio" + "strings" "io" "fmt" "os" @@ -11,7 +12,7 @@ import ( ) // Open opens filename and loads cards into new deck -func ReadCardsToDeck(d *core.Deck, filename string) error { +func ReadCardsToDeck(d *core.Deck, filename string, include_sides bool) error { err := filepath.Walk(filename, func(path string, info os.FileInfo, e error) error { if e != nil { return e @@ -32,7 +33,7 @@ func ReadCardsToDeck(d *core.Deck, filename string) error { return fmt.Errorf("Error: Unable to open file \"%s\"", filename) } else { defer f.Close() - ReadCardsToDeckHelper(f, d, abs_path) + ReadCardsToDeckHelper(f, d, abs_path, include_sides) } return nil @@ -41,9 +42,9 @@ func ReadCardsToDeck(d *core.Deck, filename string) error { return err } -func ReadCardsToDeckHelper(r io.Reader, d *core.Deck, f string) { +func ReadCardsToDeckHelper(r io.Reader, d *core.Deck, f string, include_sides bool) { // Initialization. - facts := [][]string{} + facts := []string{} state := false var td *core.Deck @@ -55,27 +56,25 @@ func ReadCardsToDeckHelper(r io.Reader, d *core.Deck, f string) { t := scanner.Text() if state { - if t == "@" { - facts = append(facts, []string{}) - } else if t == "@>" { - td.AddFacts(facts, f) - facts = [][]string{{}} + if t == "@>" { + td.AddCardFromSides(f, strings.Join(facts, " "), include_sides) + + facts = []string{} } else if t == "<@" { - td.AddFacts(facts, f) + td.AddCardFromSides(f, strings.Join(facts, " "), include_sides) + for i := 0; i < td.Len(); i++ { d.AddCard(td.GetCard(i)) } state = false } else { - if i := len(facts)-1; i >= 0 { - facts[i] = append(facts[i], t) - } + facts = append(facts, t) } } else if t == "@>" { // create td td = core.NewDeck() state = true - facts = [][]string{{}} + facts = []string{} } } diff --git a/main.go b/main.go index 20faa05..9fb6671 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ func main() { } for _, f := range c.Files { - if err := file.ReadCardsToDeck(d, f); err != nil { + if err := file.ReadCardsToDeck(d, f, c.IsSides); err != nil { fmt.Printf("Error: File \"%s\" does not exist!\n", f) os.Exit(1) } diff --git a/termboxgui/helpers.go b/termboxgui/helpers.go index 21a86fa..31df211 100644 --- a/termboxgui/helpers.go +++ b/termboxgui/helpers.go @@ -100,10 +100,10 @@ func tbvertical(x int, color termbox.Attribute) { func tbprint_card(c *core.Card, amount int) { y := 0 - for i := 0; i < len(c.Facts) && i < amount; i++ { + for i := 0; i < c.Len() && i < amount; i++ { color := termbox.ColorCyan if i > 0 { color = termbox.ColorWhite } - _, y = tbprintwrap(0, y, color, coldef, c.Facts[i]) + _, y = tbprintwrap(0, y, color, coldef, c.GetFact(i)) y++ } } @@ -112,7 +112,7 @@ func tbprint_statusbar(d *core.Deck) { _, h := termbox.Size() color := termbox.ColorBlue tbhorizontal(h-2, color) - msg := fmt.Sprintf("%d concards - %s", d.Len(), d.TopCard().File) + msg := fmt.Sprintf("%d cards - %s", d.Len(), d.TopCard().GetFile()) tbprint(0, h-2, termbox.ColorWhite|termbox.AttrBold, color, msg) } diff --git a/termboxgui/tboxapp.go b/termboxgui/tboxapp.go index 7e8fc70..29d0ccc 100644 --- a/termboxgui/tboxapp.go +++ b/termboxgui/tboxapp.go @@ -81,7 +81,7 @@ func TermBoxRun(d *core.Deck, cfg *file.Config) error { d.TopToEnd() save(d) } else if inp == "e" { - err := file.EditFile(d, cfg) + err := file.EditFile(d, cfg, file.ReadCards, file.EditCards) if err != nil { update_stat_msg(err.Error(), termbox.ColorRed) @@ -111,7 +111,7 @@ func TermBoxRun(d *core.Deck, cfg *file.Config) error { } } else if inp == " " || inp == "\r" { card_shown++ - if l := len(d.GetCard(0).Facts); card_shown > l { + if l := d.GetCard(0).Len(); card_shown > l { card_shown = 1 } }