DEV Community

Cover image for Attempting to Learn Go - Ghost to Hugo 3
Steve Layton
Steve Layton

Posted on • Originally published at shindakun.dev on

Attempting to Learn Go - Ghost to Hugo 3

Intro

We’re back once again to continue our work on the ghost2hugo prototype. So far we can open the JSON file, load the data into memory, and print out the Markdown for the first post. The next step is to make sure we can read and process every post included in the backup.

Looping

We’re going to replace all the code that prints a single article with a loop that prints out each article. For a refresher, here is the code that prints the first article.

    c := "`" + db.Db[0].Data.Posts[0].Mobiledoc + "`"

    un, err := strconv.Unquote(c)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Printf("%v", un)

    var md Mobiledoc

    err = json.Unmarshal([]byte(un), &md)
    if err != nil {
        fmt.Println(err)
    }

    fmt.Printf("%#v", md)

    card := md.Cards[0][1]
    fmt.Printf("\n\ncard: %#v\n", card)

    bbb := card.(map[string]interface{})
    fmt.Println(bbb["markdown"])
Enter fullscreen mode Exit fullscreen mode

Most of this will live inside of our new loop.

    for i := 0; i < len(db.Db[0].Data.Posts); i++ {
        fmt.Println(db.Db[0].Data.Posts[i].Title)
        fmt.Println(db.Db[0].Data.Posts[i].Slug)
        fmt.Println(db.Db[0].Data.Posts[i].Status)
        fmt.Println(db.Db[0].Data.Posts[i].CreatedAt)
        fmt.Println(db.Db[0].Data.Posts[i].UpdatedAt)

        cc := "`" + db.Db[0].Data.Posts[i].Mobiledoc + "`"

        ucn, err := strconv.Unquote(cc)
        if err != nil {
            fmt.Println(err, ucn)
        }
        fmt.Printf("\n\n\n%v\n\n\n", ucn)

        var md Mobiledoc

        err = json.Unmarshal([]byte(ucn), &md)
        if err != nil {
            fmt.Println(err)
        }

        card := md.Cards[0][1]
        bbb := card.(map[string]interface{})

        fmt.Println(bbb["markdown"])
        }
    }
Enter fullscreen mode Exit fullscreen mode

And when we run the code this time we see a bunch of articles fly by! Until, that is, we hit an error! The invalid syntax error indicates this is a problem with strconv.Unquote().

invalid syntax 

unexpected end of JSON input
panic: runtime error: index out of range [0] with length 0

goroutine 1 [running]:
main.main()
        /Users/steve/Code/ghost2hugo/main.go:214 +0x769
exit status 2
Enter fullscreen mode Exit fullscreen mode

Roadblocks

Well that’s no good, we can’t just have our code crash out like that! Let’s take a closer look at the problem and see if we can work our way through it. OK, now for the master of all debugging techniques! We add a fmt.Println to print out the chunk of text.

        fmt.Println(cc)
Enter fullscreen mode Exit fullscreen mode

Running the code again we crash out as expected but this time we’re presented with the problem text:

`{"version":"0.3.1","markups":[],"atoms":[],"cards":[["markdown",{"cardName":"card-markdown","markdown":"\n* Incresed title to 64 characters. Should be more then enough. \n * ics and other code work over at angstridden dot net tonight.\n\nALTER TABLE `posts` CHANGE `posttitle` `posttitle` VARCHAR( 64 ) NOT NULL\n\n\n"}]],"sections":[[10,0]],"ghostVersion":"3.0"}`
Enter fullscreen mode Exit fullscreen mode

See the problem? Yeah, it looks like there are back ticks in the Markdown that are causing the string convert to not work as expected. To get around this we’re going to do a simple strings.ReplaceAll before the conversion to change the back tick to something else. We can then reverse that process to get the normal text. We’ll replace the back tick with “%'”.

        c := strings.ReplaceAll(db.Db[0].Data.Posts[i].Mobiledoc, "`", "%'")
        cc := "`" + c + "`"
Enter fullscreen mode Exit fullscreen mode

After we “unquote” we can run another strings.ReplaceAll to convert back.

        ucn = strings.ReplaceAll(ucn, "%'", "`")
Enter fullscreen mode Exit fullscreen mode

Running again we run into another problem!

---
Archives
archives-post
published
2007-07-01 09:46:18 +0000 UTC
2007-08-12 20:50:02 +0000 UTC
`{"version":"0.3.1","markups":[],"atoms":[],"cards":[],"sections":[[1,"p",[[0,[],0,""]]]],"ghostVersion":"3.0"}`
panic: runtime error: index out of range [0] with length 0

goroutine 1 [running]:
main.main()
        /Users/steve/Code/ghost2hugo/main.go:214 +0x7a6
exit status 2
Enter fullscreen mode Exit fullscreen mode

Argh!

More Roadblocks

Our naive implementation from the previous post has returned to bite us! We were assuming that cards exist.

        card := md.Cards[0][1]
        bbb := card.(map[string]interface{})
Enter fullscreen mode Exit fullscreen mode

Looking at the returned JSON confirms this.

{"version":"0.3.1","markups":[],"atoms":[],"cards":[],"sections":[[1,"p",[[0,[],0,""]]]],"ghostVersion":"3.0"}

Enter fullscreen mode Exit fullscreen mode

Now, how do we check to see if the cards field is empty?! I noodled on it a little bit and figured the best way would be to use reflect.ValueOf().Len(). This will check the length of md.Cards to ensure we actually have some data.

        if reflect.ValueOf(md.Cards).Len() > 0 {
            card := md.Cards[0][1]
            bbb := card.(map[string]interface{})

            fmt.Println(bbb["markdown"])
        }
Enter fullscreen mode Exit fullscreen mode

On rerunning we get what looks to be all the posts. Except the final draft which looks odd! It turns out this one has no Markdown card. Is this the only one? I certainly hope so since it looks like it may be a pain to convert to Markdown.

Questing
questing
draft
2019-08-23 21:26:44 +0000 UTC
2021-04-12 04:53:26 +0000 UTC
`{"version":"0.3.1","atoms":[],"cards":[["paywall",{}]],"markups":[["a",["href","https://shindakun.dev"]],["a",["href","https://dev.to/shindakun"]]],"sections":[[1,"p",[[0,[],0,"It's been quite sometime since I've posted anything here. I've done a bit of posting over on "],[0,[0],1,"shindakun.dev"],[0,[],0," and over on "],[0,[1],1,"DEV"],[0,[],0," which has been kind of nice. But, those don't really cover gaming at all. But, I updated the site recently and everyone once in a while I use the site to try something out for work and seeing an old post over and over was no good."]]],[1,"p",[[0,[],0,"I only seem to have a small sliver of time for gaming now. With everything else I want/need to get done there is only so much time. Heed this warning - don't get older! Just kidding, it's not so bad."]]],[1,"p",[[0,[],0,"My 5 year old hasn't been introduced into gaming much outside of some basic games she can play on her tablet. I have discovered that she enjoys watching me play the digital version of Warhammer Quest. Which is good since she's going to be getting a crash course in some more difficult board games sooner or later. I have a copy of the Gloomhaven digital board game and the boxed game (and expansion) but haven't actually event opened it yet. Maybe I should fix that this weekend..."]]],[10,0],[1,"p",[]]],"ghostVersion":"3.0"}`
json: cannot unmarshal string into Go struct field Mobiledoc.sections of type int
<nil>
Enter fullscreen mode Exit fullscreen mode

Looks like we also have an issue unmarshaling, on that last line there. This is easy enough to fix for now. We just need to update sections in our Mobiledoc struct to use [][]interface{} and not [][]int.

type Mobiledoc struct {
    Version string `json:"version"`
    Markups []interface{} `json:"markups"`
    Atoms []interface{} `json:"atoms"`
    Cards [][]interface{} `json:"cards"`
    Sections [][]interface{} `json:"sections"`
    GhostVersion string `json:"ghostVersion"`
}
Enter fullscreen mode Exit fullscreen mode

Next Time

We’ve made some good progress over the last few posts. On the first look it seems we’re extracting what we need. I think we’re ready to do a little refactoring, remove our current debug print statements, and get ready for the next part of our converter.



Code Listing

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "os"
    "reflect"
    "strconv"
    "strings"
    "time"
)

type GhostDatabase struct {
    Db []struct {
        Meta struct {
            ExportedOn int64 `json:"exported_on"`
            Version string `json:"version"`
        } `json:"meta"`
        Data struct {
            Posts []struct {
                ID string `json:"id"`
                UUID string `json:"uuid"`
                Title string `json:"title"`
                Slug string `json:"slug"`
                Mobiledoc string `json:"mobiledoc"`
                HTML string `json:"html"`
                CommentID string `json:"comment_id"`
                Plaintext string `json:"plaintext"`
                FeatureImage interface{} `json:"feature_image"`
                Featured int `json:"featured"`
                Type string `json:"type"`
                Status string `json:"status"`
                Locale interface{} `json:"locale"`
                Visibility string `json:"visibility"`
                EmailRecipientFilter string `json:"email_recipient_filter"`
                AuthorID string `json:"author_id"`
                CreatedAt time.Time `json:"created_at"`
                UpdatedAt time.Time `json:"updated_at"`
                PublishedAt time.Time `json:"published_at"`
                CustomExcerpt interface{} `json:"custom_excerpt"`
                CodeinjectionHead interface{} `json:"codeinjection_head"`
                CodeinjectionFoot interface{} `json:"codeinjection_foot"`
                CustomTemplate interface{} `json:"custom_template"`
                CanonicalURL interface{} `json:"canonical_url"`
            } `json:"posts"`
            PostsAuthors []struct {
                ID string `json:"id"`
                PostID string `json:"post_id"`
                AuthorID string `json:"author_id"`
                SortOrder int `json:"sort_order"`
            } `json:"posts_authors"`
            PostsMeta []interface{} `json:"posts_meta"`
            PostsTags []struct {
                ID string `json:"id"`
                PostID string `json:"post_id"`
                TagID string `json:"tag_id"`
                SortOrder int `json:"sort_order"`
            } `json:"posts_tags"`
            Roles []struct {
                ID string `json:"id"`
                Name string `json:"name"`
                Description string `json:"description"`
                CreatedAt time.Time `json:"created_at"`
                UpdatedAt time.Time `json:"updated_at"`
            } `json:"roles"`
            RolesUsers []struct {
                ID string `json:"id"`
                RoleID string `json:"role_id"`
                UserID string `json:"user_id"`
            } `json:"roles_users"`
            Settings []struct {
                ID string `json:"id"`
                Group string `json:"group"`
                Key string `json:"key"`
                Value string `json:"value"`
                Type string `json:"type"`
                Flags interface{} `json:"flags"`
                CreatedAt time.Time `json:"created_at"`
                UpdatedAt time.Time `json:"updated_at"`
            } `json:"settings"`
            Tags []struct {
                ID string `json:"id"`
                Name string `json:"name"`
                Slug string `json:"slug"`
                Description interface{} `json:"description"`
                FeatureImage interface{} `json:"feature_image"`
                ParentID interface{} `json:"parent_id"`
                Visibility string `json:"visibility"`
                OgImage interface{} `json:"og_image"`
                OgTitle interface{} `json:"og_title"`
                OgDescription interface{} `json:"og_description"`
                TwitterImage interface{} `json:"twitter_image"`
                TwitterTitle interface{} `json:"twitter_title"`
                TwitterDescription interface{} `json:"twitter_description"`
                MetaTitle interface{} `json:"meta_title"`
                MetaDescription interface{} `json:"meta_description"`
                CodeinjectionHead interface{} `json:"codeinjection_head"`
                CodeinjectionFoot interface{} `json:"codeinjection_foot"`
                CanonicalURL interface{} `json:"canonical_url"`
                AccentColor interface{} `json:"accent_color"`
                CreatedAt time.Time `json:"created_at"`
                UpdatedAt time.Time `json:"updated_at"`
            } `json:"tags"`
            Users []struct {
                ID string `json:"id"`
                Name string `json:"name"`
                Slug string `json:"slug"`
                Password string `json:"password"`
                Email string `json:"email"`
                ProfileImage string `json:"profile_image"`
                CoverImage interface{} `json:"cover_image"`
                Bio interface{} `json:"bio"`
                Website interface{} `json:"website"`
                Location interface{} `json:"location"`
                Facebook interface{} `json:"facebook"`
                Twitter interface{} `json:"twitter"`
                Accessibility string `json:"accessibility"`
                Status string `json:"status"`
                Locale interface{} `json:"locale"`
                Visibility string `json:"visibility"`
                MetaTitle interface{} `json:"meta_title"`
                MetaDescription interface{} `json:"meta_description"`
                Tour interface{} `json:"tour"`
                LastSeen time.Time `json:"last_seen"`
                CreatedAt time.Time `json:"created_at"`
                UpdatedAt time.Time `json:"updated_at"`
            } `json:"users"`
        } `json:"data"`
    } `json:"db"`
}

type Mobiledoc struct {
    Version string `json:"version"`
    Markups []interface{} `json:"markups"`
    Atoms []interface{} `json:"atoms"`
    Cards [][]interface{} `json:"cards"`
    Sections [][]interface{} `json:"sections"`
    GhostVersion string `json:"ghostVersion"`
}

func main() {
    fmt.Println("ghost2hugo")

    file, err := os.Open("shindakun-dot-net.ghost.2022-03-18-22-02-58.json")
    if err != nil {
        fmt.Println(err)
    }

    defer file.Close()

    b, err := io.ReadAll(file)
    if err != nil {
        fmt.Println(err)
    }

    var db GhostDatabase

    err = json.Unmarshal(b, &db)
    if err != nil {
        fmt.Println(err)
    }

    for i := 0; i < len(db.Db[0].Data.Posts); i++ {
        fmt.Println(db.Db[0].Data.Posts[i].Title)
        fmt.Println(db.Db[0].Data.Posts[i].Slug)
        fmt.Println(db.Db[0].Data.Posts[i].Status)
        fmt.Println(db.Db[0].Data.Posts[i].CreatedAt)
        fmt.Println(db.Db[0].Data.Posts[i].UpdatedAt)

        c := strings.ReplaceAll(db.Db[0].Data.Posts[i].Mobiledoc, "`", "%'")
        cc := "`" + c + "`"

        fmt.Println(cc)

        ucn, err := strconv.Unquote(cc)
        if err != nil {
            fmt.Println(err, ucn)
        }

        ucn = strings.ReplaceAll(ucn, "%'", "`")

        var md Mobiledoc

        err = json.Unmarshal([]byte(ucn), &md)
        if err != nil {
            fmt.Println(err)
        }

        if reflect.ValueOf(md.Cards).Len() > 0 {
            card := md.Cards[0][1]
            bbb := card.(map[string]interface{})

            fmt.Println(bbb["markdown"])
        }

        fmt.Println("---")
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)