feat: implement markup rendering

Signed-off-by: Sphericalkat <amolele@gmail.com>
This commit is contained in:
Amogh Lele 2023-05-28 16:13:44 +05:30
parent 3de9bfc58c
commit 7b6e7d7edb
8 changed files with 195 additions and 52 deletions

View File

@ -1,13 +1,12 @@
package routes package routes
import ( import (
"fmt" "html/template"
"log"
"strings"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/medium.rip/pkg/client" "github.com/medium.rip/pkg/client"
"github.com/medium.rip/pkg/converters"
) )
func show(c *fiber.Ctx) error { func show(c *fiber.Ctx) error {
@ -23,23 +22,15 @@ func show(c *fiber.Ctx) error {
post := e.Data.Post post := e.Data.Post
publishDate := time.UnixMilli(e.Data.Post.CreatedAt) publishDate := time.UnixMilli(e.Data.Post.CreatedAt)
log.Println(publishDate)
var sb strings.Builder p := converters.ConvertParagraphs(post.Content.BodyModel.Paragraphs)
for _, node := range post.Content.BodyModel.Paragraphs {
switch node.Type {
case "H3":
sb.WriteString(fmt.Sprintf("<h3>%s</h3>", node.Text))
}
}
return c.Render("show", fiber.Map { return c.Render("show", fiber.Map {
"Title": post.Title, "Title": post.Title,
"UserId": post.Creator.ID, "UserId": post.Creator.ID,
"Author": post.Creator.Name, "Author": post.Creator.Name,
"PublishDate": publishDate.Format(time.DateOnly), "PublishDate": publishDate.Format(time.DateOnly),
"Nodes": post.Content.BodyModel.Paragraphs, "Paragraphs": template.HTML(p),
}) })
} }

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .Title }}</title> <title>{{ .Title }}</title>
<link rel="stylesheet" href="/assets/index-544d9ea3.css"> <link rel="stylesheet" href="/assets/index-d9a17752.css">
</head> </head>
<body> <body>
<h1 class="text-2xl">Hello, World!</h1> <h1 class="text-2xl">Hello, World!</h1>

View File

@ -14,21 +14,14 @@
rel="stylesheet"> rel="stylesheet">
<title>{{ .Title }}</title> <title>{{ .Title }}</title>
<link rel="stylesheet" href="/assets/show-7a43d2b5.css"> <link rel="stylesheet" href="/assets/show-9b4da228.css">
</head> </head>
<body class="flex flex-col h-full w-full items-center"> <body class="flex flex-col h-full w-full items-center">
<article class="prose lg:prose-xl pt-20"> <article class="prose lg:prose-xl p-20">
<h2 class="text-2xl">{{.Title}}</h2> <h2 class="text-2xl">{{.Title}}</h2>
<p><a href="https://medium.com/u/{{.UserId}}">{{.Author}}</a> on {{.PublishDate}}</p> <p><a href="https://medium.com/u/{{.UserId}}">{{.Author}}</a> on {{.PublishDate}}</p>
{{range .Nodes}} {{.Paragraphs}}
{{if eq .Type "H3"}}
<h3>{{.Text}}</h3>
{{end}}
{{if eq .Type "IMG"}}
{{end}}
{{end}}
</article> </article>
</body> </body>

View File

@ -17,17 +17,10 @@
</head> </head>
<body class="flex flex-col h-full w-full items-center"> <body class="flex flex-col h-full w-full items-center">
<article class="prose lg:prose-xl pt-20"> <article class="prose lg:prose-xl p-20">
<h2 class="text-2xl">{{.Title}}</h2> <h2 class="text-2xl">{{.Title}}</h2>
<p><a href="https://medium.com/u/{{.UserId}}">{{.Author}}</a> on {{.PublishDate}}</p> <p><a href="https://medium.com/u/{{.UserId}}">{{.Author}}</a> on {{.PublishDate}}</p>
{{range .Nodes}} {{.Paragraphs}}
{{if eq .type "H3"}}
<h3>.text</h3>
{{end}}
{{if eq .type "IMG"}}
<h3>.text</h3>
{{end}}
{{end}}
</article> </article>
</body> </body>

View File

@ -9,20 +9,20 @@ import (
) )
type RangeWithMarkup struct { type RangeWithMarkup struct {
Range []int Range []int
Markups []entities.Markup Markups []entities.Markup
} }
func unique(intSlice []int) []int { func unique(intSlice []int) []int {
keys := make(map[int]bool) keys := make(map[int]bool)
list := []int{} list := []int{}
for _, entry := range intSlice { for _, entry := range intSlice {
if _, value := keys[entry]; !value { if _, value := keys[entry]; !value {
keys[entry] = true keys[entry] = true
list = append(list, entry) list = append(list, entry)
} }
} }
return list return list
} }
func ranges(text string, markups []entities.Markup) []RangeWithMarkup { func ranges(text string, markups []entities.Markup) []RangeWithMarkup {
@ -54,14 +54,14 @@ func ranges(text string, markups []entities.Markup) []RangeWithMarkup {
// check if this markup is covered by the range // check if this markup is covered by the range
coveredMarkups := make([]entities.Markup, 0) coveredMarkups := make([]entities.Markup, 0)
for _, m := range markups { for _, m := range markups {
if (int(m.Start) >= start && int(m.Start) < end) || (int(m.End - 1) >= start && int(m.End - 1) < end) { if (int(m.Start) >= start && int(m.Start) < end) || (int(m.End-1) >= start && int(m.End-1) < end) {
coveredMarkups = append(coveredMarkups, m) coveredMarkups = append(coveredMarkups, m)
} }
} }
// append the range // append the range
ranges = append(ranges, RangeWithMarkup{ ranges = append(ranges, RangeWithMarkup{
Range: []int{start, end}, Range: []int{start, end},
Markups: coveredMarkups, Markups: coveredMarkups,
}) })
} }
@ -69,22 +69,22 @@ func ranges(text string, markups []entities.Markup) []RangeWithMarkup {
return ranges return ranges
} }
func Convert(text string, markups []entities.Markup) string { func ConvertMarkup(text string, markups []entities.Markup) string {
var markedUp strings.Builder var markedUp strings.Builder
for _, r := range ranges(text, markups) { for _, r := range ranges(text, markups) {
textToWrap := string(text[r.Range[0]:r.Range[1]]) textToWrap := string(text[r.Range[0]:r.Range[1]])
markedUp.WriteString(WrapInMarkups(textToWrap, r.Markups)) markedUp.WriteString(wrapInMarkups(textToWrap, r.Markups))
} }
return markedUp.String() return markedUp.String()
} }
func WrapInMarkups(child string, markups []entities.Markup) string { func wrapInMarkups(child string, markups []entities.Markup) string {
if len(markups) == 0 { if len(markups) == 0 {
return child return child
} }
markedUp := markupNodeInContainer(child, markups[0]) markedUp := markupNodeInContainer(child, markups[0])
return WrapInMarkups(markedUp, markups[1:]) return wrapInMarkups(markedUp, markups[1:])
} }
func markupNodeInContainer(child string, markup entities.Markup) string { func markupNodeInContainer(child string, markup entities.Markup) string {

View File

@ -58,7 +58,7 @@ func TestRanges(t *testing.T) {
} }
func TestConvert(t *testing.T) { func TestConvert(t *testing.T) {
markup := Convert("strong and emphasized only", []entities.Markup{ markup := ConvertMarkup("strong and emphasized only", []entities.Markup{
{ {
Type: "STRONG", Type: "STRONG",
Start: 0, Start: 0,

View File

@ -0,0 +1,144 @@
package converters
import (
"fmt"
"strings"
"github.com/medium.rip/pkg/entities"
)
const IMAGE_HOST = "https://cdn-images-1.medium.com/fit/c"
const MAX_WIDTH = 800
const FALLBACK_HEIGHT = 600
type Image struct {
ID string
OriginalHeight int64
OriginalWidth int64
}
func (i *Image) Initialize(originalWidth *int64, originalHeight *int64) {
if originalWidth != nil {
i.OriginalWidth = *originalWidth
} else {
i.OriginalWidth = MAX_WIDTH
}
if originalHeight != nil {
i.OriginalHeight = *originalHeight
} else {
i.OriginalHeight = FALLBACK_HEIGHT
}
}
func (i *Image) width() int64 {
if i.OriginalWidth > MAX_WIDTH {
return MAX_WIDTH
} else {
return i.OriginalWidth
}
}
func (i *Image) src() string {
return fmt.Sprintf("%s/%d/%d/%s", IMAGE_HOST, i.width(), i.height(), i.ID)
}
func (i *Image) height() int64 {
if i.OriginalWidth > MAX_WIDTH {
return i.OriginalHeight * int64(i.ratio())
} else {
return i.OriginalHeight
}
}
func (i *Image) ratio() float32 {
return float32(MAX_WIDTH) / float32(i.OriginalWidth)
}
func ConvertParagraphs(paragraphs []entities.Paragraph) string {
if len(paragraphs) == 0 {
return ""
}
var ps strings.Builder
for i, p := range paragraphs {
switch p.Type {
case "BQ", "MIXTAPE_EMBED", "PQ":
children := ConvertMarkup(p.Text, p.Markups)
ps.WriteString(fmt.Sprintf("<blockquote><p>%s</p></blockquote>", children))
case "H2":
children := ConvertMarkup(p.Text, p.Markups)
if p.Name != "" {
ps.WriteString(fmt.Sprintf("<h2 id=\"%s\">%s</h2>", p.Name, children))
} else {
ps.WriteString(fmt.Sprintf("<h2>%s</h2>", children))
}
case "H3":
children := ConvertMarkup(p.Text, p.Markups)
if p.Name != "" {
ps.WriteString(fmt.Sprintf("<h3 id=\"%s\">%s</h3>", p.Name, children))
} else {
ps.WriteString(fmt.Sprintf("<h3>%s</h3>", children))
}
case "H4":
children := ConvertMarkup(p.Text, p.Markups)
if p.Name != "" {
ps.WriteString(fmt.Sprintf("<h4 id=\"%s\">%s</h4>", p.Name, children))
} else {
ps.WriteString(fmt.Sprintf("<h4>%s</h4>", children))
}
// TODO: handle IFRAME
case "IMG":
ps.WriteString(convertImg(p))
case "OLI":
listItems := convertOli(paragraphs[i:])
ps.WriteString(fmt.Sprintf("<ol>%s</ol>", listItems))
case "ULI":
listItems := convertUli(paragraphs[i:])
ps.WriteString(fmt.Sprintf("<ul>%s</ul>", listItems))
case "P":
children := ConvertMarkup(p.Text, p.Markups)
ps.WriteString(fmt.Sprintf("<p>%s</p>", children))
case "PRE":
children := ConvertMarkup(p.Text, p.Markups)
ps.WriteString(fmt.Sprintf("<pre>%s</pre>", children))
case "SECTION_CAPTION":
// unused
default:
}
}
return ps.String()
}
func convertImg(p entities.Paragraph) string {
if p.Metadata != nil {
captionMarkup := ConvertMarkup(p.Text, p.Markups)
img := Image{ID : p.Metadata.ID}
img.Initialize(&p.Metadata.OriginalWidth, &p.Metadata.OriginalHeight)
return fmt.Sprintf("<figure><img src=\"%s\" width=\"%d\" /><figcaption>%s</figcaption></figure>", img.src(), img.width(), captionMarkup)
} else {
return ""
}
}
func convertOli(ps []entities.Paragraph) string {
if len(ps) != 0 && ps[0].Type == "OLI" {
p := ps[0]
children := ConvertMarkup(p.Text, p.Markups)
return fmt.Sprintf("<li>%s</li>", children) + convertOli(ps[1:])
} else {
return ""
}
}
func convertUli(ps []entities.Paragraph) string {
if len(ps) != 0 && ps[0].Type == "ULI" {
p := ps[0]
children := ConvertMarkup(p.Text, p.Markups)
return fmt.Sprintf("<li>%s</li>", children) + convertUli(ps[1:])
} else {
return ""
}
}

View File

@ -0,0 +1,22 @@
package converters
import (
"testing"
"github.com/medium.rip/pkg/entities"
)
func TestConvertOli(t *testing.T) {
oli := entities.Paragraph{
Name: "1-1",
Type: "OLI",
Text: "This is an ordered list item.",
Markups: []entities.Markup{},
}
olis := []entities.Paragraph{oli}
oliHTML := ConvertParagraphs(olis)
expected := "<ol><li>This is an ordered list item.</li></ol>"
if oliHTML != expected {
t.Errorf("ConvertParagraphs(olis) = %s; want %s", oliHTML, expected)
}
}