Изучаем 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