Изучаем 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» и расширить код для использования внешних шаблонов. Мне кажется, что это не должно потребовать намного больше времени и работы, посмотрю, какая занятость у меня в понедельник на следующей неделе. Увидимся в следующий раз!
Другие статьи из цикла:
- Изучаем Go - создание загрузчика (часть 1)
- Изучаем Go - создание загрузчика (часть 2)
- Изучаем Go - создание загрузчика (часть 3)
- Изучаем Go - создание загрузчика (часть 4)
- Изучаем Go - создание загрузчика (часть 5)
- Изучаем Go - Использование REST API
- Изучаем Go - Продолжаем работать с REST API
- Изучаем Go - Отправка REST-запросов
- Изучаем Go - Используем REST API в паре с шаблонами проектирования
- Изучаем Go – Наконец-то используем модули
- Изучаем Go - Давайте станем модульными снова!
- Изучаем Go - Сборка DevLog Часть 01
- Изучаем Go - Сборка DevLog Часть 02
- Изучаем Go - Сборка DevLog Часть 03
- Изучаем Go - Сборка DevLog Часть 04