<commit_before>package bs

import (
	"errors"
	"fmt"
	"log"
	"net/url"
	"strings"

	"github.com/PuerkitoBio/goquery"
	"github.com/missdeer/golib/httputil"
)

type Book struct {
	Tag          string        `json:"tag"`
	Origin       string        `json:"origin"`
	Name         string        `json:"name"`
	Author       string        `json:"author"`
	BookmarkList []interface{} `json:"bookmarkList"`
	ChapterURL   string        `json:"chapterUrl"`
	// BookURL        string            `json:"book_url"`
	CoverURL         string            `json:"coverUrl"`
	Kind             string            `json:"kind"`
	LastChapter      string            `json:"lastChapter"`
	FinalRefreshData UnixTime          `json:"finalRefreshData"`
	NoteURL          string            `json:"noteUrl"`
	Introduce        string            `json:"introduce"`
	ChapterList      []*Chapter        `json:"-"`
	BookSourceInst   *BookSource       `json:"-"`
	Page             *goquery.Document `json:"-"`
}

func (b Book) String() string {
	return fmt.Sprintf("%s( %s )", b.Name, b.NoteURL)
}

func (b *Book) GetBookSource() *BookSource {
	if b.BookSourceInst != nil {
		return b.BookSourceInst
	}
	if b.Tag == "" {
		if b.NoteURL == "" {
			return nil
		}
		b.Tag = httputil.GetHostByURL(b.NoteURL)
	}
	if bsItem, ok := bsCache.Get(b.Tag); ok {
		if bs, ok := bsItem.(BookSource); ok {
			b.BookSourceInst = &bs
			b.Origin = bs.BookSourceName
			return &bs
		} else {
			return nil
		}
	}
	return nil
}

// NewBookFromURL create new Book instance from URL
func NewBookFromURL(bookURL string) (*Book, error) {
	if bookURL == "" {
		return nil, errors.New("no url.")
	}
	_, err := url.ParseRequestURI(bookURL)
	if err != nil {
		return nil, err
	}
	b := &Book{
		NoteURL: bookURL,
		Tag:     httputil.GetHostByURL(bookURL),
	}
	b.GetAuthor()
	b.GetIntroduce()
	return b, nil
}

func (b *Book) getBookPage() (*goquery.Document, error) {
	if b.Page != nil {
		return b.Page, nil
	}
	bs := b.GetBookSource()
	if b.NoteURL != "" && bs != nil {
		p, err := httputil.GetPage(b.NoteURL, b.GetBookSource().HTTPUserAgent)
		if err == nil {
			doc, err := goquery.NewDocumentFromReader(p)
			if err == nil {
				b.Page = doc
				return b.Page, nil
			}
		}
		return nil, err
	}
	return nil, errors.New("can't get book page.")
}

func (b *Book) GetChapterURL() string {
	if b.ChapterURL != "" {
		return b.ChapterURL
	}
	doc, err := b.getBookPage()
	if err == nil {
		_, chapterURL := ParseRules(doc, b.BookSourceInst.RuleChapterURL)
		if chapterURL != "" {
			log.Printf("chapter url is: %s", chapterURL)
			b.ChapterURL = chapterURL
			return b.ChapterURL
		}
	} else {
		log.Printf("get chapterURL error:%s\n", err.Error())
	}
	return b.NoteURL
}

func (b *Book) GetChapterList() []*Chapter {
	b.UpdateChapterList(len(b.ChapterList))
	return b.ChapterList
}

func (b *Book) UpdateChapterList(startFrom int) error {
	var doc *goquery.Document
	var err error
	bs := b.GetBookSource()
	// if b.ChapterURL != "" && bs != nil {
	p, err := httputil.GetPage(b.GetChapterURL(), b.GetBookSource().HTTPUserAgent)
	log.Printf("%s chapterlist url is:%s .", b.Name, b.ChapterURL)
	if err != nil {
		log.Printf("error while getting chapter list page: %s", err.Error())
	}
	doc, err = goquery.NewDocumentFromReader(p)
	if err != nil {
		log.Printf("error while parsing chapter list page to goquery: %s", err.Error())
	}
	// }
	if doc == nil {
		log.Printf("%s no chapterurl found.got by bookurl.", bs.BookSourceName)
		doc, err = b.getBookPage()
		if err != nil {
			return err
		}
	}
	sel, _ := ParseRules(doc, b.BookSourceInst.RuleChapterList)
	if sel != nil {
		sel.Each(func(i int, s *goquery.Selection) {
			if i < startFrom {
				return
			}
			_, name := ParseRules(s, b.BookSourceInst.RuleChapterName)
			_, url := ParseRules(s, b.BookSourceInst.RuleContentURL)
			if strings.HasPrefix(url, "/") {
				url = fmt.Sprintf("%s%s", b.BookSourceInst.BookSourceURL, url)
			}
			b.ChapterList = append(b.ChapterList, &Chapter{
				ChapterTitle: name,
				ChapterURL:   url,
				BelongToBook: b,
				Index:        i,
			})
		})
	}
	return nil
}

func (b *Book) GetName() string {
	if b.Name != "" {
		return b.Name
	}
	doc, err := b.getBookPage()
	if err == nil {
		_, title := ParseRules(doc, b.BookSourceInst.RuleBookName)
		if title != "" {
			b.Name = title
		}
	} else {
		log.Printf("get title error:%s\n", err.Error())
	}
	return b.Name
}

func (b *Book) GetIntroduce() string {
	if b.Introduce != "" {
		return b.Introduce
	}
	doc, err := b.getBookPage()
	if err == nil {
		_, intro := ParseRules(doc, b.BookSourceInst.RuleIntroduce)
		if intro != "" {
			b.Introduce = intro
		}
	} else {
		log.Printf("get introduce error:%s\n", err.Error())
	}
	return b.Introduce
}

func (b *Book) GetAuthor() string {
	if b.Author == "" {

		doc, err := b.getBookPage()
		if err == nil {
			_, intro := ParseRules(doc, b.BookSourceInst.RuleBookAuthor)
			if intro != "" {
				b.Author = intro
			}
		} else {
			log.Printf("get author error:%s\n", err.Error())
		}
	}
	return b.Author
}
<commit_msg>(*)book WIP<commit_after>package bs

import (
	"errors"
	"fmt"
	"log"
	"net/url"
	"strings"

	"github.com/PuerkitoBio/goquery"
	"github.com/missdeer/golib/httputil"
)

type Book struct {
	Tag          string        `json:"tag"`
	Origin       string        `json:"origin"`
	Name         string        `json:"name"`
	Author       string        `json:"author"`
	BookmarkList []interface{} `json:"bookmarkList"`
	ChapterURL   string        `json:"chapterUrl"`
	// BookURL        string            `json:"book_url"`
	CoverURL         string            `json:"coverUrl"`
	Kind             string            `json:"kind"`
	LastChapter      string            `json:"lastChapter"`
	FinalRefreshData UnixTime          `json:"finalRefreshData"`
	NoteURL          string            `json:"noteUrl"`
	Introduce        string            `json:"introduce"`
	ChapterList      []*Chapter        `json:"-"`
	BookSourceInst   *BookSource       `json:"-"`
	Page             *goquery.Document `json:"-"`
}

func (b Book) String() string {
	return fmt.Sprintf("%s( %s )", b.Name, b.NoteURL)
}

// FindBookSource find book source for a specified book
func FindBookSource(b *Book) *BookSource {
	if b.BookSourceInst != nil {
		return b.BookSourceInst
	}
	if b.Tag == "" {
		if b.NoteURL == "" {
			return nil
		}
		b.Tag = httputil.GetHostByURL(b.NoteURL)
	}
	if bsItem, ok := bsCache.Get(b.Tag); ok {
		if bs, ok := bsItem.(BookSource); ok {
			b.BookSourceInst = &bs
			b.Origin = bs.BookSourceName
			return &bs
		} else {
			return nil
		}
	}
	return nil
}

// NewBookFromURL create new Book instance from URL
func NewBookFromURL(bookURL string) (*Book, error) {
	if bookURL == "" {
		return nil, errors.New("no url.")
	}
	_, err := url.ParseRequestURI(bookURL)
	if err != nil {
		return nil, err
	}
	b := &Book{
		NoteURL: bookURL,
		Tag:     httputil.GetHostByURL(bookURL),
	}
	b.GetAuthor()
	b.GetIntroduce()
	return b, nil
}

func (b *Book) getBookPage() (*goquery.Document, error) {
	if b.Page != nil {
		return b.Page, nil
	}
	bs := FindBookSource(b)
	if b.NoteURL != "" && bs != nil {
		p, err := httputil.GetPage(b.NoteURL, FindBookSource(b).HTTPUserAgent)
		if err == nil {
			doc, err := goquery.NewDocumentFromReader(p)
			if err == nil {
				b.Page = doc
				return b.Page, nil
			}
		}
		return nil, err
	}
	return nil, errors.New("can't get book page.")
}

func (b *Book) GetChapterURL() string {
	if b.ChapterURL != "" {
		return b.ChapterURL
	}
	doc, err := b.getBookPage()
	if err == nil {
		_, chapterURL := ParseRules(doc, b.BookSourceInst.RuleChapterURL)
		if chapterURL != "" {
			log.Printf("chapter url is: %s", chapterURL)
			b.ChapterURL = chapterURL
			return b.ChapterURL
		}
	} else {
		log.Printf("get chapterURL error:%s\n", err.Error())
	}
	return b.NoteURL
}

func (b *Book) GetChapterList() []*Chapter {
	b.UpdateChapterList(len(b.ChapterList))
	return b.ChapterList
}

func (b *Book) UpdateChapterList(startFrom int) error {
	var doc *goquery.Document
	var err error
	bs := FindBookSource(b)
	// if b.ChapterURL != "" && bs != nil {
	p, err := httputil.GetPage(b.GetChapterURL(), FindBookSource(b).HTTPUserAgent)
	log.Printf("%s chapterlist url is:%s .", b.Name, b.ChapterURL)
	if err != nil {
		log.Printf("error while getting chapter list page: %s", err.Error())
	}
	doc, err = goquery.NewDocumentFromReader(p)
	if err != nil {
		log.Printf("error while parsing chapter list page to goquery: %s", err.Error())
	}
	// }
	if doc == nil {
		log.Printf("%s no chapterurl found.got by bookurl.", bs.BookSourceName)
		doc, err = b.getBookPage()
		if err != nil {
			return err
		}
	}
	sel, _ := ParseRules(doc, b.BookSourceInst.RuleChapterList)
	if sel != nil {
		sel.Each(func(i int, s *goquery.Selection) {
			if i < startFrom {
				return
			}
			_, name := ParseRules(s, b.BookSourceInst.RuleChapterName)
			_, url := ParseRules(s, b.BookSourceInst.RuleContentURL)
			if strings.HasPrefix(url, "/") {
				url = fmt.Sprintf("%s%s", b.BookSourceInst.BookSourceURL, url)
			}
			b.ChapterList = append(b.ChapterList, &Chapter{
				ChapterTitle: name,
				ChapterURL:   url,
				BelongToBook: b,
				Index:        i,
			})
		})
	}
	return nil
}

func (b *Book) GetName() string {
	if b.Name != "" {
		return b.Name
	}
	doc, err := b.getBookPage()
	if err == nil {
		_, title := ParseRules(doc, b.BookSourceInst.RuleBookName)
		if title != "" {
			b.Name = title
		}
	} else {
		log.Printf("get title error:%s\n", err.Error())
	}
	return b.Name
}

func (b *Book) GetIntroduce() string {
	if b.Introduce != "" {
		return b.Introduce
	}
	doc, err := b.getBookPage()
	if err == nil {
		_, intro := ParseRules(doc, b.BookSourceInst.RuleIntroduce)
		if intro != "" {
			b.Introduce = intro
		}
	} else {
		log.Printf("get introduce error:%s\n", err.Error())
	}
	return b.Introduce
}

func (b *Book) GetAuthor() string {
	if b.Author == "" {
		doc, err := b.getBookPage()
		if err == nil {
			_, intro := ParseRules(doc, b.BookSourceInst.RuleBookAuthor)
			if intro != "" {
				b.Author = intro
			}
		} else {
			log.Printf("get author error:%s\n", err.Error())
		}
	}
	return b.Author
}
<|endoftext|>


<commit_before>package edit

import (
	"errors"
	"fmt"
	"strconv"
	"strings"

	"github.com/elves/elvish/edit/ui"
)

// Command history listing mode.

var _ = registerBuiltins(modeHistoryListing, map[string]func(*Editor){
	"start":                   histlistStart,
	"toggle-dedup":            histlistToggleDedup,
	"toggle-case-sensitivity": histlistToggleCaseSensitivity,
})

func init() {
	registerBindings(modeHistoryListing, modeHistoryListing,
		map[ui.Key]string{
			{'G', ui.Ctrl}: "toggle-case-sensitivity",
			{'D', ui.Ctrl}: "toggle-dedup",
		})
}

// ErrStoreOffline is thrown when an operation requires the storage backend, but
// it is offline.
var ErrStoreOffline = errors.New("store offline")

type histlist struct {
	*listing
	all             []string
	dedup           bool
	caseInsensitive bool
	last            map[string]int
	shown           []string
	index           []int
	indexWidth      int
}

func newHistlist(cmds []string) *listing {
	last := make(map[string]int)
	for i, entry := range cmds {
		last[entry] = i
	}
	hl := &histlist{
		// This has to be here for the initializatio to work :(
		listing: &listing{},
		all:     cmds, last: last, indexWidth: len(strconv.Itoa(len(cmds) - 1))}
	l := newListing(modeHistoryListing, hl)
	hl.listing = &l
	return &l
}

func (hl *histlist) ModeTitle(i int) string {
	s := " HISTORY "
	if hl.dedup {
		s += "(dedup on) "
	}
	if hl.caseInsensitive {
		s += "(case-insensitive) "
	}
	return s
}

func (*histlist) CursorOnModeLine() bool {
	return true
}

func (hl *histlist) Len() int {
	return len(hl.shown)
}

func (hl *histlist) Show(i int) (string, ui.Styled) {
	return fmt.Sprintf("%d", hl.index[i]), ui.Unstyled(hl.shown[i])
}

func (hl *histlist) Filter(filter string) int {
	hl.updateShown()
	return len(hl.shown) - 1
}

func (hl *histlist) toggleDedup() {
	hl.dedup = !hl.dedup
	hl.updateShown()
}

func (hl *histlist) toggleCaseSensitivity() {
	hl.caseInsensitive = !hl.caseInsensitive
	hl.updateShown()
}

func (hl *histlist) updateShown() {
	hl.shown = nil
	hl.index = nil
	dedup := hl.dedup
	filter := hl.filter
	if hl.caseInsensitive {
		filter = strings.ToLower(filter)
	}
	for i, entry := range hl.all {
		fentry := entry
		if hl.caseInsensitive {
			fentry = strings.ToLower(entry)
		}
		if (!dedup || hl.last[entry] == i) && strings.Contains(fentry, filter) {
			hl.index = append(hl.index, i)
			hl.shown = append(hl.shown, entry)
		}
	}
	hl.selected = len(hl.shown) - 1
}

// Editor interface.

func (hl *histlist) Accept(i int, ed *Editor) {
	line := hl.shown[i]
	if len(ed.line) > 0 {
		line = "\n" + line
	}
	ed.insertAtDot(line)
}

func histlistStart(ed *Editor) {
	cmds, err := getCmds(ed)
	if err != nil {
		ed.Notify("%v", err)
		return
	}

	ed.mode = newHistlist(cmds)
}

func getCmds(ed *Editor) ([]string, error) {
	if ed.daemon == nil {
		return nil, ErrStoreOffline
	}
	return ed.historyFuser.AllCmds()
}

func histlistToggleDedup(ed *Editor) {
	if hl := getHistlist(ed); hl != nil {
		hl.toggleDedup()
	}
}

func histlistToggleCaseSensitivity(ed *Editor) {
	if hl := getHistlist(ed); hl != nil {
		hl.toggleCaseSensitivity()
	}
}

func getHistlist(ed *Editor) *histlist {
	if l, ok := ed.mode.(*listing); ok {
		if hl, ok := l.provider.(*histlist); ok {
			return hl
		}
	}
	return nil
}
<commit_msg>Start histlist in dedup mode by default.<commit_after>package edit

import (
	"errors"
	"fmt"
	"strconv"
	"strings"

	"github.com/elves/elvish/edit/ui"
)

// Command history listing mode.

var _ = registerBuiltins(modeHistoryListing, map[string]func(*Editor){
	"start":                   histlistStart,
	"toggle-dedup":            histlistToggleDedup,
	"toggle-case-sensitivity": histlistToggleCaseSensitivity,
})

func init() {
	registerBindings(modeHistoryListing, modeHistoryListing,
		map[ui.Key]string{
			{'G', ui.Ctrl}: "toggle-case-sensitivity",
			{'D', ui.Ctrl}: "toggle-dedup",
		})
}

// ErrStoreOffline is thrown when an operation requires the storage backend, but
// it is offline.
var ErrStoreOffline = errors.New("store offline")

type histlist struct {
	*listing
	all             []string
	dedup           bool
	caseInsensitive bool
	last            map[string]int
	shown           []string
	index           []int
	indexWidth      int
}

func newHistlist(cmds []string) *listing {
	last := make(map[string]int)
	for i, entry := range cmds {
		last[entry] = i
	}
	hl := &histlist{
		// This has to be here for the initialization to work :(
		listing:    &listing{},
		all:        cmds,
		dedup:      true,
		last:       last,
		indexWidth: len(strconv.Itoa(len(cmds) - 1)),
	}
	l := newListing(modeHistoryListing, hl)
	hl.listing = &l
	return &l
}

func (hl *histlist) ModeTitle(i int) string {
	s := " HISTORY "
	if hl.dedup {
		s += "(dedup on) "
	}
	if hl.caseInsensitive {
		s += "(case-insensitive) "
	}
	return s
}

func (*histlist) CursorOnModeLine() bool {
	return true
}

func (hl *histlist) Len() int {
	return len(hl.shown)
}

func (hl *histlist) Show(i int) (string, ui.Styled) {
	return fmt.Sprintf("%d", hl.index[i]), ui.Unstyled(hl.shown[i])
}

func (hl *histlist) Filter(filter string) int {
	hl.updateShown()
	return len(hl.shown) - 1
}

func (hl *histlist) toggleDedup() {
	hl.dedup = !hl.dedup
	hl.updateShown()
}

func (hl *histlist) toggleCaseSensitivity() {
	hl.caseInsensitive = !hl.caseInsensitive
	hl.updateShown()
}

func (hl *histlist) updateShown() {
	hl.shown = nil
	hl.index = nil
	dedup := hl.dedup
	filter := hl.filter
	if hl.caseInsensitive {
		filter = strings.ToLower(filter)
	}
	for i, entry := range hl.all {
		fentry := entry
		if hl.caseInsensitive {
			fentry = strings.ToLower(entry)
		}
		if (!dedup || hl.last[entry] == i) && strings.Contains(fentry, filter) {
			hl.index = append(hl.index, i)
			hl.shown = append(hl.shown, entry)
		}
	}
	hl.selected = len(hl.shown) - 1
}

// Editor interface.

func (hl *histlist) Accept(i int, ed *Editor) {
	line := hl.shown[i]
	if len(ed.line) > 0 {
		line = "\n" + line
	}
	ed.insertAtDot(line)
}

func histlistStart(ed *Editor) {
	cmds, err := getCmds(ed)
	if err != nil {
		ed.Notify("%v", err)
		return
	}

	ed.mode = newHistlist(cmds)
}

func getCmds(ed *Editor) ([]string, error) {
	if ed.daemon == nil {
		return nil, ErrStoreOffline
	}
	return ed.historyFuser.AllCmds()
}

func histlistToggleDedup(ed *Editor) {
	if hl := getHistlist(ed); hl != nil {
		hl.toggleDedup()
	}
}

func histlistToggleCaseSensitivity(ed *Editor) {
	if hl := getHistlist(ed); hl != nil {
		hl.toggleCaseSensitivity()
	}
}

func getHistlist(ed *Editor) *histlist {
	if l, ok := ed.mode.(*listing); ok {
		if hl, ok := l.provider.(*histlist); ok {
			return hl
		}
	}
	return nil
}
<|endoftext|>


<commit_before>// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package internal

import (
	"context"
	"crypto/ed25519"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"reflect"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/matrix-org/dendrite/keyserver/api"
	"github.com/matrix-org/gomatrixserverlib"
)

var (
	ctx = context.Background()
)

type mockKeyChangeProducer struct {
	events []api.DeviceMessage
}

func (p *mockKeyChangeProducer) ProduceKeyChanges(keys []api.DeviceMessage) error {
	p.events = append(p.events, keys...)
	return nil
}

type mockDeviceListUpdaterDatabase struct {
	staleUsers   map[string]bool
	prevIDsExist func(string, []int) bool
	storedKeys   []api.DeviceMessage
}

// StaleDeviceLists returns a list of user IDs ending with the domains provided who have stale device lists.
// If no domains are given, all user IDs with stale device lists are returned.
func (d *mockDeviceListUpdaterDatabase) StaleDeviceLists(ctx context.Context, domains []gomatrixserverlib.ServerName) ([]string, error) {
	var result []string
	for userID := range d.staleUsers {
		_, remoteServer, err := gomatrixserverlib.SplitID('@', userID)
		if err != nil {
			return nil, err
		}
		if len(domains) == 0 {
			result = append(result, userID)
			continue
		}
		for _, d := range domains {
			if remoteServer == d {
				result = append(result, userID)
				break
			}
		}
	}
	return result, nil
}

// MarkDeviceListStale sets the stale bit for this user to isStale.
func (d *mockDeviceListUpdaterDatabase) MarkDeviceListStale(ctx context.Context, userID string, isStale bool) error {
	d.staleUsers[userID] = isStale
	return nil
}

// StoreRemoteDeviceKeys persists the given keys. Keys with the same user ID and device ID will be replaced. An empty KeyJSON removes the key
// for this (user, device). Does not modify the stream ID for keys.
func (d *mockDeviceListUpdaterDatabase) StoreRemoteDeviceKeys(ctx context.Context, keys []api.DeviceMessage, clear []string) error {
	d.storedKeys = append(d.storedKeys, keys...)
	return nil
}

// PrevIDsExists returns true if all prev IDs exist for this user.
func (d *mockDeviceListUpdaterDatabase) PrevIDsExists(ctx context.Context, userID string, prevIDs []int) (bool, error) {
	return d.prevIDsExist(userID, prevIDs), nil
}

func (d *mockDeviceListUpdaterDatabase) DeviceKeysJSON(ctx context.Context, keys []api.DeviceMessage) error {
	return nil
}

type mockDeviceListUpdaterAPI struct {
}

func (d *mockDeviceListUpdaterAPI) PerformUploadDeviceKeys(ctx context.Context, req *api.PerformUploadDeviceKeysRequest, res *api.PerformUploadDeviceKeysResponse) {

}

type roundTripper struct {
	fn func(*http.Request) (*http.Response, error)
}

func (t *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
	return t.fn(req)
}

func newFedClient(tripper func(*http.Request) (*http.Response, error)) *gomatrixserverlib.FederationClient {
	_, pkey, _ := ed25519.GenerateKey(nil)
	fedClient := gomatrixserverlib.NewFederationClient(
		gomatrixserverlib.ServerName("example.test"), gomatrixserverlib.KeyID("ed25519:test"), pkey,
	)
	fedClient.Client = *gomatrixserverlib.NewClient(
		gomatrixserverlib.WithTransport(&roundTripper{tripper}),
	)
	return fedClient
}

// Test that the device keys get persisted and emitted if we have the previous IDs.
func TestUpdateHavePrevID(t *testing.T) {
	db := &mockDeviceListUpdaterDatabase{
		staleUsers: make(map[string]bool),
		prevIDsExist: func(string, []int) bool {
			return true
		},
	}
	ap := &mockDeviceListUpdaterAPI{}
	producer := &mockKeyChangeProducer{}
	updater := NewDeviceListUpdater(db, ap, producer, nil, 1)
	event := gomatrixserverlib.DeviceListUpdateEvent{
		DeviceDisplayName: "Foo Bar",
		Deleted:           false,
		DeviceID:          "FOO",
		Keys:              []byte(`{"key":"value"}`),
		PrevID:            []int{0},
		StreamID:          1,
		UserID:            "@alice:localhost",
	}
	err := updater.Update(ctx, event)
	if err != nil {
		t.Fatalf("Update returned an error: %s", err)
	}
	want := api.DeviceMessage{
		Type:     api.TypeDeviceKeyUpdate,
		StreamID: event.StreamID,
		DeviceKeys: &api.DeviceKeys{
			DeviceID:    event.DeviceID,
			DisplayName: event.DeviceDisplayName,
			KeyJSON:     event.Keys,
			UserID:      event.UserID,
		},
	}
	if !reflect.DeepEqual(producer.events, []api.DeviceMessage{want}) {
		t.Errorf("Update didn't produce correct event, got %v want %v", producer.events, want)
	}
	if !reflect.DeepEqual(db.storedKeys, []api.DeviceMessage{want}) {
		t.Errorf("DB didn't store correct event, got %v want %v", db.storedKeys, want)
	}
	if db.staleUsers[event.UserID] {
		t.Errorf("%s incorrectly marked as stale", event.UserID)
	}
}

// Test that device keys are fetched from the remote server if we are missing prev IDs
// and that the user's devices are marked as stale until it succeeds.
func TestUpdateNoPrevID(t *testing.T) {
	db := &mockDeviceListUpdaterDatabase{
		staleUsers: make(map[string]bool),
		prevIDsExist: func(string, []int) bool {
			return false
		},
	}
	ap := &mockDeviceListUpdaterAPI{}
	producer := &mockKeyChangeProducer{}
	remoteUserID := "@alice:example.somewhere"
	var wg sync.WaitGroup
	wg.Add(1)
	keyJSON := `{"user_id":"` + remoteUserID + `","device_id":"JLAFKJWSCS","algorithms":["m.olm.v1.curve25519-aes-sha2","m.megolm.v1.aes-sha2"],"keys":{"curve25519:JLAFKJWSCS":"3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI","ed25519:JLAFKJWSCS":"lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"},"signatures":{"` + remoteUserID + `":{"ed25519:JLAFKJWSCS":"dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA"}}}`
	fedClient := newFedClient(func(req *http.Request) (*http.Response, error) {
		defer wg.Done()
		if req.URL.Path != "/_matrix/federation/v1/user/devices/"+url.PathEscape(remoteUserID) {
			return nil, fmt.Errorf("test: invalid path: %s", req.URL.Path)
		}
		return &http.Response{
			StatusCode: 200,
			Body: ioutil.NopCloser(strings.NewReader(`
			{
				"user_id": "` + remoteUserID + `",
				"stream_id": 5,
				"devices": [
				  {
					"device_id": "JLAFKJWSCS",
					"keys": ` + keyJSON + `,
					"device_display_name": "Mobile Phone"
				  }
				]
			  }
			`)),
		}, nil
	})
	updater := NewDeviceListUpdater(db, ap, producer, fedClient, 2)
	if err := updater.Start(); err != nil {
		t.Fatalf("failed to start updater: %s", err)
	}
	event := gomatrixserverlib.DeviceListUpdateEvent{
		DeviceDisplayName: "Mobile Phone",
		Deleted:           false,
		DeviceID:          "another_device_id",
		Keys:              []byte(`{"key":"value"}`),
		PrevID:            []int{3},
		StreamID:          4,
		UserID:            remoteUserID,
	}
	err := updater.Update(ctx, event)
	if err != nil {
		t.Fatalf("Update returned an error: %s", err)
	}
	t.Log("waiting for /users/devices to be called...")
	wg.Wait()
	// wait a bit for db to be updated...
	time.Sleep(100 * time.Millisecond)
	want := api.DeviceMessage{
		Type:     api.TypeDeviceKeyUpdate,
		StreamID: 5,
		DeviceKeys: &api.DeviceKeys{
			DeviceID:    "JLAFKJWSCS",
			DisplayName: "Mobile Phone",
			UserID:      remoteUserID,
			KeyJSON:     []byte(keyJSON),
		},
	}
	// Now we should have a fresh list and the keys and emitted something
	if db.staleUsers[event.UserID] {
		t.Errorf("%s still marked as stale", event.UserID)
	}
	if !reflect.DeepEqual(producer.events, []api.DeviceMessage{want}) {
		t.Logf("len got %d len want %d", len(producer.events[0].KeyJSON), len(want.KeyJSON))
		t.Errorf("Update didn't produce correct event, got %v want %v", producer.events, want)
	}
	if !reflect.DeepEqual(db.storedKeys, []api.DeviceMessage{want}) {
		t.Errorf("DB didn't store correct event, got %v want %v", db.storedKeys, want)
	}

}
<commit_msg>Add unit test for device list update debouncing (#2220)<commit_after>// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package internal

import (
	"context"
	"crypto/ed25519"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"reflect"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/matrix-org/dendrite/keyserver/api"
	"github.com/matrix-org/gomatrixserverlib"
)

var (
	ctx = context.Background()
)

type mockKeyChangeProducer struct {
	events []api.DeviceMessage
}

func (p *mockKeyChangeProducer) ProduceKeyChanges(keys []api.DeviceMessage) error {
	p.events = append(p.events, keys...)
	return nil
}

type mockDeviceListUpdaterDatabase struct {
	staleUsers   map[string]bool
	prevIDsExist func(string, []int) bool
	storedKeys   []api.DeviceMessage
	mu           sync.Mutex // protect staleUsers
}

// StaleDeviceLists returns a list of user IDs ending with the domains provided who have stale device lists.
// If no domains are given, all user IDs with stale device lists are returned.
func (d *mockDeviceListUpdaterDatabase) StaleDeviceLists(ctx context.Context, domains []gomatrixserverlib.ServerName) ([]string, error) {
	d.mu.Lock()
	defer d.mu.Unlock()
	var result []string
	for userID, isStale := range d.staleUsers {
		if !isStale {
			continue
		}
		_, remoteServer, err := gomatrixserverlib.SplitID('@', userID)
		if err != nil {
			return nil, err
		}
		if len(domains) == 0 {
			result = append(result, userID)
			continue
		}
		for _, d := range domains {
			if remoteServer == d {
				result = append(result, userID)
				break
			}
		}
	}
	return result, nil
}

// MarkDeviceListStale sets the stale bit for this user to isStale.
func (d *mockDeviceListUpdaterDatabase) MarkDeviceListStale(ctx context.Context, userID string, isStale bool) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	d.staleUsers[userID] = isStale
	return nil
}

// StoreRemoteDeviceKeys persists the given keys. Keys with the same user ID and device ID will be replaced. An empty KeyJSON removes the key
// for this (user, device). Does not modify the stream ID for keys.
func (d *mockDeviceListUpdaterDatabase) StoreRemoteDeviceKeys(ctx context.Context, keys []api.DeviceMessage, clear []string) error {
	d.storedKeys = append(d.storedKeys, keys...)
	return nil
}

// PrevIDsExists returns true if all prev IDs exist for this user.
func (d *mockDeviceListUpdaterDatabase) PrevIDsExists(ctx context.Context, userID string, prevIDs []int) (bool, error) {
	return d.prevIDsExist(userID, prevIDs), nil
}

func (d *mockDeviceListUpdaterDatabase) DeviceKeysJSON(ctx context.Context, keys []api.DeviceMessage) error {
	return nil
}

type mockDeviceListUpdaterAPI struct {
}

func (d *mockDeviceListUpdaterAPI) PerformUploadDeviceKeys(ctx context.Context, req *api.PerformUploadDeviceKeysRequest, res *api.PerformUploadDeviceKeysResponse) {

}

type roundTripper struct {
	fn func(*http.Request) (*http.Response, error)
}

func (t *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
	return t.fn(req)
}

func newFedClient(tripper func(*http.Request) (*http.Response, error)) *gomatrixserverlib.FederationClient {
	_, pkey, _ := ed25519.GenerateKey(nil)
	fedClient := gomatrixserverlib.NewFederationClient(
		gomatrixserverlib.ServerName("example.test"), gomatrixserverlib.KeyID("ed25519:test"), pkey,
	)
	fedClient.Client = *gomatrixserverlib.NewClient(
		gomatrixserverlib.WithTransport(&roundTripper{tripper}),
	)
	return fedClient
}

// Test that the device keys get persisted and emitted if we have the previous IDs.
func TestUpdateHavePrevID(t *testing.T) {
	db := &mockDeviceListUpdaterDatabase{
		staleUsers: make(map[string]bool),
		prevIDsExist: func(string, []int) bool {
			return true
		},
	}
	ap := &mockDeviceListUpdaterAPI{}
	producer := &mockKeyChangeProducer{}
	updater := NewDeviceListUpdater(db, ap, producer, nil, 1)
	event := gomatrixserverlib.DeviceListUpdateEvent{
		DeviceDisplayName: "Foo Bar",
		Deleted:           false,
		DeviceID:          "FOO",
		Keys:              []byte(`{"key":"value"}`),
		PrevID:            []int{0},
		StreamID:          1,
		UserID:            "@alice:localhost",
	}
	err := updater.Update(ctx, event)
	if err != nil {
		t.Fatalf("Update returned an error: %s", err)
	}
	want := api.DeviceMessage{
		Type:     api.TypeDeviceKeyUpdate,
		StreamID: event.StreamID,
		DeviceKeys: &api.DeviceKeys{
			DeviceID:    event.DeviceID,
			DisplayName: event.DeviceDisplayName,
			KeyJSON:     event.Keys,
			UserID:      event.UserID,
		},
	}
	if !reflect.DeepEqual(producer.events, []api.DeviceMessage{want}) {
		t.Errorf("Update didn't produce correct event, got %v want %v", producer.events, want)
	}
	if !reflect.DeepEqual(db.storedKeys, []api.DeviceMessage{want}) {
		t.Errorf("DB didn't store correct event, got %v want %v", db.storedKeys, want)
	}
	if db.staleUsers[event.UserID] {
		t.Errorf("%s incorrectly marked as stale", event.UserID)
	}
}

// Test that device keys are fetched from the remote server if we are missing prev IDs
// and that the user's devices are marked as stale until it succeeds.
func TestUpdateNoPrevID(t *testing.T) {
	db := &mockDeviceListUpdaterDatabase{
		staleUsers: make(map[string]bool),
		prevIDsExist: func(string, []int) bool {
			return false
		},
	}
	ap := &mockDeviceListUpdaterAPI{}
	producer := &mockKeyChangeProducer{}
	remoteUserID := "@alice:example.somewhere"
	var wg sync.WaitGroup
	wg.Add(1)
	keyJSON := `{"user_id":"` + remoteUserID + `","device_id":"JLAFKJWSCS","algorithms":["m.olm.v1.curve25519-aes-sha2","m.megolm.v1.aes-sha2"],"keys":{"curve25519:JLAFKJWSCS":"3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI","ed25519:JLAFKJWSCS":"lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"},"signatures":{"` + remoteUserID + `":{"ed25519:JLAFKJWSCS":"dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA"}}}`
	fedClient := newFedClient(func(req *http.Request) (*http.Response, error) {
		defer wg.Done()
		if req.URL.Path != "/_matrix/federation/v1/user/devices/"+url.PathEscape(remoteUserID) {
			return nil, fmt.Errorf("test: invalid path: %s", req.URL.Path)
		}
		return &http.Response{
			StatusCode: 200,
			Body: ioutil.NopCloser(strings.NewReader(`
			{
				"user_id": "` + remoteUserID + `",
				"stream_id": 5,
				"devices": [
				  {
					"device_id": "JLAFKJWSCS",
					"keys": ` + keyJSON + `,
					"device_display_name": "Mobile Phone"
				  }
				]
			  }
			`)),
		}, nil
	})
	updater := NewDeviceListUpdater(db, ap, producer, fedClient, 2)
	if err := updater.Start(); err != nil {
		t.Fatalf("failed to start updater: %s", err)
	}
	event := gomatrixserverlib.DeviceListUpdateEvent{
		DeviceDisplayName: "Mobile Phone",
		Deleted:           false,
		DeviceID:          "another_device_id",
		Keys:              []byte(`{"key":"value"}`),
		PrevID:            []int{3},
		StreamID:          4,
		UserID:            remoteUserID,
	}
	err := updater.Update(ctx, event)
	if err != nil {
		t.Fatalf("Update returned an error: %s", err)
	}
	t.Log("waiting for /users/devices to be called...")
	wg.Wait()
	// wait a bit for db to be updated...
	time.Sleep(100 * time.Millisecond)
	want := api.DeviceMessage{
		Type:     api.TypeDeviceKeyUpdate,
		StreamID: 5,
		DeviceKeys: &api.DeviceKeys{
			DeviceID:    "JLAFKJWSCS",
			DisplayName: "Mobile Phone",
			UserID:      remoteUserID,
			KeyJSON:     []byte(keyJSON),
		},
	}
	// Now we should have a fresh list and the keys and emitted something
	if db.staleUsers[event.UserID] {
		t.Errorf("%s still marked as stale", event.UserID)
	}
	if !reflect.DeepEqual(producer.events, []api.DeviceMessage{want}) {
		t.Logf("len got %d len want %d", len(producer.events[0].KeyJSON), len(want.KeyJSON))
		t.Errorf("Update didn't produce correct event, got %v want %v", producer.events, want)
	}
	if !reflect.DeepEqual(db.storedKeys, []api.DeviceMessage{want}) {
		t.Errorf("DB didn't store correct event, got %v want %v", db.storedKeys, want)
	}

}

// Test that if we make N calls to ManualUpdate for the same user, we only do it once, assuming the
// update is still ongoing.
func TestDebounce(t *testing.T) {
	db := &mockDeviceListUpdaterDatabase{
		staleUsers: make(map[string]bool),
		prevIDsExist: func(string, []int) bool {
			return true
		},
	}
	ap := &mockDeviceListUpdaterAPI{}
	producer := &mockKeyChangeProducer{}
	fedCh := make(chan *http.Response, 1)
	srv := gomatrixserverlib.ServerName("example.com")
	userID := "@alice:example.com"
	keyJSON := `{"user_id":"` + userID + `","device_id":"JLAFKJWSCS","algorithms":["m.olm.v1.curve25519-aes-sha2","m.megolm.v1.aes-sha2"],"keys":{"curve25519:JLAFKJWSCS":"3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI","ed25519:JLAFKJWSCS":"lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"},"signatures":{"` + userID + `":{"ed25519:JLAFKJWSCS":"dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA"}}}`
	incomingFedReq := make(chan struct{})
	fedClient := newFedClient(func(req *http.Request) (*http.Response, error) {
		if req.URL.Path != "/_matrix/federation/v1/user/devices/"+url.PathEscape(userID) {
			return nil, fmt.Errorf("test: invalid path: %s", req.URL.Path)
		}
		close(incomingFedReq)
		return <-fedCh, nil
	})
	updater := NewDeviceListUpdater(db, ap, producer, fedClient, 1)
	if err := updater.Start(); err != nil {
		t.Fatalf("failed to start updater: %s", err)
	}

	// hit this 5 times
	var wg sync.WaitGroup
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func() {
			defer wg.Done()
			if err := updater.ManualUpdate(context.Background(), srv, userID); err != nil {
				t.Errorf("ManualUpdate: %s", err)
			}
		}()
	}

	// wait until the updater hits federation
	select {
	case <-incomingFedReq:
	case <-time.After(time.Second):
		t.Fatalf("timed out waiting for updater to hit federation")
	}

	// user should be marked as stale
	if !db.staleUsers[userID] {
		t.Errorf("user %s not marked as stale", userID)
	}
	// now send the response over federation
	fedCh <- &http.Response{
		StatusCode: 200,
		Body: ioutil.NopCloser(strings.NewReader(`
		{
			"user_id": "` + userID + `",
			"stream_id": 5,
			"devices": [
			  {
				"device_id": "JLAFKJWSCS",
				"keys": ` + keyJSON + `,
				"device_display_name": "Mobile Phone"
			  }
			]
		  }
		`)),
	}
	close(fedCh)
	// wait until all 5 ManualUpdates return. If we hit federation again we won't send a response
	// and should panic with read on a closed channel
	wg.Wait()

	// user is no longer stale now
	if db.staleUsers[userID] {
		t.Errorf("user %s is marked as stale", userID)
	}
}
<|endoftext|>


<commit_before>package cli_test

import (
	. "github.com/jwaldrip/odin/cli"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("CLI Start", func() {

	var cli *CLI
	var cmd Command
	var didRun bool

	BeforeEach(func() {
		didRun = false
		runFn := func(c Command) {
			cmd = c
			didRun = true
		}
		cli = New("v1.0.0", "sample description", runFn)
		cli.ErrorHandling = PanicOnError
		//cli.Mute()
	})

	Describe("DefineSubCommand", func() {
		It("allow the defined command to run", func() {
			var subDidRun bool
			cli.DefineSubCommand("foo", "sub command", func(c Command) { subDidRun = true })
			cli.Start("cmd", "foo")
			Expect(subDidRun).To(Equal(true))
		})
	})

	Describe("AddSubCommand", func() {
		It("allow the defined command to run", func() {
			var subDidRun bool
			sub := NewSubCommand("foo", "sub command", func(c Command) { subDidRun = true })
			cli.AddSubCommand(sub)
			cli.Start("cmd", "foo")
			Expect(subDidRun).To(Equal(true))
		})

		It("should inherit ErrorHandling and fail when missing params", func() {
			cli.Mute()
			var subDidRun bool
			sub := NewSubCommand("foo", "sub command", func(c Command) { subDidRun = true }, "bar")
			cli.AddSubCommand(sub)
			Expect(func() { cli.Start("cmd", "foo") }).Should(Panic())
			Expect(subDidRun).To(Equal(false))
		})
	})

	Describe("AddSubCommand", func() {
		It("allow the defined command to run", func() {
			var subOneDidRun bool
			var subTwoDidRun bool
			subOne := NewSubCommand("foo", "sub command", func(c Command) { subOneDidRun = true })
			subTwo := NewSubCommand("bar", "sub command", func(c Command) { subTwoDidRun = true })
			cli.AddSubCommands(subOne, subTwo)
			cli.Start("cmd", "foo")
			cli.Start("cmd", "bar")
			Expect(subOneDidRun).To(Equal(true))
			Expect(subTwoDidRun).To(Equal(true))
		})
	})
})
<commit_msg>Correct name of describe block for AddSubCommands<commit_after>package cli_test

import (
	. "github.com/jwaldrip/odin/cli"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("CLI Start", func() {

	var cli *CLI
	var cmd Command
	var didRun bool

	BeforeEach(func() {
		didRun = false
		runFn := func(c Command) {
			cmd = c
			didRun = true
		}
		cli = New("v1.0.0", "sample description", runFn)
		cli.ErrorHandling = PanicOnError
		//cli.Mute()
	})

	Describe("DefineSubCommand", func() {
		It("allow the defined command to run", func() {
			var subDidRun bool
			cli.DefineSubCommand("foo", "sub command", func(c Command) { subDidRun = true })
			cli.Start("cmd", "foo")
			Expect(subDidRun).To(Equal(true))
		})
	})

	Describe("AddSubCommand", func() {
		It("allow the defined command to run", func() {
			var subDidRun bool
			sub := NewSubCommand("foo", "sub command", func(c Command) { subDidRun = true })
			cli.AddSubCommand(sub)
			cli.Start("cmd", "foo")
			Expect(subDidRun).To(Equal(true))
		})

		It("should inherit ErrorHandling and fail when missing params", func() {
			cli.Mute()
			var subDidRun bool
			sub := NewSubCommand("foo", "sub command", func(c Command) { subDidRun = true }, "bar")
			cli.AddSubCommand(sub)
			Expect(func() { cli.Start("cmd", "foo") }).Should(Panic())
			Expect(subDidRun).To(Equal(false))
		})
	})

	Describe("AddSubCommands", func() {
		It("allow the defined command to run", func() {
			var subOneDidRun bool
			var subTwoDidRun bool
			subOne := NewSubCommand("foo", "sub command", func(c Command) { subOneDidRun = true })
			subTwo := NewSubCommand("bar", "sub command", func(c Command) { subTwoDidRun = true })
			cli.AddSubCommands(subOne, subTwo)
			cli.Start("cmd", "foo")
			cli.Start("cmd", "bar")
			Expect(subOneDidRun).To(Equal(true))
			Expect(subTwoDidRun).To(Equal(true))
		})
	})
})
<|endoftext|>


<commit_before>package editor

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"

	"github.com/nsf/termbox-go"
	"github.com/wx13/sith/config"
	"github.com/wx13/sith/file"
	"github.com/wx13/sith/terminal"
	"github.com/wx13/sith/ui"
)

// Editor is the main editor object. It orchestrates the terminal,
// the buffer, etc.
type Editor struct {
	screen     *terminal.Screen
	file       *file.File
	files      []*file.File
	fileIdx    int
	fileIdxPrv int
	keyboard   *terminal.Keyboard
	flushChan  chan struct{}
	keymap     KeyMap
	xKeymap    KeyMap

	searchHist  []string
	replaceHist []string

	copyBuffer []string
	copyContig int
	copyHist   [][]string

	cfg config.Config
}

// NewEditor creates a new Editor object.
func NewEditor() *Editor {
	return &Editor{
		flushChan:  make(chan struct{}, 1),
		screen:     terminal.NewScreen(),
		copyBuffer: []string{},
		copyContig: 0,
		copyHist:   [][]string{},
		cfg:        config.CreateConfig(),
	}
}

// OpenNewFile offers a file selection menu to choose a new file to open.
func (editor *Editor) OpenNewFile() {
	dir, _ := os.Getwd()
	dir += "/"
	filename := ""
	for {
		files, _ := ioutil.ReadDir(dir)
		dotdot, err := os.Stat("../")
		if err == nil {
			files = append([]os.FileInfo{dotdot}, files...)
		}
		names := []string{}
		for _, file := range files {
			if file.IsDir() {
				names = append(names, file.Name()+"/")
			} else {
				names = append(names, file.Name())
			}
		}
		menu := ui.NewMenu(editor.screen, editor.keyboard)
		idx, key := menu.Choose(names, 0, "ctrlO")
		editor.Flush()
		if idx < 0 || key == "cancel" {
			return
		}
		if key == "ctrlO" {
			var err error
			p := ui.MakePrompt(editor.screen, editor.keyboard)
			filename, err = p.Ask(dir, nil)
			if err != nil {
				editor.screen.Notify("Unknown answer")
				return
			}
			break
		}
		chosenFile := files[idx]
		if chosenFile.IsDir() {
			dir = filepath.Clean(dir+chosenFile.Name()) + "/"
		} else {
			filename = names[idx]
			break
		}
	}
	cwd, _ := os.Getwd()
	chosenFile, _ := filepath.Rel(cwd, dir+filename)
	editor.OpenFile(chosenFile)
	editor.fileIdxPrv = editor.fileIdx
	editor.fileIdx = len(editor.files) - 1
	editor.file = editor.files[editor.fileIdx]
}

// OpenFile opens a specified file.
func (editor *Editor) OpenFile(name string) {
	file := file.NewFile(name, editor.flushChan, editor.screen, editor.cfg)
	editor.files = append(editor.files, file)
}

// OpenFiles opens a set of specified files.
func (editor *Editor) OpenFiles(fileNames []string) {
	for _, name := range fileNames {
		editor.OpenFile(name)
	}
	if len(editor.files) == 0 {
		editor.files = append(editor.files, file.NewFile("", editor.flushChan, editor.screen, editor.cfg))
	}
	editor.fileIdx = 0
	editor.fileIdxPrv = 0
	editor.file = editor.files[0]
}

// ReloadAll re-reads all open buffers.
func (editor *Editor) ReloadAll() {
	for _, file := range editor.files {
		file.Reload()
	}
}

// Quit closes all the files and exits the editor.
func (editor *Editor) Quit() {
	for range editor.files {
		if !editor.CloseFile() {
			editor.NextFile()
		}
	}
}

// CloseFile closes the current file.
func (editor *Editor) CloseFile() bool {
	editor.Flush()
	idx := editor.fileIdx
	if !editor.files[idx].Close() {
		return false
	}
	editor.files = append(editor.files[:idx], editor.files[idx+1:]...)
	if len(editor.files) == 0 {
		editor.screen.Close()
		return true
	}
	editor.NextFile()
	return true
}

// Listen is the main editor loop.
func (editor *Editor) Listen() {

	editor.keyboard = terminal.NewKeyboard()
	editor.keymap = editor.MakeKeyMap()
	editor.xKeymap = editor.MakeExtraKeyMap()
	for {
		cmd, r := editor.keyboard.GetKey()
		editor.handleCmd(cmd, r)
		editor.copyContig--
		editor.RequestFlush()
	}

}

func (editor *Editor) handleCmd(cmd string, r rune) {
	ans := editor.keymap.Run(cmd)
	if ans == "" {
		return
	}
	if ans == "char" {
		editor.file.InsertChar(r)
	} else {
		editor.screen.Notify("Unknown keypress")
	}
}

// ExtraMode allows for additional keypresses.
func (editor *Editor) ExtraMode() {
	p := ui.MakePrompt(editor.screen, editor.keyboard)
	r := p.GetRune("key:")
	ans := editor.xKeymap.Run(string(r))
	if len(ans) > 0 {
		editor.screen.Notify("Unknown command")
	}
}

// NextFile cycles to the next open file.
func (editor *Editor) NextFile() {
	editor.SwitchFile(editor.fileIdx + 1)
}

// PrevFile cycles to the previous open file.
func (editor *Editor) PrevFile() {
	editor.SwitchFile(editor.fileIdx - 1)
}

// LastFile toggles between the two most recent files.
func (editor *Editor) LastFile() {
	editor.SwitchFile(editor.fileIdxPrv)
}

// SelectFile offers a menu to select from open files.
func (editor *Editor) SelectFile() {
	names := []string{}
	for _, file := range editor.files {
		status := ""
		if file.IsModified() {
			status = "*"
		}
		if file.FileChanged() {
			status += "+"
		}
		names = append(names, status+file.Name)
	}
	menu := ui.NewMenu(editor.screen, editor.keyboard)
	idx, cmd := menu.Choose(names, editor.fileIdx)
	if idx >= 0 && cmd == "" {
		editor.SwitchFile(idx)
	}
}

// SetCharMode offers a menu for selecting the character
// display mode.
func (editor *Editor) SetCharMode() {
	modes := editor.screen.ListCharModes()
	menu := ui.NewMenu(editor.screen, editor.keyboard)
	idx, cmd := menu.Choose(modes, 0)
	if idx >= 0 && cmd == "" {
		editor.screen.SetCharMode(idx)
	}
}

// CmdMenu offers a menu of available commands.
func (editor *Editor) CmdMenu() {

	keys := editor.keymap.Keys()
	sort.Strings(keys)
	names := editor.keymap.DisplayNames(keys, "")

	xkeys := editor.xKeymap.Keys()
	sort.Strings(xkeys)
	xnames := editor.xKeymap.DisplayNames(xkeys, "Alt-6 ")

	names = append(names, xnames...)

	menu := ui.NewMenu(editor.screen, editor.keyboard)
	idx, cancel := menu.Choose(names, 0)
	if idx < 0 || cancel != "" {
		return
	}

	if idx < len(keys) {
		key := keys[idx]
		editor.keymap.Run(key)
	} else {
		key := xkeys[idx-len(keys)]
		editor.xKeymap.Run(key)
	}

}

// Save saves the buffer to the file.
func (editor *Editor) Save() {
	editor.file.RequestSave()
}

// SaveAll saves all the open buffers.
func (editor *Editor) SaveAll() {
	for _, file := range editor.files {
		file.RequestSave()
	}
}

// SaveAs prompts for a file to save to.
func (editor *Editor) SaveAs() {
	p := ui.MakePrompt(editor.screen, editor.keyboard)
	filename, err := p.Ask("Save to:", nil)
	if err != nil {
		editor.screen.Notify("Cancelled")
		return
	}
	editor.file.Name = filename
	editor.Save()
}

func intMod(a, n int) int {
	if a < 0 {
		return a - n*((a-n+1)/n)
	}
	return a - n*(a/n)
}

// SwitchFile changes to a new file buffer.
func (editor *Editor) SwitchFile(n int) {
	n = intMod(n, len(editor.files))
	editor.fileIdxPrv = editor.fileIdx
	editor.fileIdx = n
	editor.file = editor.files[n]
}

// HighlightCursors highlights all the multi-cursors.
func (editor *Editor) HighlightCursors() {
	cells := termbox.CellBuffer()
	cols, rows := terminal.Size()
	r0, c0 := editor.file.GetCursor(0)
	for k := range editor.file.MultiCursor.Cursors()[1:] {
		r, c := editor.file.GetCursor(k + 1)
		if r < 0 || r > rows || c < 0 || c > cols {
			continue
		}
		j := r*cols + c
		if j < 0 || j >= len(cells) {
			continue
		}
		if r == r0 && c == c0 {
			cells[j].Bg |= termbox.AttrBold
			cells[j].Fg |= termbox.AttrBold | termbox.ColorYellow
		} else {
			cells[j].Bg |= termbox.AttrReverse
			cells[j].Fg |= termbox.AttrReverse
		}
	}
}

// Flush writes the current buffer to the screen.
func (editor *Editor) Flush() {
	editor.file.Flush()
	editor.HighlightCursors()
	editor.UpdateStatus()
	editor.screen.Flush()
}

// KeepFlushed waits for flush requests, and then flushes
// to the screen.
func (editor *Editor) KeepFlushed() {
	go func() {
		for {
			<-editor.flushChan
			editor.Flush()
		}
	}()
}

// RequestFlush requests a flush event (async).
func (editor *Editor) RequestFlush() {
	select {
	case editor.flushChan <- struct{}{}:
	default:
	}
}

func (editor *Editor) getFilename(maxNameLen int) string {
	name := editor.file.Name
	nameLen := len(name)
	if nameLen > maxNameLen {
		name = name[0:maxNameLen/2] + "..." + name[nameLen-maxNameLen/2:nameLen]
	}
	return name
}

func (editor *Editor) writeModStatus(row, col int) int {
	if editor.file.IsModified() {
		editor.screen.WriteStringColor(row, col-3, "M  ", terminal.ColorRed, terminal.ColorDefault)
		return 3
	}
	for _, file := range editor.files {
		if file.IsModified() {
			editor.screen.WriteStringColor(row, col-3, "M  ", terminal.ColorYellow, terminal.ColorDefault)
			return 3
		}
	}
	return 0
}

func (editor *Editor) writeSyncStatus(row, col int) int {
	if editor.file.FileChanged() {
		editor.screen.WriteStringColor(row, col-3, "S  ", terminal.ColorRed, terminal.ColorDefault)
		return 3
	}
	for _, file := range editor.files {
		if file.FileChanged() {
			editor.screen.WriteStringColor(row, col-3, "S  ", terminal.ColorYellow, terminal.ColorDefault)
			return 3
		}
	}
	return 0
}

// UpdateStatus updates the status line.
func (editor *Editor) UpdateStatus() {
	cols, rows := terminal.Size()

	name := editor.getFilename(cols / 3)
	message := fmt.Sprintf("%s (%d/%d)   %d/%d,%d",
		name,
		editor.fileIdx,
		len(editor.files),
		editor.file.MultiCursor.GetRow(0),
		editor.file.Length()-1,
		editor.file.MultiCursor.GetCol(0),
	)
	col := cols - len(message)
	editor.screen.WriteString(rows-1, col, message)
	editor.screen.WriteString(rows-1, 0, "[ Sith 0.6.1 ]")
	editor.screen.DecorateStatusLine()
	col -= editor.writeModStatus(rows-1, col)
	col -= editor.writeSyncStatus(rows-1, col)
	editor.file.WriteStatus(rows-1, col)
	editor.screen.SetCursor(editor.file.GetCursor(0))
}
<commit_msg>allow user to reorder file buffers, using the file selection menu<commit_after>package editor

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"

	"github.com/nsf/termbox-go"
	"github.com/wx13/sith/config"
	"github.com/wx13/sith/file"
	"github.com/wx13/sith/terminal"
	"github.com/wx13/sith/ui"
)

// Editor is the main editor object. It orchestrates the terminal,
// the buffer, etc.
type Editor struct {
	screen     *terminal.Screen
	file       *file.File
	files      []*file.File
	fileIdx    int
	fileIdxPrv int
	keyboard   *terminal.Keyboard
	flushChan  chan struct{}
	keymap     KeyMap
	xKeymap    KeyMap

	searchHist  []string
	replaceHist []string

	copyBuffer []string
	copyContig int
	copyHist   [][]string

	cfg config.Config
}

// NewEditor creates a new Editor object.
func NewEditor() *Editor {
	return &Editor{
		flushChan:  make(chan struct{}, 1),
		screen:     terminal.NewScreen(),
		copyBuffer: []string{},
		copyContig: 0,
		copyHist:   [][]string{},
		cfg:        config.CreateConfig(),
	}
}

// OpenNewFile offers a file selection menu to choose a new file to open.
func (editor *Editor) OpenNewFile() {
	dir, _ := os.Getwd()
	dir += "/"
	filename := ""
	for {
		files, _ := ioutil.ReadDir(dir)
		dotdot, err := os.Stat("../")
		if err == nil {
			files = append([]os.FileInfo{dotdot}, files...)
		}
		names := []string{}
		for _, file := range files {
			if file.IsDir() {
				names = append(names, file.Name()+"/")
			} else {
				names = append(names, file.Name())
			}
		}
		menu := ui.NewMenu(editor.screen, editor.keyboard)
		idx, key := menu.Choose(names, 0, "ctrlO")
		editor.Flush()
		if idx < 0 || key == "cancel" {
			return
		}
		if key == "ctrlO" {
			var err error
			p := ui.MakePrompt(editor.screen, editor.keyboard)
			filename, err = p.Ask(dir, nil)
			if err != nil {
				editor.screen.Notify("Unknown answer")
				return
			}
			break
		}
		chosenFile := files[idx]
		if chosenFile.IsDir() {
			dir = filepath.Clean(dir+chosenFile.Name()) + "/"
		} else {
			filename = names[idx]
			break
		}
	}
	cwd, _ := os.Getwd()
	chosenFile, _ := filepath.Rel(cwd, dir+filename)
	editor.OpenFile(chosenFile)
	editor.fileIdxPrv = editor.fileIdx
	editor.fileIdx = len(editor.files) - 1
	editor.file = editor.files[editor.fileIdx]
}

// OpenFile opens a specified file.
func (editor *Editor) OpenFile(name string) {
	file := file.NewFile(name, editor.flushChan, editor.screen, editor.cfg)
	editor.files = append(editor.files, file)
}

// OpenFiles opens a set of specified files.
func (editor *Editor) OpenFiles(fileNames []string) {
	for _, name := range fileNames {
		editor.OpenFile(name)
	}
	if len(editor.files) == 0 {
		editor.files = append(editor.files, file.NewFile("", editor.flushChan, editor.screen, editor.cfg))
	}
	editor.fileIdx = 0
	editor.fileIdxPrv = 0
	editor.file = editor.files[0]
}

// ReloadAll re-reads all open buffers.
func (editor *Editor) ReloadAll() {
	for _, file := range editor.files {
		file.Reload()
	}
}

// Quit closes all the files and exits the editor.
func (editor *Editor) Quit() {
	for range editor.files {
		if !editor.CloseFile() {
			editor.NextFile()
		}
	}
}

// CloseFile closes the current file.
func (editor *Editor) CloseFile() bool {
	editor.Flush()
	idx := editor.fileIdx
	if !editor.files[idx].Close() {
		return false
	}
	editor.files = append(editor.files[:idx], editor.files[idx+1:]...)
	if len(editor.files) == 0 {
		editor.screen.Close()
		return true
	}
	editor.NextFile()
	return true
}

// Listen is the main editor loop.
func (editor *Editor) Listen() {

	editor.keyboard = terminal.NewKeyboard()
	editor.keymap = editor.MakeKeyMap()
	editor.xKeymap = editor.MakeExtraKeyMap()
	for {
		cmd, r := editor.keyboard.GetKey()
		editor.handleCmd(cmd, r)
		editor.copyContig--
		editor.RequestFlush()
	}

}

func (editor *Editor) handleCmd(cmd string, r rune) {
	ans := editor.keymap.Run(cmd)
	if ans == "" {
		return
	}
	if ans == "char" {
		editor.file.InsertChar(r)
	} else {
		editor.screen.Notify("Unknown keypress")
	}
}

// ExtraMode allows for additional keypresses.
func (editor *Editor) ExtraMode() {
	p := ui.MakePrompt(editor.screen, editor.keyboard)
	r := p.GetRune("key:")
	ans := editor.xKeymap.Run(string(r))
	if len(ans) > 0 {
		editor.screen.Notify("Unknown command")
	}
}

// NextFile cycles to the next open file.
func (editor *Editor) NextFile() {
	editor.SwitchFile(editor.fileIdx + 1)
}

// PrevFile cycles to the previous open file.
func (editor *Editor) PrevFile() {
	editor.SwitchFile(editor.fileIdx - 1)
}

// LastFile toggles between the two most recent files.
func (editor *Editor) LastFile() {
	editor.SwitchFile(editor.fileIdxPrv)
}

// SelectFile offers a menu to select from open files.
func (editor *Editor) SelectFile() {
	menu := ui.NewMenu(editor.screen, editor.keyboard)
	idx := editor.fileIdx
	cmd := ""
	// Allow user to swap file order.
	for {
		names := []string{}
		for _, file := range editor.files {
			status := ""
			if file.IsModified() {
				status = "*"
			}
			if file.FileChanged() {
				status += "+"
			}
			names = append(names, status+file.Name)
		}
		idx, cmd = menu.Choose(names, idx, "ctrlJ", "ctrlK")
		if cmd == "ctrlJ" {
			if idx >= len(editor.files)-1 {
				continue
			}
			file := editor.files[idx]
			editor.files[idx] = editor.files[idx+1]
			idx++
			editor.files[idx] = file
			continue
		}
		if cmd == "ctrlK" {
			if idx == 0 {
				continue
			}
			file := editor.files[idx]
			editor.files[idx] = editor.files[idx-1]
			idx--
			editor.files[idx] = file
			continue
		}
		break
	}
	// Switch file.
	if idx >= 0 && cmd == "" {
		editor.SwitchFile(idx)
	}
}

// SetCharMode offers a menu for selecting the character
// display mode.
func (editor *Editor) SetCharMode() {
	modes := editor.screen.ListCharModes()
	menu := ui.NewMenu(editor.screen, editor.keyboard)
	idx, cmd := menu.Choose(modes, 0)
	if idx >= 0 && cmd == "" {
		editor.screen.SetCharMode(idx)
	}
}

// CmdMenu offers a menu of available commands.
func (editor *Editor) CmdMenu() {

	keys := editor.keymap.Keys()
	sort.Strings(keys)
	names := editor.keymap.DisplayNames(keys, "")

	xkeys := editor.xKeymap.Keys()
	sort.Strings(xkeys)
	xnames := editor.xKeymap.DisplayNames(xkeys, "Alt-6 ")

	names = append(names, xnames...)

	menu := ui.NewMenu(editor.screen, editor.keyboard)
	idx, cancel := menu.Choose(names, 0)
	if idx < 0 || cancel != "" {
		return
	}

	if idx < len(keys) {
		key := keys[idx]
		editor.keymap.Run(key)
	} else {
		key := xkeys[idx-len(keys)]
		editor.xKeymap.Run(key)
	}

}

// Save saves the buffer to the file.
func (editor *Editor) Save() {
	editor.file.RequestSave()
}

// SaveAll saves all the open buffers.
func (editor *Editor) SaveAll() {
	for _, file := range editor.files {
		file.RequestSave()
	}
}

// SaveAs prompts for a file to save to.
func (editor *Editor) SaveAs() {
	p := ui.MakePrompt(editor.screen, editor.keyboard)
	filename, err := p.Ask("Save to:", nil)
	if err != nil {
		editor.screen.Notify("Cancelled")
		return
	}
	editor.file.Name = filename
	editor.Save()
}

func intMod(a, n int) int {
	if a < 0 {
		return a - n*((a-n+1)/n)
	}
	return a - n*(a/n)
}

// SwitchFile changes to a new file buffer.
func (editor *Editor) SwitchFile(n int) {
	n = intMod(n, len(editor.files))
	editor.fileIdxPrv = editor.fileIdx
	editor.fileIdx = n
	editor.file = editor.files[n]
}

// HighlightCursors highlights all the multi-cursors.
func (editor *Editor) HighlightCursors() {
	cells := termbox.CellBuffer()
	cols, rows := terminal.Size()
	r0, c0 := editor.file.GetCursor(0)
	for k := range editor.file.MultiCursor.Cursors()[1:] {
		r, c := editor.file.GetCursor(k + 1)
		if r < 0 || r > rows || c < 0 || c > cols {
			continue
		}
		j := r*cols + c
		if j < 0 || j >= len(cells) {
			continue
		}
		if r == r0 && c == c0 {
			cells[j].Bg |= termbox.AttrBold
			cells[j].Fg |= termbox.AttrBold | termbox.ColorYellow
		} else {
			cells[j].Bg |= termbox.AttrReverse
			cells[j].Fg |= termbox.AttrReverse
		}
	}
}

// Flush writes the current buffer to the screen.
func (editor *Editor) Flush() {
	editor.file.Flush()
	editor.HighlightCursors()
	editor.UpdateStatus()
	editor.screen.Flush()
}

// KeepFlushed waits for flush requests, and then flushes
// to the screen.
func (editor *Editor) KeepFlushed() {
	go func() {
		for {
			<-editor.flushChan
			editor.Flush()
		}
	}()
}

// RequestFlush requests a flush event (async).
func (editor *Editor) RequestFlush() {
	select {
	case editor.flushChan <- struct{}{}:
	default:
	}
}

func (editor *Editor) getFilename(maxNameLen int) string {
	name := editor.file.Name
	nameLen := len(name)
	if nameLen > maxNameLen {
		name = name[0:maxNameLen/2] + "..." + name[nameLen-maxNameLen/2:nameLen]
	}
	return name
}

func (editor *Editor) writeModStatus(row, col int) int {
	if editor.file.IsModified() {
		editor.screen.WriteStringColor(row, col-3, "M  ", terminal.ColorRed, terminal.ColorDefault)
		return 3
	}
	for _, file := range editor.files {
		if file.IsModified() {
			editor.screen.WriteStringColor(row, col-3, "M  ", terminal.ColorYellow, terminal.ColorDefault)
			return 3
		}
	}
	return 0
}

func (editor *Editor) writeSyncStatus(row, col int) int {
	if editor.file.FileChanged() {
		editor.screen.WriteStringColor(row, col-3, "S  ", terminal.ColorRed, terminal.ColorDefault)
		return 3
	}
	for _, file := range editor.files {
		if file.FileChanged() {
			editor.screen.WriteStringColor(row, col-3, "S  ", terminal.ColorYellow, terminal.ColorDefault)
			return 3
		}
	}
	return 0
}

// UpdateStatus updates the status line.
func (editor *Editor) UpdateStatus() {
	cols, rows := terminal.Size()

	name := editor.getFilename(cols / 3)
	message := fmt.Sprintf("%s (%d/%d)   %d/%d,%d",
		name,
		editor.fileIdx,
		len(editor.files),
		editor.file.MultiCursor.GetRow(0),
		editor.file.Length()-1,
		editor.file.MultiCursor.GetCol(0),
	)
	col := cols - len(message)
	editor.screen.WriteString(rows-1, col, message)
	editor.screen.WriteString(rows-1, 0, "[ Sith 0.6.1 ]")
	editor.screen.DecorateStatusLine()
	col -= editor.writeModStatus(rows-1, col)
	col -= editor.writeSyncStatus(rows-1, col)
	editor.file.WriteStatus(rows-1, col)
	editor.screen.SetCursor(editor.file.GetCursor(0))
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
	"text/tabwriter"

	flag "github.com/jessevdk/go-flags"
)

//Config are concrete and stored in configuration file
type Config struct {
	ImageDir   string `long:"dir" description:"Target Directory" default:"img" ini-name:"downdir"`
	QDepth     int    `short:"q" long:"queue" description:"Length of the queue buffer" default:"50" ini-name:"queue_depth"`
	Key        string `short:"k" long:"key" description:"Derpibooru API key" ini-name:"key"`
	LogFilters Bool   `long:"logfilter" optional:" " optional-value:"true" description:"Enable logging of filtered images" ini-name:"logfilter"`
}

//FlagOpts are runtime boolean flags
type FlagOpts struct {
	NoHTTPS     bool `long:"nohttps" description:"Disable HTTPS and try to download insecurely"`
	UnsafeHTTPS bool `long:"unsafe-https" description:"Disable HTTPS security verification"`
}

//FiltOpts are filtration parameters
type FiltOpts struct {
	Score  int  `long:"score" description:"Filter option, minimal score of image for it to be downloaded"`
	Faves  int  `long:"faves" description:"Filter option, minimal amount of people who favored image for it to be downloaded"`
	ScoreF bool `no-flag:" "`
	FavesF bool `no-flag:" "`
}

//TagOpts are options relevant to searching by tags
type TagOpts struct {
	Tag       string `short:"t" long:"tag" description:"Tag to download"`
	StartPage int    `short:"p" long:"startpage" description:"Starting page for search" default:"1"`
	StopPage  int    `short:"n" long:"stoppage" description:"Stopping page for search, default - parse all search pages"`
}

//Options provide program-wide options. At maximum, we got one persistent global and one short-living copy for writing in config file
type Options struct {
	*Config
	*FlagOpts
	*FiltOpts
	*TagOpts
	Args struct {
		IDs []int `description:"Image IDs to download" optional:"yes"`
	} `positional-args:"yes"`
}

func getOptions() (opts *Options, args []string) {
	opts = new(Options)
	args, inisets := opts.Setup()
	opts.Config.checkedWriteIni(inisets)
	return
}

//checkedWriteIni writes configuration into file without overwriting old one if unchanged
//As i totally forgot how it does so if configuration file is empty:
//nil and zero values and emtpy strings get overwritten by defaults when reading command line
//as old values are now different, defaults got written into the file
func (sets *Config) checkedWriteIni(oldsets *Config) {
	if sets.isEqual(oldsets) { //If nothing to write, no double-writing files
		return
	}

	inifile, err := os.OpenFile("config.ini", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)

	if err != nil {
		lFatal("Could  not create configuration file")
	}

	defer func() {
		err = inifile.Close()
		if err != nil {
			lFatal("Could  not close configuration file")
		}
	}()

	err = sets.prettyWriteIni(inifile)

	if err != nil {
		lFatal("Could  not write in configuration file")
	}
}

//prettyWriteIni Uses tabwriter to make pretty ini file with
func (sets *Config) prettyWriteIni(inifile io.Writer) error {
	tb := tabwriter.NewWriter(inifile, 10, 8, 0, ' ', 0) //Tabs! Elastic! Pretty!

	fmt.Fprintf(tb, "key \t= %s\n", sets.Key)
	fmt.Fprintf(tb, "queue_depth \t= %s\n", strconv.Itoa(sets.QDepth))
	fmt.Fprintf(tb, "downdir \t= %s\n", sets.ImageDir)
	fmt.Fprintf(tb, "logfilter \t= %t\n", sets.LogFilters)

	return tb.Flush() //Returns and passes error upstairs
}

//compareStatic compares only options I want to preserve across launches.
func (sets *Config) isEqual(b *Config) bool {
	if b == nil {
		return false
	}
	if sets.ImageDir == b.ImageDir &&
		sets.QDepth == b.QDepth &&
		sets.Key == b.Key &&
		sets.LogFilters == b.LogFilters {
		return true
	}
	return false
}

//Setup reads static config from file and runtime options from commandline
//It also preserves static config for later comparsion with runtime to prevent
//rewriting it when no changes are made
func (opts *Options) Setup() ([]string, *Config) {
	err := flag.IniParse("config.ini", opts)
	if err != nil {
		switch err.(type) {
		default:
			lFatal(err)
		case *os.PathError:
			lWarn("config.ini not found, using defaults")
		}
	}
	inisets := *opts.Config //copy value instead of reference - or we will get no results later

	args, err := flag.Parse(opts)
	checkFlagError(err) //Here we scream if something goes wrong and provide help if something goes meh.

	for _, arg := range os.Args {
		if strings.Contains(arg, "--score") {
			opts.ScoreF = true
		}
		if strings.Contains(arg, "--faves") {
			opts.FavesF = true
		}
	}
	return args, &inisets
}

func checkFlagError(err error) {
	if err == nil {
		return
	}

	flagError := err.(*flag.Error)

	switch flagError.Type {
	case flag.ErrHelp:
		os.Exit(0) //Why fall through when asked for help? Just exit with suggestion
	case flag.ErrUnknownFlag:
		fmt.Println("Use --help to view all available options")
		os.Exit(0)
	default:
		lFatal("Can't parse flags: ", err)
	}
}

type Bool bool

func (b *Bool) UnmarshalFlag(value string) error {
	if value == "true" {
		*b = true
	} else if value == "false" {
		*b = false
	} else {
		return fmt.Errorf("only `true' and `false' are valid values, not `%s'", value)
	}

	return nil
}

func (b Bool) MarshalFlag() string {
	if b {
		return "true"
	}

	return "false"
}
<commit_msg>Made sure that things are correct in interface implementation<commit_after>package main

import (
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
	"text/tabwriter"

	flag "github.com/jessevdk/go-flags"
)

//Config are concrete and stored in configuration file
type Config struct {
	ImageDir   string `long:"dir" description:"Target Directory" default:"img" ini-name:"downdir"`
	QDepth     int    `short:"q" long:"queue" description:"Length of the queue buffer" default:"50" ini-name:"queue_depth"`
	Key        string `short:"k" long:"key" description:"Derpibooru API key" ini-name:"key"`
	LogFilters Bool   `long:"logfilter" optional:" " optional-value:"true" description:"Enable logging of filtered images" ini-name:"logfilter"`
}

//FlagOpts are runtime boolean flags
type FlagOpts struct {
	NoHTTPS     bool `long:"nohttps" description:"Disable HTTPS and try to download insecurely"`
	UnsafeHTTPS bool `long:"unsafe-https" description:"Disable HTTPS security verification"`
}

//FiltOpts are filtration parameters
type FiltOpts struct {
	Score  int  `long:"score" description:"Filter option, minimal score of image for it to be downloaded"`
	Faves  int  `long:"faves" description:"Filter option, minimal amount of people who favored image for it to be downloaded"`
	ScoreF bool `no-flag:" "`
	FavesF bool `no-flag:" "`
}

//TagOpts are options relevant to searching by tags
type TagOpts struct {
	Tag       string `short:"t" long:"tag" description:"Tag to download"`
	StartPage int    `short:"p" long:"startpage" description:"Starting page for search" default:"1"`
	StopPage  int    `short:"n" long:"stoppage" description:"Stopping page for search, default - parse all search pages"`
}

//Options provide program-wide options. At maximum, we got one persistent global and one short-living copy for writing in config file
type Options struct {
	*Config
	*FlagOpts
	*FiltOpts
	*TagOpts
	Args struct {
		IDs []int `description:"Image IDs to download" optional:"yes"`
	} `positional-args:"yes"`
}

func getOptions() (opts *Options, args []string) {
	opts = new(Options)
	args, inisets := opts.Setup()
	opts.Config.checkedWriteIni(inisets)
	return
}

//checkedWriteIni writes configuration into file without overwriting old one if unchanged
//As i totally forgot how it does so if configuration file is empty:
//nil and zero values and emtpy strings get overwritten by defaults when reading command line
//as old values are now different, defaults got written into the file
func (sets *Config) checkedWriteIni(oldsets *Config) {
	if sets.isEqual(oldsets) { //If nothing to write, no double-writing files
		return
	}

	inifile, err := os.OpenFile("config.ini", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)

	if err != nil {
		lFatal("Could  not create configuration file")
	}

	defer func() {
		err = inifile.Close()
		if err != nil {
			lFatal("Could  not close configuration file")
		}
	}()

	err = sets.prettyWriteIni(inifile)

	if err != nil {
		lFatal("Could  not write in configuration file")
	}
}

//prettyWriteIni Uses tabwriter to make pretty ini file with
func (sets *Config) prettyWriteIni(inifile io.Writer) error {
	tb := tabwriter.NewWriter(inifile, 10, 8, 0, ' ', 0) //Tabs! Elastic! Pretty!

	fmt.Fprintf(tb, "key \t= %s\n", sets.Key)
	fmt.Fprintf(tb, "queue_depth \t= %s\n", strconv.Itoa(sets.QDepth))
	fmt.Fprintf(tb, "downdir \t= %s\n", sets.ImageDir)
	fmt.Fprintf(tb, "logfilter \t= %t\n", sets.LogFilters)

	return tb.Flush() //Returns and passes error upstairs
}

//compareStatic compares only options I want to preserve across launches.
func (sets *Config) isEqual(b *Config) bool {
	if b == nil {
		return false
	}
	if sets.ImageDir == b.ImageDir &&
		sets.QDepth == b.QDepth &&
		sets.Key == b.Key &&
		sets.LogFilters == b.LogFilters {
		return true
	}
	return false
}

//Setup reads static config from file and runtime options from commandline
//It also preserves static config for later comparsion with runtime to prevent
//rewriting it when no changes are made
func (opts *Options) Setup() ([]string, *Config) {
	err := flag.IniParse("config.ini", opts)
	if err != nil {
		switch err.(type) {
		default:
			lFatal(err)
		case *os.PathError:
			lWarn("config.ini not found, using defaults")
		}
	}
	inisets := *opts.Config //copy value instead of reference - or we will get no results later

	args, err := flag.Parse(opts)
	checkFlagError(err) //Here we scream if something goes wrong and provide help if something goes meh.

	for _, arg := range os.Args {
		if strings.Contains(arg, "--score") {
			opts.ScoreF = true
		}
		if strings.Contains(arg, "--faves") {
			opts.FavesF = true
		}
	}
	return args, &inisets
}

func checkFlagError(err error) {
	if err == nil {
		return
	}

	flagError := err.(*flag.Error)

	switch flagError.Type {
	case flag.ErrHelp:
		os.Exit(0) //Why fall through when asked for help? Just exit with suggestion
	case flag.ErrUnknownFlag:
		fmt.Println("Use --help to view all available options")
		os.Exit(0)
	default:
		lFatal("Can't parse flags: ", err)
	}
}

//Bool is a bool, because this is suggested method for go-flags to work with a boolean flag that could be flipped both ways
type Bool bool

//UnmarshalFlag satisfies flags.Unmarshaler interface for Bool
func (b *Bool) UnmarshalFlag(value string) error {
	if value == "true" {
		*b = true
	} else if value == "false" {
		*b = false
	} else {
		return fmt.Errorf("only `true' and `false' are valid values, not `%s'", value)
	}

	return nil
}

//MarshalFlag satisfies flags.Marshaler interface for Bool
func (b Bool) MarshalFlag() (string, error) {
	if b {
		return "true", nil
	}

	return "false", nil
}
<|endoftext|>


<commit_before>package golog

import (
	"flag"
	"fmt"
	"os"
	"sync"
)

// A MultiLogOuter is a LogOuter with multiple keyed LogOuters. All functions
// should be safe to call in a multi-threaded environment.
type MultiLogOuter interface {
	LogOuter
	// Add the LogOuter, associating it with the key.
	AddLogOuter(key string, outer LogOuter)
	// Remove the LogOuter associated with the key.
	RemoveLogOuter(key string)
}

// A MultiLogOuter than can also be used as a flag for setting logfiles.
// For example, it is possible to use a logger other than default via:
// 	var myOuter MultiLogOuterFlag = NewMultiLogOuter()
//
// 	func init() {
// 		flag.Var(myOuter,
// 			"mypack.logfile",
// 			"Log to file - can be provided multiple times")
// 	}
type MultiLogOuterFlag interface {
	MultiLogOuter
	flag.Value
}

type multiLogOuterImpl struct {
	lock   sync.Mutex
	outers map[string]LogOuter
}

func (l *multiLogOuterImpl) String() string {
	// TODO(awreece) better string
	return fmt.Sprint("\"", l.outers, "\"")
}

func (l *multiLogOuterImpl) Set(name string) bool {
	if outer, err := NewFileLogOuter(name); err != nil {
		os.Stderr.WriteString(
			fmt.Sprint("Error opening file for logging", name,
				": ", err))
		return false
	} else {
		l.AddLogOuter(name, outer)
		return true
	}

	panic("Code never reaches here, this mollifies the compiler.")
}

func (l *multiLogOuterImpl) AddLogOuter(key string, outer LogOuter) {
	l.lock.Lock()
	defer l.lock.Unlock()

	l.outers[key] = outer
}

func (l *multiLogOuterImpl) RemoveLogOuter(key string) {
	l.lock.Lock()
	defer l.lock.Unlock()

	l.outers[key] = nil, false
}

func (l *multiLogOuterImpl) Output(m *LogMessage) {
	l.lock.Lock()
	defer l.lock.Unlock()

	for _, outer := range l.outers {
		outer.Output(m)
	}
}

var defaultLogOuters MultiLogOuterFlag = NewMultiLogOuter()

// Create a new MultiLogOuter initialized with a mapping of "default" to the
// default MultiLogOuter.
func NewDefaultMultiLogOuter() MultiLogOuterFlag {
	return &multiLogOuterImpl{
		outers: map[string]LogOuter{"default": defaultLogOuters},
	}
}

// Create an empty new MutliLogOuter.
func NewMultiLogOuter() MultiLogOuterFlag {
	return &multiLogOuterImpl{outers: make(map[string]LogOuter)}
}

func init() {
	flag.Var(defaultLogOuters, "golog.logfile",
		"Log to given file - can be provided multiple times to log "+
			"to multiple files")
}
<commit_msg>Better string for MultiLogOuter<commit_after>package golog

import (
	"bytes"
	"flag"
	"fmt"
	"os"
	"sync"
)

// A MultiLogOuter is a LogOuter with multiple keyed LogOuters. All functions
// should be safe to call in a multi-threaded environment.
type MultiLogOuter interface {
	LogOuter
	// Add the LogOuter, associating it with the key.
	AddLogOuter(key string, outer LogOuter)
	// Remove the LogOuter associated with the key.
	RemoveLogOuter(key string)
}

// A MultiLogOuter than can also be used as a flag for setting logfiles.
// For example, it is possible to use a logger other than default via:
// 	var myOuter MultiLogOuterFlag = NewMultiLogOuter()
//
// 	func init() {
// 		flag.Var(myOuter,
// 			"mypack.logfile",
// 			"Log to file - can be provided multiple times")
// 	}
type MultiLogOuterFlag interface {
	MultiLogOuter
	flag.Value
}

type multiLogOuterImpl struct {
	lock   sync.Mutex
	outers map[string]LogOuter
}

func (l *multiLogOuterImpl) String() string {
	l.lock.Lock()
	defer l.lock.Unlock()

	var buf bytes.Buffer
	buf.WriteString("\"")

	var first bool = true
	for filename, _ := range l.outers {
		if first {
			first = false
		} else {
			buf.WriteString(",")
		}
		buf.WriteString(filename)
	}

	buf.WriteString("\"")
	return buf.String()
}

func (l *multiLogOuterImpl) Set(name string) bool {
	if outer, err := NewFileLogOuter(name); err != nil {
		os.Stderr.WriteString(
			fmt.Sprint("Error opening file for logging", name,
				": ", err))
		return false
	} else {
		l.AddLogOuter(name, outer)
		return true
	}

	panic("Code never reaches here, this mollifies the compiler.")
}

func (l *multiLogOuterImpl) AddLogOuter(key string, outer LogOuter) {
	l.lock.Lock()
	defer l.lock.Unlock()

	l.outers[key] = outer
}

func (l *multiLogOuterImpl) RemoveLogOuter(key string) {
	l.lock.Lock()
	defer l.lock.Unlock()

	l.outers[key] = nil, false
}

func (l *multiLogOuterImpl) Output(m *LogMessage) {
	l.lock.Lock()
	defer l.lock.Unlock()

	for _, outer := range l.outers {
		outer.Output(m)
	}
}

var defaultLogOuters MultiLogOuterFlag = NewMultiLogOuter()

// Create a new MultiLogOuter initialized with a mapping of "default" to the
// default MultiLogOuter.
func NewDefaultMultiLogOuter() MultiLogOuterFlag {
	return &multiLogOuterImpl{
		outers: map[string]LogOuter{"default": defaultLogOuters},
	}
}

// Create an empty new MutliLogOuter.
func NewMultiLogOuter() MultiLogOuterFlag {
	return &multiLogOuterImpl{outers: make(map[string]LogOuter)}
}

func init() {
	flag.Var(defaultLogOuters, "golog.logfile",
		"Log to given file - can be provided multiple times to log "+
			"to multiple files")
}
<|endoftext|>


<commit_before>package editor

import (
	"fmt"
	"strings"

	"github.com/elpinal/coco3/screen"
)

type search struct {
	streamSet
	*editor

	basic *basic
}

func newSearch(s streamSet, e *editor) *search {
	return &search{
		streamSet: s,
		editor:    e,
		basic:     &basic{},
	}
}

func (se *search) Mode() mode {
	return modeSearch
}

func (se *search) Position() int {
	return se.basic.pos + 1
}

func (se *search) Runes() []rune {
	return se.buf
}

func (se *search) Message() []rune {
	return append([]rune{'/'}, se.basic.buf...)
}

func (se *search) Highlight() *screen.Hi {
	return nil
}

func (se *search) Run() (end continuity, next mode, err error) {
	next = modeSearch
	r, _, err := se.in.ReadRune()
	if err != nil {
		return end, next, err
	}
	switch r {
	case CharCtrlM, CharCtrlJ:
	case CharEscape, CharCtrlC:
		return end, modeNormal, err
	case CharBackspace, CharCtrlH:
		if len(se.basic.buf) == 0 {
			next = modeNormal
			return
		}
		se.basic.delete(se.basic.pos-1, se.basic.pos)
	case CharCtrlB:
		se.basic.move(0)
	case CharCtrlE:
		se.basic.move(len(se.basic.buf))
	case CharCtrlU:
		se.basic.delete(0, se.basic.pos)
	case CharCtrlW:
		// FIXME: It's redundant.
		ed := newEditor()
		ed.pos = se.basic.pos
		ed.buf = se.basic.buf
		pos := ed.pos
		ed.wordBackward()
		se.basic.delete(pos, ed.pos)
		return
	default:
		se.basic.insert([]rune{r}, se.basic.pos)
	}
	if r != CharCtrlM && r != CharCtrlJ {
		return
	}
	next = modeNormal
	s := string(se.basic.buf)
	if s == "" {
		return
	}
	i, err := se.search(s)
	if err != nil {
		return end, next, err
	}
	se.move(i)
	return
}

func (se *search) search(s string) (int, error) {
	i := strings.Index(string(se.buf[se.pos:]), s)
	if i < 0 {
		return 0, fmt.Errorf("pattern not found: %q", s)
	}
	return i, nil
}
<commit_msg>Fix a bug where cursor position is wrong while searching<commit_after>package editor

import (
	"fmt"
	"strings"

	"github.com/elpinal/coco3/screen"
)

type search struct {
	streamSet
	*editor

	basic *basic
}

func newSearch(s streamSet, e *editor) *search {
	return &search{
		streamSet: s,
		editor:    e,
		basic:     &basic{},
	}
}

func (se *search) Mode() mode {
	return modeSearch
}

func (se *search) Position() int {
	return se.basic.pos + 1
}

func (se *search) Runes() []rune {
	return se.buf
}

func (se *search) Message() []rune {
	return append([]rune{'/'}, se.basic.buf...)
}

func (se *search) Highlight() *screen.Hi {
	return nil
}

func (se *search) Run() (end continuity, next mode, err error) {
	next = modeSearch
	r, _, err := se.in.ReadRune()
	if err != nil {
		return end, next, err
	}
	switch r {
	case CharCtrlM, CharCtrlJ:
	case CharEscape, CharCtrlC:
		return end, modeNormal, err
	case CharBackspace, CharCtrlH:
		if len(se.basic.buf) == 0 {
			next = modeNormal
			return
		}
		se.basic.delete(se.basic.pos-1, se.basic.pos)
	case CharCtrlB:
		se.basic.move(0)
	case CharCtrlE:
		se.basic.move(len(se.basic.buf))
	case CharCtrlU:
		se.basic.delete(0, se.basic.pos)
	case CharCtrlW:
		// FIXME: It's redundant.
		ed := newEditor()
		ed.pos = se.basic.pos
		ed.buf = se.basic.buf
		pos := ed.pos
		ed.wordBackward()
		se.basic.delete(pos, ed.pos)
		return
	default:
		se.basic.insert([]rune{r}, se.basic.pos)
	}
	if r != CharCtrlM && r != CharCtrlJ {
		return
	}
	next = modeNormal
	s := string(se.basic.buf)
	if s == "" {
		return
	}
	i, err := se.search(s)
	if err != nil {
		return end, next, err
	}
	se.move(i)
	return
}

func (se *search) search(s string) (int, error) {
	i := strings.Index(string(se.buf[se.pos:]), s)
	if i < 0 {
		return 0, fmt.Errorf("pattern not found: %q", s)
	}
	return i+se.pos, nil
}
<|endoftext|>


<commit_before>// +build ignore

package main

import (
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/PuerkitoBio/goquery"
)

// elemNameMap translates lowercase HTML tag names from the MDN source into a
// proper Go style name with MixedCaps and initialisms:
//
//  https://github.com/golang/go/wiki/CodeReviewComments#mixed-caps
//  https://github.com/golang/go/wiki/CodeReviewComments#initialisms
//
var elemNameMap = map[string]string{
	"a":          "Anchor",
	"abbr":       "Abbreviation",
	"b":          "Bold",
	"bdi":        "BidirectionalIsolation",
	"bdo":        "BidirectionalOverride",
	"blockquote": "BlockQuote",
	"br":         "Break",
	"cite":       "Citation",
	"col":        "Column",
	"colgroup":   "ColumnGroup",
	"datalist":   "DataList",
	"dd":         "Description",
	"del":        "DeletedText",
	"dfn":        "Definition",
	"dl":         "DescriptionList",
	"dt":         "DefinitionTerm",
	"em":         "Emphasis",
	"fieldset":   "FieldSet",
	"figcaption": "FigureCaption",
	"h1":         "Header1",
	"h2":         "Header2",
	"h3":         "Header3",
	"h4":         "Header4",
	"h5":         "Header5",
	"h6":         "Header6",
	"hgroup":     "HeadingsGroup",
	"hr":         "HorizontalRule",
	"i":          "Italic",
	"iframe":     "InlineFrame",
	"img":        "Image",
	"ins":        "InsertedText",
	"kbd":        "KeyboardInput",
	"li":         "ListItem",
	"menuitem":   "MenuItem",
	"nav":        "Navigation",
	"noframes":   "NoFrames",
	"noscript":   "NoScript",
	"ol":         "OrderedList",
	"optgroup":   "OptionsGroup",
	"p":          "Paragraph",
	"param":      "Parameter",
	"pre":        "Preformatted",
	"q":          "Quote",
	"rp":         "RubyParenthesis",
	"rt":         "RubyText",
	"rtc":        "RubyTextContainer",
	"s":          "Strikethrough",
	"samp":       "Sample",
	"sub":        "Subscript",
	"sup":        "Superscript",
	"tbody":      "TableBody",
	"textarea":   "TextArea",
	"td":         "TableData",
	"tfoot":      "TableFoot",
	"th":         "TableHeader",
	"thead":      "TableHead",
	"tr":         "TableRow",
	"u":          "Underline",
	"ul":         "UnorderedList",
	"var":        "Variable",
	"wbr":        "WordBreakOpportunity",
}

func main() {
	doc, err := goquery.NewDocument("https://developer.mozilla.org/en-US/docs/Web/HTML/Element")
	if err != nil {
		panic(err)
	}

	file, err := os.Create("elem.gen.go")
	if err != nil {
		panic(err)
	}
	defer file.Close()

	fmt.Fprint(file, `//go:generate go run generate.go

// Package elem defines markup to create DOM elements.
//
// Generated from "HTML element reference" by Mozilla Contributors,
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element, licensed under
// CC-BY-SA 2.5.
package elem

import "github.com/gopherjs/vecty"
`)

	doc.Find(".quick-links a").Each(func(i int, s *goquery.Selection) {
		link, _ := s.Attr("href")
		if !strings.HasPrefix(link, "/en-US/docs/Web/HTML/Element/") {
			return
		}

		if s.Parent().Find(".icon-trash, .icon-thumbs-down-alt, .icon-warning-sign").Length() > 0 {
			return
		}

		desc, _ := s.Attr("title")

		text := s.Text()
		if text == "Heading elements" {
			writeElem(file, "h1", desc, link)
			writeElem(file, "h2", desc, link)
			writeElem(file, "h3", desc, link)
			writeElem(file, "h4", desc, link)
			writeElem(file, "h5", desc, link)
			writeElem(file, "h6", desc, link)
			return
		}

		name := text[1 : len(text)-1]
		if name == "html" || name == "head" || name == "body" {
			return
		}

		writeElem(file, name, desc, link)
	})
}

func writeElem(w io.Writer, name, desc, link string) {
	funName := elemNameMap[name]
	if funName == "" {
		funName = capitalize(name)
	}

	// Descriptions for elements generally read as:
	//
	//  The HTML <foobar> element ...
	//
	// Because these are consistent (sometimes with varying captalization,
	// however) we can exploit that fact to reword the documentation in proper
	// Go style:
	//
	//  Foobar ...
	//
	generalLowercase := fmt.Sprintf("the html <%s> element", strings.ToLower(name))

	// Replace a number of 'no-break space' unicode characters which exist in
	// the descriptions with normal spaces.
	desc = strings.Replace(desc, "\u00a0", " ", -1)
	if l := len(generalLowercase); len(desc) > l && strings.HasPrefix(strings.ToLower(desc), generalLowercase) {
		desc = fmt.Sprintf("%s%s", funName, desc[l:])
	}

	fmt.Fprintf(w, `%s
//
// https://developer.mozilla.org%s
func %s(markup ...vecty.Markup) *vecty.Element {
	e := &vecty.Element{TagName: "%s"}
	vecty.List(markup).Apply(e)
	return e
}
`, descToComments(desc), link, funName, name)
}

func capitalize(s string) string {
	return strings.ToUpper(s[:1]) + s[1:]
}

func descToComments(desc string) string {
	c := ""
	length := 80
	for _, word := range strings.Split(desc, " ") {
		if length+len(word)+1 > 80 {
			length = 3
			c += "\n//"
		}
		c += " " + word
		length += len(word) + 1
	}
	return c
}
<commit_msg>elem: update for new component architecture<commit_after>// +build ignore

package main

import (
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/PuerkitoBio/goquery"
)

// elemNameMap translates lowercase HTML tag names from the MDN source into a
// proper Go style name with MixedCaps and initialisms:
//
//  https://github.com/golang/go/wiki/CodeReviewComments#mixed-caps
//  https://github.com/golang/go/wiki/CodeReviewComments#initialisms
//
var elemNameMap = map[string]string{
	"a":          "Anchor",
	"abbr":       "Abbreviation",
	"b":          "Bold",
	"bdi":        "BidirectionalIsolation",
	"bdo":        "BidirectionalOverride",
	"blockquote": "BlockQuote",
	"br":         "Break",
	"cite":       "Citation",
	"col":        "Column",
	"colgroup":   "ColumnGroup",
	"datalist":   "DataList",
	"dd":         "Description",
	"del":        "DeletedText",
	"dfn":        "Definition",
	"dl":         "DescriptionList",
	"dt":         "DefinitionTerm",
	"em":         "Emphasis",
	"fieldset":   "FieldSet",
	"figcaption": "FigureCaption",
	"h1":         "Header1",
	"h2":         "Header2",
	"h3":         "Header3",
	"h4":         "Header4",
	"h5":         "Header5",
	"h6":         "Header6",
	"hgroup":     "HeadingsGroup",
	"hr":         "HorizontalRule",
	"i":          "Italic",
	"iframe":     "InlineFrame",
	"img":        "Image",
	"ins":        "InsertedText",
	"kbd":        "KeyboardInput",
	"li":         "ListItem",
	"menuitem":   "MenuItem",
	"nav":        "Navigation",
	"noframes":   "NoFrames",
	"noscript":   "NoScript",
	"ol":         "OrderedList",
	"optgroup":   "OptionsGroup",
	"p":          "Paragraph",
	"param":      "Parameter",
	"pre":        "Preformatted",
	"q":          "Quote",
	"rp":         "RubyParenthesis",
	"rt":         "RubyText",
	"rtc":        "RubyTextContainer",
	"s":          "Strikethrough",
	"samp":       "Sample",
	"sub":        "Subscript",
	"sup":        "Superscript",
	"tbody":      "TableBody",
	"textarea":   "TextArea",
	"td":         "TableData",
	"tfoot":      "TableFoot",
	"th":         "TableHeader",
	"thead":      "TableHead",
	"tr":         "TableRow",
	"u":          "Underline",
	"ul":         "UnorderedList",
	"var":        "Variable",
	"wbr":        "WordBreakOpportunity",
}

func main() {
	doc, err := goquery.NewDocument("https://developer.mozilla.org/en-US/docs/Web/HTML/Element")
	if err != nil {
		panic(err)
	}

	file, err := os.Create("elem.gen.go")
	if err != nil {
		panic(err)
	}
	defer file.Close()

	fmt.Fprint(file, `//go:generate go run generate.go

// Package elem defines markup to create DOM elements.
//
// Generated from "HTML element reference" by Mozilla Contributors,
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element, licensed under
// CC-BY-SA 2.5.
package elem

import "github.com/gopherjs/vecty"
`)

	doc.Find(".quick-links a").Each(func(i int, s *goquery.Selection) {
		link, _ := s.Attr("href")
		if !strings.HasPrefix(link, "/en-US/docs/Web/HTML/Element/") {
			return
		}

		if s.Parent().Find(".icon-trash, .icon-thumbs-down-alt, .icon-warning-sign").Length() > 0 {
			return
		}

		desc, _ := s.Attr("title")

		text := s.Text()
		if text == "Heading elements" {
			writeElem(file, "h1", desc, link)
			writeElem(file, "h2", desc, link)
			writeElem(file, "h3", desc, link)
			writeElem(file, "h4", desc, link)
			writeElem(file, "h5", desc, link)
			writeElem(file, "h6", desc, link)
			return
		}

		name := text[1 : len(text)-1]
		if name == "html" || name == "head" {
			return
		}

		writeElem(file, name, desc, link)
	})
}

func writeElem(w io.Writer, name, desc, link string) {
	funName := elemNameMap[name]
	if funName == "" {
		funName = capitalize(name)
	}

	// Descriptions for elements generally read as:
	//
	//  The HTML <foobar> element ...
	//
	// Because these are consistent (sometimes with varying captalization,
	// however) we can exploit that fact to reword the documentation in proper
	// Go style:
	//
	//  Foobar ...
	//
	generalLowercase := fmt.Sprintf("the html <%s> element", strings.ToLower(name))

	// Replace a number of 'no-break space' unicode characters which exist in
	// the descriptions with normal spaces.
	desc = strings.Replace(desc, "\u00a0", " ", -1)
	if l := len(generalLowercase); len(desc) > l && strings.HasPrefix(strings.ToLower(desc), generalLowercase) {
		desc = fmt.Sprintf("%s%s", funName, desc[l:])
	}

	fmt.Fprintf(w, `%s
//
// https://developer.mozilla.org%s
func %s(markup ...vecty.MarkupOrComponentOrHTML) *vecty.HTML {
	return vecty.Tag("%s", markup...)
}
`, descToComments(desc), link, funName, name)
}

func capitalize(s string) string {
	return strings.ToUpper(s[:1]) + s[1:]
}

func descToComments(desc string) string {
	c := ""
	length := 80
	for _, word := range strings.Split(desc, " ") {
		if length+len(word)+1 > 80 {
			length = 3
			c += "\n//"
		}
		c += " " + word
		length += len(word) + 1
	}
	return c
}
<|endoftext|>