Compare commits
2 Commits
4878495914
...
c23030733f
Author | SHA1 | Date | |
---|---|---|---|
|
c23030733f | ||
|
e7b5b55f67 |
@ -8,6 +8,7 @@ published_posts:
|
||||
tags:
|
||||
- foo
|
||||
series: testing
|
||||
format: md
|
||||
body: |
|
||||
|
||||
This here's a test post containing various markdown elements in its body.
|
||||
@ -59,17 +60,83 @@ published_posts:
|
||||
|
||||
Here's a real picture of cyberspace.
|
||||
|
||||
![not a sound stage]({{ AssetURL "galaxy.jpg" }})
|
||||
{{ Image "galaxy.jpg" }}
|
||||
|
||||
This has been a great post.
|
||||
|
||||
- id: empty-test
|
||||
title: Empty Test
|
||||
- id: gemtext-test
|
||||
title: Gemtext Test
|
||||
description: A little post containing different kinds of gemtext elements.
|
||||
tags:
|
||||
- foo
|
||||
series: testing
|
||||
format: gmi
|
||||
body: |
|
||||
|
||||
This here's a test post containing various markdown elements in its body. It's useful for making sure that posts will look good (generally).
|
||||
|
||||
## Let's Begin
|
||||
|
||||
There's various things worth testing. Like lists:
|
||||
|
||||
* Foo
|
||||
* Bar
|
||||
* Baz
|
||||
|
||||
So many!
|
||||
|
||||
### A Subsection
|
||||
|
||||
And it only gets crazier from here!
|
||||
|
||||
Check out this code block.
|
||||
|
||||
```
|
||||
// It's like actually being in the matrix
|
||||
for !dead {
|
||||
if awake {
|
||||
work()
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Edgy.
|
||||
|
||||
#### Side-note
|
||||
|
||||
Did you know that the terms "cyberspace" and "matrix" are attributable to a book from 1984 called _Neuromancer_?
|
||||
|
||||
> The 1999 cyberpunk science fiction film The Matrix particularly draws from Neuromancer both eponym and usage of the term "matrix".
|
||||
> - Wikipedia
|
||||
|
||||
Here's a real picture of cyberspace.
|
||||
|
||||
{{ Image "galaxy.jpg" "Definitely not a sound stage" }}
|
||||
|
||||
This has been a great post.
|
||||
|
||||
=> / Here's a link outa here!
|
||||
|
||||
- id: empty-markdown-test
|
||||
title: Empty Markdown Test
|
||||
description:
|
||||
tags:
|
||||
- foo
|
||||
- bar
|
||||
series: testing
|
||||
format: md
|
||||
body: ""
|
||||
|
||||
- id: empty-gemtext-test
|
||||
title: Empty Gemtext Test
|
||||
description:
|
||||
tags:
|
||||
- foo
|
||||
- bar
|
||||
series: testing
|
||||
format: gmi
|
||||
body: ""
|
||||
|
||||
- id: little-markdown-test
|
||||
@ -78,6 +145,17 @@ published_posts:
|
||||
tags:
|
||||
- bar
|
||||
series: testing
|
||||
format: md
|
||||
body: |
|
||||
This page is almost empty.
|
||||
|
||||
- id: little-gemtext-test
|
||||
title: Little Gemtext Test
|
||||
description: A post with almost no content.
|
||||
tags:
|
||||
- bar
|
||||
series: testing
|
||||
format: gmi
|
||||
body: |
|
||||
This page is almost empty.
|
||||
|
||||
|
73
src/gmi/gemtext.go
Normal file
73
src/gmi/gemtext.go
Normal file
@ -0,0 +1,73 @@
|
||||
package gmi
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func hasImgExt(p string) bool {
|
||||
switch path.Ext(strings.ToLower(p)) {
|
||||
case ".jpg", ".jpeg", ".png", ".gif", ".svg":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// matches `=> dstURL [optional description]`
|
||||
var linkRegexp = regexp.MustCompile(`^=>\s+(\S+)\s*(.*?)\s*$`)
|
||||
|
||||
// GemtextToMarkdown reads a gemtext formatted body from the Reader and writes
|
||||
// the markdown version of that body to the Writer.
|
||||
func GemtextToMarkdown(dst io.Writer, src io.Reader) error {
|
||||
|
||||
bufSrc := bufio.NewReader(src)
|
||||
|
||||
for {
|
||||
|
||||
line, err := bufSrc.ReadString('\n')
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return fmt.Errorf("reading: %w", err)
|
||||
}
|
||||
|
||||
last := err == io.EOF
|
||||
|
||||
if match := linkRegexp.FindStringSubmatch(line); len(match) > 0 {
|
||||
|
||||
isImg := hasImgExt(match[1])
|
||||
|
||||
descr := match[2]
|
||||
|
||||
if descr != "" {
|
||||
// ok
|
||||
} else if isImg {
|
||||
descr = "Image"
|
||||
} else {
|
||||
descr = "Link"
|
||||
}
|
||||
|
||||
log.Printf("descr:%q", descr)
|
||||
|
||||
line = fmt.Sprintf("[%s](%s)\n", descr, match[1])
|
||||
|
||||
if isImg {
|
||||
line = "!" + line
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := dst.Write([]byte(line)); err != nil {
|
||||
return fmt.Errorf("writing: %w", err)
|
||||
}
|
||||
|
||||
if last {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
51
src/gmi/gemtext_test.go
Normal file
51
src/gmi/gemtext_test.go
Normal file
@ -0,0 +1,51 @@
|
||||
package gmi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGemtextToMarkdown(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
in, exp string
|
||||
}{
|
||||
{
|
||||
in: "",
|
||||
exp: "",
|
||||
},
|
||||
{
|
||||
in: "=> foo",
|
||||
exp: "[Link](foo)\n",
|
||||
},
|
||||
{
|
||||
in: "what\n=> foo\n=> bar",
|
||||
exp: "what\n[Link](foo)\n[Link](bar)\n",
|
||||
},
|
||||
{
|
||||
in: "=> foo description is here ",
|
||||
exp: "[description is here](foo)\n",
|
||||
},
|
||||
{
|
||||
in: "=> img.png",
|
||||
exp: "![Image](img.png)\n",
|
||||
},
|
||||
{
|
||||
in: "=> img.png description is here ",
|
||||
exp: "![description is here](img.png)\n",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
|
||||
got := new(bytes.Buffer)
|
||||
err := GemtextToMarkdown(got, bytes.NewBufferString(test.in))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.exp, got.String())
|
||||
})
|
||||
}
|
||||
}
|
2
src/gmi/gmi.go
Normal file
2
src/gmi/gmi.go
Normal file
@ -0,0 +1,2 @@
|
||||
// Package gmi contains utilities for working with gemini and gemtext
|
||||
package gmi
|
@ -15,37 +15,68 @@ import (
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/gmi"
|
||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
|
||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
|
||||
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
||||
)
|
||||
|
||||
func (a *api) parsePostBody(post post.Post) (*txttpl.Template, error) {
|
||||
func (a *api) parsePostBody(p post.Post) (*txttpl.Template, error) {
|
||||
tpl := txttpl.New("root")
|
||||
tpl = tpl.Funcs(txttpl.FuncMap(a.tplFuncs()))
|
||||
|
||||
tpl = txttpl.Must(tpl.New("image.html").Parse(mustReadTplFile("image.html")))
|
||||
tpl = tpl.Funcs(txttpl.FuncMap{
|
||||
"Image": func(id string) (string, error) {
|
||||
|
||||
tplPayload := struct {
|
||||
ID string
|
||||
Resizable bool
|
||||
}{
|
||||
ID: id,
|
||||
Resizable: isImgResizable(id),
|
||||
}
|
||||
if p.Format == post.FormatMarkdown {
|
||||
tpl = tpl.Funcs(txttpl.FuncMap{
|
||||
"Image": func(id string) (string, error) {
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := tpl.ExecuteTemplate(buf, "image.html", tplPayload); err != nil {
|
||||
return "", err
|
||||
}
|
||||
tplPayload := struct {
|
||||
ID string
|
||||
Descr string
|
||||
Resizable bool
|
||||
}{
|
||||
ID: id,
|
||||
// I could use variadic args to make this work, I think
|
||||
Descr: "TODO: proper alt text",
|
||||
Resizable: isImgResizable(id),
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
},
|
||||
})
|
||||
buf := new(bytes.Buffer)
|
||||
if err := tpl.ExecuteTemplate(buf, "image.html", tplPayload); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tpl, err := tpl.New(post.ID + "-body.html").Parse(post.Body)
|
||||
return buf.String(), nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if p.Format == post.FormatGemtext {
|
||||
tpl = tpl.Funcs(txttpl.FuncMap{
|
||||
"Image": func(id, descr string) (string, error) {
|
||||
|
||||
tplPayload := struct {
|
||||
ID string
|
||||
Descr string
|
||||
Resizable bool
|
||||
}{
|
||||
ID: id,
|
||||
Descr: descr,
|
||||
Resizable: isImgResizable(id),
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := tpl.ExecuteTemplate(buf, "image.html", tplPayload); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
tpl, err := tpl.New(p.ID + "-body.html").Parse(p.Body)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -73,6 +104,16 @@ func (a *api) postToPostTplPayload(storedPost post.StoredPost) (postTplPayload,
|
||||
return postTplPayload{}, fmt.Errorf("executing post body as template: %w", err)
|
||||
}
|
||||
|
||||
if storedPost.Format == post.FormatGemtext {
|
||||
|
||||
prevBodyBuf := bodyBuf
|
||||
bodyBuf = new(bytes.Buffer)
|
||||
|
||||
if err := gmi.GemtextToMarkdown(bodyBuf, prevBodyBuf); err != nil {
|
||||
return postTplPayload{}, fmt.Errorf("converting gemtext to markdown: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// this helps the markdown renderer properly parse pages which end in a
|
||||
// `</script>` tag... I don't know why.
|
||||
_, _ = bodyBuf.WriteString("\n")
|
||||
@ -324,10 +365,12 @@ func (a *api) editPostHandler(isDraft bool) http.Handler {
|
||||
Post post.StoredPost
|
||||
Tags []string
|
||||
IsDraft bool
|
||||
Formats []post.Format
|
||||
}{
|
||||
Post: storedPost,
|
||||
Tags: tags,
|
||||
IsDraft: isDraft,
|
||||
Formats: post.Formats,
|
||||
}
|
||||
|
||||
executeTemplate(rw, r, tpl, tplPayload)
|
||||
@ -336,12 +379,23 @@ func (a *api) editPostHandler(isDraft bool) http.Handler {
|
||||
|
||||
func postFromPostReq(r *http.Request) (post.Post, error) {
|
||||
|
||||
formatStr := r.PostFormValue("format")
|
||||
if formatStr == "" {
|
||||
return post.Post{}, errors.New("format is required")
|
||||
}
|
||||
|
||||
format, err := post.FormatFromString(formatStr)
|
||||
if err != nil {
|
||||
return post.Post{}, fmt.Errorf("parsing format: %w", err)
|
||||
}
|
||||
|
||||
p := post.Post{
|
||||
ID: r.PostFormValue("id"),
|
||||
Title: r.PostFormValue("title"),
|
||||
Description: r.PostFormValue("description"),
|
||||
Tags: strings.Fields(r.PostFormValue("tags")),
|
||||
Series: r.PostFormValue("series"),
|
||||
Format: format,
|
||||
}
|
||||
|
||||
// textareas encode newlines as CRLF for historical reasons
|
||||
@ -353,7 +407,7 @@ func postFromPostReq(r *http.Request) (post.Post, error) {
|
||||
p.Title == "" ||
|
||||
p.Body == "" ||
|
||||
len(p.Tags) == 0 {
|
||||
return post.Post{}, errors.New("ID, Title, Tags, and Body are all required")
|
||||
return post.Post{}, errors.New("id, ritle, tags, and body are all required")
|
||||
}
|
||||
|
||||
return p, nil
|
||||
|
@ -61,6 +61,11 @@ func (a *api) manageAssetsURL(abs bool) string {
|
||||
return a.blogURL("assets?method=manage", abs)
|
||||
}
|
||||
|
||||
func (a *api) assetURL(id string, abs bool) string {
|
||||
path := filepath.Join("assets", id)
|
||||
return a.blogURL(path, false)
|
||||
}
|
||||
|
||||
func (a *api) draftPostURL(id string, abs bool) string {
|
||||
path := filepath.Join("drafts", id)
|
||||
return a.blogURL(path, abs)
|
||||
@ -96,8 +101,7 @@ func (a *api) tplFuncs() template.FuncMap {
|
||||
return a.postURL(id, false)
|
||||
},
|
||||
"AssetURL": func(id string) string {
|
||||
path := filepath.Join("assets", id)
|
||||
return a.blogURL(path, false)
|
||||
return a.assetURL(id, false)
|
||||
},
|
||||
"DraftURL": func(id string) string {
|
||||
return a.draftPostURL(id, false)
|
||||
|
@ -1,5 +1,8 @@
|
||||
<div style="text-align: center;">
|
||||
<a href="{{ AssetURL .ID }}" target="_blank">
|
||||
<img src="{{ AssetURL .ID }}{{ if .Resizable }}?w=800{{ end }}" />
|
||||
<img
|
||||
src="{{ AssetURL .ID }}{{ if .Resizable }}?w=800{{ end }}"
|
||||
alt="{{ .Descr }}"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -25,6 +25,7 @@
|
||||
<input
|
||||
name="id"
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g. how-to-fly-a-kite"
|
||||
value="{{ .Payload.Post.ID }}" />
|
||||
{{ else if .Payload.IsDraft }}
|
||||
@ -43,6 +44,7 @@
|
||||
<input
|
||||
name="tags"
|
||||
type="text"
|
||||
required
|
||||
value="{{- range $i, $tag := .Payload.Post.Tags -}}
|
||||
{{- if ne $i 0 }} {{ end }}{{ $tag -}}
|
||||
{{- end -}}
|
||||
@ -75,6 +77,7 @@
|
||||
<input
|
||||
name="title"
|
||||
type="text"
|
||||
required
|
||||
value="{{ .Payload.Post.Title }}" />
|
||||
</td>
|
||||
</tr>
|
||||
@ -89,11 +92,33 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Format</td>
|
||||
<td>
|
||||
<select name="format" required>
|
||||
<option value=""></option>
|
||||
|
||||
{{ $format := .Payload.Post.Format }}
|
||||
{{ range .Payload.Formats -}}
|
||||
<option
|
||||
{{- if eq . $format }}
|
||||
selected
|
||||
{{- end }}
|
||||
value="{{ . }}" >
|
||||
{{ . }}
|
||||
</option>
|
||||
{{- end }}
|
||||
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
<p>
|
||||
<textarea
|
||||
name="body"
|
||||
required
|
||||
placeholder="Post body"
|
||||
style="width:100%;height: 75vh;"
|
||||
>
|
||||
|
@ -49,22 +49,24 @@ func (s *draftStore) Set(post Post) error {
|
||||
|
||||
_, err = s.db.db.Exec(
|
||||
`INSERT INTO post_drafts (
|
||||
id, title, description, tags, series, body
|
||||
id, title, description, tags, series, body, format
|
||||
)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?)
|
||||
(?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
title=excluded.title,
|
||||
description=excluded.description,
|
||||
tags=excluded.tags,
|
||||
series=excluded.series,
|
||||
body=excluded.body`,
|
||||
body=excluded.body,
|
||||
format=excluded.format`,
|
||||
post.ID,
|
||||
post.Title,
|
||||
&sql.NullString{String: post.Description, Valid: len(post.Description) > 0},
|
||||
&sql.NullString{String: string(tagsJSON), Valid: len(post.Tags) > 0},
|
||||
&sql.NullString{String: post.Series, Valid: post.Series != ""},
|
||||
post.Body,
|
||||
post.Format,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@ -86,7 +88,7 @@ func (s *draftStore) get(
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
p.id, p.title, p.description, p.tags, p.series, p.body
|
||||
p.id, p.title, p.description, p.tags, p.series, p.body, p.format
|
||||
FROM post_drafts p
|
||||
` + where + `
|
||||
ORDER BY p.id ASC`
|
||||
@ -118,7 +120,7 @@ func (s *draftStore) get(
|
||||
|
||||
err := rows.Scan(
|
||||
&post.ID, &post.Title, &description, &tags, &series,
|
||||
&post.Body,
|
||||
&post.Body, &post.Format,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
@ -72,6 +72,7 @@ func TestDraftStore(t *testing.T) {
|
||||
post.Series = "whatever"
|
||||
post.Body = "anything"
|
||||
post.Tags = []string{"bar", "baz"}
|
||||
post.Format = FormatGemtext
|
||||
|
||||
err = h.store.Set(post)
|
||||
assert.NoError(t, err)
|
||||
|
44
src/post/format.go
Normal file
44
src/post/format.go
Normal file
@ -0,0 +1,44 @@
|
||||
package post
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrFormatStringMalformed indicates that a string could not be converted to a
|
||||
// Format.
|
||||
var ErrFormatStringMalformed = errors.New("format string malformed")
|
||||
|
||||
// Format describes the format of the body of a Post.
|
||||
type Format string
|
||||
|
||||
// Enumeration of possible formats.
|
||||
const (
|
||||
FormatMarkdown Format = "md"
|
||||
FormatGemtext Format = "gmi"
|
||||
)
|
||||
|
||||
// Formats slice of all possible Formats.
|
||||
var Formats = []Format{
|
||||
FormatMarkdown,
|
||||
FormatGemtext,
|
||||
}
|
||||
|
||||
var strsToFormats = func() map[string]Format {
|
||||
|
||||
m := map[string]Format{}
|
||||
|
||||
for _, f := range Formats {
|
||||
m[string(f)] = f
|
||||
}
|
||||
|
||||
return m
|
||||
}()
|
||||
|
||||
// FormatFromString parses a string into a Format, or returns
|
||||
// ErrFormatStringMalformed.
|
||||
func FormatFromString(str string) (Format, error) {
|
||||
|
||||
if f, ok := strsToFormats[str]; ok {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
return "", ErrFormatStringMalformed
|
||||
}
|
@ -34,6 +34,7 @@ type Post struct {
|
||||
Tags []string // only alphanumeric supported
|
||||
Series string
|
||||
Body string
|
||||
Format Format
|
||||
}
|
||||
|
||||
// StoredPost is a Post which has been stored in a Store, and has been given
|
||||
@ -103,22 +104,24 @@ func (s *store) Set(post Post, now time.Time) (bool, error) {
|
||||
|
||||
_, err := tx.Exec(
|
||||
`INSERT INTO posts (
|
||||
id, title, description, series, published_at, body
|
||||
id, title, description, series, published_at, body, format
|
||||
)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?)
|
||||
(?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
title=excluded.title,
|
||||
description=excluded.description,
|
||||
series=excluded.series,
|
||||
last_updated_at=?,
|
||||
body=excluded.body`,
|
||||
body=excluded.body,
|
||||
format=excluded.format`,
|
||||
post.ID,
|
||||
post.Title,
|
||||
&sql.NullString{String: post.Description, Valid: len(post.Description) > 0},
|
||||
&sql.NullString{String: post.Series, Valid: post.Series != ""},
|
||||
nowSQL,
|
||||
post.Body,
|
||||
post.Format,
|
||||
nowSQL,
|
||||
)
|
||||
|
||||
@ -174,8 +177,7 @@ func (s *store) get(
|
||||
query := `
|
||||
SELECT
|
||||
p.id, p.title, p.description, p.series, GROUP_CONCAT(pt.tag),
|
||||
p.published_at, p.last_updated_at,
|
||||
p.body
|
||||
p.published_at, p.last_updated_at, p.body, p.format
|
||||
FROM posts p
|
||||
LEFT JOIN post_tags pt ON (p.id = pt.post_id)
|
||||
` + where + `
|
||||
@ -208,8 +210,7 @@ func (s *store) get(
|
||||
|
||||
err := rows.Scan(
|
||||
&post.ID, &post.Title, &description, &series, &tag,
|
||||
&publishedAt, &lastUpdatedAt,
|
||||
&post.Body,
|
||||
&publishedAt, &lastUpdatedAt, &post.Body, &post.Format,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
@ -37,9 +37,10 @@ func TestNewID(t *testing.T) {
|
||||
func testPost(i int) Post {
|
||||
istr := strconv.Itoa(i)
|
||||
return Post{
|
||||
ID: istr,
|
||||
Title: istr,
|
||||
Body: istr,
|
||||
ID: istr,
|
||||
Title: istr,
|
||||
Body: istr,
|
||||
Format: FormatMarkdown,
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,7 +109,7 @@ func TestStore(t *testing.T) {
|
||||
post.Tags = []string{"foo", "bar"}
|
||||
|
||||
first, err := h.store.Set(post, now)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, err, "post:%+v", post)
|
||||
assert.True(t, first)
|
||||
|
||||
gotPost, err := h.store.GetByID(post.ID)
|
||||
@ -130,6 +131,7 @@ func TestStore(t *testing.T) {
|
||||
post.Series = "whatever"
|
||||
post.Body = "anything"
|
||||
post.Tags = []string{"bar", "baz"}
|
||||
post.Format = FormatGemtext
|
||||
|
||||
first, err = h.store.Set(post, newNow)
|
||||
assert.NoError(t, err)
|
||||
|
@ -66,6 +66,13 @@ var migrations = &migrate.MemoryMigrationSource{Migrations: []*migrate.Migration
|
||||
`ALTER TABLE posts DROP COLUMN description_old`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "4",
|
||||
Up: []string{
|
||||
`ALTER TABLE post_drafts ADD COLUMN format TEXT DEFAULT 'md'`,
|
||||
`ALTER TABLE posts ADD COLUMN format TEXT DEFAULT 'md'`,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
// SQLDB is a sqlite3 database which can be used by storage interfaces within
|
||||
|
Loading…
Reference in New Issue
Block a user