├── .gitignore ├── go.mod ├── util.go ├── .github └── workflows │ ├── test.yaml │ └── coverage.yaml ├── util_test.go ├── go.sum ├── LICENSE ├── html.go ├── examples ├── main.go └── test.json ├── markdown.go ├── types.go ├── html_test.go ├── markdown_test.go ├── README.md ├── handlers.go └── handlers_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | examples/editorjs.* -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/davidscottmills/goeditorjs 2 | 3 | go 1.15 4 | 5 | require github.com/stretchr/testify v1.6.1 6 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package goeditorjs 2 | 3 | import "encoding/json" 4 | 5 | // parseEditorJSON parses editorJS data 6 | func parseEditorJSON(editorJSData string) (*editorJS, error) { 7 | result := &editorJS{} 8 | err := json.Unmarshal([]byte(editorJSData), result) 9 | if err != nil { 10 | return nil, err 11 | } 12 | return result, err 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: test and build 6 | jobs: 7 | test: 8 | strategy: 9 | matrix: 10 | go-version: [1.14.x, 1.15.x] 11 | os: [ubuntu-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | - name: Test 21 | run: go test -v -race ./... -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package goeditorjs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_parseEditorJSON(t *testing.T) { 10 | editorJSData := `{"time": 1607709186831,"blocks": [{"type": "header","data": {"text": "Heading 1","level": 1}}],"version": "2.19.1"}` 11 | editorJS, err := parseEditorJSON(editorJSData) 12 | require.NoError(t, err) 13 | require.Len(t, editorJS.Blocks, 1) 14 | } 15 | 16 | func Test_parseEditorJSON_Err_Empty(t *testing.T) { 17 | editorJSData := `` 18 | 19 | _, err := parseEditorJSON(editorJSData) 20 | require.Error(t, err) 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: coverage 6 | jobs: 7 | coverage: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Install Go 11 | if: success() 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: 1.14.x 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Calc coverage 18 | run: go test -v -covermode=count -coverprofile=coverage.out 19 | - name: Convert coverage to lcov 20 | uses: jandelgado/gcov2lcov-action@v1.0.0 21 | with: 22 | infile: coverage.out 23 | outfile: coverage.lcov 24 | - name: Coveralls 25 | uses: coverallsapp/github-action@v1.1.2 26 | with: 27 | github-token: ${{ secrets.github_token }} 28 | path-to-lcov: coverage.lcov -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 8 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 davidscottmills 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /html.go: -------------------------------------------------------------------------------- 1 | package goeditorjs 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // HTMLEngine is the engine that creates the HTML from EditorJS blocks 8 | type HTMLEngine struct { 9 | BlockHandlers map[string]HTMLBlockHandler 10 | } 11 | 12 | // HTMLBlockHandler is an interface for a plugable EditorJS HTML generator 13 | type HTMLBlockHandler interface { 14 | Type() string // Type returns the type the block handler supports as a string 15 | GenerateHTML(editorJSBlock EditorJSBlock) (string, error) 16 | } 17 | 18 | // NewHTMLEngine creates a new HTMLEngine 19 | func NewHTMLEngine() *HTMLEngine { 20 | bhs := make(map[string]HTMLBlockHandler) 21 | return &HTMLEngine{BlockHandlers: bhs} 22 | } 23 | 24 | // RegisterBlockHandlers registers or overrides a block handlers for blockType given by HTMLBlockHandler.Type() 25 | func (htmlEngine *HTMLEngine) RegisterBlockHandlers(handlers ...HTMLBlockHandler) { 26 | for _, bh := range handlers { 27 | htmlEngine.BlockHandlers[bh.Type()] = bh 28 | } 29 | } 30 | 31 | // GenerateHTML generates html from the editorJS using configured set of HTML handlers 32 | func (htmlEngine *HTMLEngine) GenerateHTML(editorJSData string) (string, error) { 33 | result := "" 34 | ejs, err := parseEditorJSON(editorJSData) 35 | if err != nil { 36 | return "", err 37 | } 38 | for _, block := range ejs.Blocks { 39 | if generator, ok := htmlEngine.BlockHandlers[block.Type]; ok { 40 | html, err := generator.GenerateHTML(block) 41 | if err != nil { 42 | return result, err 43 | } 44 | result += html 45 | } else { 46 | return "", fmt.Errorf("%w, Block Type: %s", ErrBlockHandlerNotFound, block.Type) 47 | } 48 | } 49 | 50 | return result, nil 51 | } 52 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | 7 | "github.com/davidscottmills/goeditorjs" 8 | ) 9 | 10 | func main() { 11 | content, err := ioutil.ReadFile("test.json") 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | 16 | ejs := string(content) 17 | 18 | // Generate HTML and save it to a file 19 | htmlEngine := goeditorjs.NewHTMLEngine() 20 | htmlEngine.RegisterBlockHandlers( 21 | &goeditorjs.HeaderHandler{}, 22 | &goeditorjs.ParagraphHandler{}, 23 | &goeditorjs.ListHandler{}, 24 | &goeditorjs.CodeBoxHandler{}, 25 | &goeditorjs.RawHTMLHandler{}, 26 | &goeditorjs.ImageHandler{Options: &goeditorjs.ImageHandlerOptions{ 27 | StretchClass: "imageStretched", 28 | BorderClass: "imageBorder", 29 | BackgroundClass: "imageBackground"}}, 30 | ) 31 | html, err := htmlEngine.GenerateHTML(ejs) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | err = ioutil.WriteFile("editorjs.html", []byte(html), 0644) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | // Generate markdown and save it to a file 42 | markdownEngine := goeditorjs.NewMarkdownEngine() 43 | markdownEngine.RegisterBlockHandlers( 44 | &goeditorjs.HeaderHandler{}, 45 | &goeditorjs.ParagraphHandler{}, 46 | &goeditorjs.ListHandler{}, 47 | &goeditorjs.CodeBoxHandler{}, 48 | &goeditorjs.RawHTMLHandler{}, 49 | &goeditorjs.ImageHandler{Options: &goeditorjs.ImageHandlerOptions{ 50 | StretchClass: "imageStretched", 51 | BorderClass: "imageBorder", 52 | BackgroundClass: "imageBackground"}}, 53 | ) 54 | md, err := markdownEngine.GenerateMarkdown(ejs) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | 59 | err = ioutil.WriteFile("editorjs.md", []byte(md), 0644) 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /markdown.go: -------------------------------------------------------------------------------- 1 | package goeditorjs 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // MarkdownEngine is the engine that creates the HTML from EditorJS blocks 9 | type MarkdownEngine struct { 10 | BlockHandlers map[string]MarkdownBlockHandler 11 | } 12 | 13 | // MarkdownBlockHandler is an interface for a plugable EditorJS HTML generator 14 | type MarkdownBlockHandler interface { 15 | Type() string // Type returns the type the block handler supports as a string 16 | GenerateMarkdown(editorJSBlock EditorJSBlock) (string, error) 17 | } 18 | 19 | // NewMarkdownEngine creates a new MarkdownEngine 20 | func NewMarkdownEngine() *MarkdownEngine { 21 | bhs := make(map[string]MarkdownBlockHandler) 22 | return &MarkdownEngine{BlockHandlers: bhs} 23 | } 24 | 25 | // RegisterBlockHandlers registers or overrides a block handlers for blockType given by MarkdownBlockHandler.Type() 26 | func (markdownEngine *MarkdownEngine) RegisterBlockHandlers(handlers ...MarkdownBlockHandler) { 27 | for _, bh := range handlers { 28 | markdownEngine.BlockHandlers[bh.Type()] = bh 29 | } 30 | } 31 | 32 | // GenerateMarkdown generates markdown from the editorJS using configured set of markdown handlers 33 | func (markdownEngine *MarkdownEngine) GenerateMarkdown(editorJSData string) (string, error) { 34 | results := []string{} 35 | ejs, err := parseEditorJSON(editorJSData) 36 | if err != nil { 37 | return "", err 38 | } 39 | for _, block := range ejs.Blocks { 40 | if generator, ok := markdownEngine.BlockHandlers[block.Type]; ok { 41 | md, err := generator.GenerateMarkdown(block) 42 | if err != nil { 43 | return "", err 44 | } 45 | results = append(results, md) 46 | } else { 47 | return "", fmt.Errorf("%w, Block Type: %s", ErrBlockHandlerNotFound, block.Type) 48 | } 49 | } 50 | 51 | return strings.Join(results, "\n\n"), nil 52 | } 53 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package goeditorjs 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | // editorJS rpresents the Editor JS data 9 | type editorJS struct { 10 | Blocks []EditorJSBlock `json:"blocks"` 11 | } 12 | 13 | // EditorJSBlock type 14 | type EditorJSBlock struct { 15 | Type string `json:"type"` 16 | // Data is the Data for an editorJS block in the form of RawMessage ([]byte). It is left up to the Handler to parse the Data field 17 | Data json.RawMessage `json:"data"` 18 | } 19 | 20 | var ( 21 | //ErrBlockHandlerNotFound is returned from GenerateHTML when the HTML engine doesn't have a registered handler 22 | //for that type and the HTMLEngine is set to return on errors. 23 | ErrBlockHandlerNotFound = errors.New("Handler not found for block type") 24 | ) 25 | 26 | // header represents header data from EditorJS 27 | type header struct { 28 | Text string `json:"text"` 29 | Level int `json:"level"` 30 | } 31 | 32 | // paragraph represents paragraph data from EditorJS 33 | type paragraph struct { 34 | Text string `json:"text"` 35 | Alignment string `json:"alignment"` 36 | } 37 | 38 | // list represents list data from EditorJS 39 | type list struct { 40 | Style string `json:"style"` 41 | Items []string `json:"items"` 42 | } 43 | 44 | // codeBox represents code box data from EditorJS 45 | type codeBox struct { 46 | Code string `json:"code"` 47 | Language string `json:"language"` 48 | } 49 | 50 | // raw represents raw html data from EditorJS 51 | type raw struct { 52 | HTML string `json:"html"` 53 | } 54 | 55 | // image represents image data from EditorJS 56 | type image struct { 57 | File file `json:"file"` 58 | Caption string `json:"caption"` 59 | WithBorder bool `json:"withBorder"` 60 | WithBackground bool `json:"withBackground"` 61 | Stretched bool `json:"stretched"` 62 | } 63 | 64 | type file struct { 65 | URL string `json:"url"` 66 | } 67 | -------------------------------------------------------------------------------- /html_test.go: -------------------------------------------------------------------------------- 1 | package goeditorjs_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/davidscottmills/goeditorjs" 8 | "github.com/stretchr/testify/mock" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type mockHTMLBlockHandler struct { 13 | mock.Mock 14 | typeName string 15 | } 16 | 17 | func (m *mockHTMLBlockHandler) GenerateHTML(editorJSBlock goeditorjs.EditorJSBlock) (string, error) { 18 | args := m.Called(editorJSBlock) 19 | return args.String(0), args.Error(1) 20 | } 21 | 22 | func (m *mockHTMLBlockHandler) Type() string { 23 | return m.typeName 24 | } 25 | 26 | func Test_NewHTMLEngine(t *testing.T) { 27 | eng := goeditorjs.NewHTMLEngine() 28 | require.NotNil(t, eng) 29 | require.NotNil(t, eng.BlockHandlers) 30 | } 31 | 32 | func Test_HTMLEngine_RegisterBlockHandler(t *testing.T) { 33 | bh1 := &mockHTMLBlockHandler{typeName: "header"} 34 | bh2 := &mockHTMLBlockHandler{typeName: "list"} 35 | eng := &goeditorjs.HTMLEngine{BlockHandlers: make(map[string]goeditorjs.HTMLBlockHandler)} 36 | eng.RegisterBlockHandlers(bh1, bh2) 37 | require.Equal(t, eng.BlockHandlers["header"], bh1) 38 | require.Equal(t, eng.BlockHandlers["list"], bh2) 39 | } 40 | 41 | func Test_GenerateHTML_Returns_Parse_Err(t *testing.T) { 42 | eng := &goeditorjs.HTMLEngine{BlockHandlers: make(map[string]goeditorjs.HTMLBlockHandler)} 43 | _, err := eng.GenerateHTML(``) 44 | require.Error(t, err) 45 | } 46 | 47 | func Test_GenerateHTML_NoHandler_Should_Err(t *testing.T) { 48 | editorJSData := `{"time": 1607709186831,"blocks": [{"type": "header","data": {"text": "Heading 1","level": 1}}],"version": "2.19.1"}` 49 | eng := &goeditorjs.HTMLEngine{BlockHandlers: make(map[string]goeditorjs.HTMLBlockHandler)} 50 | _, err := eng.GenerateHTML(editorJSData) 51 | require.Error(t, err) 52 | require.True(t, errors.Is(err, goeditorjs.ErrBlockHandlerNotFound)) 53 | } 54 | 55 | func Test_GenerateHTML_Returns_Err_From_Handler(t *testing.T) { 56 | bh := &mockHTMLBlockHandler{} 57 | mockErr := errors.New("Mock Error") 58 | bh.On("GenerateHTML", mock.Anything).Return("", mockErr) 59 | editorJSData := `{"time": 1607709186831,"blocks": [{"type": "header","data": {"text": "Heading 1","level": 1}}],"version": "2.19.1"}` 60 | eng := &goeditorjs.HTMLEngine{BlockHandlers: make(map[string]goeditorjs.HTMLBlockHandler)} 61 | eng.BlockHandlers["header"] = bh 62 | _, err := eng.GenerateHTML(editorJSData) 63 | require.Error(t, err) 64 | require.Equal(t, mockErr, err) 65 | bh.AssertCalled(t, "GenerateHTML", mock.Anything) 66 | } 67 | 68 | func Test_GenerateHTML_Result_Includes_Handler_Result(t *testing.T) { 69 | bh := &mockHTMLBlockHandler{} 70 | handlerResult := "

Hello World

" 71 | bh.On("GenerateHTML", mock.Anything).Return(handlerResult, nil) 72 | editorJSData := `{"time": 1607709186831,"blocks": [{"type": "header","data": {"text": "Heading 1","level": 1}}],"version": "2.19.1"}` 73 | eng := &goeditorjs.HTMLEngine{BlockHandlers: make(map[string]goeditorjs.HTMLBlockHandler)} 74 | eng.BlockHandlers["header"] = bh 75 | result, _ := eng.GenerateHTML(editorJSData) 76 | require.Contains(t, result, handlerResult) 77 | bh.AssertCalled(t, "GenerateHTML", mock.Anything) 78 | } 79 | -------------------------------------------------------------------------------- /markdown_test.go: -------------------------------------------------------------------------------- 1 | package goeditorjs_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/davidscottmills/goeditorjs" 8 | "github.com/stretchr/testify/mock" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type mockMarkdownBlockHandler struct { 13 | mock.Mock 14 | typeName string 15 | } 16 | 17 | func (m *mockMarkdownBlockHandler) GenerateMarkdown(editorJSBlock goeditorjs.EditorJSBlock) (string, error) { 18 | args := m.Called(editorJSBlock) 19 | return args.String(0), args.Error(1) 20 | } 21 | 22 | func (m *mockMarkdownBlockHandler) Type() string { 23 | return m.typeName 24 | } 25 | 26 | func Test_Markdown(t *testing.T) { 27 | eng := goeditorjs.NewMarkdownEngine() 28 | require.NotNil(t, eng) 29 | require.NotNil(t, eng.BlockHandlers) 30 | } 31 | 32 | func Test_MarkdownEngine_RegisterBlockHandler(t *testing.T) { 33 | bh1 := &mockMarkdownBlockHandler{typeName: "header"} 34 | bh2 := &mockMarkdownBlockHandler{typeName: "list"} 35 | eng := &goeditorjs.MarkdownEngine{BlockHandlers: make(map[string]goeditorjs.MarkdownBlockHandler)} 36 | eng.RegisterBlockHandlers(bh1, bh2) 37 | require.Equal(t, eng.BlockHandlers["header"], bh1) 38 | require.Equal(t, eng.BlockHandlers["list"], bh2) 39 | } 40 | 41 | func Test_GenerateMarkdown_Returns_Parse_Err(t *testing.T) { 42 | eng := &goeditorjs.MarkdownEngine{BlockHandlers: make(map[string]goeditorjs.MarkdownBlockHandler)} 43 | _, err := eng.GenerateMarkdown(``) 44 | require.Error(t, err) 45 | } 46 | 47 | func Test_GenerateMarkdown_NoHandler_Should_Err(t *testing.T) { 48 | editorJSData := `{"time": 1607709186831,"blocks": [{"type": "header","data": {"text": "Heading 1","level": 1}}],"version": "2.19.1"}` 49 | eng := &goeditorjs.MarkdownEngine{BlockHandlers: make(map[string]goeditorjs.MarkdownBlockHandler)} 50 | _, err := eng.GenerateMarkdown(editorJSData) 51 | require.Error(t, err) 52 | require.True(t, errors.Is(err, goeditorjs.ErrBlockHandlerNotFound)) 53 | } 54 | 55 | func Test_GenerateMarkdown_Returns_Err_From_Handler(t *testing.T) { 56 | bh := &mockMarkdownBlockHandler{} 57 | mockErr := errors.New("Mock Error") 58 | bh.On("GenerateMarkdown", mock.Anything).Return("", mockErr) 59 | editorJSData := `{"time": 1607709186831,"blocks": [{"type": "header","data": {"text": "Heading 1","level": 1}}],"version": "2.19.1"}` 60 | eng := &goeditorjs.MarkdownEngine{BlockHandlers: make(map[string]goeditorjs.MarkdownBlockHandler)} 61 | eng.BlockHandlers["header"] = bh 62 | _, err := eng.GenerateMarkdown(editorJSData) 63 | require.Error(t, err) 64 | require.Equal(t, mockErr, err) 65 | bh.AssertCalled(t, "GenerateMarkdown", mock.Anything) 66 | } 67 | 68 | func Test_GenerateMarkdown_Result_Includes_Handler_Result(t *testing.T) { 69 | bh := &mockMarkdownBlockHandler{} 70 | handlerResult := "# Hello World" 71 | bh.On("GenerateMarkdown", mock.Anything).Return(handlerResult, nil) 72 | editorJSData := `{"time": 1607709186831,"blocks": [{"type": "header","data": {"text": "Heading 1","level": 1}}],"version": "2.19.1"}` 73 | eng := &goeditorjs.MarkdownEngine{BlockHandlers: make(map[string]goeditorjs.MarkdownBlockHandler)} 74 | eng.BlockHandlers["header"] = bh 75 | result, _ := eng.GenerateMarkdown(editorJSData) 76 | require.Contains(t, result, handlerResult) 77 | bh.AssertCalled(t, "GenerateMarkdown", mock.Anything) 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # davidscottmills/goeditorjs 2 | 3 | An extensible library that converts [editor.js](https://editorjs.io/) data into HTML or markdown. 4 | 5 | [![test and build](https://github.com/davidscottmills/goeditorjs/workflows/test%20and%20build/badge.svg)](https://github.com/davidscottmills/goeditorjs/actions?query=workflow%3A%22test+and+build%22) 6 | [![Coverage Status](https://coveralls.io/repos/github/davidscottmills/goeditorjs/badge.svg?branch=main)](https://coveralls.io/github/davidscottmills/goeditorjs?branch=main) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/davidscottmills/goeditorjs)](https://goreportcard.com/report/github.com/davidscottmills/goeditorjs) 8 | [![Documentation](https://godoc.org/github.com/davidscottmills/goeditorjs?status.svg)](http://godoc.org/github.com/davidscottmills/goeditorjs) 9 | [![GitHub issues](https://img.shields.io/github/issues/davidscottmills/goeditorjs.svg)](https://github.com/davidscottmills/goeditorjs/issues) 10 | [![license](https://img.shields.io/github/license/davidscottmills/goeditorjs.svg?maxAge=2592000)](https://github.com/davidscottmills/goeditorjs/LICENSE.md) 11 | [![GitHub go.mod Go version of a Go module](https://img.shields.io/github/go-mod/go-version/davidscottmills/goeditorjs.svg)](https://github.com/davidscottmills/goeditorjs) 12 | 13 | ## Installation 14 | 15 | ```bash 16 | go get github.com/davidscottmills/goeditorjs 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```go 22 | package main 23 | 24 | import ( 25 | "io/ioutil" 26 | "log" 27 | 28 | "github.com/davidscottmills/goeditorjs" 29 | ) 30 | 31 | func main() { 32 | content, err := ioutil.ReadFile("editorjs_output.json") 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | ejs := string(content) 38 | 39 | // HTML 40 | // Get the HTML engine 41 | htmlEngine := goeditorjs.NewHTMLEngine() 42 | // Register the handlers you wish to use 43 | htmlEngine.RegisterBlockHandlers( 44 | &goeditorjs.HeaderHandler{}, 45 | &goeditorjs.ParagraphHandler{}, 46 | &goeditorjs.ListHandler{}, 47 | &goeditorjs.CodeBoxHandler{}, 48 | ) 49 | // Generate the html 50 | html, err := htmlEngine.GenerateHTML(ejs) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | // Do something with the html output. In this case, write it to a file. 56 | err = ioutil.WriteFile("editorjs.html", []byte(html), 0644) 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | 61 | // Generate markdown and save it to a file 62 | // Get the markdown engine 63 | markdownEngine := goeditorjs.NewMarkdownEngine() 64 | // Register the handlers you wish to use 65 | markdownEngine.RegisterBlockHandlers( 66 | &goeditorjs.HeaderHandler{}, 67 | &goeditorjs.ParagraphHandler{}, 68 | &goeditorjs.ListHandler{}, 69 | &goeditorjs.CodeBoxHandler{}, 70 | ) 71 | // Generate the markdown 72 | md, err := markdownEngine.GenerateMarkdown(ejs) 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | 77 | // Do something with the md output. In this case, write it to a file. 78 | err = ioutil.WriteFile("editorjs.md", []byte(md), 0644) 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | } 83 | ``` 84 | 85 | ## Using a Custom Handler 86 | 87 | You can create and use your own handler in either engine by implementing the required interface and registering it. 88 | This package provides two interfaces for handlers. 89 | 90 | - `HTMLBlockHandler` 91 | 92 | ```go 93 | type HTMLBlockHandler interface { 94 | Type() string // Type returns the type the block handler supports as a string 95 | GenerateHTML(editorJSBlock EditorJSBlock) (string, error) // Return associated HTML 96 | } 97 | ``` 98 | 99 | - `MarkdownBlockHandler` 100 | ```go 101 | type MarkdownBlockHandler interface { 102 | Type() string // Type returns the type the block handler supports as a string 103 | GenerateMarkdown(editorJSBlock EditorJSBlock) (string, error) // Return associated markdown 104 | } 105 | ``` 106 | 107 | If you're only planning to use the HTMLEngine, then you only need to implement the `HTMLBlockHandler` interface. The same goes for markdown. 108 | 109 | Once you've met the required interface, register the handler for use in the engine. 110 | 111 | ```go 112 | htmlEngine := goeditorjs.NewHTMLEngine() 113 | // Register the handlers you wish to use 114 | htmlEngine.RegisterBlockHandlers( 115 | &MyCustomBlockHandler{}, 116 | ) 117 | ``` 118 | 119 | Below is an example of how the header handle is implemented. 120 | 121 | ```go 122 | package header 123 | 124 | import ( 125 | "encoding/json" 126 | "fmt" 127 | ) 128 | 129 | // HeaderHandler is the default HeaderHandler for EditorJS HTML generation 130 | type HeaderHandler struct { 131 | // Notice that you could put some configurable options in this struct and then use them in your handler 132 | } 133 | 134 | // Header represents header data from EditorJS 135 | type Header struct { 136 | Text string `json:"text"` 137 | Level int `json:"level"` 138 | } 139 | 140 | func (*HeaderHandler) parse(editorJSBlock EditorJSBlock) (*Header, error) { 141 | header := &Header{} 142 | return header, json.Unmarshal(editorJSBlock.Data, header) 143 | } 144 | 145 | // Type "header" 146 | func (*HeaderHandler) Type() string { 147 | return "header" 148 | } 149 | 150 | // GenerateHTML generates html for HeaderBlocks 151 | func (h *HeaderHandler) GenerateHTML(editorJSBlock EditorJSBlock) (string, error) { 152 | header, err := h.parse(editorJSBlock) 153 | if err != nil { 154 | return "", err 155 | } 156 | 157 | return fmt.Sprintf("%s", header.Level, header.Text, header.Level), nil 158 | } 159 | 160 | // GenerateMarkdown generates markdown for HeaderBlocks 161 | func (h *HeaderHandler) GenerateMarkdown(editorJSBlock EditorJSBlock) (string, error) { 162 | header, err := h.parse(editorJSBlock) 163 | if err != nil { 164 | return "", err 165 | } 166 | 167 | return fmt.Sprintf("%s %s", strings.Repeat("#", header.Level), header.Text), nil 168 | } 169 | ``` 170 | 171 | ## TODO 172 | 173 | - Provide more handlers (table, etc.) 174 | -------------------------------------------------------------------------------- /examples/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "time": 1607971736361, 3 | "blocks": [ 4 | { 5 | "type": "header", 6 | "data": { 7 | "text": "Heading 1", 8 | "level": 1 9 | } 10 | }, 11 | { 12 | "type": "header", 13 | "data": { 14 | "text": "Heading 2", 15 | "level": 2 16 | } 17 | }, 18 | { 19 | "type": "header", 20 | "data": { 21 | "text": "Heading 3", 22 | "level": 3 23 | } 24 | }, 25 | { 26 | "type": "header", 27 | "data": { 28 | "text": "Heading 4", 29 | "level": 4 30 | } 31 | }, 32 | { 33 | "type": "header", 34 | "data": { 35 | "text": "Heading 5", 36 | "level": 5 37 | } 38 | }, 39 | { 40 | "type": "header", 41 | "data": { 42 | "text": "Heading 6", 43 | "level": 6 44 | } 45 | }, 46 | { 47 | "type": "header", 48 | "data": { 49 | "text": "Ordered List", 50 | "level": 2 51 | } 52 | }, 53 | { 54 | "type": "list", 55 | "data": { 56 | "style": "ordered", 57 | "items": [ 58 | "One", 59 | "Two", 60 | "Three" 61 | ] 62 | } 63 | }, 64 | { 65 | "type": "header", 66 | "data": { 67 | "text": "Unordered List", 68 | "level": 2 69 | } 70 | }, 71 | { 72 | "type": "list", 73 | "data": { 74 | "style": "unordered", 75 | "items": [ 76 | "Fizz", 77 | "Buzz", 78 | "FizzBuzz" 79 | ] 80 | } 81 | }, 82 | { 83 | "type": "header", 84 | "data": { 85 | "text": "Paragraph", 86 | "level": 2 87 | } 88 | }, 89 | { 90 | "type": "paragraph", 91 | "data": { 92 | "text": "This is a paragraph", 93 | "alignment": "left" 94 | } 95 | }, 96 | { 97 | "type": "header", 98 | "data": { 99 | "text": "Code Block", 100 | "level": 2 101 | } 102 | }, 103 | { 104 | "type": "header", 105 | "data": { 106 | "text": "Go Code", 107 | "level": 3 108 | } 109 | }, 110 | { 111 | "type": "codeBox", 112 | "data": { 113 | "code": "package main

import \"fmt\"

func main() {
fmt.Println(\"Hello World\")
}
", 114 | "language": "go", 115 | "theme": "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.18.1/build/styles/dracula.min.css" 116 | } 117 | }, 118 | { 119 | "type": "header", 120 | "data": { 121 | "text": "TODO: Add More Languages", 122 | "level": 3 123 | } 124 | }, 125 | { 126 | "type": "header", 127 | "data": { 128 | "text": "Paragraph with Styling", 129 | "level": 2 130 | } 131 | }, 132 | { 133 | "type": "paragraph", 134 | "data": { 135 | "text": "This is a paragraph with styling and formatting. This is an example of some class text.", 136 | "alignment": "left" 137 | } 138 | }, 139 | { 140 | "type": "header", 141 | "data": { 142 | "text": "Formatting", 143 | "level": 2 144 | } 145 | }, 146 | { 147 | "type": "paragraph", 148 | "data": { 149 | "text": "Bold Italic", 150 | "alignment": "left" 151 | } 152 | }, 153 | { 154 | "type": "header", 155 | "data": { 156 | "text": "Links", 157 | "level": 2 158 | } 159 | }, 160 | { 161 | "type": "paragraph", 162 | "data": { 163 | "text": "Link to a github issue.
", 164 | "alignment": "left" 165 | } 166 | }, 167 | { 168 | "type": "header", 169 | "data": { 170 | "text": "Alignment", 171 | "level": 2 172 | } 173 | }, 174 | { 175 | "type": "paragraph", 176 | "data": { 177 | "text": "Left", 178 | "alignment": "left" 179 | } 180 | }, 181 | { 182 | "type": "paragraph", 183 | "data": { 184 | "text": "Center", 185 | "alignment": "center" 186 | } 187 | }, 188 | { 189 | "type": "paragraph", 190 | "data": { 191 | "text": "Right", 192 | "alignment": "right" 193 | } 194 | }, 195 | { 196 | "type": "image", 197 | "data": { 198 | "file": { 199 | "url": "https://www.w3schools.com/html/pic_trulli.jpg" 200 | }, 201 | "caption": "Example Captions", 202 | "withBorder": true, 203 | "stretched": true, 204 | "withBackground": true 205 | } 206 | } 207 | ], 208 | "version": "2.19.1" 209 | } -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package goeditorjs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "regexp" 7 | "sort" 8 | "strings" 9 | ) 10 | 11 | // HeaderHandler is the default HeaderHandler for EditorJS HTML generation 12 | type HeaderHandler struct{} 13 | 14 | func (*HeaderHandler) parse(editorJSBlock EditorJSBlock) (*header, error) { 15 | header := &header{} 16 | return header, json.Unmarshal(editorJSBlock.Data, header) 17 | } 18 | 19 | // Type "header" 20 | func (*HeaderHandler) Type() string { 21 | return "header" 22 | } 23 | 24 | // GenerateHTML generates html for HeaderBlocks 25 | func (h *HeaderHandler) GenerateHTML(editorJSBlock EditorJSBlock) (string, error) { 26 | header, err := h.parse(editorJSBlock) 27 | if err != nil { 28 | return "", err 29 | } 30 | 31 | return fmt.Sprintf("%s", header.Level, header.Text, header.Level), nil 32 | } 33 | 34 | // GenerateMarkdown generates markdown for HeaderBlocks 35 | func (h *HeaderHandler) GenerateMarkdown(editorJSBlock EditorJSBlock) (string, error) { 36 | header, err := h.parse(editorJSBlock) 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | return fmt.Sprintf("%s %s", strings.Repeat("#", header.Level), header.Text), nil 42 | } 43 | 44 | // ParagraphHandler is the default ParagraphHandler for EditorJS HTML generation 45 | type ParagraphHandler struct{} 46 | 47 | func (*ParagraphHandler) parse(editorJSBlock EditorJSBlock) (*paragraph, error) { 48 | paragraph := ¶graph{} 49 | return paragraph, json.Unmarshal(editorJSBlock.Data, paragraph) 50 | } 51 | 52 | // Type "paragraph" 53 | func (*ParagraphHandler) Type() string { 54 | return "paragraph" 55 | } 56 | 57 | // GenerateHTML generates html for ParagraphBlocks 58 | func (h *ParagraphHandler) GenerateHTML(editorJSBlock EditorJSBlock) (string, error) { 59 | paragraph, err := h.parse(editorJSBlock) 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | if paragraph.Alignment != "left" { 65 | return fmt.Sprintf(`

%s

`, paragraph.Alignment, paragraph.Text), nil 66 | } 67 | 68 | return fmt.Sprintf(`

%s

`, paragraph.Text), nil 69 | } 70 | 71 | // GenerateMarkdown generates markdown for ParagraphBlocks 72 | func (h *ParagraphHandler) GenerateMarkdown(editorJSBlock EditorJSBlock) (string, error) { 73 | paragraph, err := h.parse(editorJSBlock) 74 | if err != nil { 75 | return "", err 76 | } 77 | 78 | if paragraph.Alignment != "left" { 79 | // Native markdown doesn't support alignment, so we'll use html instead. 80 | return fmt.Sprintf(`

%s

`, paragraph.Alignment, paragraph.Text), nil 81 | } 82 | 83 | return paragraph.Text, nil 84 | } 85 | 86 | // ListHandler is the default ListHandler for EditorJS HTML generation 87 | type ListHandler struct{} 88 | 89 | func (*ListHandler) parse(editorJSBlock EditorJSBlock) (*list, error) { 90 | list := &list{} 91 | return list, json.Unmarshal(editorJSBlock.Data, list) 92 | } 93 | 94 | // Type "list" 95 | func (*ListHandler) Type() string { 96 | return "list" 97 | } 98 | 99 | // GenerateHTML generates html for ListBlocks 100 | func (h *ListHandler) GenerateHTML(editorJSBlock EditorJSBlock) (string, error) { 101 | list, err := h.parse(editorJSBlock) 102 | if err != nil { 103 | return "", err 104 | } 105 | 106 | result := "" 107 | if list.Style == "ordered" { 108 | result = "
    %s
" 109 | } else { 110 | result = "" 111 | } 112 | 113 | innerData := "" 114 | for _, s := range list.Items { 115 | innerData += fmt.Sprintf("
  • %s
  • ", s) 116 | } 117 | 118 | return fmt.Sprintf(result, innerData), nil 119 | } 120 | 121 | // GenerateMarkdown generates markdown for ListBlocks 122 | func (h *ListHandler) GenerateMarkdown(editorJSBlock EditorJSBlock) (string, error) { 123 | list, err := h.parse(editorJSBlock) 124 | if err != nil { 125 | return "", err 126 | } 127 | 128 | listItemPrefix := "" 129 | if list.Style == "ordered" { 130 | listItemPrefix = "1. " 131 | } else { 132 | listItemPrefix = "- " 133 | } 134 | 135 | results := []string{} 136 | for _, s := range list.Items { 137 | results = append(results, listItemPrefix+s) 138 | } 139 | 140 | return strings.Join(results, "\n"), nil 141 | } 142 | 143 | // CodeBoxHandler is the default CodeBoxHandler for EditorJS HTML generation 144 | type CodeBoxHandler struct{} 145 | 146 | func (*CodeBoxHandler) parse(editorJSBlock EditorJSBlock) (*codeBox, error) { 147 | codeBox := &codeBox{} 148 | return codeBox, json.Unmarshal(editorJSBlock.Data, codeBox) 149 | } 150 | 151 | // Type "codeBox" 152 | func (*CodeBoxHandler) Type() string { 153 | return "codeBox" 154 | } 155 | 156 | // GenerateHTML generates html for CodeBoxBlocks 157 | func (h *CodeBoxHandler) GenerateHTML(editorJSBlock EditorJSBlock) (string, error) { 158 | codeBox, err := h.parse(editorJSBlock) 159 | if err != nil { 160 | return "", err 161 | } 162 | 163 | return fmt.Sprintf(`
    %s
    `, codeBox.Language, codeBox.Code), nil 164 | } 165 | 166 | // GenerateMarkdown generates markdown for CodeBoxBlocks 167 | func (h *CodeBoxHandler) GenerateMarkdown(editorJSBlock EditorJSBlock) (string, error) { 168 | codeBox, err := h.parse(editorJSBlock) 169 | if err != nil { 170 | return "", err 171 | } 172 | 173 | codeBox.Code = strings.ReplaceAll(codeBox.Code, "
    ", "\n") 174 | codeBox.Code = removeHTMLTags(codeBox.Code) 175 | 176 | return fmt.Sprintf("```%s\n%s\n```", codeBox.Language, codeBox.Code), nil 177 | } 178 | 179 | func removeHTMLTags(in string) string { 180 | // regex to match html tag 181 | const pattern = `(<\/?[a-zA-A]+?[^>]*\/?>)*` 182 | r := regexp.MustCompile(pattern) 183 | groups := r.FindAllString(in, -1) 184 | // should replace long string first 185 | sort.Slice(groups, func(i, j int) bool { 186 | return len(groups[i]) > len(groups[j]) 187 | }) 188 | for _, group := range groups { 189 | if strings.TrimSpace(group) != "" { 190 | in = strings.ReplaceAll(in, group, "") 191 | } 192 | } 193 | return in 194 | } 195 | 196 | // RawHTMLHandler is the default raw handler for EditorJS HTML generation 197 | type RawHTMLHandler struct{} 198 | 199 | // Type "raw" 200 | func (*RawHTMLHandler) Type() string { 201 | return "raw" 202 | } 203 | 204 | // GenerateHTML generates html for rawBlocks 205 | func (h *RawHTMLHandler) GenerateHTML(editorJSBlock EditorJSBlock) (string, error) { 206 | return h.raw(editorJSBlock) 207 | 208 | } 209 | 210 | // GenerateMarkdown generates markdown for rawBlocks 211 | func (h *RawHTMLHandler) GenerateMarkdown(editorJSBlock EditorJSBlock) (string, error) { 212 | return h.raw(editorJSBlock) 213 | } 214 | 215 | func (h *RawHTMLHandler) raw(editorJSBlock EditorJSBlock) (string, error) { 216 | raw := &raw{} 217 | err := json.Unmarshal(editorJSBlock.Data, raw) 218 | if err != nil { 219 | return "", err 220 | } 221 | 222 | return raw.HTML, nil 223 | } 224 | 225 | // ImageHandler is the default ImageHandler for EditorJS HTML generation 226 | type ImageHandler struct { 227 | // Options are made available to the GenerateHTML and GenerateMarkdown functions. 228 | // If not provided, DefaultImageHandlerOptions will be used. 229 | Options *ImageHandlerOptions 230 | } 231 | 232 | // ImageHandlerOptions are the options available to the ImageHandler 233 | type ImageHandlerOptions struct { 234 | BorderClass string 235 | StretchClass string 236 | BackgroundClass string 237 | } 238 | 239 | // DefaultImageHandlerOptions are the default options available to the ImageHandler 240 | var DefaultImageHandlerOptions = &ImageHandlerOptions{ 241 | StretchClass: "image-tool--stretched", 242 | BorderClass: "image-tool--withBorder", 243 | BackgroundClass: "image-tool--withBackground"} 244 | 245 | func (*ImageHandler) parse(editorJSBlock EditorJSBlock) (*image, error) { 246 | image := &image{} 247 | return image, json.Unmarshal(editorJSBlock.Data, image) 248 | } 249 | 250 | // Type "image" 251 | func (*ImageHandler) Type() string { 252 | return "image" 253 | } 254 | 255 | // GenerateHTML generates html for ImageBlocks 256 | func (h *ImageHandler) GenerateHTML(editorJSBlock EditorJSBlock) (string, error) { 257 | image, err := h.parse(editorJSBlock) 258 | if err != nil { 259 | return "", err 260 | } 261 | 262 | return h.generateHTML(image) 263 | } 264 | 265 | // GenerateMarkdown generates markdown for ImageBlocks 266 | func (h *ImageHandler) GenerateMarkdown(editorJSBlock EditorJSBlock) (string, error) { 267 | image, err := h.parse(editorJSBlock) 268 | if err != nil { 269 | return "", err 270 | } 271 | 272 | if image.Stretched || image.WithBackground || image.WithBorder { 273 | return h.generateHTML(image) 274 | } 275 | return fmt.Sprintf(`![alt text](%s "%s")`, image.File.URL, image.Caption), nil 276 | 277 | } 278 | 279 | func (h *ImageHandler) generateHTML(image *image) (string, error) { 280 | if h.Options == nil { 281 | h.Options = DefaultImageHandlerOptions 282 | } 283 | 284 | classes := []string{} 285 | if image.Stretched { 286 | classes = append(classes, h.Options.StretchClass) 287 | } 288 | 289 | if image.WithBorder { 290 | classes = append(classes, h.Options.BorderClass) 291 | } 292 | 293 | if image.WithBackground { 294 | classes = append(classes, h.Options.BackgroundClass) 295 | } 296 | 297 | class := "" 298 | if len(classes) > 0 { 299 | class = fmt.Sprintf(`class="%s"`, strings.Join(classes, " ")) 300 | } 301 | 302 | return fmt.Sprintf(`%s`, image.File.URL, image.Caption, class), nil 303 | } 304 | -------------------------------------------------------------------------------- /handlers_test.go: -------------------------------------------------------------------------------- 1 | package goeditorjs_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/davidscottmills/goeditorjs" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_HeaderHandler_Type(t *testing.T) { 12 | h := &goeditorjs.HeaderHandler{} 13 | require.Equal(t, "header", h.Type()) 14 | } 15 | 16 | func Test_HeaderHandler_GenerateHTML_Returns_Parse_Err(t *testing.T) { 17 | h := &goeditorjs.HeaderHandler{} 18 | _, err := h.GenerateHTML(goeditorjs.EditorJSBlock{Type: "header", Data: []byte{}}) 19 | require.Error(t, err) 20 | } 21 | 22 | func Test_HeaderHandler_GenerateHTML(t *testing.T) { 23 | bhh := &goeditorjs.HeaderHandler{} 24 | testData := []struct { 25 | data string 26 | expectedResult string 27 | }{ 28 | {data: `{"text": "Heading","level": 1}`, expectedResult: "

    Heading

    "}, 29 | {data: `{"text": "Heading","level": 2}`, expectedResult: "

    Heading

    "}, 30 | {data: `{"text": "Heading","level": 3}`, expectedResult: "

    Heading

    "}, 31 | {data: `{"text": "Heading","level": 4}`, expectedResult: "

    Heading

    "}, 32 | {data: `{"text": "Heading","level": 5}`, expectedResult: "
    Heading
    "}, 33 | {data: `{"text": "Heading","level": 6}`, expectedResult: "
    Heading
    "}, 34 | } 35 | 36 | for _, td := range testData { 37 | jsonData := []byte(td.data) 38 | ejsBlock := goeditorjs.EditorJSBlock{Type: "header", Data: jsonData} 39 | html, _ := bhh.GenerateHTML(ejsBlock) 40 | require.Equal(t, td.expectedResult, html) 41 | } 42 | } 43 | 44 | func Test_HeaderHandler_GenerateMarkdown_Returns_Parse_Err(t *testing.T) { 45 | h := &goeditorjs.HeaderHandler{} 46 | _, err := h.GenerateMarkdown(goeditorjs.EditorJSBlock{Type: "header", Data: []byte{}}) 47 | require.Error(t, err) 48 | } 49 | 50 | func Test_HeaderHandler_GenerateMarkdown(t *testing.T) { 51 | bhh := &goeditorjs.HeaderHandler{} 52 | testData := []struct { 53 | data string 54 | expectedResult string 55 | }{ 56 | {data: `{"text": "Heading","level": 1}`, expectedResult: "# Heading"}, 57 | {data: `{"text": "Heading","level": 2}`, expectedResult: "## Heading"}, 58 | {data: `{"text": "Heading","level": 3}`, expectedResult: "### Heading"}, 59 | {data: `{"text": "Heading","level": 4}`, expectedResult: "#### Heading"}, 60 | {data: `{"text": "Heading","level": 5}`, expectedResult: "##### Heading"}, 61 | {data: `{"text": "Heading","level": 6}`, expectedResult: "###### Heading"}, 62 | } 63 | 64 | for _, td := range testData { 65 | jsonData := []byte(td.data) 66 | ejsBlock := goeditorjs.EditorJSBlock{Type: "header", Data: jsonData} 67 | md, _ := bhh.GenerateMarkdown(ejsBlock) 68 | require.Equal(t, td.expectedResult, md) 69 | } 70 | } 71 | 72 | func Test_ParagraphHandler_Type(t *testing.T) { 73 | h := &goeditorjs.ParagraphHandler{} 74 | require.Equal(t, "paragraph", h.Type()) 75 | } 76 | 77 | func Test_ParagraphHandler_GenerateHTML_Returns_Parse_Err(t *testing.T) { 78 | h := &goeditorjs.ParagraphHandler{} 79 | _, err := h.GenerateHTML(goeditorjs.EditorJSBlock{Type: "paragraph", Data: []byte{}}) 80 | require.Error(t, err) 81 | } 82 | 83 | func Test_ParagraphHandler_GenerateHTML_Left(t *testing.T) { 84 | bph := &goeditorjs.ParagraphHandler{} 85 | jsonData := []byte(`{"text": "paragraph","alignment": "left"}`) 86 | ejsBlock := goeditorjs.EditorJSBlock{Type: "paragraph", Data: jsonData} 87 | html, _ := bph.GenerateHTML(ejsBlock) 88 | require.Equal(t, "

    paragraph

    ", html) 89 | } 90 | 91 | func Test_ParagraphHandler_GenerateHTML_Center_Right(t *testing.T) { 92 | bph := &goeditorjs.ParagraphHandler{} 93 | testData := []struct { 94 | alignment string 95 | data string 96 | }{ 97 | {alignment: "center", data: `{"text": "paragraph","alignment": "center"}`}, 98 | {alignment: "right", data: `{"text": "paragraph","alignment": "right"}`}, 99 | } 100 | 101 | for _, td := range testData { 102 | jsonData := []byte(td.data) 103 | ejsBlock := goeditorjs.EditorJSBlock{Type: "paragraph", Data: jsonData} 104 | html, _ := bph.GenerateHTML(ejsBlock) 105 | require.Equal(t, fmt.Sprintf(`

    paragraph

    `, td.alignment), html) 106 | } 107 | } 108 | 109 | func Test_ParagraphHandler_GenerateMarkdown_Returns_Parse_Err(t *testing.T) { 110 | h := &goeditorjs.ParagraphHandler{} 111 | _, err := h.GenerateMarkdown(goeditorjs.EditorJSBlock{Type: "paragraph", Data: []byte{}}) 112 | require.Error(t, err) 113 | } 114 | 115 | func Test_ParagraphHandler_GenerateMarkdown_Left(t *testing.T) { 116 | bph := &goeditorjs.ParagraphHandler{} 117 | jsonData := []byte(`{"text": "paragraph","alignment": "left"}`) 118 | ejsBlock := goeditorjs.EditorJSBlock{Type: "paragraph", Data: jsonData} 119 | md, _ := bph.GenerateMarkdown(ejsBlock) 120 | require.Equal(t, "paragraph", md) 121 | } 122 | 123 | func Test_ParagraphHandler_GenerateMarkdown_Center_Right(t *testing.T) { 124 | bph := &goeditorjs.ParagraphHandler{} 125 | testData := []struct { 126 | alignment string 127 | data string 128 | }{ 129 | {alignment: "center", data: `{"text": "paragraph","alignment": "center"}`}, 130 | {alignment: "right", data: `{"text": "paragraph","alignment": "right"}`}, 131 | } 132 | 133 | for _, td := range testData { 134 | jsonData := []byte(td.data) 135 | ejsBlock := goeditorjs.EditorJSBlock{Type: "paragraph", Data: jsonData} 136 | md, _ := bph.GenerateMarkdown(ejsBlock) 137 | require.Equal(t, fmt.Sprintf(`

    paragraph

    `, td.alignment), md) 138 | } 139 | } 140 | 141 | func Test_ListHandler_Type(t *testing.T) { 142 | h := &goeditorjs.ListHandler{} 143 | require.Equal(t, "list", h.Type()) 144 | } 145 | 146 | func Test_ListHandler_GenerateHTML_Returns_Parse_Err(t *testing.T) { 147 | h := &goeditorjs.ListHandler{} 148 | _, err := h.GenerateHTML(goeditorjs.EditorJSBlock{Type: "list", Data: []byte{}}) 149 | require.Error(t, err) 150 | } 151 | 152 | func Test_ListHandler_GenerateHTML(t *testing.T) { 153 | blh := &goeditorjs.ListHandler{} 154 | testData := []struct { 155 | data string 156 | expectedResult string 157 | }{ 158 | {data: `{"style": "ordered", "items": ["one", "two", "three"]}`, 159 | expectedResult: "
    1. one
    2. two
    3. three
    "}, 160 | {data: `{"style": "unordered", "items": ["one", "two", "three"]}`, 161 | expectedResult: ""}, 162 | } 163 | 164 | for _, td := range testData { 165 | jsonData := []byte(td.data) 166 | ejsBlock := goeditorjs.EditorJSBlock{Type: "list", Data: jsonData} 167 | html, _ := blh.GenerateHTML(ejsBlock) 168 | require.Equal(t, td.expectedResult, html) 169 | } 170 | } 171 | 172 | func Test_ListHandler_GenerateMarkdown_Returns_Parse_Err(t *testing.T) { 173 | h := &goeditorjs.ListHandler{} 174 | _, err := h.GenerateMarkdown(goeditorjs.EditorJSBlock{Type: "list", Data: []byte{}}) 175 | require.Error(t, err) 176 | } 177 | 178 | func Test_ListHandler_GenerateMarkdown(t *testing.T) { 179 | blh := &goeditorjs.ListHandler{} 180 | testData := []struct { 181 | data string 182 | expectedResult string 183 | }{ 184 | {data: `{"style": "ordered", "items": ["one", "two", "three"]}`, 185 | expectedResult: "1. one\n1. two\n1. three"}, 186 | {data: `{"style": "unordered", "items": ["one", "two", "three"]}`, 187 | expectedResult: "- one\n- two\n- three"}, 188 | } 189 | 190 | for _, td := range testData { 191 | jsonData := []byte(td.data) 192 | ejsBlock := goeditorjs.EditorJSBlock{Type: "list", Data: jsonData} 193 | md, _ := blh.GenerateMarkdown(ejsBlock) 194 | require.Equal(t, td.expectedResult, md) 195 | } 196 | } 197 | 198 | func Test_CodeBoxHandler_Type(t *testing.T) { 199 | h := &goeditorjs.CodeBoxHandler{} 200 | require.Equal(t, "codeBox", h.Type()) 201 | } 202 | 203 | func Test_CodeBoxHandler_GenerateHTML_Returns_Parse_Err(t *testing.T) { 204 | h := &goeditorjs.CodeBoxHandler{} 205 | _, err := h.GenerateHTML(goeditorjs.EditorJSBlock{Type: "codeBox", Data: []byte{}}) 206 | require.Error(t, err) 207 | } 208 | 209 | func Test_CodeBoxHandler_GenerateHTML(t *testing.T) { 210 | bcbh := &goeditorjs.CodeBoxHandler{} 211 | jsonData := []byte(`{"language": "go", "code": "func main(){fmt.Println(\"HelloWorld\")}"}`) 212 | ejsBlock := goeditorjs.EditorJSBlock{Type: "codeBox", Data: jsonData} 213 | expectedResult := `
    func main(){fmt.Println("HelloWorld")}
    ` 214 | html, _ := bcbh.GenerateHTML(ejsBlock) 215 | require.Equal(t, expectedResult, html) 216 | } 217 | 218 | func Test_CodeBoxHandler_GenerateMarkdown_Returns_Parse_Err(t *testing.T) { 219 | h := &goeditorjs.CodeBoxHandler{} 220 | _, err := h.GenerateMarkdown(goeditorjs.EditorJSBlock{Type: "codeBox", Data: []byte{}}) 221 | require.Error(t, err) 222 | } 223 | 224 | func Test_CodeBoxHandler_GenerateMarkdown(t *testing.T) { 225 | bcbh := &goeditorjs.CodeBoxHandler{} 226 | jsonData := []byte(`{"language": "go", "code": "func main(){fmt.Println(\"HelloWorld\")}"}`) 227 | ejsBlock := goeditorjs.EditorJSBlock{Type: "codeBox", Data: jsonData} 228 | expectedResult := "```go\nfunc main(){fmt.Println(\"HelloWorld\")}\n```" 229 | md, _ := bcbh.GenerateMarkdown(ejsBlock) 230 | require.Equal(t, expectedResult, md) 231 | } 232 | 233 | func Test_CodeBoxHandler_GenerateMarkdown_Clean(t *testing.T) { 234 | bcbh := &goeditorjs.CodeBoxHandler{} 235 | jsonData := []byte(`{"language": "go", "code": "package main

    import \"fmt\"

    func main() {
    fmt.Println(\"Hello World\")
    }
    "}`) 236 | ejsBlock := goeditorjs.EditorJSBlock{Type: "codeBox", Data: jsonData} 237 | expectedResult := "```go\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n fmt.Println(\"Hello World\")\n}\n```" 238 | md, _ := bcbh.GenerateMarkdown(ejsBlock) 239 | require.Equal(t, expectedResult, md) 240 | } 241 | 242 | func Test_RawHTMLHandler_Type(t *testing.T) { 243 | h := &goeditorjs.RawHTMLHandler{} 244 | require.Equal(t, "raw", h.Type()) 245 | } 246 | 247 | func Test_RawHTMLHandler_GenerateHTML_Returns_Parse_Err(t *testing.T) { 248 | h := &goeditorjs.RawHTMLHandler{} 249 | _, err := h.GenerateHTML(goeditorjs.EditorJSBlock{Type: "raw", Data: []byte{}}) 250 | require.Error(t, err) 251 | } 252 | 253 | func Test_RawHTMLHandler_GenerateHTML(t *testing.T) { 254 | h := &goeditorjs.RawHTMLHandler{} 255 | jsonData := []byte(`{"html": "
    Any HTML code
    "}`) 256 | ejsBlock := goeditorjs.EditorJSBlock{Type: "raw", Data: jsonData} 257 | expectedResult := `
    Any HTML code
    ` 258 | html, _ := h.GenerateHTML(ejsBlock) 259 | require.Equal(t, expectedResult, html) 260 | } 261 | 262 | func Test_RawHTMLHandler_GenerateMarkdown_Returns_Parse_Err(t *testing.T) { 263 | h := &goeditorjs.RawHTMLHandler{} 264 | _, err := h.GenerateMarkdown(goeditorjs.EditorJSBlock{Type: "raw", Data: []byte{}}) 265 | require.Error(t, err) 266 | } 267 | 268 | func Test_RawHTMLHandler_GenerateMarkdown(t *testing.T) { 269 | h := &goeditorjs.RawHTMLHandler{} 270 | jsonData := []byte(`{"html": "
    Any HTML code
    "}`) 271 | ejsBlock := goeditorjs.EditorJSBlock{Type: "raw", Data: jsonData} 272 | expectedResult := `
    Any HTML code
    ` 273 | md, _ := h.GenerateMarkdown(ejsBlock) 274 | require.Equal(t, expectedResult, md) 275 | } 276 | 277 | func Test_ImageHandler_Type(t *testing.T) { 278 | h := &goeditorjs.ImageHandler{} 279 | require.Equal(t, "image", h.Type()) 280 | } 281 | 282 | func Test_ImageHandler_GenerateHTML_Returns_Parse_Err(t *testing.T) { 283 | h := &goeditorjs.ImageHandler{} 284 | _, err := h.GenerateHTML(goeditorjs.EditorJSBlock{Type: "image", Data: []byte{}}) 285 | require.Error(t, err) 286 | } 287 | 288 | func Test_ImageHandler_GenerateHTML(t *testing.T) { 289 | h := &goeditorjs.ImageHandler{} 290 | 291 | testData := []struct { 292 | data string 293 | expectedResult string 294 | }{ 295 | // Full test with defaults 296 | {data: `{"file":{"url": "https://www.w3schools.com/html/pic_trulli.jpg"},"caption": "Example Captions","withBorder": true,"stretched": true,"withBackground": true}`, 297 | expectedResult: `Example Captions`}, 298 | // No captions 299 | {data: `{"file":{"url": "https://www.w3schools.com/html/pic_trulli.jpg"},"caption": "","withBorder": true,"stretched": true,"withBackground": true}`, 300 | expectedResult: ``}, 301 | // Border 302 | {data: `{"file":{"url": "https://www.w3schools.com/html/pic_trulli.jpg"},"caption": "","withBorder": true,"stretched": false,"withBackground": false}`, 303 | expectedResult: ``}, 304 | // Stretch 305 | {data: `{"file":{"url": "https://www.w3schools.com/html/pic_trulli.jpg"},"caption": "","withBorder": false,"stretched": true,"withBackground": false}`, 306 | expectedResult: ``}, 307 | // Background 308 | {data: `{"file":{"url": "https://www.w3schools.com/html/pic_trulli.jpg"},"caption": "","withBorder": false,"stretched": false,"withBackground": true}`, 309 | expectedResult: ``}, 310 | // No classes 311 | {data: `{"file":{"url": "https://www.w3schools.com/html/pic_trulli.jpg"},"caption": "","withBorder": false,"stretched": false,"withBackground": false}`, 312 | expectedResult: ``}, 313 | } 314 | 315 | for _, td := range testData { 316 | jsonData := []byte(td.data) 317 | ejsBlock := goeditorjs.EditorJSBlock{Type: "image", Data: jsonData} 318 | result, _ := h.GenerateHTML(ejsBlock) 319 | require.Equal(t, td.expectedResult, result) 320 | } 321 | } 322 | 323 | func Test_ImageHandler_GenerateMarkdown_Returns_Parse_Err(t *testing.T) { 324 | h := &goeditorjs.ImageHandler{} 325 | _, err := h.GenerateMarkdown(goeditorjs.EditorJSBlock{Type: "image", Data: []byte{}}) 326 | require.Error(t, err) 327 | } 328 | 329 | func Test_ImageHandler_GenerateMarkdown(t *testing.T) { 330 | h := &goeditorjs.ImageHandler{} 331 | 332 | testData := []struct { 333 | data string 334 | expectedResult string 335 | }{ 336 | // Full test with defaults 337 | {data: `{"file":{"url": "https://www.w3schools.com/html/pic_trulli.jpg"},"caption": "Example Captions","withBorder": true,"stretched": true,"withBackground": true}`, 338 | expectedResult: `Example Captions`}, 339 | // No captions 340 | {data: `{"file":{"url": "https://www.w3schools.com/html/pic_trulli.jpg"},"caption": "","withBorder": true,"stretched": true,"withBackground": true}`, 341 | expectedResult: ``}, 342 | // Border 343 | {data: `{"file":{"url": "https://www.w3schools.com/html/pic_trulli.jpg"},"caption": "","withBorder": true,"stretched": false,"withBackground": false}`, 344 | expectedResult: ``}, 345 | // Stretch 346 | {data: `{"file":{"url": "https://www.w3schools.com/html/pic_trulli.jpg"},"caption": "","withBorder": false,"stretched": true,"withBackground": false}`, 347 | expectedResult: ``}, 348 | // Background 349 | {data: `{"file":{"url": "https://www.w3schools.com/html/pic_trulli.jpg"},"caption": "","withBorder": false,"stretched": false,"withBackground": true}`, 350 | expectedResult: ``}, 351 | // No classes or caption 352 | {data: `{"file":{"url": "https://www.w3schools.com/html/pic_trulli.jpg"},"caption": "","withBorder": false,"stretched": false,"withBackground": false}`, 353 | expectedResult: `![alt text](https://www.w3schools.com/html/pic_trulli.jpg "")`}, 354 | // No classes 355 | {data: `{"file":{"url": "https://www.w3schools.com/html/pic_trulli.jpg"},"caption": "Some caption","withBorder": false,"stretched": false,"withBackground": false}`, 356 | expectedResult: `![alt text](https://www.w3schools.com/html/pic_trulli.jpg "Some caption")`}, 357 | } 358 | 359 | for _, td := range testData { 360 | jsonData := []byte(td.data) 361 | ejsBlock := goeditorjs.EditorJSBlock{Type: "image", Data: jsonData} 362 | result, _ := h.GenerateMarkdown(ejsBlock) 363 | require.Equal(t, td.expectedResult, result) 364 | } 365 | } 366 | --------------------------------------------------------------------------------