Skip to content

Commit

Permalink
文章和评论共用 Markdown → HTML 转换器
Browse files Browse the repository at this point in the history
  • Loading branch information
movsb committed Mar 26, 2024
1 parent cf7c9a1 commit ed38b6e
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 54 deletions.
2 changes: 1 addition & 1 deletion cmd/client/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ func (c *Client) BackupFiles(cmd *cobra.Command) {
log.Printf("Local: copy %s\n", rf[i].Path)
copyRemote(rf[i])
} else {
log.Printf("Local: modtime of dir: %s", rf[i].Path)
// log.Printf("Local: modtime of dir: %s", rf[i].Path)
path := filepath.Join(localDir, lf[j].Path)
t := time.Unix(int64(rf[i].Time), 0)
if err := os.Chtimes(path, t, t); err != nil {
Expand Down
61 changes: 21 additions & 40 deletions service/comment.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package service

import (
"bytes"
"context"
"fmt"
"log"
Expand All @@ -10,19 +9,14 @@ import (
"time"
"unicode/utf8"

mathjax "github.com/litao91/goldmark-mathjax"
"github.com/movsb/taoblog/modules/auth"
"github.com/movsb/taoblog/modules/exception"
"github.com/movsb/taoblog/modules/utils"
"github.com/movsb/taoblog/protocols"
"github.com/movsb/taoblog/service/models"
"github.com/movsb/taoblog/service/modules/comment_notify"
"github.com/movsb/taoblog/service/modules/post_translators"
"github.com/movsb/taorm/taorm"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
Expand Down Expand Up @@ -51,10 +45,10 @@ func (s *Service) GetComment(ctx context.Context, req *protocols.GetCommentReque
// UpdateComment ...
func (s *Service) UpdateComment(ctx context.Context, req *protocols.UpdateCommentRequest) (*protocols.Comment, error) {
user := s.auth.AuthGRPC(ctx)
cmtOld := s.GetComment2(int64(req.Comment.Id))
if !user.IsAdmin() {
userIP := ipFromContext(ctx, true)
cmt := s.GetComment2(int64(req.Comment.Id))
if userIP != cmt.IP || !models.In5min(cmt.Date) {
if userIP != cmtOld.IP || !models.In5min(cmtOld.Date) {
panic(exception.NewValidationError(`超时或无权限编辑评论`))
}
}
Expand All @@ -80,7 +74,7 @@ func (s *Service) UpdateComment(ctx context.Context, req *protocols.UpdateCommen
if hasSourceType {
data[`source_type`] = req.Comment.SourceType
data[`source`] = req.Comment.Source
data[`content`] = s.convertCommentMarkdown(user, req.Comment.SourceType, req.Comment.Source)
data[`content`] = s.convertCommentMarkdown(user, req.Comment.SourceType, req.Comment.Source, cmtOld.PostID)
}
s.MustTxCall(func(txs *Service) error {
txs.tdb.Model(models.Comment{}).Where(`id=?`, req.Comment.Id).MustUpdateMap(data)
Expand Down Expand Up @@ -271,7 +265,7 @@ func (s *Service) CreateComment(ctx context.Context, in *protocols.Comment) (*pr
}
}

comment.Content = s.convertCommentMarkdown(user, in.SourceType, in.Source)
comment.Content = s.convertCommentMarkdown(user, in.SourceType, in.Source, in.PostId)

adminEmails := s.cfg.Comment.Emails

Expand Down Expand Up @@ -317,44 +311,31 @@ func (s *Service) updateCommentsCount() {
s.SetOption("comment_count", count)
}

func (s *Service) convertCommentMarkdown(user *auth.User, ty string, source string) string {
func (s *Service) convertCommentMarkdown(user *auth.User, ty string, source string, postID int64) string {
if ty != "markdown" {
panic(exception.NewValidationError("仅支持 markdown"))
}

md := goldmark.New(
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
goldmark.WithExtensions(extension.GFM),
goldmark.WithExtensions(extension.DefinitionList),
goldmark.WithExtensions(extension.Footnote),
goldmark.WithExtensions(mathjax.MathJax),
)
doc := md.Parser().Parse(text.NewReader([]byte(source)))
var md *post_translators.MarkdownTranslator

if !user.IsAdmin() {
if err := ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
switch n.Kind() {
case ast.KindHeading:
panic(exception.NewValidationError(`Markdown 不能包含标题`))
case ast.KindHTMLBlock, ast.KindRawHTML:
panic(exception.NewValidationError(`Markdown 不能包含 HTML 元素`))
}
}
return ast.WalkContinue, nil
}); err != nil {
panic(err)
}
if user.IsAdmin() {
md = post_translators.NewMarkdownTranslator(
post_translators.WithPathResolver(s.PathResolver(postID)),
)
} else {
md = post_translators.NewMarkdownTranslator(
post_translators.WithPathResolver(s.PathResolver(postID)),
post_translators.WithDisableHeadings(true),
post_translators.WithDisableHTML(true),
)
}

var buf bytes.Buffer
if err := md.Renderer().Render(&buf, []byte(source), doc); err != nil {
_, content, err := md.Translate(source)
if err != nil {
panic(exception.NewValidationError("不能转换 markdown"))
}

return buf.String()
return content
}

// SetCommentPostID 把某条顶级评论及其子评论转移到另一篇文章下
Expand Down Expand Up @@ -390,7 +371,7 @@ func (s *Service) SetCommentPostID(ctx context.Context, in *protocols.SetComment

func (s *Service) PreviewComment(ctx context.Context, in *protocols.PreviewCommentRequest) (*protocols.PreviewCommentResponse, error) {
user := s.auth.AuthGRPC(ctx)
html := s.convertCommentMarkdown(user, `markdown`, in.Markdown)
html := s.convertCommentMarkdown(user, `markdown`, in.Markdown, 0)
return &protocols.PreviewCommentResponse{Html: html}, nil
}

Expand Down
91 changes: 80 additions & 11 deletions service/modules/post_translators/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

mathjax "github.com/litao91/goldmark-mathjax"
wikitable "github.com/movsb/goldmark-wiki-table"
"github.com/movsb/taoblog/modules/exception"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
Expand All @@ -29,7 +30,10 @@ import (

// MarkdownTranslator ...
type MarkdownTranslator struct {
pathResolver PathResolver
pathResolver PathResolver
removeTitleHeading bool // 是否移除 H1
disableHeadings bool // 评论中不允许标题
disableHTML bool // 禁止 HTML 元素
}

var (
Expand All @@ -40,8 +44,48 @@ func init() {
imageKind = ast.NewNodeKind(`image`)
}

func (me *MarkdownTranslator) SetPathResolver(pathResolver PathResolver) {
me.pathResolver = pathResolver
type Option func(me *MarkdownTranslator) error

// 解析 Markdown 中的相对链接。
func WithPathResolver(pathResolver PathResolver) Option {
return func(me *MarkdownTranslator) error {
me.pathResolver = pathResolver
return nil
}
}

// 移除 Markdown 中的标题(适用于文章)。
func WithRemoveTitleHeading(remove bool) Option {
return func(me *MarkdownTranslator) error {
me.removeTitleHeading = remove
return nil
}
}

// 不允许评论中存在任何级别的“标题”。
func WithDisableHeadings(disable bool) Option {
return func(me *MarkdownTranslator) error {
me.disableHeadings = disable
return nil
}
}

// 不允许使用 HTML 标签。
func WithDisableHTML(disable bool) Option {
return func(me *MarkdownTranslator) error {
me.disableHTML = disable
return nil
}
}

func NewMarkdownTranslator(options ...Option) *MarkdownTranslator {
me := &MarkdownTranslator{}
for _, option := range options {
if err := option(me); err != nil {
log.Println(err)
}
}
return me
}

// Translate ...
Expand Down Expand Up @@ -73,11 +117,12 @@ func (me *MarkdownTranslator) Translate(source string) (string, string, error) {
heading := p.(*ast.Heading)
switch heading.Level {
case 1:
title = string(heading.Text(sourceBytes))
p = p.NextSibling()
parent := heading.Parent()
parent.RemoveChild(parent, heading)
continue
if !me.disableHeadings && me.removeTitleHeading {
title = string(heading.Text(sourceBytes))
p = p.NextSibling()
parent := heading.Parent()
parent.RemoveChild(parent, heading)
}
}
case p.Kind() == ast.KindParagraph:
para := p.(*ast.Paragraph)
Expand All @@ -98,6 +143,24 @@ func (me *MarkdownTranslator) Translate(source string) (string, string, error) {
panic(`max depth`)
}

if err := ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
switch n.Kind() {
case ast.KindHeading:
if me.disableHeadings {
panic(exception.NewValidationError(`Markdown 不能包含标题`))
}
case ast.KindHTMLBlock, ast.KindRawHTML:
if me.disableHTML {
panic(exception.NewValidationError(`Markdown 不能包含 HTML 元素`))
}
}
}
return ast.WalkContinue, nil
}); err != nil {
panic(err)
}

rdr := md.Renderer()
if reg, ok := rdr.(renderer.NodeRendererFuncRegisterer); ok {
reg.Register(imageKind, me.renderImage)
Expand Down Expand Up @@ -204,9 +267,15 @@ func size(path string) (int, int) {
return 0, 0
}
width, height := imgConfig.Width, imgConfig.Height
if strings.Contains(filepath.Base(path), `@2x.`) {
width /= 2
height /= 2

for i := 1; i <= 10; i++ {
scaleFmt := fmt.Sprintf(`@%dx.`, i)
if strings.Contains(filepath.Base(path), scaleFmt) {
width /= i
height /= i
break
}
}

return width, height
}
6 changes: 4 additions & 2 deletions service/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,10 @@ func (s *Service) getPostContent(id int64) (string, error) {
var tr post_translators.PostTranslator
switch p.SourceType {
case `markdown`:
mt := &post_translators.MarkdownTranslator{}
mt.SetPathResolver(s.PathResolver(id))
mt := post_translators.NewMarkdownTranslator(
post_translators.WithPathResolver(s.PathResolver(id)),
post_translators.WithRemoveTitleHeading(true),
)
tr = mt
case `html`:
tr = &post_translators.HTMLTranslator{}
Expand Down

0 comments on commit ed38b6e

Please sign in to comment.