diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index fff187b9ec..442c420e5c 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -59,34 +59,35 @@ type ServerCommand struct { SSL SSLGroup `group:"ssl" namespace:"ssl" env-namespace:"SSL"` ImageProxy ImageProxyGroup `group:"image-proxy" namespace:"image-proxy" env-namespace:"IMAGE_PROXY"` - Sites []string `long:"site" env:"SITE" default:"remark" description:"site names" env-delim:","` - AnonymousVote bool `long:"anon-vote" env:"ANON_VOTE" description:"enable anonymous votes (works only with VOTES_IP enabled)"` - AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" default:"" description:"admin basic auth password"` - BackupLocation string `long:"backup" env:"BACKUP_PATH" default:"./var/backup" description:"backups location"` - MaxBackupFiles int `long:"max-back" env:"MAX_BACKUP_FILES" default:"10" description:"max backups to keep"` - LegacyImageProxy bool `long:"img-proxy" env:"IMG_PROXY" description:"[deprecated, use image-proxy.http2https] enable image proxy"` - MaxCommentSize int `long:"max-comment" env:"MAX_COMMENT_SIZE" default:"2048" description:"max comment size"` - MaxVotes int `long:"max-votes" env:"MAX_VOTES" default:"-1" description:"maximum number of votes per comment"` - RestrictVoteIP bool `long:"votes-ip" env:"VOTES_IP" description:"restrict votes from the same ip"` - DurationVoteIP time.Duration `long:"votes-ip-time" env:"VOTES_IP_TIME" default:"5m" description:"same ip vote duration"` - LowScore int `long:"low-score" env:"LOW_SCORE" default:"-5" description:"low score threshold"` - CriticalScore int `long:"critical-score" env:"CRITICAL_SCORE" default:"-10" description:"critical score threshold"` - PositiveScore bool `long:"positive-score" env:"POSITIVE_SCORE" description:"enable positive score only"` - ReadOnlyAge int `long:"read-age" env:"READONLY_AGE" default:"0" description:"read-only age of comments, days"` - EditDuration time.Duration `long:"edit-time" env:"EDIT_TIME" default:"5m" description:"edit window"` - AdminEdit bool `long:"admin-edit" env:"ADMIN_EDIT" description:"unlimited edit for admins"` - Port int `long:"port" env:"REMARK_PORT" default:"8080" description:"port"` - Address string `long:"address" env:"REMARK_ADDRESS" default:"" description:"listening address"` - WebRoot string `long:"web-root" env:"REMARK_WEB_ROOT" default:"./web" description:"web root directory"` - UpdateLimit float64 `long:"update-limit" env:"UPDATE_LIMIT" default:"0.5" description:"updates/sec limit"` - RestrictedWords []string `long:"restricted-words" env:"RESTRICTED_WORDS" description:"words prohibited to use in comments" env-delim:","` - RestrictedNames []string `long:"restricted-names" env:"RESTRICTED_NAMES" description:"names prohibited to use by user" env-delim:","` - EnableEmoji bool `long:"emoji" env:"EMOJI" description:"enable emoji"` - SimpleView bool `long:"simple-view" env:"SIMPLE_VIEW" description:"minimal comment editor mode"` - ProxyCORS bool `long:"proxy-cors" env:"PROXY_CORS" description:"disable internal CORS and delegate it to proxy"` - AllowedHosts []string `long:"allowed-hosts" env:"ALLOWED_HOSTS" description:"limit hosts/sources allowed to embed comments" env-delim:","` - SubscribersOnly bool `long:"subscribers-only" env:"SUBSCRIBERS_ONLY" description:"enable commenting only for Patreon subscribers"` - DisableSignature bool `long:"disable-signature" env:"DISABLE_SIGNATURE" description:"disable server signature in headers"` + Sites []string `long:"site" env:"SITE" default:"remark" description:"site names" env-delim:","` + AnonymousVote bool `long:"anon-vote" env:"ANON_VOTE" description:"enable anonymous votes (works only with VOTES_IP enabled)"` + AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" default:"" description:"admin basic auth password"` + BackupLocation string `long:"backup" env:"BACKUP_PATH" default:"./var/backup" description:"backups location"` + MaxBackupFiles int `long:"max-back" env:"MAX_BACKUP_FILES" default:"10" description:"max backups to keep"` + LegacyImageProxy bool `long:"img-proxy" env:"IMG_PROXY" description:"[deprecated, use image-proxy.http2https] enable image proxy"` + MaxCommentSize int `long:"max-comment" env:"MAX_COMMENT_SIZE" default:"2048" description:"max comment size"` + MaxVotes int `long:"max-votes" env:"MAX_VOTES" default:"-1" description:"maximum number of votes per comment"` + RestrictVoteIP bool `long:"votes-ip" env:"VOTES_IP" description:"restrict votes from the same ip"` + DurationVoteIP time.Duration `long:"votes-ip-time" env:"VOTES_IP_TIME" default:"5m" description:"same ip vote duration"` + LowScore int `long:"low-score" env:"LOW_SCORE" default:"-5" description:"low score threshold"` + CriticalScore int `long:"critical-score" env:"CRITICAL_SCORE" default:"-10" description:"critical score threshold"` + PositiveScore bool `long:"positive-score" env:"POSITIVE_SCORE" description:"enable positive score only"` + ReadOnlyAge int `long:"read-age" env:"READONLY_AGE" default:"0" description:"read-only age of comments, days"` + EditDuration time.Duration `long:"edit-time" env:"EDIT_TIME" default:"5m" description:"edit window"` + AdminEdit bool `long:"admin-edit" env:"ADMIN_EDIT" description:"unlimited edit for admins"` + Port int `long:"port" env:"REMARK_PORT" default:"8080" description:"port"` + Address string `long:"address" env:"REMARK_ADDRESS" default:"" description:"listening address"` + WebRoot string `long:"web-root" env:"REMARK_WEB_ROOT" default:"./web" description:"web root directory"` + UpdateLimit float64 `long:"update-limit" env:"UPDATE_LIMIT" default:"0.5" description:"updates/sec limit"` + RestrictedWords []string `long:"restricted-words" env:"RESTRICTED_WORDS" description:"words prohibited to use in comments" env-delim:","` + RestrictedNames []string `long:"restricted-names" env:"RESTRICTED_NAMES" description:"names prohibited to use by user" env-delim:","` + EnableEmoji bool `long:"emoji" env:"EMOJI" description:"enable emoji"` + SimpleView bool `long:"simple-view" env:"SIMPLE_VIEW" description:"minimal comment editor mode"` + ProxyCORS bool `long:"proxy-cors" env:"PROXY_CORS" description:"disable internal CORS and delegate it to proxy"` + AllowedHosts []string `long:"allowed-hosts" env:"ALLOWED_HOSTS" description:"limit hosts/sources allowed to embed comments" env-delim:","` + SubscribersOnly bool `long:"subscribers-only" env:"SUBSCRIBERS_ONLY" description:"enable commenting only for Patreon subscribers"` + DisableSignature bool `long:"disable-signature" env:"DISABLE_SIGNATURE" description:"disable server signature in headers"` + DisableFancyTextFormatting bool `long:"disable-fancy-text-formatting" env:"DISABLE_FANCY_TEXT_FORMATTING" description:"disable fancy comments text formatting (replacement of quotes, dashes, fractions, etc)"` Auth struct { TTL struct { @@ -541,7 +542,7 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) { Cache: loadingCache, NativeImporter: &migrator.Native{DataStore: dataService}, DisqusImporter: &migrator.Disqus{DataStore: dataService}, - WordPressImporter: &migrator.WordPress{DataStore: dataService}, + WordPressImporter: &migrator.WordPress{DataStore: dataService, DisableFancyTextFormatting: s.DisableFancyTextFormatting}, CommentoImporter: &migrator.Commento{DataStore: dataService}, NativeExporter: &migrator.Native{DataStore: dataService}, URLMapperMaker: migrator.NewURLMapper, @@ -576,33 +577,34 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) { } srv := &api.Rest{ - Version: s.Revision, - DataService: dataService, - WebRoot: s.WebRoot, - WebFS: webFS, - RemarkURL: s.RemarkURL, - ImageProxy: imgProxy, - CommentFormatter: commentFormatter, - Migrator: migr, - ReadOnlyAge: s.ReadOnlyAge, - SharedSecret: s.SharedSecret, - Authenticator: authenticator, - Cache: loadingCache, - NotifyService: notifyService, - TelegramService: telegramService, - SSLConfig: sslConfig, - UpdateLimiter: s.UpdateLimit, - ImageService: imageService, - EmailNotifications: contains("email", s.Notify.Users), - TelegramNotifications: contains("telegram", s.Notify.Users) && telegramService != nil, - EmojiEnabled: s.EnableEmoji, - AnonVote: s.AnonymousVote && s.RestrictVoteIP, - SimpleView: s.SimpleView, - ProxyCORS: s.ProxyCORS, - AllowedAncestors: s.AllowedHosts, - SendJWTHeader: s.Auth.SendJWTHeader, - SubscribersOnly: s.SubscribersOnly, - DisableSignature: s.DisableSignature, + Version: s.Revision, + DataService: dataService, + WebRoot: s.WebRoot, + WebFS: webFS, + RemarkURL: s.RemarkURL, + ImageProxy: imgProxy, + CommentFormatter: commentFormatter, + Migrator: migr, + ReadOnlyAge: s.ReadOnlyAge, + SharedSecret: s.SharedSecret, + Authenticator: authenticator, + Cache: loadingCache, + NotifyService: notifyService, + TelegramService: telegramService, + SSLConfig: sslConfig, + UpdateLimiter: s.UpdateLimit, + ImageService: imageService, + EmailNotifications: contains("email", s.Notify.Users), + TelegramNotifications: contains("telegram", s.Notify.Users) && telegramService != nil, + EmojiEnabled: s.EnableEmoji, + AnonVote: s.AnonymousVote && s.RestrictVoteIP, + SimpleView: s.SimpleView, + ProxyCORS: s.ProxyCORS, + AllowedAncestors: s.AllowedHosts, + SendJWTHeader: s.Auth.SendJWTHeader, + SubscribersOnly: s.SubscribersOnly, + DisableSignature: s.DisableSignature, + DisableFancyTextFormatting: s.DisableFancyTextFormatting, } srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = s.LowScore, s.CriticalScore diff --git a/backend/app/migrator/wordpress.go b/backend/app/migrator/wordpress.go index 1bb224c3ac..8607de824b 100644 --- a/backend/app/migrator/wordpress.go +++ b/backend/app/migrator/wordpress.go @@ -16,7 +16,8 @@ const wpTimeLayout = "2006-01-02 15:04:05" // WordPress implements Importer from WP xml type WordPress struct { - DataStore Store + DataStore Store + DisableFancyTextFormatting bool } type wpItem struct { @@ -138,7 +139,7 @@ func (w *WordPress) convert(r io.Reader, siteID string) chan store.Comment { ParentID: comment.PID, Imported: true, } - commentsCh <- commentFormatter.Format(c) + commentsCh <- commentFormatter.Format(c, w.DisableFancyTextFormatting) stats.inpComments++ if stats.inpComments%1000 == 0 { log.Printf("[DEBUG] processed %d comments", stats.inpComments) diff --git a/backend/app/migrator/wordpress_test.go b/backend/app/migrator/wordpress_test.go index 79bb7226ac..2fd8e4006b 100644 --- a/backend/app/migrator/wordpress_test.go +++ b/backend/app/migrator/wordpress_test.go @@ -24,7 +24,7 @@ func TestWordPress_Import(t *testing.T) { dataStore := service.DataStore{Engine: b, AdminStore: admin.NewStaticStore("12345", nil, []string{}, "")} defer dataStore.Close() - wp := WordPress{DataStore: &dataStore} + wp := WordPress{DataStore: &dataStore, DisableFancyTextFormatting: false} size, err := wp.Import(strings.NewReader(xmlTestWP), siteID) assert.NoError(t, err) assert.Equal(t, 3, size) @@ -41,7 +41,7 @@ func TestWordPress_Import(t *testing.T) { assert.Equal(t, "e8b1e92bbcf5b9bb88472f9bdb82d1b8c7ed39d6", c.User.IP) ts, _ := time.Parse(wpTimeLayout, "2010-08-18 15:19:14") assert.Equal(t, ts, c.Timestamp) - assert.Equal(t, c.Text, "

Mekkatorque was over in that tent up to the right

\n") + assert.Equal(t, "

«Mekkatorque» was over in that tent up to the right

\n", c.Text) assert.True(t, c.Imported) posts, err := dataStore.List(siteID, 0, 0) @@ -54,6 +54,18 @@ func TestWordPress_Import(t *testing.T) { count, err := dataStore.Count(store.Locator{URL: "https://realmenweardress.es/2010/07/do-you-rp/", SiteID: siteID}) assert.NoError(t, err) assert.Equal(t, 3, count) + + // test with DisableFancyTextFormatting + wp = WordPress{DataStore: &dataStore, DisableFancyTextFormatting: true} + size, err = wp.Import(strings.NewReader(xmlTestWP), siteID) + assert.NoError(t, err) + assert.Equal(t, 3, size) + + last, err = dataStore.Last(siteID, 10, time.Time{}, adminUser) + assert.NoError(t, err) + require.Equal(t, 3, len(last), "3 comments imported") + + assert.Equal(t, "

"Mekkatorque" was over in that tent up to the right

\n", last[0].Text) } func TestWordPress_Convert(t *testing.T) { @@ -247,7 +259,7 @@ var xmlTestWP = ` - + 13 diff --git a/backend/app/rest/api/rest.go b/backend/app/rest/api/rest.go index ea18f180a7..40ef95cdc1 100644 --- a/backend/app/rest/api/rest.go +++ b/backend/app/rest/api/rest.go @@ -59,16 +59,17 @@ type Rest struct { Low int Critical int } - UpdateLimiter float64 - EmailNotifications bool - TelegramNotifications bool - EmojiEnabled bool - SimpleView bool - ProxyCORS bool - SendJWTHeader bool - AllowedAncestors []string // sets Content-Security-Policy "frame-ancestors ..." - SubscribersOnly bool - DisableSignature bool // prevent signature from being added to headers + UpdateLimiter float64 + EmailNotifications bool + TelegramNotifications bool + EmojiEnabled bool + SimpleView bool + ProxyCORS bool + SendJWTHeader bool + AllowedAncestors []string // sets Content-Security-Policy "frame-ancestors ..." + SubscribersOnly bool + DisableSignature bool // prevent signature from being added to headers + DisableFancyTextFormatting bool // disables SmartyPants in the comment text rendering of the posted comments SSLConfig SSLConfig httpsServer *http.Server @@ -369,16 +370,17 @@ func (s *Rest) controllerGroups() (public, private, admin, rss) { } privGrp := private{ - dataService: s.DataService, - cache: s.Cache, - imageService: s.ImageService, - commentFormatter: s.CommentFormatter, - readOnlyAge: s.ReadOnlyAge, - authenticator: s.Authenticator, - notifyService: s.NotifyService, - telegramService: s.TelegramService, - remarkURL: s.RemarkURL, - anonVote: s.AnonVote, + dataService: s.DataService, + cache: s.Cache, + imageService: s.ImageService, + commentFormatter: s.CommentFormatter, + readOnlyAge: s.ReadOnlyAge, + authenticator: s.Authenticator, + notifyService: s.NotifyService, + telegramService: s.TelegramService, + remarkURL: s.RemarkURL, + anonVote: s.AnonVote, + disableFancyTextFormatting: s.DisableFancyTextFormatting, } admGrp := admin{ diff --git a/backend/app/rest/api/rest_private.go b/backend/app/rest/api/rest_private.go index 1b97440642..4ef450f0e3 100644 --- a/backend/app/rest/api/rest_private.go +++ b/backend/app/rest/api/rest_private.go @@ -34,16 +34,17 @@ import ( ) type private struct { - dataService privStore - cache LoadingCache - readOnlyAge int - commentFormatter *store.CommentFormatter - imageService *image.Service - notifyService *notify.Service - authenticator *auth.Service - telegramService telegramService - remarkURL string - anonVote bool + dataService privStore + cache LoadingCache + readOnlyAge int + commentFormatter *store.CommentFormatter + imageService *image.Service + notifyService *notify.Service + authenticator *auth.Service + telegramService telegramService + remarkURL string + anonVote bool + disableFancyTextFormatting bool // disables SmartyPants in the comment text rendering of the posted comments } // telegramService is a subset of Telegram service used for setting up user telegram notifications @@ -88,7 +89,7 @@ func (s *private) previewCommentCtrl(w http.ResponseWriter, r *http.Request) { return } - comment = s.commentFormatter.Format(comment) + comment = s.commentFormatter.Format(comment, s.disableFancyTextFormatting) comment.Sanitize() // check if images are valid, omit proxied images as they are lazy-loaded @@ -128,7 +129,7 @@ func (s *private) createCommentCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment", rest.ErrCommentValidation) return } - comment = s.commentFormatter.Format(comment) + comment = s.commentFormatter.Format(comment, s.disableFancyTextFormatting) // check if images are valid, omit proxied images as they are lazy-loaded for _, id := range s.imageService.ExtractNonProxiedPictures(comment.Text) { @@ -212,7 +213,7 @@ func (s *private) updateCommentCtrl(w http.ResponseWriter, r *http.Request) { } editReq := service.EditRequest{ - Text: s.commentFormatter.FormatText(edit.Text), + Text: s.commentFormatter.FormatText(edit.Text, s.disableFancyTextFormatting), Orig: edit.Text, Summary: edit.Summary, Delete: edit.Delete, diff --git a/backend/app/rest/api/rest_private_test.go b/backend/app/rest/api/rest_private_test.go index 0b67034025..0be42560e4 100644 --- a/backend/app/rest/api/rest_private_test.go +++ b/backend/app/rest/api/rest_private_test.go @@ -368,6 +368,56 @@ func TestRest_CreateAndGet(t *testing.T) { assert.Equal(t, store.User{Name: "admin", ID: "admin", Admin: true, Blocked: false, IP: ""}, comment.User, "no ip") } +func TestRest_CreateWithQuotes(t *testing.T) { + ts, srv, teardown := startupT(t) + defer teardown() + + // create comment with quotes with smartypants + resp, err := post(t, ts.URL+"/api/v1/comment", + `{"text": "smartpants \"quoted\" text", "locator":{"url": "https://radio-t.com/blah1", "site": "remark42"}}`) + require.NoError(t, err) + require.Equal(t, http.StatusCreated, resp.StatusCode) + b, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + c := R.JSON{} + err = json.Unmarshal(b, &c) + assert.NoError(t, err) + id := c["id"].(string) + + // get created comment by id as non-admin + res, code := getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=remark42&url=https://radio-t.com/blah1", ts.URL, id)) + assert.Equal(t, http.StatusOK, code) + comment := store.Comment{} + err = json.Unmarshal([]byte(res), &comment) + assert.NoError(t, err) + assert.Equal(t, "

smartpants «quoted» text

\n", comment.Text) + assert.Equal(t, "smartpants \"quoted\" text", comment.Orig) + + // create comment with quotes without smartypants + srv.privRest.disableFancyTextFormatting = true + resp, err = post(t, ts.URL+"/api/v1/comment", + `{"text": "no_smartpants \"quoted\" text", "locator":{"url": "https://radio-t.com/blah1", "site": "remark42"}}`) + require.NoError(t, err) + require.Equal(t, http.StatusCreated, resp.StatusCode) + b, err = io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + c = R.JSON{} + err = json.Unmarshal(b, &c) + assert.NoError(t, err) + id = c["id"].(string) + + // get created comment by id as non-admin + res, code = getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=remark42&url=https://radio-t.com/blah1", ts.URL, id)) + assert.Equal(t, http.StatusOK, code) + comment = store.Comment{} + err = json.Unmarshal([]byte(res), &comment) + assert.NoError(t, err) + assert.Equal(t, "

no_smartpants "quoted" text

\n", comment.Text) + assert.Equal(t, "no_smartpants \"quoted\" text", comment.Orig) +} + func TestRest_Update(t *testing.T) { ts, _, teardown := startupT(t) defer teardown() diff --git a/backend/app/rest/api/rest_public_test.go b/backend/app/rest/api/rest_public_test.go index 57cdf29e37..8eef46ab9d 100644 --- a/backend/app/rest/api/rest_public_test.go +++ b/backend/app/rest/api/rest_public_test.go @@ -84,6 +84,24 @@ func TestRest_Preview(t *testing.T) { string(b), "/pics-remark42/staging/dev_user/62/bad_picture: no such file or directory\"}\n", ) + + // test quotes with and without smartypants + resp, err = post(t, ts.URL+"/api/v1/preview", `{"text": "\"quoted\" text", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + b, err = io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + assert.Equal(t, "

«quoted» text

\n", string(b)) + + srv.privRest.disableFancyTextFormatting = true + resp, err = post(t, ts.URL+"/api/v1/preview", `{"text": "\"quoted\" text", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + b, err = io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + assert.Equal(t, "

"quoted" text

\n", string(b)) } func TestRest_PreviewWithWrongImage(t *testing.T) { diff --git a/backend/app/store/formatter.go b/backend/app/store/formatter.go index 864a9b9fe5..6855ae4068 100644 --- a/backend/app/store/formatter.go +++ b/backend/app/store/formatter.go @@ -35,14 +35,16 @@ func NewCommentFormatter(converters ...CommentConverter) *CommentFormatter { } // Format comment fields -func (f *CommentFormatter) Format(c Comment) Comment { - c.Text = f.FormatText(c.Text) +func (f *CommentFormatter) Format(c Comment, raw bool) Comment { + c.Text = f.FormatText(c.Text, raw) return c } // FormatText converts text with markdown processor, applies external converters and shortens links -func (f *CommentFormatter) FormatText(txt string) (res string) { - mdExt, rend := GetMdExtensionsAndRenderer() +// +// raw=true disables SmartyPants for HTML rendering (replacement of quotes, dashes, fractions, etc). +func (f *CommentFormatter) FormatText(txt string, raw bool) (res string) { + mdExt, rend := GetMdExtensionsAndRenderer(raw) res = string(bf.Run([]byte(txt), bf.WithExtensions(mdExt), bf.WithRenderer(rend))) res = f.unEscape(res) @@ -120,14 +122,19 @@ func (f *CommentFormatter) lazyImage(commentHTML string) (resHTML string) { // GetMdExtensionsAndRenderer returns blackfriday extensions and renderer used for rendering markdown // within store module. -func GetMdExtensionsAndRenderer() (bf.Extensions, *bfchroma.Renderer) { +// +// raw=true disables SmartyPants for HTML rendering (replacement of quotes, dashes, fractions, etc). +func GetMdExtensionsAndRenderer(raw bool) (bf.Extensions, *bfchroma.Renderer) { mdExt := bf.NoIntraEmphasis | bf.Tables | bf.FencedCode | bf.Strikethrough | bf.SpaceHeadings | bf.HardLineBreak | bf.BackslashLineBreak | bf.Autolink - rend := bf.NewHTMLRenderer(bf.HTMLRendererParameters{ - Flags: bf.Smartypants | bf.SmartypantsFractions | bf.SmartypantsDashes | bf.SmartypantsAngledQuotes, - }) + flags := bf.HTMLFlags(0) + if !raw { + flags = bf.Smartypants | bf.SmartypantsFractions | bf.SmartypantsDashes | bf.SmartypantsAngledQuotes + } + + rend := bf.NewHTMLRenderer(bf.HTMLRendererParameters{Flags: flags}) extRend := bfchroma.NewRenderer(bfchroma.Extend(rend), bfchroma.ChromaOptions(html.WithClasses(true))) return mdExt, extRend diff --git a/backend/app/store/formatter_test.go b/backend/app/store/formatter_test.go index 28045e2a5a..0b954798c5 100644 --- a/backend/app/store/formatter_test.go +++ b/backend/app/store/formatter_test.go @@ -2,6 +2,7 @@ package store import ( "strconv" + "strings" "testing" "time" @@ -35,6 +36,12 @@ func TestFormatter_FormatText(t *testing.T) { "lazy image", }, {"— not translated #354", "

— not translated #354

\n!converted", "mdash"}, + {`no_smartpants "quoted" text`, "

no_smartpants "quoted" text

\n!converted", "normal quotes without smartpants"}, + {`"quoted" text`, "

«quoted» text

\n!converted", "normal quotes with smartpants"}, + {`no_smartpants “quoted” text`, "

no_smartpants “quoted” text

\n!converted", "curly quotes without smartpants"}, + {`“quoted” text`, "

“quoted” text

\n!converted", "curly quotes with smartpants"}, + {`no_smartpants «quoted» text`, "

no_smartpants «quoted» text

\n!converted", "French guillemets without smartpants"}, + {`«quoted» text`, "

«quoted» text

\n!converted", "French guillemets with smartpants"}, {"smth\n```go\nfunc main(aa string) int {return 0}\n```", `

smth

func main(aa string) int {return 0}
 
!converted`, "code with language"}, @@ -45,20 +52,22 @@ func TestFormatter_FormatText(t *testing.T) { for _, tt := range tbl { tt := tt t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.out, f.FormatText(tt.in)) + raw := strings.HasPrefix(tt.in, `no_smartpants`) + t.Logf("raw: %v", raw) + assert.Equal(t, tt.out, f.FormatText(tt.in, raw)) }) } } func TestFormatter_FormatTextNoConverter(t *testing.T) { f := NewCommentFormatter() - assert.Equal(t, "

12345

\n", f.FormatText("12345")) + assert.Equal(t, "

12345

\n", f.FormatText("12345", false)) } func TestFormatter_FormatTextConverterFunc(t *testing.T) { fn := CommentConverterFunc(func(text string) string { return "zz!" + text }) f := NewCommentFormatter(fn) - assert.Equal(t, "zz!

12345

\n", f.FormatText("12345")) + assert.Equal(t, "zz!

12345

\n", f.FormatText("12345", false)) } func TestFormatter_FormatComment(t *testing.T) { @@ -78,7 +87,7 @@ func TestFormatter_FormatComment(t *testing.T) { f := NewCommentFormatter(mockConverter{}) exp := comment exp.Text = "

blah

\n\n

xyz

\n!converted" - assert.Equal(t, exp, f.Format(comment)) + assert.Equal(t, exp, f.Format(comment, false)) } func TestFormatter_ShortenAutoLinks(t *testing.T) { diff --git a/backend/app/store/service/service.go b/backend/app/store/service/service.go index 964e9a307d..d811602b35 100644 --- a/backend/app/store/service/service.go +++ b/backend/app/store/service/service.go @@ -646,7 +646,9 @@ func (s *DataStore) ValidateComment(c *store.Comment) error { return fmt.Errorf("empty user info") } - mdExt, rend := store.GetMdExtensionsAndRenderer() + // for validation purposes it's not important if SmartyPants formatting is disabled or enabled, + // while for storing the comment that flag is set based on user preference + mdExt, rend := store.GetMdExtensionsAndRenderer(false) parser := bf.New(bf.WithRenderer(rend), bf.WithExtensions(bf.CommonExtensions), bf.WithExtensions(mdExt)) var wrongLinkError error parser.Parse([]byte(c.Orig)).Walk(func(node *bf.Node, entering bool) bf.WalkStatus { diff --git a/site/src/docs/configuration/parameters/index.md b/site/src/docs/configuration/parameters/index.md index b3577c12a5..12c8262ae4 100644 --- a/site/src/docs/configuration/parameters/index.md +++ b/site/src/docs/configuration/parameters/index.md @@ -159,6 +159,7 @@ services: | update-limit | UPDATE_LIMIT | `0.5` | updates/sec limit | | subscribers-only | SUBSCRIBERS_ONLY | `false` | enable commenting only for Patreon subscribers | | disable-signature | DISABLE_SIGNATURE | `false` | disable server signature in headers | +| disable-fancy-text-formatting | DISABLE_FANCY_HTML_FORMATTING | `false` | disable fancy comments text formatting (replacement of quotes, dashes, fractions, etc) | | admin-passwd | ADMIN_PASSWD | none (disabled) | password for `admin` basic auth | | dbg | DEBUG | `false` | debug mode |