У вас включен AdBlock или иной блокировщик рекламы.

Пожалуйста, отключите его, доход от рекламы помогает развитию сайта и появлению новых статей.

Спасибо за понимание.

В другой раз
DevGang блог о програмировании
Авторизоваться

Изучаем Go - Сборка DevLog Часть 03

Создаем блог разработчика

Итак, снова мы! Если вы следили за моими постами - добро пожаловать обратно, а если нет - то милости прошу на борт! На этот раз мы продолжим расширять базовый код, который написали для создания генератора статического сайта. На этой неделе мы перепрыгнули немного вперед и сейчас находимся на том моменте, когда мы можем преобразовать весь каталог хорошо отформатированных маркдаун файлов в HTML, готовый для размещения. Как бы там ни было, давайте продолжим изучать Go …

Углубляемся

Я все еще думаю над тем, как продемонстрировать вам обновленный код, но, т.к кодовая база достаточно мала, можно просто начать сверху и двигаться вниз. Я не собираюсь заниматься частями кода, которые не изменились с прошлой недели, но если вы не читали предыдущие посты, я предлагаю вам сделать это!

Теперь давайте сделаем первые реальные изменения.

package mainimport (
 "bufio"
 "bytes"
 "fmt"
 "html/template"
 "io/ioutil"
 "os"
 "regexp"
 "strings" "github.com/microcosm-cc/bluemonday"
 "github.com/russross/blackfriday"
 yaml "gopkg.in/yaml.v2"
)const delim = "---"type post struct {
 Title       string
 Published   bool
 Description string
 Tags        []string
 CoverImage  string
 Series      string
 PostBody    template.HTML
}var templ = `

 
   {{.Title}}
   
   
   
   
   
   
 
 
   

{{.Title}}

{{.PostBody}}
`

Нейминг - это сложно

Здесь мы переходим к первому (небольшому) изменению, функция такая же, как и на прошлой неделе, но я переименовал ее из loadFile() в getContents(). Такое наименование точнее отражает ее функционал. 

func getContents(f *string) ([]byte, error) {
 b, err := ioutil.ReadFile(*f)
 if err != nil {
   return nil, err
 }
 return b, nil
}

В этом разделе вы должны заметить одну вещь: я постарался разбить весь код на отдельные функции. Благодаря этому мы можем писать тесты и поддерживать наш метод main(). Ниже у нас есть метод parseFM(), который завершает наш un-marshaling и возвращает первый этап нашего начального вопроса. Более корректным был бы вызов метода parseYAML(), но пока мы оставими все как есть. isNil() не изменился и до сих пор не используется, я прослежу за этим до самого конца поста.

func parseFM(b *[]byte) (map[string]interface{}, error) {
 m := make(map[string]interface{})
 err := yaml.Unmarshal(*b, &m)
 if err != nil {
   msg := fmt.Sprintf("error: %v\ninput:\n%s", err, b)
   return nil, fmt.Errorf(msg)
 }
 return m, nil
}func isNil(i interface{}) bool {
 if i != nil {
   return false
 }
 return true
}

Как и прежде, нам нужно разделить данные, которые мы загрузили из маркдаун файла. Вызов метода splitData() позаботится об этом. Я вводил различные искаженные данные, и каждый раз казалось, что оператор if работает так, как и ожидалось. Я также проверил свой текущий набор записей dev.down для маркдаун, и каждый сконвертировался без проблем. Вы можете найти все исходники на dev.shindakun.net.

func splitData(f *[]byte) ([][]byte, error) {
 b := bytes.Split(*f, []byte(delim))
 if len(b) < 3 || len(b[0]) != 0 {
   return nil, fmt.Errorf("Front matter is damaged")
 }
 return b, nil
}

После того, как мы разделили данные и спарсили начальную тему, мы вызываем метод makePost(), чтобы построить post структуру и шаблон. Перед тем, как записать файл на диск, мы выполняем шаблон и я возвращаю сообщение *post. Будет удобно использовать этот вариант, когда я начну создание разметки в index.html. И опять таки, на самом деле, мы не используем isNil(), как предполагалось, в основном потому, что мне не нравится беспорядок if/else, который этот метод создает. В ближайшее время мы это исправим. 

// makePost creates the post struct, returns that and the template HTML
func makePost(fm map[string]interface{}, contents []byte,
 s [][]byte) (*template.Template, *post) {
 p := &post{} if isNil(fm["title"]) {
   panic("isNil tripped at title")
 } else {
   p.Title = fm["title"].(string)
 }
 p.Published = fm["published"].(bool)
 p.Description = fm["description"].(string) // TODO: Strip space after comma prior to parse?
 tmp := fm["tags"].(string)
 p.Tags = strings.Split(tmp, ", ") p.CoverImage = fm["cover_image"].(string)
 p.Series = fm["series"].(string) pBody := contents[len(s[1])+(len(delim)*2):] out := blackfriday.Run(pBody) bm := bluemonday.UGCPolicy()
 bm.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code")
 p.PostBody = template.HTML(bm.SanitizeBytes(out)) tm := template.Must(template.New("msg").Parse(templ))
 return tm, p
}

Итак, мы дошли до нашей функции main()! Справа вы увидите, что теперь для получения списка содержимого текущего файла мы используем ioutil.ReadDir().

func main() {
 d, err := ioutil.ReadDir(".")
 if err != nil {
   panic(err)
 }

Затем мы перемещаемся по списку текущего каталога и сразу проверяем, заканчивается ли текущий файл расширением .md. Если да, то адрес нашей строки будет передан в getContents(), и если все пойдет удачно, мы вернем содержимое

for _, f := range d {
   if t := f.Name(); strings.HasSuffix(t, ".md") {
     contents, err := getContents(&t)
     if err != nil {
       panic(err)
     }

Мы передадим указатель на наше содержимое в метод splitData(), а затем передадим второй фрагмент s, то есть s[1], в parseFM(). Эти методы по сути - базовые строительные блоки всего нашего творения. Как splitData(), так и parseFM() можно было бы переместить в makePost(), тогда мы могли бы передать содержимое непосредственно в него и выполнить всю работу там. Мы обязательно будем учитывать это при рефакторинге.

s, err := splitData(&contents)
     if err != nil {
       panic(err)
     }     fm, err := parseFM(&s[1])
     if err != nil {
       msg := fmt.Sprintf("error: %v\ninput:\n%s", err, s[1])
       panic(msg)
     }     tm, p := makePost(fm, contents, s)

В этой текущей версии кода мы собираемся взять текущее имя файла и отключить расширение. Затем мы создадим новый файл с тем же именем, что и у оригинала, и добавим .html. Все это происходит в одном и том же каталоге, и, правда говоря, мы  немного небрежно к этому относимся, потому что не проверяем существующие файлы. os.Create() использует 0666 (или эквивалентные разрешения в Windows) для прав доступа к файлам, поэтому теоретически мы без проблем должны иметь возможность писать и перезаписывать. Это еще один случай, когда мы можем не делать так, как считается «правильным» - вместо этого мы просто итеративно ведем процесс разработки . Это позволяет всем увидеть, как изменился код, баги и все такое.

Файл создан, о, мы затем используем bufio.NewWriter(), чтобы создать «Writer» с буфером. Затем мы можем выполнить наш шаблон и записать непосредственно в файл, который закрывается после очистки буфера.

 fin := strings.TrimSuffix(t, ".md")
     o, err := os.Create(fin + ".html")
     if err != nil {
       panic(err)
     }
     defer o.Close()     buf := bufio.NewWriter(o)     err = tm.Execute(buf, p)
     if err != nil {
       panic(err)
     }
     buf.Flush()
   }
 }
}

Если у нас остались какие-либо файлы, мы возвращаемся к началу цикла и делаем все заново!

В следующий раз

Почти все компоненты в сборе! Но сначала нам нужно создать и заполнить index.html. Думаю, что я реализую это в течение следующего дня и выложу follow up пост. После этого мне нужно решить, писать ли еще публикацию из серии «dev site» и расширить код для использования внешних шаблонов. Мне кажется, что это не должно потребовать намного больше времени и работы, посмотрю, какая занятость у меня в понедельник на следующей неделе. Увидимся в следующий раз!

Другие статьи из цикла:

#Golang