mirror of
				https://github.com/SphericalKat/medium.rip.git
				synced 2025-10-31 10:55: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 | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"strings" | ||||
| 	"html/template" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| 	"github.com/medium.rip/pkg/client" | ||||
| 	"github.com/medium.rip/pkg/converters" | ||||
| ) | ||||
| 
 | ||||
| func show(c *fiber.Ctx) error { | ||||
| @ -23,23 +22,15 @@ func show(c *fiber.Ctx) error { | ||||
| 
 | ||||
| 	post := e.Data.Post | ||||
| 	publishDate := time.UnixMilli(e.Data.Post.CreatedAt) | ||||
| 	log.Println(publishDate) | ||||
| 
 | ||||
| 	var sb strings.Builder | ||||
| 
 | ||||
| 	for _, node := range post.Content.BodyModel.Paragraphs { | ||||
| 		switch node.Type { | ||||
| 		case "H3": | ||||
| 			sb.WriteString(fmt.Sprintf("<h3>%s</h3>", node.Text)) | ||||
| 		} | ||||
| 	} | ||||
| 	p := converters.ConvertParagraphs(post.Content.BodyModel.Paragraphs) | ||||
| 
 | ||||
| 	return c.Render("show", fiber.Map { | ||||
| 		"Title": post.Title, | ||||
| 		"UserId": post.Creator.ID, | ||||
| 		"Author": post.Creator.Name, | ||||
| 		"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"> | ||||
|      | ||||
|     <title>{{ .Title }}</title> | ||||
|   <link rel="stylesheet" href="/assets/index-544d9ea3.css"> | ||||
|   <link rel="stylesheet" href="/assets/index-d9a17752.css"> | ||||
| </head> | ||||
| <body> | ||||
|     <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"> | ||||
| 
 | ||||
|     <title>{{ .Title }}</title> | ||||
|   <link rel="stylesheet" href="/assets/show-7a43d2b5.css"> | ||||
|   <link rel="stylesheet" href="/assets/show-9b4da228.css"> | ||||
| </head> | ||||
| 
 | ||||
| <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> | ||||
|         <p><a href="https://medium.com/u/{{.UserId}}">{{.Author}}</a> on {{.PublishDate}}</p> | ||||
|         {{range .Nodes}} | ||||
|             {{if eq .Type "H3"}} | ||||
|                 <h3>{{.Text}}</h3> | ||||
|             {{end}} | ||||
|             {{if eq .Type "IMG"}} | ||||
|                  | ||||
|             {{end}} | ||||
|         {{end}} | ||||
|         {{.Paragraphs}} | ||||
|     </article> | ||||
| </body> | ||||
| 
 | ||||
|  | ||||
| @ -17,17 +17,10 @@ | ||||
| </head> | ||||
| 
 | ||||
| <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> | ||||
|         <p><a href="https://medium.com/u/{{.UserId}}">{{.Author}}</a> on {{.PublishDate}}</p> | ||||
|         {{range .Nodes}} | ||||
|             {{if eq .type "H3"}} | ||||
|                 <h3>.text</h3> | ||||
|             {{end}} | ||||
|             {{if eq .type "IMG"}} | ||||
|                 <h3>.text</h3> | ||||
|             {{end}} | ||||
|         {{end}} | ||||
|         {{.Paragraphs}} | ||||
|     </article> | ||||
| </body> | ||||
| 
 | ||||
|  | ||||
| @ -9,20 +9,20 @@ import ( | ||||
| ) | ||||
| 
 | ||||
| type RangeWithMarkup struct { | ||||
| 	Range  []int | ||||
| 	Range   []int | ||||
| 	Markups []entities.Markup | ||||
| } | ||||
| 
 | ||||
| func unique(intSlice []int) []int { | ||||
|     keys := make(map[int]bool) | ||||
|     list := []int{}	 | ||||
|     for _, entry := range intSlice { | ||||
|         if _, value := keys[entry]; !value { | ||||
|             keys[entry] = true | ||||
|             list = append(list, entry) | ||||
|         } | ||||
|     }     | ||||
|     return list | ||||
| 	keys := make(map[int]bool) | ||||
| 	list := []int{} | ||||
| 	for _, entry := range intSlice { | ||||
| 		if _, value := keys[entry]; !value { | ||||
| 			keys[entry] = true | ||||
| 			list = append(list, entry) | ||||
| 		} | ||||
| 	} | ||||
| 	return list | ||||
| } | ||||
| 
 | ||||
| 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
 | ||||
| 		coveredMarkups := make([]entities.Markup, 0) | ||||
| 		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) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// append the range
 | ||||
| 		ranges = append(ranges, RangeWithMarkup{ | ||||
| 			Range: []int{start, end}, | ||||
| 			Range:   []int{start, end}, | ||||
| 			Markups: coveredMarkups, | ||||
| 		}) | ||||
| 	} | ||||
| @ -69,22 +69,22 @@ func ranges(text string, markups []entities.Markup) []RangeWithMarkup { | ||||
| 	return ranges | ||||
| } | ||||
| 
 | ||||
| func Convert(text string, markups []entities.Markup) string { | ||||
| func ConvertMarkup(text string, markups []entities.Markup) string { | ||||
| 	var markedUp strings.Builder | ||||
| 	for _, r := range ranges(text, markups) { | ||||
| 		textToWrap := string(text[r.Range[0]:r.Range[1]]) | ||||
| 		markedUp.WriteString(WrapInMarkups(textToWrap, r.Markups)) | ||||
| 		markedUp.WriteString(wrapInMarkups(textToWrap, r.Markups)) | ||||
| 	} | ||||
| 
 | ||||
| 	return markedUp.String() | ||||
| } | ||||
| 
 | ||||
| func WrapInMarkups(child string, markups []entities.Markup) string { | ||||
| func wrapInMarkups(child string, markups []entities.Markup) string { | ||||
| 	if len(markups) == 0 { | ||||
| 		return child | ||||
| 	} | ||||
| 	markedUp := markupNodeInContainer(child, markups[0]) | ||||
| 	return WrapInMarkups(markedUp, markups[1:]) | ||||
| 	return wrapInMarkups(markedUp, markups[1:]) | ||||
| } | ||||
| 
 | ||||
| func markupNodeInContainer(child string, markup entities.Markup) string { | ||||
| @ -105,4 +105,4 @@ func markupNodeInContainer(child string, markup entities.Markup) string { | ||||
| 		return fmt.Sprintf(`<code>%s</code>`, child) | ||||
| 	} | ||||
| 	return child | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -58,7 +58,7 @@ func TestRanges(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", | ||||
| 			Start: 0, | ||||
| @ -74,4 +74,4 @@ func TestConvert(t *testing.T) { | ||||
| 	if markup != "<strong>strong </strong><em><strong>and</strong></em><em> emphasized</em> only" { | ||||
| 		t.Errorf("Expected markup to be <strong>strong </strong><em><strong>and</strong></em><em> emphasized</em> only, got %s", markup) | ||||
| 	} | ||||
| } | ||||
| } | ||||
|  | ||||
							
								
								
									
										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