mirror of
https://github.com/SphericalKat/medium.rip.git
synced 2024-11-16 11:25:57 +00:00
feat: implement markup rendering
Signed-off-by: Sphericalkat <amolele@gmail.com>
This commit is contained in:
parent
3de9bfc58c
commit
7b6e7d7edb
@ -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),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2
frontend/dist/index.html
vendored
2
frontend/dist/index.html
vendored
@ -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>
|
||||||
|
13
frontend/dist/show.html
vendored
13
frontend/dist/show.html
vendored
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
144
pkg/converters/paragraph_converter.go
Normal file
144
pkg/converters/paragraph_converter.go
Normal 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 ""
|
||||||
|
}
|
||||||
|
}
|
22
pkg/converters/paragraph_converter_test.go
Normal file
22
pkg/converters/paragraph_converter_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user