Expose public/ API as submodule (#23345)
* model -> public/model * plugin -> public/plugin * public/model/utils -> public/utils * platform/shared/mlog -> public/shared/mlog * platform/shared/i18n -> public/shared/i18n * platform/shared/markdown -> public/shared/markdown * platform/services/timezones -> public/shared/timezones * channels/einterfaces -> einterfaces * expose public/ submodule * go mod tidy * .github: cache-dependency-path, setup-go-work * modules-tidy for public/ too * remove old gomodtidy
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
070ee26081
Коммит
bb02b35048
@@ -17,9 +17,9 @@ import (
|
||||
"github.com/aws/aws-sdk-go/service/marketplacemetering/marketplacemeteringiface"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type AwsMeter struct {
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin/plugintest/mock"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/plugin/plugintest/mock"
|
||||
)
|
||||
|
||||
type mockMarketplaceMeteringClient struct {
|
||||
|
||||
2
server/platform/services/cache/cache.go
поставляемый
2
server/platform/services/cache/cache.go
поставляемый
@@ -7,7 +7,7 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
// ErrKeyNotFound is the error when the given key is not found
|
||||
|
||||
2
server/platform/services/cache/lru.go
поставляемый
2
server/platform/services/cache/lru.go
поставляемый
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/tinylib/msgp/msgp"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
// LRU is a thread-safe fixed size LRU cache.
|
||||
|
||||
2
server/platform/services/cache/lru_striped.go
поставляемый
2
server/platform/services/cache/lru_striped.go
поставляемый
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
// LRUStriped keeps LRU caches in buckets in order to lower mutex contention.
|
||||
|
||||
2
server/platform/services/cache/lru_striped_test.go
поставляемый
2
server/platform/services/cache/lru_striped_test.go
поставляемый
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
func makeLRUPredictableTestData(num int) [][2]string {
|
||||
|
||||
2
server/platform/services/cache/lru_test.go
поставляемый
2
server/platform/services/cache/lru_test.go
поставляемый
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
func TestLRU(t *testing.T) {
|
||||
|
||||
2
server/platform/services/cache/provider.go
поставляемый
2
server/platform/services/cache/provider.go
поставляемый
@@ -6,7 +6,7 @@ package cache
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
// CacheOptions contains options for initializing a cache
|
||||
|
||||
2
server/platform/services/cache/provider_test.go
поставляемый
2
server/platform/services/cache/provider_test.go
поставляемый
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
func TestNewCache(t *testing.T) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package configservice
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
// An interface representing something that contains a Config, such as the app.App struct
|
||||
|
||||
@@ -6,7 +6,7 @@ package docextractor
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
type combineExtractor struct {
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/utils/testutils"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/httpservice"
|
||||
)
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/configservice"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/httpservice"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
)
|
||||
|
||||
var ErrNotEnabled = Error{errors.New("imageproxy.ImageProxy: image proxy not enabled")}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
var imageContentTypes = []string{
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/utils/testutils"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/httpservice"
|
||||
)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/httpservice"
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
// AcceptInvitation is called when accepting an invitation to connect with a remote cluster.
|
||||
|
||||
@@ -6,12 +6,12 @@ package remotecluster
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin/plugintest/mock"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/plugin/plugintest/mock"
|
||||
"github.com/mattermost/mattermost-server/server/v8/einterfaces"
|
||||
)
|
||||
|
||||
type mockServer struct {
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
// pingLoop periodically sends a ping to all remote clusters.
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wiggin77/merror"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -6,8 +6,8 @@ package remotecluster
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
// ReceiveIncomingMsg is called by the Rest API layer, or websocket layer (future), when a Remote Cluster
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wiggin77/merror"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/filestore"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type SendFileResultFunc func(us *model.UploadSession, rc *model.RemoteCluster, resp *Response, err error)
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
|
||||
"github.com/wiggin77/merror"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
type SendMsgResultFunc func(msg model.RemoteClusterMsg, rc *model.RemoteCluster, resp *Response, err error)
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
type SendProfileImageResultFunc func(userId string, rc *model.RemoteCluster, resp *Response, err error)
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/einterfaces"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
func TestService_AddTopicListener(t *testing.T) {
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"github.com/blevesearch/bleve/v2/analysis/analyzer/standard"
|
||||
"github.com/blevesearch/bleve/v2/mapping"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store/searchlayer"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store/searchtest"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store/sqlstore"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/testlib"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/searchengine"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ package bleveengine
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/searchengine"
|
||||
)
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/jobs"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/searchengine/bleveengine"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/jobs"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/utils/testutils"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/searchengine/bleveengine"
|
||||
)
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
"github.com/blevesearch/bleve/v2/search/query"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
const DeletePostsBatchSize = 500
|
||||
|
||||
@@ -6,7 +6,7 @@ package bleveengine
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
func createPost(userId string, channelId string) *model.Post {
|
||||
|
||||
@@ -6,7 +6,7 @@ package searchengine
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
type SearchEngineInterface interface {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
model "github.com/mattermost/mattermost-server/server/v8/model"
|
||||
model "github.com/mattermost/mattermost-server/server/public/model"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
time "time"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package searchengine
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
func NewBroker(cfg *model.Config) *Broker {
|
||||
|
||||
@@ -6,7 +6,7 @@ package searchengine
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/searchengine/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/remotecluster"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
)
|
||||
|
||||
// postsToAttachments returns the file attachments for a slice of posts that need to be synchronized.
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/remotecluster"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
)
|
||||
|
||||
// channelInviteMsg represents an invitation for a remote cluster to start sharing a channel.
|
||||
|
||||
@@ -13,10 +13,10 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin/plugintest/mock"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/plugin/plugintest/mock"
|
||||
)
|
||||
|
||||
func TestOnReceiveChannelInvite(t *testing.T) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
filestore "github.com/mattermost/mattermost-server/server/v8/platform/shared/filestore"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
model "github.com/mattermost/mattermost-server/server/v8/model"
|
||||
model "github.com/mattermost/mattermost-server/server/public/model"
|
||||
|
||||
request "github.com/mattermost/mattermost-server/server/v8/channels/app/request"
|
||||
)
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
package sharedchannel
|
||||
|
||||
import (
|
||||
mlog "github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
mlog "github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
model "github.com/mattermost/mattermost-server/server/v8/model"
|
||||
model "github.com/mattermost/mattermost-server/server/public/model"
|
||||
|
||||
remotecluster "github.com/mattermost/mattermost-server/server/v8/platform/services/remotecluster"
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ package sharedchannel
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
// syncMsg represents a change in content (post add/edit/delete, reaction add/remove, users).
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin/plugintest/mock"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/utils"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/plugin/plugintest/mock"
|
||||
)
|
||||
|
||||
func TestProcessPermalinkToRemote(t *testing.T) {
|
||||
|
||||
@@ -10,12 +10,12 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/remotecluster"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/filestore"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/remotecluster"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
)
|
||||
|
||||
func (scs *Service) onReceiveSyncMessage(msg model.RemoteClusterMsg, rc *model.RemoteCluster, response *remotecluster.Response) error {
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/remotecluster"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type syncTask struct {
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
|
||||
"github.com/wiggin77/merror"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/remotecluster"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type sendSyncMsgResultFunc func(syncResp SyncResponse, err error)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
// fixMention replaces any mentions in a post for the user with the user's real username.
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func slackConvertTimeStamp(ts string) int64 {
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func slackParseChannels(data io.Reader, channelType model.ChannelType) ([]slackChannel, error) {
|
||||
|
||||
@@ -18,12 +18,12 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/utils"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type slackChannel struct {
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
)
|
||||
|
||||
func TestSlackConvertTimeStamp(t *testing.T) {
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
httpservice "github.com/mattermost/mattermost-server/server/v8/platform/services/httpservice"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
model "github.com/mattermost/mattermost-server/server/v8/model"
|
||||
model "github.com/mattermost/mattermost-server/server/public/model"
|
||||
|
||||
plugin "github.com/mattermost/mattermost-server/server/v8/plugin"
|
||||
plugin "github.com/mattermost/mattermost-server/server/public/plugin"
|
||||
|
||||
product "github.com/mattermost/mattermost-server/server/v8/channels/product"
|
||||
)
|
||||
|
||||
@@ -14,15 +14,15 @@ import (
|
||||
|
||||
rudder "github.com/rudderlabs/analytics-go"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/product"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/utils"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/httpservice"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/marketplace"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/searchengine"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -21,16 +21,16 @@ import (
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin/plugintest"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/product"
|
||||
storeMocks "github.com/mattermost/mattermost-server/server/v8/channels/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost-server/server/v8/config"
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/httpservice"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/searchengine"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/services/telemetry/mocks"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/v8/plugin"
|
||||
"github.com/mattermost/mattermost-server/server/v8/plugin/plugintest"
|
||||
)
|
||||
|
||||
type FakeConfigService struct {
|
||||
|
||||
@@ -1,599 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package timezones
|
||||
|
||||
var DefaultSupportedTimezones = []string{
|
||||
"Africa/Abidjan",
|
||||
"Africa/Accra",
|
||||
"Africa/Addis_Ababa",
|
||||
"Africa/Algiers",
|
||||
"Africa/Asmara",
|
||||
"Africa/Asmera",
|
||||
"Africa/Bamako",
|
||||
"Africa/Bangui",
|
||||
"Africa/Banjul",
|
||||
"Africa/Bissau",
|
||||
"Africa/Blantyre",
|
||||
"Africa/Brazzaville",
|
||||
"Africa/Bujumbura",
|
||||
"Africa/Cairo",
|
||||
"Africa/Casablanca",
|
||||
"Africa/Ceuta",
|
||||
"Africa/Conakry",
|
||||
"Africa/Dakar",
|
||||
"Africa/Dar_es_Salaam",
|
||||
"Africa/Djibouti",
|
||||
"Africa/Douala",
|
||||
"Africa/El_Aaiun",
|
||||
"Africa/Freetown",
|
||||
"Africa/Gaborone",
|
||||
"Africa/Harare",
|
||||
"Africa/Johannesburg",
|
||||
"Africa/Juba",
|
||||
"Africa/Kampala",
|
||||
"Africa/Khartoum",
|
||||
"Africa/Kigali",
|
||||
"Africa/Kinshasa",
|
||||
"Africa/Lagos",
|
||||
"Africa/Libreville",
|
||||
"Africa/Lome",
|
||||
"Africa/Luanda",
|
||||
"Africa/Lubumbashi",
|
||||
"Africa/Lusaka",
|
||||
"Africa/Malabo",
|
||||
"Africa/Maputo",
|
||||
"Africa/Maseru",
|
||||
"Africa/Mbabane",
|
||||
"Africa/Mogadishu",
|
||||
"Africa/Monrovia",
|
||||
"Africa/Nairobi",
|
||||
"Africa/Ndjamena",
|
||||
"Africa/Niamey",
|
||||
"Africa/Nouakchott",
|
||||
"Africa/Ouagadougou",
|
||||
"Africa/Porto-Novo",
|
||||
"Africa/Sao_Tome",
|
||||
"Africa/Timbuktu",
|
||||
"Africa/Tripoli",
|
||||
"Africa/Tunis",
|
||||
"Africa/Windhoek",
|
||||
"America/Adak",
|
||||
"America/Anchorage",
|
||||
"America/Anguilla",
|
||||
"America/Antigua",
|
||||
"America/Araguaina",
|
||||
"America/Argentina/Buenos_Aires",
|
||||
"America/Argentina/Catamarca",
|
||||
"America/Argentina/ComodRivadavia",
|
||||
"America/Argentina/Cordoba",
|
||||
"America/Argentina/Jujuy",
|
||||
"America/Argentina/La_Rioja",
|
||||
"America/Argentina/Mendoza",
|
||||
"America/Argentina/Rio_Gallegos",
|
||||
"America/Argentina/Salta",
|
||||
"America/Argentina/San_Juan",
|
||||
"America/Argentina/San_Luis",
|
||||
"America/Argentina/Tucuman",
|
||||
"America/Argentina/Ushuaia",
|
||||
"America/Aruba",
|
||||
"America/Asuncion",
|
||||
"America/Atikokan",
|
||||
"America/Atka",
|
||||
"America/Bahia",
|
||||
"America/Bahia_Banderas",
|
||||
"America/Barbados",
|
||||
"America/Belem",
|
||||
"America/Belize",
|
||||
"America/Blanc-Sablon",
|
||||
"America/Boa_Vista",
|
||||
"America/Bogota",
|
||||
"America/Boise",
|
||||
"America/Buenos_Aires",
|
||||
"America/Cambridge_Bay",
|
||||
"America/Campo_Grande",
|
||||
"America/Cancun",
|
||||
"America/Caracas",
|
||||
"America/Catamarca",
|
||||
"America/Cayenne",
|
||||
"America/Cayman",
|
||||
"America/Chicago",
|
||||
"America/Chihuahua",
|
||||
"America/Coral_Harbour",
|
||||
"America/Cordoba",
|
||||
"America/Costa_Rica",
|
||||
"America/Creston",
|
||||
"America/Cuiaba",
|
||||
"America/Curacao",
|
||||
"America/Danmarkshavn",
|
||||
"America/Dawson",
|
||||
"America/Dawson_Creek",
|
||||
"America/Denver",
|
||||
"America/Detroit",
|
||||
"America/Dominica",
|
||||
"America/Edmonton",
|
||||
"America/Eirunepe",
|
||||
"America/El_Salvador",
|
||||
"America/Ensenada",
|
||||
"America/Fort_Nelson",
|
||||
"America/Fort_Wayne",
|
||||
"America/Fortaleza",
|
||||
"America/Glace_Bay",
|
||||
"America/Godthab",
|
||||
"America/Goose_Bay",
|
||||
"America/Grand_Turk",
|
||||
"America/Grenada",
|
||||
"America/Guadeloupe",
|
||||
"America/Guatemala",
|
||||
"America/Guayaquil",
|
||||
"America/Guyana",
|
||||
"America/Halifax",
|
||||
"America/Havana",
|
||||
"America/Hermosillo",
|
||||
"America/Indiana/Indianapolis",
|
||||
"America/Indiana/Knox",
|
||||
"America/Indiana/Marengo",
|
||||
"America/Indiana/Petersburg",
|
||||
"America/Indiana/Tell_City",
|
||||
"America/Indiana/Vevay",
|
||||
"America/Indiana/Vincennes",
|
||||
"America/Indiana/Winamac",
|
||||
"America/Indianapolis",
|
||||
"America/Inuvik",
|
||||
"America/Iqaluit",
|
||||
"America/Jamaica",
|
||||
"America/Jujuy",
|
||||
"America/Juneau",
|
||||
"America/Kentucky/Louisville",
|
||||
"America/Kentucky/Monticello",
|
||||
"America/Knox_IN",
|
||||
"America/Kralendijk",
|
||||
"America/La_Paz",
|
||||
"America/Lima",
|
||||
"America/Los_Angeles",
|
||||
"America/Louisville",
|
||||
"America/Lower_Princes",
|
||||
"America/Maceio",
|
||||
"America/Managua",
|
||||
"America/Manaus",
|
||||
"America/Marigot",
|
||||
"America/Martinique",
|
||||
"America/Matamoros",
|
||||
"America/Mazatlan",
|
||||
"America/Mendoza",
|
||||
"America/Menominee",
|
||||
"America/Merida",
|
||||
"America/Metlakatla",
|
||||
"America/Mexico_City",
|
||||
"America/Miquelon",
|
||||
"America/Moncton",
|
||||
"America/Monterrey",
|
||||
"America/Montevideo",
|
||||
"America/Montreal",
|
||||
"America/Montserrat",
|
||||
"America/Nassau",
|
||||
"America/New_York",
|
||||
"America/Nipigon",
|
||||
"America/Nome",
|
||||
"America/Noronha",
|
||||
"America/North_Dakota/Beulah",
|
||||
"America/North_Dakota/Center",
|
||||
"America/North_Dakota/New_Salem",
|
||||
"America/Ojinaga",
|
||||
"America/Panama",
|
||||
"America/Pangnirtung",
|
||||
"America/Paramaribo",
|
||||
"America/Phoenix",
|
||||
"America/Port-au-Prince",
|
||||
"America/Port_of_Spain",
|
||||
"America/Porto_Acre",
|
||||
"America/Porto_Velho",
|
||||
"America/Puerto_Rico",
|
||||
"America/Punta_Arenas",
|
||||
"America/Rainy_River",
|
||||
"America/Rankin_Inlet",
|
||||
"America/Recife",
|
||||
"America/Regina",
|
||||
"America/Resolute",
|
||||
"America/Rio_Branco",
|
||||
"America/Rosario",
|
||||
"America/Santa_Isabel",
|
||||
"America/Santarem",
|
||||
"America/Santiago",
|
||||
"America/Santo_Domingo",
|
||||
"America/Sao_Paulo",
|
||||
"America/Scoresbysund",
|
||||
"America/Shiprock",
|
||||
"America/Sitka",
|
||||
"America/St_Barthelemy",
|
||||
"America/St_Johns",
|
||||
"America/St_Kitts",
|
||||
"America/St_Lucia",
|
||||
"America/St_Thomas",
|
||||
"America/St_Vincent",
|
||||
"America/Swift_Current",
|
||||
"America/Tegucigalpa",
|
||||
"America/Thule",
|
||||
"America/Thunder_Bay",
|
||||
"America/Tijuana",
|
||||
"America/Toronto",
|
||||
"America/Tortola",
|
||||
"America/Vancouver",
|
||||
"America/Virgin",
|
||||
"America/Whitehorse",
|
||||
"America/Winnipeg",
|
||||
"America/Yakutat",
|
||||
"America/Yellowknife",
|
||||
"Antarctica/Casey",
|
||||
"Antarctica/Davis",
|
||||
"Antarctica/DumontDUrville",
|
||||
"Antarctica/Macquarie",
|
||||
"Antarctica/Mawson",
|
||||
"Antarctica/McMurdo",
|
||||
"Antarctica/Palmer",
|
||||
"Antarctica/Rothera",
|
||||
"Antarctica/South_Pole",
|
||||
"Antarctica/Syowa",
|
||||
"Antarctica/Troll",
|
||||
"Antarctica/Vostok",
|
||||
"Arctic/Longyearbyen",
|
||||
"Asia/Aden",
|
||||
"Asia/Almaty",
|
||||
"Asia/Amman",
|
||||
"Asia/Anadyr",
|
||||
"Asia/Aqtau",
|
||||
"Asia/Aqtobe",
|
||||
"Asia/Ashgabat",
|
||||
"Asia/Ashkhabad",
|
||||
"Asia/Atyrau",
|
||||
"Asia/Baghdad",
|
||||
"Asia/Bahrain",
|
||||
"Asia/Baku",
|
||||
"Asia/Bangkok",
|
||||
"Asia/Barnaul",
|
||||
"Asia/Beirut",
|
||||
"Asia/Bishkek",
|
||||
"Asia/Brunei",
|
||||
"Asia/Calcutta",
|
||||
"Asia/Chita",
|
||||
"Asia/Choibalsan",
|
||||
"Asia/Chongqing",
|
||||
"Asia/Chungking",
|
||||
"Asia/Colombo",
|
||||
"Asia/Dacca",
|
||||
"Asia/Damascus",
|
||||
"Asia/Dhaka",
|
||||
"Asia/Dili",
|
||||
"Asia/Dubai",
|
||||
"Asia/Dushanbe",
|
||||
"Asia/Famagusta",
|
||||
"Asia/Gaza",
|
||||
"Asia/Harbin",
|
||||
"Asia/Hebron",
|
||||
"Asia/Ho_Chi_Minh",
|
||||
"Asia/Hong_Kong",
|
||||
"Asia/Hovd",
|
||||
"Asia/Irkutsk",
|
||||
"Asia/Istanbul",
|
||||
"Asia/Jakarta",
|
||||
"Asia/Jayapura",
|
||||
"Asia/Jerusalem",
|
||||
"Asia/Kabul",
|
||||
"Asia/Kamchatka",
|
||||
"Asia/Karachi",
|
||||
"Asia/Kashgar",
|
||||
"Asia/Kathmandu",
|
||||
"Asia/Katmandu",
|
||||
"Asia/Khandyga",
|
||||
"Asia/Kolkata",
|
||||
"Asia/Krasnoyarsk",
|
||||
"Asia/Kuala_Lumpur",
|
||||
"Asia/Kuching",
|
||||
"Asia/Kuwait",
|
||||
"Asia/Macao",
|
||||
"Asia/Macau",
|
||||
"Asia/Magadan",
|
||||
"Asia/Makassar",
|
||||
"Asia/Manila",
|
||||
"Asia/Muscat",
|
||||
"Asia/Nicosia",
|
||||
"Asia/Novokuznetsk",
|
||||
"Asia/Novosibirsk",
|
||||
"Asia/Omsk",
|
||||
"Asia/Oral",
|
||||
"Asia/Phnom_Penh",
|
||||
"Asia/Pontianak",
|
||||
"Asia/Pyongyang",
|
||||
"Asia/Qatar",
|
||||
"Asia/Qyzylorda",
|
||||
"Asia/Rangoon",
|
||||
"Asia/Riyadh",
|
||||
"Asia/Saigon",
|
||||
"Asia/Sakhalin",
|
||||
"Asia/Samarkand",
|
||||
"Asia/Seoul",
|
||||
"Asia/Shanghai",
|
||||
"Asia/Singapore",
|
||||
"Asia/Srednekolymsk",
|
||||
"Asia/Taipei",
|
||||
"Asia/Tashkent",
|
||||
"Asia/Tbilisi",
|
||||
"Asia/Tehran",
|
||||
"Asia/Tel_Aviv",
|
||||
"Asia/Thimbu",
|
||||
"Asia/Thimphu",
|
||||
"Asia/Tokyo",
|
||||
"Asia/Tomsk",
|
||||
"Asia/Ujung_Pandang",
|
||||
"Asia/Ulaanbaatar",
|
||||
"Asia/Ulan_Bator",
|
||||
"Asia/Urumqi",
|
||||
"Asia/Ust-Nera",
|
||||
"Asia/Vientiane",
|
||||
"Asia/Vladivostok",
|
||||
"Asia/Yakutsk",
|
||||
"Asia/Yangon",
|
||||
"Asia/Yekaterinburg",
|
||||
"Asia/Yerevan",
|
||||
"Atlantic/Azores",
|
||||
"Atlantic/Bermuda",
|
||||
"Atlantic/Canary",
|
||||
"Atlantic/Cape_Verde",
|
||||
"Atlantic/Faeroe",
|
||||
"Atlantic/Faroe",
|
||||
"Atlantic/Jan_Mayen",
|
||||
"Atlantic/Madeira",
|
||||
"Atlantic/Reykjavik",
|
||||
"Atlantic/South_Georgia",
|
||||
"Atlantic/St_Helena",
|
||||
"Atlantic/Stanley",
|
||||
"Australia/ACT",
|
||||
"Australia/Adelaide",
|
||||
"Australia/Brisbane",
|
||||
"Australia/Broken_Hill",
|
||||
"Australia/Canberra",
|
||||
"Australia/Currie",
|
||||
"Australia/Darwin",
|
||||
"Australia/Eucla",
|
||||
"Australia/Hobart",
|
||||
"Australia/LHI",
|
||||
"Australia/Lindeman",
|
||||
"Australia/Lord_Howe",
|
||||
"Australia/Melbourne",
|
||||
"Australia/NSW",
|
||||
"Australia/North",
|
||||
"Australia/Perth",
|
||||
"Australia/Queensland",
|
||||
"Australia/South",
|
||||
"Australia/Sydney",
|
||||
"Australia/Tasmania",
|
||||
"Australia/Victoria",
|
||||
"Australia/West",
|
||||
"Australia/Yancowinna",
|
||||
"Brazil/Acre",
|
||||
"Brazil/DeNoronha",
|
||||
"Brazil/East",
|
||||
"Brazil/West",
|
||||
"CET",
|
||||
"CST6CDT",
|
||||
"Canada/Atlantic",
|
||||
"Canada/Central",
|
||||
"Canada/Eastern",
|
||||
"Canada/Mountain",
|
||||
"Canada/Newfoundland",
|
||||
"Canada/Pacific",
|
||||
"Canada/Saskatchewan",
|
||||
"Canada/Yukon",
|
||||
"Chile/Continental",
|
||||
"Chile/EasterIsland",
|
||||
"Cuba",
|
||||
"EET",
|
||||
"EST",
|
||||
"EST5EDT",
|
||||
"Egypt",
|
||||
"Eire",
|
||||
"Etc/GMT",
|
||||
"Etc/GMT+0",
|
||||
"Etc/GMT+1",
|
||||
"Etc/GMT+10",
|
||||
"Etc/GMT+11",
|
||||
"Etc/GMT+12",
|
||||
"Etc/GMT+2",
|
||||
"Etc/GMT+3",
|
||||
"Etc/GMT+4",
|
||||
"Etc/GMT+5",
|
||||
"Etc/GMT+6",
|
||||
"Etc/GMT+7",
|
||||
"Etc/GMT+8",
|
||||
"Etc/GMT+9",
|
||||
"Etc/GMT-0",
|
||||
"Etc/GMT-1",
|
||||
"Etc/GMT-10",
|
||||
"Etc/GMT-11",
|
||||
"Etc/GMT-12",
|
||||
"Etc/GMT-13",
|
||||
"Etc/GMT-14",
|
||||
"Etc/GMT-2",
|
||||
"Etc/GMT-3",
|
||||
"Etc/GMT-4",
|
||||
"Etc/GMT-5",
|
||||
"Etc/GMT-6",
|
||||
"Etc/GMT-7",
|
||||
"Etc/GMT-8",
|
||||
"Etc/GMT-9",
|
||||
"Etc/GMT0",
|
||||
"Etc/Greenwich",
|
||||
"Etc/UCT",
|
||||
"Etc/UTC",
|
||||
"Etc/Universal",
|
||||
"Etc/Zulu",
|
||||
"Europe/Amsterdam",
|
||||
"Europe/Andorra",
|
||||
"Europe/Astrakhan",
|
||||
"Europe/Athens",
|
||||
"Europe/Belfast",
|
||||
"Europe/Belgrade",
|
||||
"Europe/Berlin",
|
||||
"Europe/Bratislava",
|
||||
"Europe/Brussels",
|
||||
"Europe/Bucharest",
|
||||
"Europe/Budapest",
|
||||
"Europe/Busingen",
|
||||
"Europe/Chisinau",
|
||||
"Europe/Copenhagen",
|
||||
"Europe/Dublin",
|
||||
"Europe/Gibraltar",
|
||||
"Europe/Guernsey",
|
||||
"Europe/Helsinki",
|
||||
"Europe/Isle_of_Man",
|
||||
"Europe/Istanbul",
|
||||
"Europe/Jersey",
|
||||
"Europe/Kaliningrad",
|
||||
"Europe/Kiev",
|
||||
"Europe/Kirov",
|
||||
"Europe/Lisbon",
|
||||
"Europe/Ljubljana",
|
||||
"Europe/London",
|
||||
"Europe/Luxembourg",
|
||||
"Europe/Madrid",
|
||||
"Europe/Malta",
|
||||
"Europe/Mariehamn",
|
||||
"Europe/Minsk",
|
||||
"Europe/Monaco",
|
||||
"Europe/Moscow",
|
||||
"Europe/Nicosia",
|
||||
"Europe/Oslo",
|
||||
"Europe/Paris",
|
||||
"Europe/Podgorica",
|
||||
"Europe/Prague",
|
||||
"Europe/Riga",
|
||||
"Europe/Rome",
|
||||
"Europe/Samara",
|
||||
"Europe/San_Marino",
|
||||
"Europe/Sarajevo",
|
||||
"Europe/Saratov",
|
||||
"Europe/Simferopol",
|
||||
"Europe/Skopje",
|
||||
"Europe/Sofia",
|
||||
"Europe/Stockholm",
|
||||
"Europe/Tallinn",
|
||||
"Europe/Tirane",
|
||||
"Europe/Tiraspol",
|
||||
"Europe/Ulyanovsk",
|
||||
"Europe/Uzhgorod",
|
||||
"Europe/Vaduz",
|
||||
"Europe/Vatican",
|
||||
"Europe/Vienna",
|
||||
"Europe/Vilnius",
|
||||
"Europe/Volgograd",
|
||||
"Europe/Warsaw",
|
||||
"Europe/Zagreb",
|
||||
"Europe/Zaporozhye",
|
||||
"Europe/Zurich",
|
||||
"GB",
|
||||
"GB-Eire",
|
||||
"GMT",
|
||||
"GMT+0",
|
||||
"GMT-0",
|
||||
"GMT0",
|
||||
"Greenwich",
|
||||
"HST",
|
||||
"Hongkong",
|
||||
"Iceland",
|
||||
"Indian/Antananarivo",
|
||||
"Indian/Chagos",
|
||||
"Indian/Christmas",
|
||||
"Indian/Cocos",
|
||||
"Indian/Comoro",
|
||||
"Indian/Kerguelen",
|
||||
"Indian/Mahe",
|
||||
"Indian/Maldives",
|
||||
"Indian/Mauritius",
|
||||
"Indian/Mayotte",
|
||||
"Indian/Reunion",
|
||||
"Iran",
|
||||
"Israel",
|
||||
"Jamaica",
|
||||
"Japan",
|
||||
"Kwajalein",
|
||||
"Libya",
|
||||
"MET",
|
||||
"MST",
|
||||
"MST7MDT",
|
||||
"Mexico/BajaNorte",
|
||||
"Mexico/BajaSur",
|
||||
"Mexico/General",
|
||||
"NZ",
|
||||
"NZ-CHAT",
|
||||
"Navajo",
|
||||
"PRC",
|
||||
"PST8PDT",
|
||||
"Pacific/Apia",
|
||||
"Pacific/Auckland",
|
||||
"Pacific/Bougainville",
|
||||
"Pacific/Chatham",
|
||||
"Pacific/Chuuk",
|
||||
"Pacific/Easter",
|
||||
"Pacific/Efate",
|
||||
"Pacific/Enderbury",
|
||||
"Pacific/Fakaofo",
|
||||
"Pacific/Fiji",
|
||||
"Pacific/Funafuti",
|
||||
"Pacific/Galapagos",
|
||||
"Pacific/Gambier",
|
||||
"Pacific/Guadalcanal",
|
||||
"Pacific/Guam",
|
||||
"Pacific/Honolulu",
|
||||
"Pacific/Johnston",
|
||||
"Pacific/Kiritimati",
|
||||
"Pacific/Kosrae",
|
||||
"Pacific/Kwajalein",
|
||||
"Pacific/Majuro",
|
||||
"Pacific/Marquesas",
|
||||
"Pacific/Midway",
|
||||
"Pacific/Nauru",
|
||||
"Pacific/Niue",
|
||||
"Pacific/Norfolk",
|
||||
"Pacific/Noumea",
|
||||
"Pacific/Pago_Pago",
|
||||
"Pacific/Palau",
|
||||
"Pacific/Pitcairn",
|
||||
"Pacific/Pohnpei",
|
||||
"Pacific/Ponape",
|
||||
"Pacific/Port_Moresby",
|
||||
"Pacific/Rarotonga",
|
||||
"Pacific/Saipan",
|
||||
"Pacific/Samoa",
|
||||
"Pacific/Tahiti",
|
||||
"Pacific/Tarawa",
|
||||
"Pacific/Tongatapu",
|
||||
"Pacific/Truk",
|
||||
"Pacific/Wake",
|
||||
"Pacific/Wallis",
|
||||
"Pacific/Yap",
|
||||
"Poland",
|
||||
"Portugal",
|
||||
"ROC",
|
||||
"ROK",
|
||||
"Singapore",
|
||||
"Turkey",
|
||||
"UCT",
|
||||
"US/Alaska",
|
||||
"US/Aleutian",
|
||||
"US/Arizona",
|
||||
"US/Central",
|
||||
"US/East-Indiana",
|
||||
"US/Eastern",
|
||||
"US/Hawaii",
|
||||
"US/Indiana-Starke",
|
||||
"US/Michigan",
|
||||
"US/Mountain",
|
||||
"US/Pacific",
|
||||
"US/Pacific-New",
|
||||
"US/Samoa",
|
||||
"UTC",
|
||||
"Universal",
|
||||
"W-SU",
|
||||
"WET",
|
||||
"Zulu",
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package timezones
|
||||
|
||||
type Timezones struct {
|
||||
supportedZones []string
|
||||
}
|
||||
|
||||
func New() *Timezones {
|
||||
timezones := Timezones{}
|
||||
|
||||
timezones.supportedZones = DefaultSupportedTimezones
|
||||
|
||||
return &timezones
|
||||
}
|
||||
|
||||
func (t *Timezones) GetSupported() []string {
|
||||
return t.supportedZones
|
||||
}
|
||||
|
||||
func DefaultUserTimezone() map[string]string {
|
||||
defaultTimezone := make(map[string]string)
|
||||
defaultTimezone["useAutomaticTimezone"] = "true"
|
||||
defaultTimezone["automaticTimezone"] = ""
|
||||
defaultTimezone["manualTimezone"] = ""
|
||||
|
||||
return defaultTimezone
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package timezones
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTimezoneConfig(t *testing.T) {
|
||||
tz1 := New()
|
||||
assert.NotEmpty(t, tz1.GetSupported())
|
||||
}
|
||||
|
||||
func TestDefaultUserTimezone(t *testing.T) {
|
||||
defaultTimezone := DefaultUserTimezone()
|
||||
require.Equal(t, "true", defaultTimezone["useAutomaticTimezone"])
|
||||
require.Empty(t, defaultTimezone["automaticTimezone"])
|
||||
require.Empty(t, defaultTimezone["manualTimezone"])
|
||||
|
||||
defaultTimezone["useAutomaticTimezone"] = "false"
|
||||
defaultTimezone["automaticTimezone"] = "EST"
|
||||
defaultTimezone["manualTimezone"] = "AST"
|
||||
|
||||
defaultTimezone2 := DefaultUserTimezone()
|
||||
require.Equal(t, "true", defaultTimezone2["useAutomaticTimezone"])
|
||||
require.Empty(t, defaultTimezone2["automaticTimezone"])
|
||||
require.Empty(t, defaultTimezone2["manualTimezone"])
|
||||
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/uber/jaeger-client-go/zipkin"
|
||||
"github.com/uber/jaeger-lib/metrics"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
// Tracer is a wrapper around Jaeger OpenTracing client, used to properly de-initialize jaeger on exit
|
||||
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/openpgp" //nolint:staticcheck
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
//go:embed pubkey.gpg
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
func TestCanIUpgradeToE0(t *testing.T) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/plugin"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin"
|
||||
)
|
||||
|
||||
// Conn is a DB driver conn implementation
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/plugin"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/plugin"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin"
|
||||
)
|
||||
|
||||
type wrapperTx struct {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/xtgo/uuid"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func randomString() string {
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"github.com/minio/minio-go/v7/pkg/encrypt"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
// S3FileBackend contains all necessary information to communicate with
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
s3 "github.com/minio/minio-go/v7"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/go-i18n/i18n"
|
||||
"github.com/mattermost/go-i18n/i18n/bundle"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
)
|
||||
|
||||
const defaultLocale = "en"
|
||||
|
||||
// TranslateFunc is the type of the translate functions
|
||||
type TranslateFunc func(translationID string, args ...any) string
|
||||
|
||||
// TranslationFuncByLocal is the type of function that takes local as a string and returns the translation function
|
||||
type TranslationFuncByLocal func(locale string) TranslateFunc
|
||||
|
||||
// T is the translate function using the default server language as fallback language
|
||||
var T TranslateFunc
|
||||
|
||||
// TDefault is the translate function using english as fallback language
|
||||
var TDefault TranslateFunc
|
||||
|
||||
var locales map[string]string = make(map[string]string)
|
||||
var defaultServerLocale string
|
||||
var defaultClientLocale string
|
||||
|
||||
// TranslationsPreInit loads translations from filesystem if they are not
|
||||
// loaded already and assigns english while loading server config
|
||||
func TranslationsPreInit(translationsDir string) error {
|
||||
if T != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set T even if we fail to load the translations. Lots of shutdown handling code will
|
||||
// segfault trying to handle the error, and the untranslated IDs are strictly better.
|
||||
T = tfuncWithFallback(defaultLocale)
|
||||
TDefault = tfuncWithFallback(defaultLocale)
|
||||
|
||||
return initTranslationsWithDir(translationsDir)
|
||||
}
|
||||
|
||||
// InitTranslations set the defaults configured in the server and initialize
|
||||
// the T function using the server default as fallback language
|
||||
func InitTranslations(serverLocale, clientLocale string) error {
|
||||
defaultServerLocale = serverLocale
|
||||
defaultClientLocale = clientLocale
|
||||
|
||||
var err error
|
||||
T, err = getTranslationsBySystemLocale()
|
||||
return err
|
||||
}
|
||||
|
||||
func initTranslationsWithDir(dir string) error {
|
||||
files, _ := os.ReadDir(dir)
|
||||
for _, f := range files {
|
||||
if filepath.Ext(f.Name()) == ".json" {
|
||||
filename := f.Name()
|
||||
locales[strings.Split(filename, ".")[0]] = filepath.Join(dir, filename)
|
||||
|
||||
if err := i18n.LoadTranslationFile(filepath.Join(dir, filename)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTranslationFuncForDir loads translations from the filesystem into a new instance of the bundle.
|
||||
// It returns a function to access loaded translations.
|
||||
func GetTranslationFuncForDir(dir string) (TranslationFuncByLocal, error) {
|
||||
var availableLocals map[string]string = make(map[string]string)
|
||||
bundle := bundle.New()
|
||||
files, _ := os.ReadDir(dir)
|
||||
for _, f := range files {
|
||||
if filepath.Ext(f.Name()) != ".json" {
|
||||
continue
|
||||
}
|
||||
|
||||
filename := f.Name()
|
||||
availableLocals[strings.Split(filename, ".")[0]] = filepath.Join(dir, filename)
|
||||
if err := bundle.LoadTranslationFile(filepath.Join(dir, filename)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return func(locale string) TranslateFunc {
|
||||
if _, ok := availableLocals[locale]; !ok {
|
||||
locale = defaultLocale
|
||||
}
|
||||
|
||||
t, _ := bundle.Tfunc(locale)
|
||||
return func(translationID string, args ...any) string {
|
||||
if translated := t(translationID, args...); translated != translationID {
|
||||
return translated
|
||||
}
|
||||
|
||||
t, _ := bundle.Tfunc(defaultLocale)
|
||||
return t(translationID, args...)
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getTranslationsBySystemLocale() (TranslateFunc, error) {
|
||||
locale := defaultServerLocale
|
||||
if _, ok := locales[locale]; !ok {
|
||||
mlog.Warn("Failed to load system translations for", mlog.String("locale", locale), mlog.String("attempting to fall back to default locale", defaultLocale))
|
||||
locale = defaultLocale
|
||||
}
|
||||
|
||||
if locales[locale] == "" {
|
||||
return nil, fmt.Errorf("failed to load system translations for '%v'", defaultLocale)
|
||||
}
|
||||
|
||||
translations := tfuncWithFallback(locale)
|
||||
if translations == nil {
|
||||
return nil, fmt.Errorf("failed to load system translations")
|
||||
}
|
||||
|
||||
mlog.Info("Loaded system translations", mlog.String("for locale", locale), mlog.String("from locale", locales[locale]))
|
||||
return translations, nil
|
||||
}
|
||||
|
||||
// GetUserTranslations get the translation function for an specific locale
|
||||
func GetUserTranslations(locale string) TranslateFunc {
|
||||
if _, ok := locales[locale]; !ok {
|
||||
locale = defaultLocale
|
||||
}
|
||||
|
||||
translations := tfuncWithFallback(locale)
|
||||
return translations
|
||||
}
|
||||
|
||||
// GetTranslationsAndLocaleFromRequest return the translation function and the
|
||||
// locale based on a request headers
|
||||
func GetTranslationsAndLocaleFromRequest(r *http.Request) (TranslateFunc, string) {
|
||||
// This is for checking against locales like pt_BR or zn_CN
|
||||
headerLocaleFull := strings.Split(r.Header.Get("Accept-Language"), ",")[0]
|
||||
// This is for checking against locales like en, es
|
||||
headerLocale := strings.Split(strings.Split(r.Header.Get("Accept-Language"), ",")[0], "-")[0]
|
||||
defaultLocale := defaultClientLocale
|
||||
if locales[headerLocaleFull] != "" {
|
||||
translations := tfuncWithFallback(headerLocaleFull)
|
||||
return translations, headerLocaleFull
|
||||
} else if locales[headerLocale] != "" {
|
||||
translations := tfuncWithFallback(headerLocale)
|
||||
return translations, headerLocale
|
||||
} else if locales[defaultLocale] != "" {
|
||||
translations := tfuncWithFallback(defaultLocale)
|
||||
return translations, headerLocale
|
||||
}
|
||||
|
||||
translations := tfuncWithFallback(defaultLocale)
|
||||
return translations, defaultLocale
|
||||
}
|
||||
|
||||
// GetSupportedLocales return a map of locale code and the file path with the
|
||||
// translations
|
||||
func GetSupportedLocales() map[string]string {
|
||||
return locales
|
||||
}
|
||||
|
||||
func tfuncWithFallback(pref string) TranslateFunc {
|
||||
t, _ := i18n.Tfunc(pref)
|
||||
return func(translationID string, args ...any) string {
|
||||
if translated := t(translationID, args...); translated != translationID {
|
||||
return translated
|
||||
}
|
||||
|
||||
t, _ := i18n.Tfunc(defaultLocale)
|
||||
return t(translationID, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// TranslateAsHTML translates the translationID provided and return a
|
||||
// template.HTML object
|
||||
func TranslateAsHTML(t TranslateFunc, translationID string, args map[string]any) template.HTML {
|
||||
message := t(translationID, escapeForHTML(args))
|
||||
message = strings.Replace(message, "[[", "<strong>", -1)
|
||||
message = strings.Replace(message, "]]", "</strong>", -1)
|
||||
return template.HTML(message)
|
||||
}
|
||||
|
||||
func escapeForHTML(arg any) any {
|
||||
switch typedArg := arg.(type) {
|
||||
case string:
|
||||
return template.HTMLEscapeString(typedArg)
|
||||
case *string:
|
||||
return template.HTMLEscapeString(*typedArg)
|
||||
case map[string]any:
|
||||
safeArg := make(map[string]any, len(typedArg))
|
||||
for key, value := range typedArg {
|
||||
safeArg[key] = escapeForHTML(value)
|
||||
}
|
||||
return safeArg
|
||||
default:
|
||||
mlog.Warn(
|
||||
"Unable to escape value for HTML template",
|
||||
mlog.Any("html_template", arg),
|
||||
mlog.String("template_type", reflect.ValueOf(arg).Type().String()),
|
||||
)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// IdentityTfunc returns a translation function that don't translate, only
|
||||
// returns the same id
|
||||
func IdentityTfunc() TranslateFunc {
|
||||
return func(translationID string, args ...any) string {
|
||||
return translationID
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/go-i18n/i18n/bundle"
|
||||
"github.com/mattermost/go-i18n/i18n/language"
|
||||
"github.com/mattermost/go-i18n/i18n/translation"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var htmlTestTranslationBundle *bundle.Bundle
|
||||
|
||||
func init() {
|
||||
htmlTestTranslationBundle = bundle.New()
|
||||
fooBold, _ := translation.NewTranslation(map[string]any{
|
||||
"id": "foo.bold",
|
||||
"translation": "<p>[[{{ .Foo }}]]</p>",
|
||||
})
|
||||
htmlTestTranslationBundle.AddTranslation(&language.Language{Tag: "en"}, fooBold)
|
||||
}
|
||||
|
||||
func TestTranslateAsHTML(t *testing.T) {
|
||||
assert.EqualValues(t, "<p><strong><i>foo</i></strong></p>", TranslateAsHTML(TranslateFunc(htmlTestTranslationBundle.MustTfunc("en")), "foo.bold", map[string]any{
|
||||
"Foo": "<i>foo</i>",
|
||||
}))
|
||||
}
|
||||
|
||||
func TestEscapeForHTML(t *testing.T) {
|
||||
stringForPointer := "<b>abc</b>"
|
||||
for name, tc := range map[string]struct {
|
||||
In any
|
||||
Expected any
|
||||
}{
|
||||
"NoHTML": {
|
||||
In: "abc",
|
||||
Expected: "abc",
|
||||
},
|
||||
"String": {
|
||||
In: "<b>abc</b>",
|
||||
Expected: "<b>abc</b>",
|
||||
},
|
||||
"StringPointer": {
|
||||
In: &stringForPointer,
|
||||
Expected: "<b>abc</b>",
|
||||
},
|
||||
"Map": {
|
||||
In: map[string]any{
|
||||
"abc": "abc",
|
||||
"123": "<b>123</b>",
|
||||
},
|
||||
Expected: map[string]any{
|
||||
"abc": "abc",
|
||||
"123": "<b>123</b>",
|
||||
},
|
||||
},
|
||||
"Unsupported": {
|
||||
In: struct{ string }{"<b>abc</b>"},
|
||||
Expected: "",
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.Expected, escapeForHTML(tc.In))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
gomail "gopkg.in/mail.v2"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Based off of extensions/autolink.c from https://github.com/github/cmark
|
||||
|
||||
var (
|
||||
DefaultURLSchemes = []string{"http", "https", "ftp", "mailto", "tel"}
|
||||
wwwAutoLinkRegex = regexp.MustCompile(`^www\d{0,3}\.`)
|
||||
)
|
||||
|
||||
// Given a string with a w at the given position, tries to parse and return a range containing a www link.
|
||||
// if one exists. If the text at the given position isn't a link, returns an empty string. Equivalent to
|
||||
// www_match from the reference code.
|
||||
func parseWWWAutolink(data string, position int) (Range, bool) {
|
||||
// Check that this isn't part of another word
|
||||
if position > 1 {
|
||||
prevChar := data[position-1]
|
||||
|
||||
if !isWhitespaceByte(prevChar) && !isAllowedBeforeWWWLink(prevChar) {
|
||||
return Range{}, false
|
||||
}
|
||||
}
|
||||
|
||||
// Check that this starts with www
|
||||
if len(data)-position < 4 || !wwwAutoLinkRegex.MatchString(data[position:]) {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
end := checkDomain(data[position:], false)
|
||||
if end == 0 {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
end += position
|
||||
|
||||
// Grab all text until the end of the string or the next whitespace character
|
||||
for end < len(data) && !isWhitespaceByte(data[end]) {
|
||||
end += 1
|
||||
}
|
||||
|
||||
// Trim trailing punctuation
|
||||
end = trimTrailingCharactersFromLink(data, position, end)
|
||||
if position == end {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
return Range{position, end}, true
|
||||
}
|
||||
|
||||
func isAllowedBeforeWWWLink(c byte) bool {
|
||||
switch c {
|
||||
case '*', '_', '~', ')':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Given a string with a : at the given position, tried to parse and return a range containing a URL scheme
|
||||
// if one exists. If the text around the given position isn't a link, returns an empty string. Equivalent to
|
||||
// url_match from the reference code.
|
||||
func parseURLAutolink(data string, position int) (Range, bool) {
|
||||
// Check that a :// exists. This doesn't match the clients that treat the slashes as optional.
|
||||
if len(data)-position < 4 || data[position+1] != '/' || data[position+2] != '/' {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
start := position - 1
|
||||
for start > 0 && isAlphanumericByte(data[start-1]) {
|
||||
start -= 1
|
||||
}
|
||||
|
||||
if start < 0 || position >= len(data) {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
// Ensure that the URL scheme is allowed and that at least one character after the scheme is valid.
|
||||
scheme := data[start:position]
|
||||
if !isSchemeAllowed(scheme) || !isValidHostCharacter(data[position+3:]) {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
end := checkDomain(data[position+3:], true)
|
||||
if end == 0 {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
end += position
|
||||
|
||||
// Grab all text until the end of the string or the next whitespace character
|
||||
for end < len(data) && !isWhitespaceByte(data[end]) {
|
||||
end += 1
|
||||
}
|
||||
|
||||
// Trim trailing punctuation
|
||||
end = trimTrailingCharactersFromLink(data, start, end)
|
||||
if start == end {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
return Range{start, end}, true
|
||||
}
|
||||
|
||||
func isSchemeAllowed(scheme string) bool {
|
||||
// Note that this doesn't support the custom URL schemes implemented by the client
|
||||
for _, allowed := range DefaultURLSchemes {
|
||||
if strings.EqualFold(allowed, scheme) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Given a string starting with a URL, returns the number of valid characters that make up the URL's domain.
|
||||
// Returns 0 if the string doesn't start with a domain name. allowShort determines whether or not the domain
|
||||
// needs to contain a period to be considered valid. Equivalent to check_domain from the reference code.
|
||||
func checkDomain(data string, allowShort bool) int {
|
||||
foundUnderscore := false
|
||||
foundPeriod := false
|
||||
|
||||
i := 1
|
||||
for ; i < len(data)-1; i++ {
|
||||
if data[i] == '_' {
|
||||
foundUnderscore = true
|
||||
break
|
||||
} else if data[i] == '.' {
|
||||
foundPeriod = true
|
||||
} else if !isValidHostCharacter(data[i:]) && data[i] != '-' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundUnderscore {
|
||||
return 0
|
||||
}
|
||||
|
||||
if allowShort {
|
||||
// If allowShort is set, accept any string of valid domain characters
|
||||
return i
|
||||
}
|
||||
|
||||
// If allowShort isn't set, a valid domain just requires at least a single period. Note that this
|
||||
// logic isn't entirely necessary because we already know the string starts with "www." when
|
||||
// this is called from parseWWWAutolink
|
||||
if foundPeriod {
|
||||
return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Returns true if the provided link starts with a valid character for a domain name. Equivalent to
|
||||
// is_valid_hostchar from the reference code.
|
||||
func isValidHostCharacter(link string) bool {
|
||||
c, _ := utf8.DecodeRuneInString(link)
|
||||
if c == utf8.RuneError {
|
||||
return false
|
||||
}
|
||||
|
||||
return !unicode.IsSpace(c) && !unicode.IsPunct(c)
|
||||
}
|
||||
|
||||
// Removes any trailing characters such as punctuation or stray brackets that shouldn't be part of the link.
|
||||
// Returns a new end position for the link. Equivalent to autolink_delim from the reference code.
|
||||
func trimTrailingCharactersFromLink(markdown string, start int, end int) int {
|
||||
runes := []rune(markdown[start:end])
|
||||
linkEnd := len(runes)
|
||||
|
||||
// Cut off the link before an open angle bracket if it contains one
|
||||
for i, c := range runes {
|
||||
if c == '<' {
|
||||
linkEnd = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for linkEnd > 0 {
|
||||
c := runes[linkEnd-1]
|
||||
|
||||
if !canEndAutolink(c) {
|
||||
// Trim trailing quotes, periods, etc
|
||||
linkEnd = linkEnd - 1
|
||||
} else if c == ';' {
|
||||
// Trim a trailing HTML entity
|
||||
newEnd := linkEnd - 2
|
||||
|
||||
for newEnd > 0 && ((runes[newEnd] >= 'a' && runes[newEnd] <= 'z') || (runes[newEnd] >= 'A' && runes[newEnd] <= 'Z')) {
|
||||
newEnd -= 1
|
||||
}
|
||||
|
||||
if newEnd < linkEnd-2 && runes[newEnd] == '&' {
|
||||
linkEnd = newEnd
|
||||
} else {
|
||||
// This isn't actually an HTML entity, so just trim the semicolon
|
||||
linkEnd = linkEnd - 1
|
||||
}
|
||||
} else if c == ')' {
|
||||
// Only allow an autolink ending with a bracket if that bracket is part of a matching pair of brackets.
|
||||
// If there are more closing brackets than opening ones, remove the extra bracket
|
||||
|
||||
numClosing := 0
|
||||
numOpening := 0
|
||||
|
||||
// Examples (input text => output linked portion):
|
||||
//
|
||||
// http://www.pokemon.com/Pikachu_(Electric)
|
||||
// => http://www.pokemon.com/Pikachu_(Electric)
|
||||
//
|
||||
// http://www.pokemon.com/Pikachu_((Electric)
|
||||
// => http://www.pokemon.com/Pikachu_((Electric)
|
||||
//
|
||||
// http://www.pokemon.com/Pikachu_(Electric))
|
||||
// => http://www.pokemon.com/Pikachu_(Electric)
|
||||
//
|
||||
// http://www.pokemon.com/Pikachu_((Electric))
|
||||
// => http://www.pokemon.com/Pikachu_((Electric))
|
||||
|
||||
for i := 0; i < linkEnd; i++ {
|
||||
if runes[i] == '(' {
|
||||
numOpening += 1
|
||||
} else if runes[i] == ')' {
|
||||
numClosing += 1
|
||||
}
|
||||
}
|
||||
|
||||
if numClosing <= numOpening {
|
||||
// There's fewer or equal closing brackets, so we've found the end of the link
|
||||
break
|
||||
}
|
||||
|
||||
linkEnd -= 1
|
||||
} else {
|
||||
// There's no special characters at the end of the link, so we're at the end
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return start + len(string(runes[:linkEnd]))
|
||||
}
|
||||
|
||||
func canEndAutolink(c rune) bool {
|
||||
switch c {
|
||||
case '?', '!', '.', ',', ':', '*', '_', '~', '\'', '"':
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,701 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseURLAutolink(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Description string
|
||||
Input string
|
||||
Position int
|
||||
Expected string
|
||||
}{
|
||||
{
|
||||
Description: "no link",
|
||||
Input: "This is an :emoji:",
|
||||
Position: 11,
|
||||
Expected: "",
|
||||
},
|
||||
{
|
||||
Description: "no link 2",
|
||||
Input: "These are two things: apple and orange",
|
||||
Position: 20,
|
||||
Expected: "",
|
||||
},
|
||||
{
|
||||
Description: "link with http",
|
||||
Input: "http://example.com and some text",
|
||||
Position: 4,
|
||||
Expected: "http://example.com",
|
||||
},
|
||||
{
|
||||
Description: "link with https",
|
||||
Input: "https://example.com and some text",
|
||||
Position: 5,
|
||||
Expected: "https://example.com",
|
||||
},
|
||||
{
|
||||
Description: "link with ftp",
|
||||
Input: "ftp://example.com and some text",
|
||||
Position: 3,
|
||||
Expected: "ftp://example.com",
|
||||
},
|
||||
{
|
||||
Description: "link with a path",
|
||||
Input: "https://example.com/abcd and some text",
|
||||
Position: 5,
|
||||
Expected: "https://example.com/abcd",
|
||||
},
|
||||
{
|
||||
Description: "link with parameters",
|
||||
Input: "ftp://example.com/abcd?foo=bar and some text",
|
||||
Position: 3,
|
||||
Expected: "ftp://example.com/abcd?foo=bar",
|
||||
},
|
||||
{
|
||||
Description: "link, not at start",
|
||||
Input: "This is https://example.com and some text",
|
||||
Position: 13,
|
||||
Expected: "https://example.com",
|
||||
},
|
||||
{
|
||||
Description: "link with a path, not at start",
|
||||
Input: "This is also http://www.example.com/abcd and some text",
|
||||
Position: 17,
|
||||
Expected: "http://www.example.com/abcd",
|
||||
},
|
||||
{
|
||||
Description: "link with parameters, not at start",
|
||||
Input: "These are https://www.example.com/abcd?foo=bar and some text",
|
||||
Position: 15,
|
||||
Expected: "https://www.example.com/abcd?foo=bar",
|
||||
},
|
||||
{
|
||||
Description: "link with trailing characters",
|
||||
Input: "This is ftp://www.example.com??",
|
||||
Position: 11,
|
||||
Expected: "ftp://www.example.com",
|
||||
},
|
||||
{
|
||||
Description: "multiple links",
|
||||
Input: "This is https://example.com/abcd and ftp://www.example.com/1234",
|
||||
Position: 13,
|
||||
Expected: "https://example.com/abcd",
|
||||
},
|
||||
{
|
||||
Description: "second of multiple links",
|
||||
Input: "This is https://example.com/abcd and ftp://www.example.com/1234",
|
||||
Position: 40,
|
||||
Expected: "ftp://www.example.com/1234",
|
||||
},
|
||||
{
|
||||
Description: "link with brackets",
|
||||
Input: "Go to ftp://www.example.com/my/page_(disambiguation) and some text",
|
||||
Position: 9,
|
||||
Expected: "ftp://www.example.com/my/page_(disambiguation)",
|
||||
},
|
||||
{
|
||||
Description: "link in brackets",
|
||||
Input: "(https://www.example.com/foo/bar)",
|
||||
Position: 6,
|
||||
Expected: "https://www.example.com/foo/bar",
|
||||
},
|
||||
{
|
||||
Description: "link in underscores",
|
||||
Input: "_http://www.example.com_",
|
||||
Position: 5,
|
||||
Expected: "http://www.example.com",
|
||||
},
|
||||
{
|
||||
Description: "link in asterisks",
|
||||
Input: "This is **ftp://example.com**",
|
||||
Position: 13,
|
||||
Expected: "ftp://example.com",
|
||||
},
|
||||
{
|
||||
Description: "link in strikethrough",
|
||||
Input: "Those were ~~https://example.com~~",
|
||||
Position: 18,
|
||||
Expected: "https://example.com",
|
||||
},
|
||||
{
|
||||
Description: "link with angle brackets",
|
||||
Input: "<b>We use http://example.com</b>",
|
||||
Position: 14,
|
||||
Expected: "http://example.com",
|
||||
},
|
||||
{
|
||||
Description: "bad link protocol",
|
||||
Input: "://///",
|
||||
Position: 0,
|
||||
Expected: "",
|
||||
},
|
||||
{
|
||||
Description: "position greater than input length",
|
||||
Input: "there is no colon",
|
||||
Position: 1000,
|
||||
Expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.Description, func(t *testing.T) {
|
||||
rawRange, ok := parseURLAutolink(testCase.Input, testCase.Position)
|
||||
|
||||
if testCase.Expected == "" {
|
||||
assert.False(t, ok)
|
||||
assert.Equal(t, Range{0, 0}, rawRange)
|
||||
} else {
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, testCase.Expected, testCase.Input[rawRange.Position:rawRange.End])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseWWWAutolink(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Description string
|
||||
Input string
|
||||
Position int
|
||||
Expected string
|
||||
}{
|
||||
{
|
||||
Description: "no link",
|
||||
Input: "This is some text",
|
||||
Position: 0,
|
||||
Expected: "",
|
||||
},
|
||||
{
|
||||
Description: "link",
|
||||
Input: "www.example.com and some text",
|
||||
Position: 0,
|
||||
Expected: "www.example.com",
|
||||
},
|
||||
{
|
||||
Description: "link with a path",
|
||||
Input: "www.example.com/abcd and some text",
|
||||
Position: 0,
|
||||
Expected: "www.example.com/abcd",
|
||||
},
|
||||
{
|
||||
Description: "link with parameters",
|
||||
Input: "www.example.com/abcd?foo=bar and some text",
|
||||
Position: 0,
|
||||
Expected: "www.example.com/abcd?foo=bar",
|
||||
},
|
||||
{
|
||||
Description: "link, not at start",
|
||||
Input: "This is www.example.com and some text",
|
||||
Position: 8,
|
||||
Expected: "www.example.com",
|
||||
},
|
||||
{
|
||||
Description: "link with a path, not at start",
|
||||
Input: "This is also www.example.com/abcd and some text",
|
||||
Position: 13,
|
||||
Expected: "www.example.com/abcd",
|
||||
},
|
||||
{
|
||||
Description: "link with parameters, not at start",
|
||||
Input: "These are www.example.com/abcd?foo=bar and some text",
|
||||
Position: 10,
|
||||
Expected: "www.example.com/abcd?foo=bar",
|
||||
},
|
||||
{
|
||||
Description: "link with trailing characters",
|
||||
Input: "This is www.example.com??",
|
||||
Position: 8,
|
||||
Expected: "www.example.com",
|
||||
},
|
||||
{
|
||||
Description: "link after current position",
|
||||
Input: "This is some text and www.example.com",
|
||||
Position: 0,
|
||||
Expected: "",
|
||||
},
|
||||
{
|
||||
Description: "multiple links",
|
||||
Input: "This is www.example.com/abcd and www.example.com/1234",
|
||||
Position: 8,
|
||||
Expected: "www.example.com/abcd",
|
||||
},
|
||||
{
|
||||
Description: "multiple links 2",
|
||||
Input: "This is www.example.com/abcd and www.example.com/1234",
|
||||
Position: 33,
|
||||
Expected: "www.example.com/1234",
|
||||
},
|
||||
{
|
||||
Description: "link with brackets",
|
||||
Input: "Go to www.example.com/my/page_(disambiguation) and some text",
|
||||
Position: 6,
|
||||
Expected: "www.example.com/my/page_(disambiguation)",
|
||||
},
|
||||
{
|
||||
Description: "link following other letters",
|
||||
Input: "aaawww.example.com and some text",
|
||||
Position: 3,
|
||||
Expected: "",
|
||||
},
|
||||
{
|
||||
Description: "link in brackets",
|
||||
Input: "(www.example.com)",
|
||||
Position: 1,
|
||||
Expected: "www.example.com",
|
||||
},
|
||||
{
|
||||
Description: "link in underscores",
|
||||
Input: "_www.example.com_",
|
||||
Position: 1,
|
||||
Expected: "www.example.com",
|
||||
},
|
||||
{
|
||||
Description: "link in asterisks",
|
||||
Input: "This is **www.example.com**",
|
||||
Position: 10,
|
||||
Expected: "www.example.com",
|
||||
},
|
||||
{
|
||||
Description: "link in strikethrough",
|
||||
Input: "Those were ~~www.example.com~~",
|
||||
Position: 13,
|
||||
Expected: "www.example.com",
|
||||
},
|
||||
{
|
||||
Description: "using www1",
|
||||
Input: "Our backup site is at www1.example.com/foo",
|
||||
Position: 22,
|
||||
Expected: "www1.example.com/foo",
|
||||
},
|
||||
{
|
||||
Description: "link with angle brackets",
|
||||
Input: "<b>We use www2.example.com</b>",
|
||||
Position: 10,
|
||||
Expected: "www2.example.com",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.Description, func(t *testing.T) {
|
||||
rawRange, ok := parseWWWAutolink(testCase.Input, testCase.Position)
|
||||
|
||||
if testCase.Expected == "" {
|
||||
assert.False(t, ok)
|
||||
assert.Equal(t, Range{0, 0}, rawRange)
|
||||
} else {
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, testCase.Expected, testCase.Input[rawRange.Position:rawRange.End])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrimTrailingCharactersFromLink(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Input string
|
||||
Start int
|
||||
End int
|
||||
ExpectedEnd int
|
||||
}{
|
||||
{
|
||||
Input: "http://www.example.com",
|
||||
ExpectedEnd: 22,
|
||||
},
|
||||
{
|
||||
Input: "http://www.example.com/abcd",
|
||||
ExpectedEnd: 27,
|
||||
},
|
||||
{
|
||||
Input: "http://www.example.com/abcd/",
|
||||
ExpectedEnd: 28,
|
||||
},
|
||||
{
|
||||
Input: "http://www.example.com/1234",
|
||||
ExpectedEnd: 27,
|
||||
},
|
||||
{
|
||||
Input: "http://www.example.com/abcd?foo=bar",
|
||||
ExpectedEnd: 35,
|
||||
},
|
||||
{
|
||||
Input: "http://www.example.com/abcd#heading",
|
||||
ExpectedEnd: 35,
|
||||
},
|
||||
{
|
||||
Input: "http://www.example.com.",
|
||||
ExpectedEnd: 22,
|
||||
},
|
||||
{
|
||||
Input: "http://www.example.com,",
|
||||
ExpectedEnd: 22,
|
||||
},
|
||||
{
|
||||
Input: "http://www.example.com?",
|
||||
ExpectedEnd: 22,
|
||||
},
|
||||
{
|
||||
Input: "http://www.example.com)",
|
||||
ExpectedEnd: 22,
|
||||
},
|
||||
{
|
||||
Input: "http://www.example.com",
|
||||
ExpectedEnd: 22,
|
||||
},
|
||||
{
|
||||
Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)",
|
||||
ExpectedEnd: 54,
|
||||
},
|
||||
{
|
||||
Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation",
|
||||
ExpectedEnd: 53,
|
||||
},
|
||||
{
|
||||
Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation))",
|
||||
ExpectedEnd: 54,
|
||||
},
|
||||
{
|
||||
Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)_(disambiguation)",
|
||||
ExpectedEnd: 71,
|
||||
},
|
||||
{
|
||||
Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation_(disambiguation))",
|
||||
ExpectedEnd: 71,
|
||||
},
|
||||
{
|
||||
Input: "http://www.example.com"",
|
||||
ExpectedEnd: 22,
|
||||
},
|
||||
{
|
||||
Input: "this is a sentence containing http://www.example.com in it",
|
||||
Start: 30,
|
||||
End: 52,
|
||||
ExpectedEnd: 52,
|
||||
},
|
||||
{
|
||||
Input: "this is a sentence containing http://www.example.com???",
|
||||
Start: 30,
|
||||
End: 55,
|
||||
ExpectedEnd: 52,
|
||||
},
|
||||
{
|
||||
Input: "http://google.com/å",
|
||||
ExpectedEnd: len("http://google.com/å"),
|
||||
},
|
||||
{
|
||||
Input: "http://google.com/å...",
|
||||
ExpectedEnd: len("http://google.com/å"),
|
||||
},
|
||||
{
|
||||
Input: "This is http://google.com/å, a link, and http://google.com/å",
|
||||
Start: 8,
|
||||
End: len("This is http://google.com/å,"),
|
||||
ExpectedEnd: len("This is http://google.com/å"),
|
||||
},
|
||||
{
|
||||
Input: "This is http://google.com/å, a link, and http://google.com/å",
|
||||
Start: 41,
|
||||
End: len("This is http://google.com/å, a link, and http://google.com/å"),
|
||||
ExpectedEnd: len("This is http://google.com/å, a link, and http://google.com/å"),
|
||||
},
|
||||
{
|
||||
Input: "This is http://google.com/å, a link, and http://google.com/å.",
|
||||
Start: 41,
|
||||
End: len("This is http://google.com/å, a link, and http://google.com/å."),
|
||||
ExpectedEnd: len("This is http://google.com/å, a link, and http://google.com/å"),
|
||||
},
|
||||
{
|
||||
Input: "http://🍄.ga/ http://x🍄.ga/",
|
||||
Start: 0,
|
||||
End: len("http://🍄.ga/"),
|
||||
ExpectedEnd: len("http://🍄.ga/"),
|
||||
},
|
||||
{
|
||||
Input: "http://🍄.ga/ http://x🍄.ga/",
|
||||
Start: len("http://🍄.ga/ "),
|
||||
End: len("http://🍄.ga/ http://x🍄.ga/"),
|
||||
ExpectedEnd: len("http://🍄.ga/ http://x🍄.ga/"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.Input, func(t *testing.T) {
|
||||
if testCase.End == 0 {
|
||||
testCase.End = len(testCase.Input) - testCase.Start
|
||||
}
|
||||
|
||||
assert.Equal(t, testCase.ExpectedEnd, trimTrailingCharactersFromLink(testCase.Input, testCase.Start, testCase.End))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutolinking(t *testing.T) {
|
||||
// These tests are adapted from https://github.com/mattermost/commonmark.js/test/mattermost.txt.
|
||||
// It is missing tests for:
|
||||
// 1. Links surrounded by emphasis (emphasis not implemented on the server)
|
||||
// 2. IPv6 addresses (not implemented on the server or by GitHub)
|
||||
// 3. Custom URL schemes (not implemented)
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
Markdown string
|
||||
ExpectedHTML string
|
||||
}{
|
||||
"valid-link-1": {
|
||||
Markdown: `http://example.com`,
|
||||
ExpectedHTML: `<p><a href="http://example.com">http://example.com</a></p>`,
|
||||
},
|
||||
"valid-link-2": {
|
||||
Markdown: `https://example.com`,
|
||||
ExpectedHTML: `<p><a href="https://example.com">https://example.com</a></p>`,
|
||||
},
|
||||
"valid-link-3": {
|
||||
Markdown: `ftp://example.com`,
|
||||
ExpectedHTML: `<p><a href="ftp://example.com">ftp://example.com</a></p>`,
|
||||
},
|
||||
// "valid-link-4": {
|
||||
// Markdown: `ts3server://example.com?port=9000`,
|
||||
// ExpectedHTML: `<p><a href="ts3server://example.com?port=9000">ts3server://example.com?port=9000</a></p>`,
|
||||
// },
|
||||
"valid-link-5": {
|
||||
Markdown: `www.example.com`,
|
||||
ExpectedHTML: `<p><a href="http://www.example.com">www.example.com</a></p>`,
|
||||
},
|
||||
"valid-link-6": {
|
||||
Markdown: `www.example.com/index`,
|
||||
ExpectedHTML: `<p><a href="http://www.example.com/index">www.example.com/index</a></p>`,
|
||||
},
|
||||
"valid-link-7": {
|
||||
Markdown: `www.example.com/index.html`,
|
||||
ExpectedHTML: `<p><a href="http://www.example.com/index.html">www.example.com/index.html</a></p>`,
|
||||
},
|
||||
"valid-link-8": {
|
||||
Markdown: `http://example.com/index/sub`,
|
||||
ExpectedHTML: `<p><a href="http://example.com/index/sub">http://example.com/index/sub</a></p>`,
|
||||
},
|
||||
"valid-link-9": {
|
||||
Markdown: `www1.example.com`,
|
||||
ExpectedHTML: `<p><a href="http://www1.example.com">www1.example.com</a></p>`,
|
||||
},
|
||||
"valid-link-10": {
|
||||
Markdown: `https://en.wikipedia.org/wiki/URLs#Syntax`,
|
||||
ExpectedHTML: `<p><a href="https://en.wikipedia.org/wiki/URLs#Syntax">https://en.wikipedia.org/wiki/URLs#Syntax</a></p>`,
|
||||
},
|
||||
"valid-link-11": {
|
||||
Markdown: `https://groups.google.com/forum/#!msg`,
|
||||
ExpectedHTML: `<p><a href="https://groups.google.com/forum/#!msg">https://groups.google.com/forum/#!msg</a></p>`,
|
||||
},
|
||||
"valid-link-12": {
|
||||
Markdown: `www.example.com/index?params=1`,
|
||||
ExpectedHTML: `<p><a href="http://www.example.com/index?params=1">www.example.com/index?params=1</a></p>`,
|
||||
},
|
||||
"valid-link-13": {
|
||||
Markdown: `www.example.com/index?params=1&other=2`,
|
||||
ExpectedHTML: `<p><a href="http://www.example.com/index?params=1&other=2">www.example.com/index?params=1&other=2</a></p>`,
|
||||
},
|
||||
"valid-link-14": {
|
||||
Markdown: `www.example.com/index?params=1;other=2`,
|
||||
ExpectedHTML: `<p><a href="http://www.example.com/index?params=1;other=2">www.example.com/index?params=1;other=2</a></p>`,
|
||||
},
|
||||
"valid-link-15": {
|
||||
Markdown: `http://www.example.com/_/page`,
|
||||
ExpectedHTML: `<p><a href="http://www.example.com/_/page">http://www.example.com/_/page</a></p>`,
|
||||
},
|
||||
"valid-link-16": {
|
||||
Markdown: `https://en.wikipedia.org/wiki/🐬`,
|
||||
ExpectedHTML: `<p><a href="https://en.wikipedia.org/wiki/%F0%9F%90%AC">https://en.wikipedia.org/wiki/🐬</a></p>`,
|
||||
},
|
||||
"valid-link-17": {
|
||||
Markdown: `http://✪df.ws/1234`,
|
||||
ExpectedHTML: `<p><a href="http://%E2%9C%AAdf.ws/1234">http://✪df.ws/1234</a></p>`,
|
||||
},
|
||||
"valid-link-18": {
|
||||
Markdown: `https://groups.google.com/forum/#!msg`,
|
||||
ExpectedHTML: `<p><a href="https://groups.google.com/forum/#!msg">https://groups.google.com/forum/#!msg</a></p>`,
|
||||
},
|
||||
"valid-link-19": {
|
||||
Markdown: `https://пример.срб/пример-26/`,
|
||||
ExpectedHTML: `<p><a href="https://%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80.%D1%81%D1%80%D0%B1/%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80-26/">https://пример.срб/пример-26/</a></p>`,
|
||||
},
|
||||
"valid-link-20": {
|
||||
Markdown: `mailto://test@example.com`,
|
||||
ExpectedHTML: `<p><a href="mailto://test@example.com">mailto://test@example.com</a></p>`,
|
||||
},
|
||||
"valid-link-21": {
|
||||
Markdown: `tel://555-123-4567`,
|
||||
ExpectedHTML: `<p><a href="tel://555-123-4567">tel://555-123-4567</a></p>`,
|
||||
},
|
||||
|
||||
"ip-address-1": {
|
||||
Markdown: `http://127.0.0.1`,
|
||||
ExpectedHTML: `<p><a href="http://127.0.0.1">http://127.0.0.1</a></p>`,
|
||||
},
|
||||
"ip-address-2": {
|
||||
Markdown: `http://192.168.1.1:4040`,
|
||||
ExpectedHTML: `<p><a href="http://192.168.1.1:4040">http://192.168.1.1:4040</a></p>`,
|
||||
},
|
||||
"ip-address-3": {
|
||||
Markdown: `http://username:password@127.0.0.1`,
|
||||
ExpectedHTML: `<p><a href="http://username:password@127.0.0.1">http://username:password@127.0.0.1</a></p>`,
|
||||
},
|
||||
"ip-address-4": {
|
||||
Markdown: `http://username:password@[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80`,
|
||||
ExpectedHTML: `<p><a href="http://username:password@%5B2001:0:5ef5:79fb:303a:62d5:3312:ff42%5D:80">http://username:password@[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80</a></p>`,
|
||||
},
|
||||
|
||||
"link-with-brackets-1": {
|
||||
Markdown: `https://en.wikipedia.org/wiki/Rendering_(computer_graphics)`,
|
||||
ExpectedHTML: `<p><a href="https://en.wikipedia.org/wiki/Rendering_(computer_graphics)">https://en.wikipedia.org/wiki/Rendering_(computer_graphics)</a></p>`,
|
||||
},
|
||||
"link-with-brackets-2": {
|
||||
Markdown: `http://example.com/more_(than)_one_(parens)`,
|
||||
ExpectedHTML: `<p><a href="http://example.com/more_(than)_one_(parens)">http://example.com/more_(than)_one_(parens)</a></p>`,
|
||||
},
|
||||
"link-with-brackets-3": {
|
||||
Markdown: `http://example.com/(something)?after=parens`,
|
||||
ExpectedHTML: `<p><a href="http://example.com/(something)?after=parens">http://example.com/(something)?after=parens</a></p>`,
|
||||
},
|
||||
"link-with-brackets-4": {
|
||||
Markdown: `http://foo.com/unicode_(✪)_in_parens`,
|
||||
ExpectedHTML: `<p><a href="http://foo.com/unicode_(%E2%9C%AA)_in_parens">http://foo.com/unicode_(✪)_in_parens</a></p>`,
|
||||
},
|
||||
|
||||
"inside-another-link-1": {
|
||||
Markdown: `[www.example.com](https://example.com)`,
|
||||
ExpectedHTML: `<p><a href="https://example.com">www.example.com</a></p>`,
|
||||
},
|
||||
"inside-another-link-2": {
|
||||
Markdown: `[http://www.example.com](https://example.com)`,
|
||||
ExpectedHTML: `<p><a href="https://example.com">http://www.example.com</a></p>`,
|
||||
},
|
||||
|
||||
"link-in-sentence-1": {
|
||||
Markdown: `(http://example.com)`,
|
||||
ExpectedHTML: `<p>(<a href="http://example.com">http://example.com</a>)</p>`,
|
||||
},
|
||||
"link-in-sentence-2": {
|
||||
Markdown: `(see http://example.com)`,
|
||||
ExpectedHTML: `<p>(see <a href="http://example.com">http://example.com</a>)</p>`,
|
||||
},
|
||||
"link-in-sentence-3": {
|
||||
Markdown: `(http://example.com watch this)`,
|
||||
ExpectedHTML: `<p>(<a href="http://example.com">http://example.com</a> watch this)</p>`,
|
||||
},
|
||||
"link-in-sentence-4": {
|
||||
Markdown: `This is a sentence with a http://example.com in it.`,
|
||||
ExpectedHTML: `<p>This is a sentence with a <a href="http://example.com">http://example.com</a> in it.</p>`,
|
||||
},
|
||||
"link-in-sentence-5": {
|
||||
Markdown: `This is a sentence with a [link](http://example.com) in it.`,
|
||||
ExpectedHTML: `<p>This is a sentence with a <a href="http://example.com">link</a> in it.</p>`,
|
||||
},
|
||||
"link-in-sentence-6": {
|
||||
Markdown: `This is a sentence with a http://example.com/_/underscore in it.`,
|
||||
ExpectedHTML: `<p>This is a sentence with a <a href="http://example.com/_/underscore">http://example.com/_/underscore</a> in it.</p>`,
|
||||
},
|
||||
"link-in-sentence-7": {
|
||||
Markdown: `This is a sentence with a link (http://example.com) in it.`,
|
||||
ExpectedHTML: `<p>This is a sentence with a link (<a href="http://example.com">http://example.com</a>) in it.</p>`,
|
||||
},
|
||||
"link-in-sentence-8": {
|
||||
Markdown: `This is a sentence with a (https://en.wikipedia.org/wiki/Rendering_(computer_graphics)) in it.`,
|
||||
ExpectedHTML: `<p>This is a sentence with a (<a href="https://en.wikipedia.org/wiki/Rendering_(computer_graphics)">https://en.wikipedia.org/wiki/Rendering_(computer_graphics)</a>) in it.</p>`,
|
||||
},
|
||||
"link-in-sentence-9": {
|
||||
Markdown: `This is a sentence with a http://192.168.1.1:4040 in it.`,
|
||||
ExpectedHTML: `<p>This is a sentence with a <a href="http://192.168.1.1:4040">http://192.168.1.1:4040</a> in it.</p>`,
|
||||
},
|
||||
"link-in-sentence-10": {
|
||||
Markdown: `This is a link to http://example.com.`,
|
||||
ExpectedHTML: `<p>This is a link to <a href="http://example.com">http://example.com</a>.</p>`,
|
||||
},
|
||||
"link-in-sentence-11": {
|
||||
Markdown: `This is a link to http://example.com*`,
|
||||
ExpectedHTML: `<p>This is a link to <a href="http://example.com">http://example.com</a>*</p>`,
|
||||
},
|
||||
"link-in-sentence-12": {
|
||||
Markdown: `This is a link to http://example.com_`,
|
||||
ExpectedHTML: `<p>This is a link to <a href="http://example.com">http://example.com</a>_</p>`,
|
||||
},
|
||||
"link-in-sentence-13": {
|
||||
Markdown: `This is a link containing http://example.com/something?with,commas,in,url, but not at the end`,
|
||||
ExpectedHTML: `<p>This is a link containing <a href="http://example.com/something?with,commas,in,url">http://example.com/something?with,commas,in,url</a>, but not at the end</p>`,
|
||||
},
|
||||
"link-in-sentence-14": {
|
||||
Markdown: `This is a question about a link http://example.com?`,
|
||||
ExpectedHTML: `<p>This is a question about a link <a href="http://example.com">http://example.com</a>?</p>`,
|
||||
},
|
||||
|
||||
"plt-7250-link-with-trailing-periods-1": {
|
||||
Markdown: `http://example.com.`,
|
||||
ExpectedHTML: `<p><a href="http://example.com">http://example.com</a>.</p>`,
|
||||
},
|
||||
"plt-7250-link-with-trailing-periods-2": {
|
||||
Markdown: `http://example.com...`,
|
||||
ExpectedHTML: `<p><a href="http://example.com">http://example.com</a>...</p>`,
|
||||
},
|
||||
"plt-7250-link-with-trailing-periods-3": {
|
||||
Markdown: `http://example.com/foo.`,
|
||||
ExpectedHTML: `<p><a href="http://example.com/foo">http://example.com/foo</a>.</p>`,
|
||||
},
|
||||
"plt-7250-link-with-trailing-periods-4": {
|
||||
Markdown: `http://example.com/foo...`,
|
||||
ExpectedHTML: `<p><a href="http://example.com/foo">http://example.com/foo</a>...</p>`,
|
||||
},
|
||||
"plt-7250-link-with-trailing-periods-5": {
|
||||
Markdown: `http://example.com/foo.bar`,
|
||||
ExpectedHTML: `<p><a href="http://example.com/foo.bar">http://example.com/foo.bar</a></p>`,
|
||||
},
|
||||
"plt-7250-link-with-trailing-periods-6": {
|
||||
Markdown: `http://example.com/foo...bar`,
|
||||
ExpectedHTML: `<p><a href="http://example.com/foo...bar">http://example.com/foo...bar</a></p>`,
|
||||
},
|
||||
|
||||
"rn-319-www-link-as-part-of-word-1": {
|
||||
Markdown: `testwww.example.com`,
|
||||
ExpectedHTML: `<p>testwww.example.com</p>`,
|
||||
},
|
||||
|
||||
"mm-10180-link-containing-period-followed-by-non-letter-1": {
|
||||
Markdown: `https://example.com/123.+Pagetitle`,
|
||||
ExpectedHTML: `<p><a href="https://example.com/123.+Pagetitle">https://example.com/123.+Pagetitle</a></p>`,
|
||||
},
|
||||
"mm-10180-link-containing-period-followed-by-non-letter-2": {
|
||||
Markdown: `https://example.com/123.?Pagetitle`,
|
||||
ExpectedHTML: `<p><a href="https://example.com/123.?Pagetitle">https://example.com/123.?Pagetitle</a></p>`,
|
||||
},
|
||||
"mm-10180-link-containing-period-followed-by-non-letter-3": {
|
||||
Markdown: `https://example.com/123.-Pagetitle`,
|
||||
ExpectedHTML: `<p><a href="https://example.com/123.-Pagetitle">https://example.com/123.-Pagetitle</a></p>`,
|
||||
},
|
||||
"mm-10180-link-containing-period-followed-by-non-letter-4": {
|
||||
Markdown: `https://example.com/123._Pagetitle`,
|
||||
ExpectedHTML: `<p><a href="https://example.com/123._Pagetitle">https://example.com/123._Pagetitle</a></p>`,
|
||||
},
|
||||
"mm-10180-link-containing-period-followed-by-non-letter-5": {
|
||||
Markdown: `https://example.com/123.+`,
|
||||
ExpectedHTML: `<p><a href="https://example.com/123.+">https://example.com/123.+</a></p>`,
|
||||
},
|
||||
"mm-10180-link-containing-period-followed-by-non-letter-6": {
|
||||
Markdown: `https://example.com/123.?`,
|
||||
ExpectedHTML: `<p><a href="https://example.com/123">https://example.com/123</a>.?</p>`,
|
||||
},
|
||||
"mm-10180-link-containing-period-followed-by-non-letter-7": {
|
||||
Markdown: `https://example.com/123.-`,
|
||||
ExpectedHTML: `<p><a href="https://example.com/123.-">https://example.com/123.-</a></p>`,
|
||||
},
|
||||
"mm-10180-link-containing-period-followed-by-non-letter-8": {
|
||||
Markdown: `https://example.com/123._`,
|
||||
ExpectedHTML: `<p><a href="https://example.com/123">https://example.com/123</a>._</p>`,
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.ExpectedHTML, RenderHTML(tc.Markdown))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
type BlockQuote struct {
|
||||
blockBase
|
||||
markdown string
|
||||
|
||||
Children []Block
|
||||
}
|
||||
|
||||
func (b *BlockQuote) Continuation(indentation int, r Range) *continuation {
|
||||
if indentation > 3 {
|
||||
return nil
|
||||
}
|
||||
s := b.markdown[r.Position:r.End]
|
||||
if s == "" || s[0] != '>' {
|
||||
return nil
|
||||
}
|
||||
remaining := Range{r.Position + 1, r.End}
|
||||
indentation, indentationBytes := countIndentation(b.markdown, remaining)
|
||||
if indentation > 0 {
|
||||
indentation--
|
||||
}
|
||||
return &continuation{
|
||||
Indentation: indentation,
|
||||
Remaining: Range{remaining.Position + indentationBytes, remaining.End},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BlockQuote) AddChild(openBlocks []Block) []Block {
|
||||
b.Children = append(b.Children, openBlocks[0])
|
||||
return openBlocks
|
||||
}
|
||||
|
||||
func blockQuoteStart(markdown string, indent int, r Range) []Block {
|
||||
if indent > 3 {
|
||||
return nil
|
||||
}
|
||||
s := markdown[r.Position:r.End]
|
||||
if s == "" || s[0] != '>' {
|
||||
return nil
|
||||
}
|
||||
|
||||
block := &BlockQuote{
|
||||
markdown: markdown,
|
||||
}
|
||||
r.Position++
|
||||
if len(s) > 1 && s[1] == ' ' {
|
||||
r.Position++
|
||||
}
|
||||
|
||||
indent, bytes := countIndentation(markdown, r)
|
||||
|
||||
ret := []Block{block}
|
||||
if descendants := blockStartOrParagraph(markdown, indent, Range{r.Position + bytes, r.End}, nil, nil); descendants != nil {
|
||||
block.Children = append(block.Children, descendants[0])
|
||||
ret = append(ret, descendants...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type continuation struct {
|
||||
Indentation int
|
||||
Remaining Range
|
||||
}
|
||||
|
||||
type Block interface {
|
||||
Continuation(indentation int, r Range) *continuation
|
||||
AddLine(indentation int, r Range) bool
|
||||
Close()
|
||||
AllowsBlockStarts() bool
|
||||
HasTrailingBlankLine() bool
|
||||
}
|
||||
|
||||
type blockBase struct{}
|
||||
|
||||
func (*blockBase) AddLine(indentation int, r Range) bool { return false }
|
||||
func (*blockBase) Close() {}
|
||||
func (*blockBase) AllowsBlockStarts() bool { return true }
|
||||
func (*blockBase) HasTrailingBlankLine() bool { return false }
|
||||
|
||||
type ContainerBlock interface {
|
||||
Block
|
||||
AddChild(openBlocks []Block) []Block
|
||||
}
|
||||
|
||||
type Range struct {
|
||||
Position int
|
||||
End int
|
||||
}
|
||||
|
||||
func closeBlocks(blocks []Block, referenceDefinitions []*ReferenceDefinition) []*ReferenceDefinition {
|
||||
for _, block := range blocks {
|
||||
block.Close()
|
||||
if p, ok := block.(*Paragraph); ok && len(p.ReferenceDefinitions) > 0 {
|
||||
referenceDefinitions = append(referenceDefinitions, p.ReferenceDefinitions...)
|
||||
}
|
||||
}
|
||||
return referenceDefinitions
|
||||
}
|
||||
|
||||
func ParseBlocks(markdown string, lines []Line) (*Document, []*ReferenceDefinition) {
|
||||
document := &Document{}
|
||||
var referenceDefinitions []*ReferenceDefinition
|
||||
|
||||
openBlocks := []Block{document}
|
||||
|
||||
for _, line := range lines {
|
||||
r := line.Range
|
||||
lastMatchIndex := 0
|
||||
|
||||
indentation, indentationBytes := countIndentation(markdown, r)
|
||||
r = Range{r.Position + indentationBytes, r.End}
|
||||
|
||||
for i, block := range openBlocks {
|
||||
if continuation := block.Continuation(indentation, r); continuation != nil {
|
||||
indentation = continuation.Indentation
|
||||
r = continuation.Remaining
|
||||
additionalIndentation, additionalIndentationBytes := countIndentation(markdown, r)
|
||||
r = Range{r.Position + additionalIndentationBytes, r.End}
|
||||
indentation += additionalIndentation
|
||||
lastMatchIndex = i
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if openBlocks[lastMatchIndex].AllowsBlockStarts() {
|
||||
if newBlocks := blockStart(markdown, indentation, r, openBlocks[:lastMatchIndex+1], openBlocks[lastMatchIndex+1:]); newBlocks != nil {
|
||||
didAdd := false
|
||||
for i := lastMatchIndex; i >= 0; i-- {
|
||||
if container, ok := openBlocks[i].(ContainerBlock); ok {
|
||||
if addedBlocks := container.AddChild(newBlocks); addedBlocks != nil {
|
||||
referenceDefinitions = closeBlocks(openBlocks[i+1:], referenceDefinitions)
|
||||
openBlocks = openBlocks[:i+1]
|
||||
openBlocks = append(openBlocks, addedBlocks...)
|
||||
didAdd = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if didAdd {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isBlank := strings.TrimSpace(markdown[r.Position:r.End]) == ""
|
||||
if paragraph, ok := openBlocks[len(openBlocks)-1].(*Paragraph); ok && !isBlank {
|
||||
paragraph.Text = append(paragraph.Text, r)
|
||||
continue
|
||||
}
|
||||
|
||||
referenceDefinitions = closeBlocks(openBlocks[lastMatchIndex+1:], referenceDefinitions)
|
||||
openBlocks = openBlocks[:lastMatchIndex+1]
|
||||
|
||||
if openBlocks[lastMatchIndex].AddLine(indentation, r) {
|
||||
continue
|
||||
}
|
||||
|
||||
if paragraph := newParagraph(markdown, r); paragraph != nil {
|
||||
for i := lastMatchIndex; i >= 0; i-- {
|
||||
if container, ok := openBlocks[i].(ContainerBlock); ok {
|
||||
if newBlocks := container.AddChild([]Block{paragraph}); newBlocks != nil {
|
||||
referenceDefinitions = closeBlocks(openBlocks[i+1:], referenceDefinitions)
|
||||
openBlocks = openBlocks[:i+1]
|
||||
openBlocks = append(openBlocks, newBlocks...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
referenceDefinitions = closeBlocks(openBlocks, referenceDefinitions)
|
||||
|
||||
return document, referenceDefinitions
|
||||
}
|
||||
|
||||
func blockStart(markdown string, indentation int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block {
|
||||
if r.Position >= r.End {
|
||||
return nil
|
||||
}
|
||||
|
||||
if start := blockQuoteStart(markdown, indentation, r); start != nil {
|
||||
return start
|
||||
} else if start := listStart(markdown, indentation, r, matchedBlocks, unmatchedBlocks); start != nil {
|
||||
return start
|
||||
} else if start := indentedCodeStart(markdown, indentation, r, matchedBlocks, unmatchedBlocks); start != nil {
|
||||
return start
|
||||
} else if start := fencedCodeStart(markdown, indentation, r); start != nil {
|
||||
return start
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func blockStartOrParagraph(markdown string, indentation int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block {
|
||||
if start := blockStart(markdown, indentation, r, matchedBlocks, unmatchedBlocks); start != nil {
|
||||
return start
|
||||
}
|
||||
if paragraph := newParagraph(markdown, r); paragraph != nil {
|
||||
return []Block{paragraph}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
type Document struct {
|
||||
blockBase
|
||||
|
||||
Children []Block
|
||||
}
|
||||
|
||||
func (b *Document) Continuation(indentation int, r Range) *continuation {
|
||||
return &continuation{
|
||||
Indentation: indentation,
|
||||
Remaining: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Document) AddChild(openBlocks []Block) []Block {
|
||||
b.Children = append(b.Children, openBlocks[0])
|
||||
return openBlocks
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type FencedCodeLine struct {
|
||||
Indentation int
|
||||
Range Range
|
||||
}
|
||||
|
||||
type FencedCode struct {
|
||||
blockBase
|
||||
markdown string
|
||||
didSeeClosingFence bool
|
||||
|
||||
Indentation int
|
||||
OpeningFence Range
|
||||
RawInfo Range
|
||||
RawCode []FencedCodeLine
|
||||
}
|
||||
|
||||
func (b *FencedCode) Code() (result string) {
|
||||
for _, code := range b.RawCode {
|
||||
result += strings.Repeat(" ", code.Indentation) + b.markdown[code.Range.Position:code.Range.End]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *FencedCode) Info() string {
|
||||
return Unescape(b.markdown[b.RawInfo.Position:b.RawInfo.End])
|
||||
}
|
||||
|
||||
func (b *FencedCode) Continuation(indentation int, r Range) *continuation {
|
||||
if b.didSeeClosingFence {
|
||||
return nil
|
||||
}
|
||||
return &continuation{
|
||||
Indentation: indentation,
|
||||
Remaining: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *FencedCode) AddLine(indentation int, r Range) bool {
|
||||
s := b.markdown[r.Position:r.End]
|
||||
if indentation <= 3 && strings.HasPrefix(s, b.markdown[b.OpeningFence.Position:b.OpeningFence.End]) {
|
||||
suffix := strings.TrimSpace(s[b.OpeningFence.End-b.OpeningFence.Position:])
|
||||
isClosingFence := true
|
||||
for _, c := range suffix {
|
||||
if c != rune(s[0]) {
|
||||
isClosingFence = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isClosingFence {
|
||||
b.didSeeClosingFence = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if indentation >= b.Indentation {
|
||||
indentation -= b.Indentation
|
||||
} else {
|
||||
indentation = 0
|
||||
}
|
||||
|
||||
b.RawCode = append(b.RawCode, FencedCodeLine{
|
||||
Indentation: indentation,
|
||||
Range: r,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *FencedCode) AllowsBlockStarts() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func fencedCodeStart(markdown string, indentation int, r Range) []Block {
|
||||
s := markdown[r.Position:r.End]
|
||||
|
||||
if !strings.HasPrefix(s, "```") && !strings.HasPrefix(s, "~~~") {
|
||||
return nil
|
||||
}
|
||||
|
||||
fenceCharacter := rune(s[0])
|
||||
fenceLength := 3
|
||||
for _, c := range s[3:] {
|
||||
if c == fenceCharacter {
|
||||
fenceLength++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for i := r.Position + fenceLength; i < r.End; i++ {
|
||||
if markdown[i] == '`' {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return []Block{
|
||||
&FencedCode{
|
||||
markdown: markdown,
|
||||
Indentation: indentation,
|
||||
RawInfo: trimRightSpace(markdown, Range{r.Position + fenceLength, r.End}),
|
||||
OpeningFence: Range{r.Position, r.Position + fenceLength},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var htmlEscaper = strings.NewReplacer(
|
||||
`&`, "&",
|
||||
`<`, "<",
|
||||
`>`, ">",
|
||||
`"`, """,
|
||||
)
|
||||
|
||||
// RenderHTML produces HTML with the same behavior as the example renderer used in the CommonMark
|
||||
// reference materials except for one slight difference: for brevity, no unnecessary whitespace is
|
||||
// inserted between elements. The output is not defined by the CommonMark spec, and it exists
|
||||
// primarily as an aid in testing.
|
||||
func RenderHTML(markdown string) string {
|
||||
return RenderBlockHTML(Parse(markdown))
|
||||
}
|
||||
|
||||
func RenderBlockHTML(block Block, referenceDefinitions []*ReferenceDefinition) (result string) {
|
||||
return renderBlockHTML(block, referenceDefinitions, false)
|
||||
}
|
||||
|
||||
func renderBlockHTML(block Block, referenceDefinitions []*ReferenceDefinition, isTightList bool) (result string) {
|
||||
switch v := block.(type) {
|
||||
case *Document:
|
||||
for _, block := range v.Children {
|
||||
result += RenderBlockHTML(block, referenceDefinitions)
|
||||
}
|
||||
case *Paragraph:
|
||||
if len(v.Text) == 0 {
|
||||
return
|
||||
}
|
||||
if !isTightList {
|
||||
result += "<p>"
|
||||
}
|
||||
for _, inline := range v.ParseInlines(referenceDefinitions) {
|
||||
result += RenderInlineHTML(inline)
|
||||
}
|
||||
if !isTightList {
|
||||
result += "</p>"
|
||||
}
|
||||
case *List:
|
||||
if v.IsOrdered {
|
||||
if v.OrderedStart != 1 {
|
||||
result += fmt.Sprintf(`<ol start="%v">`, v.OrderedStart)
|
||||
} else {
|
||||
result += "<ol>"
|
||||
}
|
||||
} else {
|
||||
result += "<ul>"
|
||||
}
|
||||
for _, block := range v.Children {
|
||||
result += renderBlockHTML(block, referenceDefinitions, !v.IsLoose)
|
||||
}
|
||||
if v.IsOrdered {
|
||||
result += "</ol>"
|
||||
} else {
|
||||
result += "</ul>"
|
||||
}
|
||||
case *ListItem:
|
||||
result += "<li>"
|
||||
for _, block := range v.Children {
|
||||
result += renderBlockHTML(block, referenceDefinitions, isTightList)
|
||||
}
|
||||
result += "</li>"
|
||||
case *BlockQuote:
|
||||
result += "<blockquote>"
|
||||
for _, block := range v.Children {
|
||||
result += RenderBlockHTML(block, referenceDefinitions)
|
||||
}
|
||||
result += "</blockquote>"
|
||||
case *FencedCode:
|
||||
if info := v.Info(); info != "" {
|
||||
language := strings.Fields(info)[0]
|
||||
result += `<pre><code class="language-` + htmlEscaper.Replace(language) + `">`
|
||||
} else {
|
||||
result += "<pre><code>"
|
||||
}
|
||||
result += htmlEscaper.Replace(v.Code()) + "</code></pre>"
|
||||
case *IndentedCode:
|
||||
result += "<pre><code>" + htmlEscaper.Replace(v.Code()) + "</code></pre>"
|
||||
default:
|
||||
panic(fmt.Sprintf("missing case for type %T", v))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func escapeURL(url string) (result string) {
|
||||
for i := 0; i < len(url); {
|
||||
switch b := url[i]; b {
|
||||
case ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '-', '_', '.', '!', '~', '*', '\'', '(', ')', '#':
|
||||
result += string(b)
|
||||
i++
|
||||
default:
|
||||
if b == '%' && i+2 < len(url) && isHexByte(url[i+1]) && isHexByte(url[i+2]) {
|
||||
result += url[i : i+3]
|
||||
i += 3
|
||||
} else if (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') {
|
||||
result += string(b)
|
||||
i++
|
||||
} else {
|
||||
result += fmt.Sprintf("%%%0X", b)
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func RenderInlineHTML(inline Inline) (result string) {
|
||||
switch v := inline.(type) {
|
||||
case *Text:
|
||||
return htmlEscaper.Replace(v.Text)
|
||||
case *HardLineBreak:
|
||||
return "<br />"
|
||||
case *SoftLineBreak:
|
||||
return "\n"
|
||||
case *CodeSpan:
|
||||
return "<code>" + htmlEscaper.Replace(v.Code) + "</code>"
|
||||
case *InlineImage:
|
||||
result += `<img src="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `" alt="` + htmlEscaper.Replace(renderImageAltText(v.Children)) + `"`
|
||||
if title := v.Title(); title != "" {
|
||||
result += ` title="` + htmlEscaper.Replace(title) + `"`
|
||||
}
|
||||
result += ` />`
|
||||
case *ReferenceImage:
|
||||
result += `<img src="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `" alt="` + htmlEscaper.Replace(renderImageAltText(v.Children)) + `"`
|
||||
if title := v.Title(); title != "" {
|
||||
result += ` title="` + htmlEscaper.Replace(title) + `"`
|
||||
}
|
||||
result += ` />`
|
||||
case *InlineLink:
|
||||
result += `<a href="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `"`
|
||||
if title := v.Title(); title != "" {
|
||||
result += ` title="` + htmlEscaper.Replace(title) + `"`
|
||||
}
|
||||
result += `>`
|
||||
for _, inline := range v.Children {
|
||||
result += RenderInlineHTML(inline)
|
||||
}
|
||||
result += "</a>"
|
||||
case *ReferenceLink:
|
||||
result += `<a href="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `"`
|
||||
if title := v.Title(); title != "" {
|
||||
result += ` title="` + htmlEscaper.Replace(title) + `"`
|
||||
}
|
||||
result += `>`
|
||||
for _, inline := range v.Children {
|
||||
result += RenderInlineHTML(inline)
|
||||
}
|
||||
result += "</a>"
|
||||
case *Autolink:
|
||||
result += `<a href="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `">`
|
||||
for _, inline := range v.Children {
|
||||
result += RenderInlineHTML(inline)
|
||||
}
|
||||
result += "</a>"
|
||||
default:
|
||||
panic(fmt.Sprintf("missing case for type %T", v))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func renderImageAltText(children []Inline) (result string) {
|
||||
for _, inline := range children {
|
||||
result += renderImageChildAltText(inline)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func renderImageChildAltText(inline Inline) (result string) {
|
||||
switch v := inline.(type) {
|
||||
case *Text:
|
||||
return v.Text
|
||||
case *InlineImage:
|
||||
for _, inline := range v.Children {
|
||||
result += renderImageChildAltText(inline)
|
||||
}
|
||||
case *InlineLink:
|
||||
for _, inline := range v.Children {
|
||||
result += renderImageChildAltText(inline)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type IndentedCodeLine struct {
|
||||
Indentation int
|
||||
Range Range
|
||||
}
|
||||
|
||||
type IndentedCode struct {
|
||||
blockBase
|
||||
markdown string
|
||||
|
||||
RawCode []IndentedCodeLine
|
||||
}
|
||||
|
||||
func (b *IndentedCode) Code() (result string) {
|
||||
for _, code := range b.RawCode {
|
||||
result += strings.Repeat(" ", code.Indentation) + b.markdown[code.Range.Position:code.Range.End]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *IndentedCode) Continuation(indentation int, r Range) *continuation {
|
||||
if indentation >= 4 {
|
||||
return &continuation{
|
||||
Indentation: indentation - 4,
|
||||
Remaining: r,
|
||||
}
|
||||
}
|
||||
s := b.markdown[r.Position:r.End]
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return &continuation{
|
||||
Remaining: r,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *IndentedCode) AddLine(indentation int, r Range) bool {
|
||||
b.RawCode = append(b.RawCode, IndentedCodeLine{
|
||||
Indentation: indentation,
|
||||
Range: r,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *IndentedCode) Close() {
|
||||
for {
|
||||
last := b.RawCode[len(b.RawCode)-1]
|
||||
s := b.markdown[last.Range.Position:last.Range.End]
|
||||
if strings.TrimRight(s, "\r\n") == "" {
|
||||
b.RawCode = b.RawCode[:len(b.RawCode)-1]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *IndentedCode) AllowsBlockStarts() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func indentedCodeStart(markdown string, indentation int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block {
|
||||
if len(unmatchedBlocks) > 0 {
|
||||
if _, ok := unmatchedBlocks[len(unmatchedBlocks)-1].(*Paragraph); ok {
|
||||
return nil
|
||||
}
|
||||
} else if len(matchedBlocks) > 0 {
|
||||
if _, ok := matchedBlocks[len(matchedBlocks)-1].(*Paragraph); ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if indentation < 4 {
|
||||
return nil
|
||||
}
|
||||
|
||||
s := markdown[r.Position:r.End]
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []Block{
|
||||
&IndentedCode{
|
||||
markdown: markdown,
|
||||
RawCode: []IndentedCodeLine{{
|
||||
Indentation: indentation - 4,
|
||||
Range: r,
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,663 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type Inline interface {
|
||||
IsInline() bool
|
||||
}
|
||||
|
||||
type inlineBase struct{}
|
||||
|
||||
func (inlineBase) IsInline() bool { return true }
|
||||
|
||||
type Text struct {
|
||||
inlineBase
|
||||
|
||||
Text string
|
||||
Range Range
|
||||
}
|
||||
|
||||
type CodeSpan struct {
|
||||
inlineBase
|
||||
|
||||
Code string
|
||||
}
|
||||
|
||||
type HardLineBreak struct {
|
||||
inlineBase
|
||||
}
|
||||
|
||||
type SoftLineBreak struct {
|
||||
inlineBase
|
||||
}
|
||||
|
||||
type InlineLinkOrImage struct {
|
||||
inlineBase
|
||||
|
||||
Children []Inline
|
||||
|
||||
RawDestination Range
|
||||
|
||||
markdown string
|
||||
rawTitle string
|
||||
}
|
||||
|
||||
func (i *InlineLinkOrImage) Destination() string {
|
||||
return Unescape(i.markdown[i.RawDestination.Position:i.RawDestination.End])
|
||||
}
|
||||
|
||||
func (i *InlineLinkOrImage) Title() string {
|
||||
return Unescape(i.rawTitle)
|
||||
}
|
||||
|
||||
type InlineLink struct {
|
||||
InlineLinkOrImage
|
||||
}
|
||||
|
||||
type InlineImage struct {
|
||||
InlineLinkOrImage
|
||||
}
|
||||
|
||||
type ReferenceLinkOrImage struct {
|
||||
inlineBase
|
||||
*ReferenceDefinition
|
||||
|
||||
Children []Inline
|
||||
}
|
||||
|
||||
type ReferenceLink struct {
|
||||
ReferenceLinkOrImage
|
||||
}
|
||||
|
||||
type ReferenceImage struct {
|
||||
ReferenceLinkOrImage
|
||||
}
|
||||
|
||||
type Autolink struct {
|
||||
inlineBase
|
||||
|
||||
Children []Inline
|
||||
|
||||
RawDestination Range
|
||||
|
||||
markdown string
|
||||
}
|
||||
|
||||
func (i *Autolink) Destination() string {
|
||||
destination := Unescape(i.markdown[i.RawDestination.Position:i.RawDestination.End])
|
||||
|
||||
if strings.HasPrefix(destination, "www") {
|
||||
destination = "http://" + destination
|
||||
}
|
||||
|
||||
return destination
|
||||
}
|
||||
|
||||
type delimiterType int
|
||||
|
||||
const (
|
||||
linkOpeningDelimiter delimiterType = iota
|
||||
imageOpeningDelimiter
|
||||
)
|
||||
|
||||
type delimiter struct {
|
||||
Type delimiterType
|
||||
IsInactive bool
|
||||
TextNode int
|
||||
Range Range
|
||||
}
|
||||
|
||||
type inlineParser struct {
|
||||
markdown string
|
||||
ranges []Range
|
||||
referenceDefinitions []*ReferenceDefinition
|
||||
|
||||
raw string
|
||||
position int
|
||||
inlines []Inline
|
||||
delimiterStack *list.List
|
||||
}
|
||||
|
||||
func newInlineParser(markdown string, ranges []Range, referenceDefinitions []*ReferenceDefinition) *inlineParser {
|
||||
return &inlineParser{
|
||||
markdown: markdown,
|
||||
ranges: ranges,
|
||||
referenceDefinitions: referenceDefinitions,
|
||||
delimiterStack: list.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *inlineParser) parseBackticks() {
|
||||
count := 1
|
||||
for i := p.position + 1; i < len(p.raw) && p.raw[i] == '`'; i++ {
|
||||
count++
|
||||
}
|
||||
opening := p.raw[p.position : p.position+count]
|
||||
search := p.position + count
|
||||
for search < len(p.raw) {
|
||||
end := strings.Index(p.raw[search:], opening)
|
||||
if end == -1 {
|
||||
break
|
||||
}
|
||||
if search+end+count < len(p.raw) && p.raw[search+end+count] == '`' {
|
||||
search += end + count
|
||||
for search < len(p.raw) && p.raw[search] == '`' {
|
||||
search++
|
||||
}
|
||||
continue
|
||||
}
|
||||
code := strings.Join(strings.Fields(p.raw[p.position+count:search+end]), " ")
|
||||
p.position = search + end + count
|
||||
p.inlines = append(p.inlines, &CodeSpan{
|
||||
Code: code,
|
||||
})
|
||||
return
|
||||
}
|
||||
p.position += len(opening)
|
||||
absPos := relativeToAbsolutePosition(p.ranges, p.position-len(opening))
|
||||
p.inlines = append(p.inlines, &Text{
|
||||
Text: opening,
|
||||
Range: Range{absPos, absPos + len(opening)},
|
||||
})
|
||||
}
|
||||
|
||||
func (p *inlineParser) parseLineEnding() {
|
||||
if p.position >= 1 && p.raw[p.position-1] == '\t' {
|
||||
p.inlines = append(p.inlines, &HardLineBreak{})
|
||||
} else if p.position >= 2 && p.raw[p.position-1] == ' ' && (p.raw[p.position-2] == '\t' || p.raw[p.position-1] == ' ') {
|
||||
p.inlines = append(p.inlines, &HardLineBreak{})
|
||||
} else {
|
||||
p.inlines = append(p.inlines, &SoftLineBreak{})
|
||||
}
|
||||
p.position++
|
||||
if p.position < len(p.raw) && p.raw[p.position] == '\n' {
|
||||
p.position++
|
||||
}
|
||||
}
|
||||
|
||||
func (p *inlineParser) parseEscapeCharacter() {
|
||||
if p.position+1 < len(p.raw) && isEscapableByte(p.raw[p.position+1]) {
|
||||
absPos := relativeToAbsolutePosition(p.ranges, p.position+1)
|
||||
p.inlines = append(p.inlines, &Text{
|
||||
Text: string(p.raw[p.position+1]),
|
||||
Range: Range{absPos, absPos + len(string(p.raw[p.position+1]))},
|
||||
})
|
||||
p.position += 2
|
||||
} else {
|
||||
absPos := relativeToAbsolutePosition(p.ranges, p.position)
|
||||
p.inlines = append(p.inlines, &Text{
|
||||
Text: `\`,
|
||||
Range: Range{absPos, absPos + 1},
|
||||
})
|
||||
p.position++
|
||||
}
|
||||
}
|
||||
|
||||
func (p *inlineParser) parseText() {
|
||||
if next := strings.IndexAny(p.raw[p.position:], "\r\n\\`&![]wW:"); next == -1 {
|
||||
absPos := relativeToAbsolutePosition(p.ranges, p.position)
|
||||
p.inlines = append(p.inlines, &Text{
|
||||
Text: strings.TrimRightFunc(p.raw[p.position:], isWhitespace),
|
||||
Range: Range{absPos, absPos + len(p.raw[p.position:])},
|
||||
})
|
||||
p.position = len(p.raw)
|
||||
} else {
|
||||
absPos := relativeToAbsolutePosition(p.ranges, p.position)
|
||||
if p.raw[p.position+next] == '\r' || p.raw[p.position+next] == '\n' {
|
||||
s := strings.TrimRightFunc(p.raw[p.position:p.position+next], isWhitespace)
|
||||
p.inlines = append(p.inlines, &Text{
|
||||
Text: s,
|
||||
Range: Range{absPos, absPos + len(s)},
|
||||
})
|
||||
} else {
|
||||
if next == 0 {
|
||||
// Always read at least one character since 'w', 'W', and ':' may not actually match another
|
||||
// type of node
|
||||
next = 1
|
||||
}
|
||||
|
||||
p.inlines = append(p.inlines, &Text{
|
||||
Text: p.raw[p.position : p.position+next],
|
||||
Range: Range{absPos, absPos + next},
|
||||
})
|
||||
}
|
||||
p.position += next
|
||||
}
|
||||
}
|
||||
|
||||
func (p *inlineParser) parseLinkOrImageDelimiter() {
|
||||
absPos := relativeToAbsolutePosition(p.ranges, p.position)
|
||||
if p.raw[p.position] == '[' {
|
||||
p.inlines = append(p.inlines, &Text{
|
||||
Text: "[",
|
||||
Range: Range{absPos, absPos + 1},
|
||||
})
|
||||
p.delimiterStack.PushBack(&delimiter{
|
||||
Type: linkOpeningDelimiter,
|
||||
TextNode: len(p.inlines) - 1,
|
||||
Range: Range{p.position, p.position + 1},
|
||||
})
|
||||
p.position++
|
||||
} else if p.raw[p.position] == '!' && p.position+1 < len(p.raw) && p.raw[p.position+1] == '[' {
|
||||
p.inlines = append(p.inlines, &Text{
|
||||
Text: "![",
|
||||
Range: Range{absPos, absPos + 2},
|
||||
})
|
||||
p.delimiterStack.PushBack(&delimiter{
|
||||
Type: imageOpeningDelimiter,
|
||||
TextNode: len(p.inlines) - 1,
|
||||
Range: Range{p.position, p.position + 2},
|
||||
})
|
||||
p.position += 2
|
||||
} else {
|
||||
p.inlines = append(p.inlines, &Text{
|
||||
Text: "!",
|
||||
Range: Range{absPos, absPos + 1},
|
||||
})
|
||||
p.position++
|
||||
}
|
||||
}
|
||||
|
||||
func (p *inlineParser) peekAtInlineLinkDestinationAndTitle(position int, isImage bool) (destination, title Range, end int, ok bool) {
|
||||
if position >= len(p.raw) || p.raw[position] != '(' {
|
||||
return
|
||||
}
|
||||
position++
|
||||
|
||||
destinationStart := nextNonWhitespace(p.raw, position)
|
||||
if destinationStart >= len(p.raw) {
|
||||
return
|
||||
} else if p.raw[destinationStart] == ')' {
|
||||
return Range{destinationStart, destinationStart}, Range{destinationStart, destinationStart}, destinationStart + 1, true
|
||||
}
|
||||
|
||||
destination, end, ok = parseLinkDestination(p.raw, destinationStart)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
position = end
|
||||
|
||||
if isImage && position < len(p.raw) && isWhitespaceByte(p.raw[position]) {
|
||||
dimensionsStart := nextNonWhitespace(p.raw, position)
|
||||
if dimensionsStart >= len(p.raw) {
|
||||
return
|
||||
}
|
||||
|
||||
if p.raw[dimensionsStart] == '=' {
|
||||
// Read optional image dimensions even if we don't use them
|
||||
_, end, ok = parseImageDimensions(p.raw, dimensionsStart)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
position = end
|
||||
}
|
||||
}
|
||||
|
||||
if position < len(p.raw) && isWhitespaceByte(p.raw[position]) {
|
||||
titleStart := nextNonWhitespace(p.raw, position)
|
||||
if titleStart >= len(p.raw) {
|
||||
return
|
||||
} else if p.raw[titleStart] == ')' {
|
||||
return destination, Range{titleStart, titleStart}, titleStart + 1, true
|
||||
}
|
||||
|
||||
if p.raw[titleStart] == '"' || p.raw[titleStart] == '\'' || p.raw[titleStart] == '(' {
|
||||
title, end, ok = parseLinkTitle(p.raw, titleStart)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
position = end
|
||||
}
|
||||
}
|
||||
|
||||
closingPosition := nextNonWhitespace(p.raw, position)
|
||||
if closingPosition >= len(p.raw) || p.raw[closingPosition] != ')' {
|
||||
return Range{}, Range{}, 0, false
|
||||
}
|
||||
|
||||
return destination, title, closingPosition + 1, true
|
||||
}
|
||||
|
||||
func (p *inlineParser) referenceDefinition(label string) *ReferenceDefinition {
|
||||
clean := strings.Join(strings.Fields(label), " ")
|
||||
for _, d := range p.referenceDefinitions {
|
||||
if strings.EqualFold(clean, strings.Join(strings.Fields(d.Label()), " ")) {
|
||||
return d
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *inlineParser) lookForLinkOrImage() {
|
||||
for element := p.delimiterStack.Back(); element != nil; element = element.Prev() {
|
||||
d := element.Value.(*delimiter)
|
||||
if d.Type != imageOpeningDelimiter && d.Type != linkOpeningDelimiter {
|
||||
continue
|
||||
}
|
||||
if d.IsInactive {
|
||||
p.delimiterStack.Remove(element)
|
||||
break
|
||||
}
|
||||
|
||||
isImage := d.Type == imageOpeningDelimiter
|
||||
|
||||
var inline Inline
|
||||
|
||||
if destination, title, next, ok := p.peekAtInlineLinkDestinationAndTitle(p.position+1, isImage); ok {
|
||||
destinationMarkdownPosition := relativeToAbsolutePosition(p.ranges, destination.Position)
|
||||
linkOrImage := InlineLinkOrImage{
|
||||
Children: append([]Inline(nil), p.inlines[d.TextNode+1:]...),
|
||||
RawDestination: Range{destinationMarkdownPosition, destinationMarkdownPosition + destination.End - destination.Position},
|
||||
markdown: p.markdown,
|
||||
rawTitle: p.raw[title.Position:title.End],
|
||||
}
|
||||
if d.Type == imageOpeningDelimiter {
|
||||
inline = &InlineImage{linkOrImage}
|
||||
} else {
|
||||
inline = &InlineLink{linkOrImage}
|
||||
}
|
||||
p.position = next
|
||||
} else {
|
||||
referenceLabel := ""
|
||||
label, next, hasLinkLabel := parseLinkLabel(p.raw, p.position+1)
|
||||
if hasLinkLabel && label.End > label.Position {
|
||||
referenceLabel = p.raw[label.Position:label.End]
|
||||
} else {
|
||||
referenceLabel = p.raw[d.Range.End:p.position]
|
||||
if !hasLinkLabel {
|
||||
next = p.position + 1
|
||||
}
|
||||
}
|
||||
if referenceLabel != "" {
|
||||
if reference := p.referenceDefinition(referenceLabel); reference != nil {
|
||||
linkOrImage := ReferenceLinkOrImage{
|
||||
ReferenceDefinition: reference,
|
||||
Children: append([]Inline(nil), p.inlines[d.TextNode+1:]...),
|
||||
}
|
||||
if d.Type == imageOpeningDelimiter {
|
||||
inline = &ReferenceImage{linkOrImage}
|
||||
} else {
|
||||
inline = &ReferenceLink{linkOrImage}
|
||||
}
|
||||
p.position = next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if inline != nil {
|
||||
if d.Type == imageOpeningDelimiter {
|
||||
p.inlines = append(p.inlines[:d.TextNode], inline)
|
||||
} else {
|
||||
p.inlines = append(p.inlines[:d.TextNode], inline)
|
||||
for inlineElement := element.Prev(); inlineElement != nil; inlineElement = inlineElement.Prev() {
|
||||
if d := inlineElement.Value.(*delimiter); d.Type == linkOpeningDelimiter {
|
||||
d.IsInactive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
p.delimiterStack.Remove(element)
|
||||
return
|
||||
}
|
||||
p.delimiterStack.Remove(element)
|
||||
break
|
||||
}
|
||||
absPos := relativeToAbsolutePosition(p.ranges, p.position)
|
||||
p.inlines = append(p.inlines, &Text{
|
||||
Text: "]",
|
||||
Range: Range{absPos, absPos + 1},
|
||||
})
|
||||
p.position++
|
||||
}
|
||||
|
||||
func CharacterReference(ref string) string {
|
||||
if ref == "" {
|
||||
return ""
|
||||
}
|
||||
if ref[0] == '#' {
|
||||
if len(ref) < 2 {
|
||||
return ""
|
||||
}
|
||||
n := 0
|
||||
if ref[1] == 'X' || ref[1] == 'x' {
|
||||
if len(ref) < 3 {
|
||||
return ""
|
||||
}
|
||||
for i := 2; i < len(ref); i++ {
|
||||
if i > 9 {
|
||||
return ""
|
||||
}
|
||||
d := ref[i]
|
||||
switch {
|
||||
case d >= '0' && d <= '9':
|
||||
n = n*16 + int(d-'0')
|
||||
case d >= 'a' && d <= 'f':
|
||||
n = n*16 + 10 + int(d-'a')
|
||||
case d >= 'A' && d <= 'F':
|
||||
n = n*16 + 10 + int(d-'A')
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := 1; i < len(ref); i++ {
|
||||
if i > 8 || ref[i] < '0' || ref[i] > '9' {
|
||||
return ""
|
||||
}
|
||||
n = n*10 + int(ref[i]-'0')
|
||||
}
|
||||
}
|
||||
c := rune(n)
|
||||
if c == '\u0000' || !utf8.ValidRune(c) {
|
||||
return string(unicode.ReplacementChar)
|
||||
}
|
||||
return string(c)
|
||||
}
|
||||
if entity, ok := htmlEntities[ref]; ok {
|
||||
return entity
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *inlineParser) parseCharacterReference() {
|
||||
absPos := relativeToAbsolutePosition(p.ranges, p.position)
|
||||
p.position++
|
||||
if semicolon := strings.IndexByte(p.raw[p.position:], ';'); semicolon == -1 {
|
||||
p.inlines = append(p.inlines, &Text{
|
||||
Text: "&",
|
||||
Range: Range{absPos, absPos + 1},
|
||||
})
|
||||
} else if s := CharacterReference(p.raw[p.position : p.position+semicolon]); s != "" {
|
||||
p.position += semicolon + 1
|
||||
p.inlines = append(p.inlines, &Text{
|
||||
Text: s,
|
||||
Range: Range{absPos, absPos + len(s)},
|
||||
})
|
||||
} else {
|
||||
p.inlines = append(p.inlines, &Text{
|
||||
Text: "&",
|
||||
Range: Range{absPos, absPos + 1},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (p *inlineParser) parseAutolink(c rune) bool {
|
||||
for element := p.delimiterStack.Back(); element != nil; element = element.Prev() {
|
||||
d := element.Value.(*delimiter)
|
||||
if !d.IsInactive {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var link Range
|
||||
if c == ':' {
|
||||
var ok bool
|
||||
link, ok = parseURLAutolink(p.raw, p.position)
|
||||
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Since the current position is at the colon, we have to rewind the parsing slightly so that
|
||||
// we don't duplicate the URL scheme
|
||||
rewind := strings.Index(p.raw[link.Position:link.End], ":")
|
||||
if rewind != -1 {
|
||||
lastInline := p.inlines[len(p.inlines)-1]
|
||||
lastText, ok := lastInline.(*Text)
|
||||
|
||||
if !ok {
|
||||
// This should never occur since parseURLAutolink will only return a non-empty value
|
||||
// when the previous text ends in a valid URL protocol which would mean that the previous
|
||||
// node is a Text node
|
||||
return false
|
||||
}
|
||||
|
||||
p.inlines = p.inlines[0 : len(p.inlines)-1]
|
||||
p.inlines = append(p.inlines, &Text{
|
||||
Text: lastText.Text[:len(lastText.Text)-rewind],
|
||||
Range: Range{lastText.Range.Position, lastText.Range.End - rewind},
|
||||
})
|
||||
p.position -= rewind
|
||||
}
|
||||
} else if c == 'w' || c == 'W' {
|
||||
var ok bool
|
||||
link, ok = parseWWWAutolink(p.raw, p.position)
|
||||
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
linkMarkdownPosition := relativeToAbsolutePosition(p.ranges, link.Position)
|
||||
linkRange := Range{linkMarkdownPosition, linkMarkdownPosition + link.End - link.Position}
|
||||
|
||||
p.inlines = append(p.inlines, &Autolink{
|
||||
Children: []Inline{
|
||||
&Text{
|
||||
Text: p.raw[link.Position:link.End],
|
||||
Range: linkRange,
|
||||
},
|
||||
},
|
||||
RawDestination: linkRange,
|
||||
markdown: p.markdown,
|
||||
})
|
||||
p.position += (link.End - link.Position)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *inlineParser) Parse() []Inline {
|
||||
for _, r := range p.ranges {
|
||||
p.raw += p.markdown[r.Position:r.End]
|
||||
}
|
||||
|
||||
for p.position < len(p.raw) {
|
||||
c, _ := utf8.DecodeRuneInString(p.raw[p.position:])
|
||||
|
||||
switch c {
|
||||
case '\r', '\n':
|
||||
p.parseLineEnding()
|
||||
case '\\':
|
||||
p.parseEscapeCharacter()
|
||||
case '`':
|
||||
p.parseBackticks()
|
||||
case '&':
|
||||
p.parseCharacterReference()
|
||||
case '!', '[':
|
||||
p.parseLinkOrImageDelimiter()
|
||||
case ']':
|
||||
p.lookForLinkOrImage()
|
||||
case 'w', 'W', ':':
|
||||
matched := p.parseAutolink(c)
|
||||
|
||||
if !matched {
|
||||
p.parseText()
|
||||
}
|
||||
default:
|
||||
p.parseText()
|
||||
}
|
||||
}
|
||||
|
||||
return p.inlines
|
||||
}
|
||||
|
||||
func ParseInlines(markdown string, ranges []Range, referenceDefinitions []*ReferenceDefinition) (inlines []Inline) {
|
||||
return newInlineParser(markdown, ranges, referenceDefinitions).Parse()
|
||||
}
|
||||
|
||||
func MergeInlineText(inlines []Inline) []Inline {
|
||||
ret := inlines[:0]
|
||||
for i, v := range inlines {
|
||||
// always add first node
|
||||
if i == 0 {
|
||||
ret = append(ret, v)
|
||||
continue
|
||||
}
|
||||
// not a text node? nothing to merge
|
||||
text, ok := v.(*Text)
|
||||
if !ok {
|
||||
ret = append(ret, v)
|
||||
continue
|
||||
}
|
||||
// previous node is not a text node? nothing to merge
|
||||
prevText, ok := ret[len(ret)-1].(*Text)
|
||||
if !ok {
|
||||
ret = append(ret, v)
|
||||
continue
|
||||
}
|
||||
// previous node is not right before this one
|
||||
if prevText.Range.End != text.Range.Position {
|
||||
ret = append(ret, v)
|
||||
continue
|
||||
}
|
||||
// we have two consecutive text nodes
|
||||
ret[len(ret)-1] = &Text{
|
||||
Text: prevText.Text + text.Text,
|
||||
Range: Range{prevText.Range.Position, text.Range.End},
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func Unescape(markdown string) string {
|
||||
var ret strings.Builder
|
||||
|
||||
position := 0
|
||||
for position < len(markdown) {
|
||||
c, cSize := utf8.DecodeRuneInString(markdown[position:])
|
||||
|
||||
switch c {
|
||||
case '\\':
|
||||
if position+1 < len(markdown) && isEscapableByte(markdown[position+1]) {
|
||||
ret.WriteByte(markdown[position+1])
|
||||
position += 2
|
||||
} else {
|
||||
ret.WriteString(`\`)
|
||||
position++
|
||||
}
|
||||
case '&':
|
||||
position++
|
||||
if semicolon := strings.IndexByte(markdown[position:], ';'); semicolon == -1 {
|
||||
ret.WriteString("&")
|
||||
} else if s := CharacterReference(markdown[position : position+semicolon]); s != "" {
|
||||
position += semicolon + 1
|
||||
ret.WriteString(s)
|
||||
} else {
|
||||
ret.WriteString("&")
|
||||
}
|
||||
default:
|
||||
ret.WriteRune(c)
|
||||
position += cSize
|
||||
}
|
||||
}
|
||||
|
||||
return ret.String()
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
// Inspect traverses the markdown tree in depth-first order. If f returns true, Inspect invokes f
|
||||
// recursively for each child of the block or inline, followed by a call of f(nil).
|
||||
func Inspect(markdown string, f func(any) bool) {
|
||||
document, referenceDefinitions := Parse(markdown)
|
||||
InspectBlock(document, func(block Block) bool {
|
||||
if !f(block) {
|
||||
return false
|
||||
}
|
||||
switch v := block.(type) {
|
||||
case *Paragraph:
|
||||
for _, inline := range MergeInlineText(v.ParseInlines(referenceDefinitions)) {
|
||||
InspectInline(inline, func(inline Inline) bool {
|
||||
return f(inline)
|
||||
})
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// InspectBlock traverses the blocks in depth-first order, starting with block. If f returns true,
|
||||
// InspectBlock invokes f recursively for each child of the block, followed by a call of f(nil).
|
||||
func InspectBlock(block Block, f func(Block) bool) {
|
||||
stack := []Block{block}
|
||||
// Using seen for backtracking
|
||||
seen := map[Block]bool{}
|
||||
|
||||
for len(stack) > 0 {
|
||||
// "peek" the node from the stack
|
||||
block := stack[len(stack)-1]
|
||||
|
||||
if seen[block] {
|
||||
// "pop" the node only when backtracking(seen)
|
||||
stack = stack[:len(stack)-1]
|
||||
f(nil)
|
||||
continue
|
||||
}
|
||||
seen[block] = true
|
||||
|
||||
// Process the node
|
||||
if !f(block) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch v := block.(type) {
|
||||
case *Document:
|
||||
for i := len(v.Children) - 1; i >= 0; i-- {
|
||||
stack = append(stack, v.Children[i])
|
||||
}
|
||||
case *List:
|
||||
for i := len(v.Children) - 1; i >= 0; i-- {
|
||||
stack = append(stack, v.Children[i])
|
||||
}
|
||||
case *ListItem:
|
||||
for i := len(v.Children) - 1; i >= 0; i-- {
|
||||
stack = append(stack, v.Children[i])
|
||||
}
|
||||
case *BlockQuote:
|
||||
for i := len(v.Children) - 1; i >= 0; i-- {
|
||||
stack = append(stack, v.Children[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InspectInline traverses the blocks in depth-first order, starting with block. If f returns true,
|
||||
// InspectInline invokes f recursively for each child of the block, followed by a call of f(nil).
|
||||
func InspectInline(inline Inline, f func(Inline) bool) {
|
||||
stack := []Inline{inline}
|
||||
// Using seen for backtracking
|
||||
seen := map[Inline]bool{}
|
||||
|
||||
for len(stack) > 0 {
|
||||
// "peek" the node from the stack
|
||||
inline := stack[len(stack)-1]
|
||||
|
||||
if seen[inline] {
|
||||
// "pop" the node only when backtracking(seen)
|
||||
stack = stack[:len(stack)-1]
|
||||
f(nil)
|
||||
continue
|
||||
}
|
||||
seen[inline] = true
|
||||
|
||||
// Process the node
|
||||
if !f(inline) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch v := inline.(type) {
|
||||
case *InlineImage:
|
||||
for i := len(v.Children) - 1; i >= 0; i-- {
|
||||
stack = append(stack, v.Children[i])
|
||||
}
|
||||
case *InlineLink:
|
||||
for i := len(v.Children) - 1; i >= 0; i-- {
|
||||
stack = append(stack, v.Children[i])
|
||||
}
|
||||
case *ReferenceImage:
|
||||
for i := len(v.Children) - 1; i >= 0; i-- {
|
||||
stack = append(stack, v.Children[i])
|
||||
}
|
||||
case *ReferenceLink:
|
||||
for i := len(v.Children) - 1; i >= 0; i-- {
|
||||
stack = append(stack, v.Children[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInspect(t *testing.T) {
|
||||
markdown := `
|
||||
[foo]: bar
|
||||
- a
|
||||
> [![]()]()
|
||||
> [![foo]][foo]
|
||||
- d
|
||||
`
|
||||
|
||||
visited := []string{}
|
||||
level := 0
|
||||
Inspect(markdown, func(blockOrInline any) bool {
|
||||
if blockOrInline == nil {
|
||||
level--
|
||||
} else {
|
||||
visited = append(visited, strings.Repeat(" ", level*4)+strings.TrimPrefix(fmt.Sprintf("%T", blockOrInline), "*markdown."))
|
||||
level++
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
assert.Equal(t, []string{
|
||||
"Document",
|
||||
" Paragraph",
|
||||
" List",
|
||||
" ListItem",
|
||||
" Paragraph",
|
||||
" Text",
|
||||
" BlockQuote",
|
||||
" Paragraph",
|
||||
" InlineLink",
|
||||
" InlineImage",
|
||||
" SoftLineBreak",
|
||||
" ReferenceLink",
|
||||
" ReferenceImage",
|
||||
" Text",
|
||||
" ListItem",
|
||||
" Paragraph",
|
||||
" Text",
|
||||
}, visited)
|
||||
}
|
||||
|
||||
var counterSink int
|
||||
|
||||
func BenchmarkInspect(b *testing.B) {
|
||||
text := `Some standard piece of text.
|
||||
|
||||
Has a link [post](https://github.com) and also has a blockquote.
|
||||
|
||||
> This is a famous quote.
|
||||
|
||||
Some bold text **Text for markdown?** to go with it.
|
||||
|
||||
At the end, some more lines`
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
Inspect(text, func(_ any) bool {
|
||||
counterSink++
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Line struct {
|
||||
Range
|
||||
}
|
||||
|
||||
func ParseLines(markdown string) []Line {
|
||||
lineStartPosition := 0
|
||||
isAfterCarriageReturn := false
|
||||
lines := make([]Line, 0, strings.Count(markdown, "\n"))
|
||||
for position, r := range markdown {
|
||||
if r == '\n' {
|
||||
lines = append(lines, Line{Range{lineStartPosition, position + 1}})
|
||||
lineStartPosition = position + 1
|
||||
} else if isAfterCarriageReturn {
|
||||
lines = append(lines, Line{Range{lineStartPosition, position}})
|
||||
lineStartPosition = position
|
||||
}
|
||||
isAfterCarriageReturn = r == '\r'
|
||||
}
|
||||
if lineStartPosition < len(markdown) {
|
||||
lines = append(lines, Line{Range{lineStartPosition, len(markdown)}})
|
||||
}
|
||||
return lines
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseLines(t *testing.T) {
|
||||
assert.Equal(t, []Line{
|
||||
{Range{0, 4}}, {Range{4, 7}},
|
||||
}, ParseLines("foo\nbar"))
|
||||
|
||||
assert.Equal(t, []Line{
|
||||
{Range{0, 5}}, {Range{5, 8}},
|
||||
}, ParseLines("foo\r\nbar"))
|
||||
|
||||
assert.Equal(t, []Line{
|
||||
{Range{0, 4}}, {Range{4, 6}}, {Range{6, 9}},
|
||||
}, ParseLines("foo\r\r\nbar"))
|
||||
|
||||
assert.Equal(t, []Line{
|
||||
{Range{0, 4}},
|
||||
}, ParseLines("foo\n"))
|
||||
|
||||
assert.Equal(t, []Line{
|
||||
{Range{0, 4}},
|
||||
}, ParseLines("foo\r"))
|
||||
|
||||
assert.Equal(t, []Line{
|
||||
{Range{0, 5}},
|
||||
}, ParseLines("foo\r\n"))
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func parseLinkDestination(markdown string, position int) (raw Range, next int, ok bool) {
|
||||
if position >= len(markdown) {
|
||||
return
|
||||
}
|
||||
|
||||
if markdown[position] == '<' {
|
||||
isEscaped := false
|
||||
|
||||
for offset, c := range []byte(markdown[position+1:]) {
|
||||
if isEscaped {
|
||||
isEscaped = false
|
||||
if isEscapableByte(c) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if c == '\\' {
|
||||
isEscaped = true
|
||||
} else if c == '<' {
|
||||
break
|
||||
} else if c == '>' {
|
||||
return Range{position + 1, position + 1 + offset}, position + 1 + offset + 1, true
|
||||
} else if isWhitespaceByte(c) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openCount := 0
|
||||
isEscaped := false
|
||||
for offset, c := range []byte(markdown[position:]) {
|
||||
if isEscaped {
|
||||
isEscaped = false
|
||||
if isEscapableByte(c) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
switch c {
|
||||
case '\\':
|
||||
isEscaped = true
|
||||
case '(':
|
||||
openCount++
|
||||
case ')':
|
||||
if openCount < 1 {
|
||||
return Range{position, position + offset}, position + offset, true
|
||||
}
|
||||
openCount--
|
||||
default:
|
||||
if isWhitespaceByte(c) {
|
||||
return Range{position, position + offset}, position + offset, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return Range{position, len(markdown)}, len(markdown), true
|
||||
}
|
||||
|
||||
func parseLinkTitle(markdown string, position int) (raw Range, next int, ok bool) {
|
||||
if position >= len(markdown) {
|
||||
return
|
||||
}
|
||||
|
||||
originalPosition := position
|
||||
|
||||
var closer byte
|
||||
switch markdown[position] {
|
||||
case '"', '\'':
|
||||
closer = markdown[position]
|
||||
case '(':
|
||||
closer = ')'
|
||||
default:
|
||||
return
|
||||
}
|
||||
position++
|
||||
|
||||
for position < len(markdown) {
|
||||
switch markdown[position] {
|
||||
case '\\':
|
||||
position++
|
||||
if position < len(markdown) && isEscapableByte(markdown[position]) {
|
||||
position++
|
||||
}
|
||||
case closer:
|
||||
return Range{originalPosition + 1, position}, position + 1, true
|
||||
default:
|
||||
position++
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func parseLinkLabel(markdown string, position int) (raw Range, next int, ok bool) {
|
||||
if position >= len(markdown) || markdown[position] != '[' {
|
||||
return
|
||||
}
|
||||
|
||||
originalPosition := position
|
||||
position++
|
||||
|
||||
for position < len(markdown) {
|
||||
switch markdown[position] {
|
||||
case '\\':
|
||||
position++
|
||||
if position < len(markdown) && isEscapableByte(markdown[position]) {
|
||||
position++
|
||||
}
|
||||
case '[':
|
||||
return
|
||||
case ']':
|
||||
if position-originalPosition >= 1000 && utf8.RuneCountInString(markdown[originalPosition:position]) >= 1000 {
|
||||
return
|
||||
}
|
||||
return Range{originalPosition + 1, position}, position + 1, true
|
||||
default:
|
||||
position++
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// As a non-standard feature, we allow image links to specify dimensions of the image by adding "=WIDTHxHEIGHT"
|
||||
// after the image destination but before the image title like .
|
||||
// Both width and height are optional, but at least one of them must be specified.
|
||||
func parseImageDimensions(markdown string, position int) (raw Range, next int, ok bool) {
|
||||
if position >= len(markdown) {
|
||||
return
|
||||
}
|
||||
|
||||
originalPosition := position
|
||||
|
||||
// Read =
|
||||
position += 1
|
||||
if position >= len(markdown) {
|
||||
return
|
||||
}
|
||||
|
||||
// Read width
|
||||
hasWidth := false
|
||||
for position < len(markdown)-1 && isNumericByte(markdown[position]) {
|
||||
hasWidth = true
|
||||
position += 1
|
||||
}
|
||||
|
||||
// Look for early end of dimensions
|
||||
if isWhitespaceByte(markdown[position]) || markdown[position] == ')' {
|
||||
return Range{originalPosition, position - 1}, position, true
|
||||
}
|
||||
|
||||
// Read the x
|
||||
if (markdown[position] != 'x' && markdown[position] != 'X') || position == len(markdown)-1 {
|
||||
return
|
||||
}
|
||||
position += 1
|
||||
|
||||
// Read height
|
||||
hasHeight := false
|
||||
for position < len(markdown)-1 && isNumericByte(markdown[position]) {
|
||||
hasHeight = true
|
||||
position += 1
|
||||
}
|
||||
|
||||
// Make sure the there's no trailing characters
|
||||
if !isWhitespaceByte(markdown[position]) && markdown[position] != ')' {
|
||||
return
|
||||
}
|
||||
|
||||
if !hasWidth && !hasHeight {
|
||||
// At least one of width or height is required
|
||||
return
|
||||
}
|
||||
|
||||
return Range{originalPosition, position - 1}, position, true
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseImageDimensions(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
Input string
|
||||
Position int
|
||||
ExpectedRange Range
|
||||
ExpectedNext int
|
||||
ExpectedOk bool
|
||||
}{
|
||||
"no dimensions, no title": {
|
||||
Input: ``,
|
||||
Position: 26,
|
||||
ExpectedRange: Range{0, 0},
|
||||
ExpectedNext: 0,
|
||||
ExpectedOk: false,
|
||||
},
|
||||
"no dimensions, title": {
|
||||
Input: ``,
|
||||
Position: 27,
|
||||
ExpectedRange: Range{0, 0},
|
||||
ExpectedNext: 0,
|
||||
ExpectedOk: false,
|
||||
},
|
||||
"only width, no title": {
|
||||
Input: ``,
|
||||
Position: 27,
|
||||
ExpectedRange: Range{27, 30},
|
||||
ExpectedNext: 31,
|
||||
ExpectedOk: true,
|
||||
},
|
||||
"only width, title": {
|
||||
Input: ``,
|
||||
Position: 27,
|
||||
ExpectedRange: Range{27, 30},
|
||||
ExpectedNext: 31,
|
||||
ExpectedOk: true,
|
||||
},
|
||||
"only height, no title": {
|
||||
Input: ``,
|
||||
Position: 27,
|
||||
ExpectedRange: Range{27, 31},
|
||||
ExpectedNext: 32,
|
||||
ExpectedOk: true,
|
||||
},
|
||||
"only height, title": {
|
||||
Input: ``,
|
||||
Position: 27,
|
||||
ExpectedRange: Range{27, 31},
|
||||
ExpectedNext: 32,
|
||||
ExpectedOk: true,
|
||||
},
|
||||
"dimensions, no title": {
|
||||
Input: ``,
|
||||
Position: 27,
|
||||
ExpectedRange: Range{27, 34},
|
||||
ExpectedNext: 35,
|
||||
ExpectedOk: true,
|
||||
},
|
||||
"dimensions, title": {
|
||||
Input: ``,
|
||||
Position: 27,
|
||||
ExpectedRange: Range{27, 34},
|
||||
ExpectedNext: 35,
|
||||
ExpectedOk: true,
|
||||
},
|
||||
"no dimensions, no title, trailing whitespace": {
|
||||
Input: ``,
|
||||
Position: 27,
|
||||
ExpectedRange: Range{0, 0},
|
||||
ExpectedNext: 0,
|
||||
ExpectedOk: false,
|
||||
},
|
||||
"only width, no title, trailing whitespace": {
|
||||
Input: ``,
|
||||
Position: 28,
|
||||
ExpectedRange: Range{28, 31},
|
||||
ExpectedNext: 32,
|
||||
ExpectedOk: true,
|
||||
},
|
||||
"only height, no title, trailing whitespace": {
|
||||
Input: ``,
|
||||
Position: 29,
|
||||
ExpectedRange: Range{29, 33},
|
||||
ExpectedNext: 34,
|
||||
ExpectedOk: true,
|
||||
},
|
||||
"dimensions, no title, trailing whitespace": {
|
||||
Input: ``,
|
||||
Position: 30,
|
||||
ExpectedRange: Range{30, 37},
|
||||
ExpectedNext: 38,
|
||||
ExpectedOk: true,
|
||||
},
|
||||
"no width or height": {
|
||||
Input: ``,
|
||||
Position: 27,
|
||||
ExpectedRange: Range{0, 0},
|
||||
ExpectedNext: 0,
|
||||
ExpectedOk: false,
|
||||
},
|
||||
"garbage 1": {
|
||||
Input: ``,
|
||||
Position: 27,
|
||||
ExpectedRange: Range{0, 0},
|
||||
ExpectedNext: 0,
|
||||
ExpectedOk: false,
|
||||
},
|
||||
"garbage 2": {
|
||||
Input: ``,
|
||||
Position: 27,
|
||||
ExpectedRange: Range{0, 0},
|
||||
ExpectedNext: 0,
|
||||
ExpectedOk: false,
|
||||
},
|
||||
"garbage 3": {
|
||||
Input: ``,
|
||||
Position: 27,
|
||||
ExpectedRange: Range{0, 0},
|
||||
ExpectedNext: 0,
|
||||
ExpectedOk: false,
|
||||
},
|
||||
"garbage 4": {
|
||||
Input: ``,
|
||||
Position: 27,
|
||||
ExpectedRange: Range{0, 0},
|
||||
ExpectedNext: 0,
|
||||
ExpectedOk: false,
|
||||
},
|
||||
"garbage 5": {
|
||||
Input: ` {
|
||||
raw, next, ok := parseImageDimensions(tc.Input, tc.Position)
|
||||
assert.Equal(t, tc.ExpectedOk, ok)
|
||||
assert.Equal(t, tc.ExpectedNext, next)
|
||||
assert.Equal(t, tc.ExpectedRange, raw)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageLinksWithDimensions(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
Markdown string
|
||||
ExpectedHTML string
|
||||
}{
|
||||
"regular link": {
|
||||
Markdown: `[link](https://example.com)`,
|
||||
ExpectedHTML: `<p><a href="https://example.com">link</a></p>`,
|
||||
},
|
||||
"image link": {
|
||||
Markdown: ``,
|
||||
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" /></p>`,
|
||||
},
|
||||
"image link with title": {
|
||||
Markdown: ``,
|
||||
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
|
||||
},
|
||||
"image link with bracketed title": {
|
||||
Markdown: `)`,
|
||||
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
|
||||
},
|
||||
"image link with width": {
|
||||
Markdown: ``,
|
||||
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" /></p>`,
|
||||
},
|
||||
"image link with width and title": {
|
||||
Markdown: ``,
|
||||
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
|
||||
},
|
||||
"image link with width and bracketed title": {
|
||||
Markdown: `)`,
|
||||
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
|
||||
},
|
||||
"image link with height": {
|
||||
Markdown: ``,
|
||||
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" /></p>`,
|
||||
},
|
||||
"image link with height and title": {
|
||||
Markdown: ``,
|
||||
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
|
||||
},
|
||||
"image link with height and bracketed title": {
|
||||
Markdown: `)`,
|
||||
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
|
||||
},
|
||||
"image link with dimensions": {
|
||||
Markdown: ``,
|
||||
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" /></p>`,
|
||||
},
|
||||
"image link with dimensions and title": {
|
||||
Markdown: ``,
|
||||
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
|
||||
},
|
||||
"image link with dimensions and bracketed title": {
|
||||
Markdown: `)`,
|
||||
ExpectedHTML: `<p><img src="https://example.com/image.png" alt="image" title="title" /></p>`,
|
||||
},
|
||||
"no image link 1": {
|
||||
Markdown: `![image]()`,
|
||||
ExpectedHTML: `<p><img src="" alt="image" /></p>`,
|
||||
},
|
||||
"no image link 2": {
|
||||
Markdown: ``,
|
||||
ExpectedHTML: `<p><img src="" alt="image" /></p>`,
|
||||
},
|
||||
"no image link with dimensions": {
|
||||
Markdown: ``,
|
||||
ExpectedHTML: `<p><img src="=500x400" alt="image" /></p>`,
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.ExpectedHTML, RenderHTML(tc.Markdown))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ListItem struct {
|
||||
blockBase
|
||||
markdown string
|
||||
hasTrailingBlankLine bool
|
||||
hasBlankLineBetweenChildren bool
|
||||
|
||||
Indentation int
|
||||
Children []Block
|
||||
}
|
||||
|
||||
func (b *ListItem) Continuation(indentation int, r Range) *continuation {
|
||||
s := b.markdown[r.Position:r.End]
|
||||
if strings.TrimSpace(s) == "" {
|
||||
if b.Children == nil {
|
||||
return nil
|
||||
}
|
||||
return &continuation{
|
||||
Remaining: r,
|
||||
}
|
||||
}
|
||||
if indentation < b.Indentation {
|
||||
return nil
|
||||
}
|
||||
return &continuation{
|
||||
Indentation: indentation - b.Indentation,
|
||||
Remaining: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ListItem) AddChild(openBlocks []Block) []Block {
|
||||
b.Children = append(b.Children, openBlocks[0])
|
||||
if b.hasTrailingBlankLine {
|
||||
b.hasBlankLineBetweenChildren = true
|
||||
}
|
||||
b.hasTrailingBlankLine = false
|
||||
return openBlocks
|
||||
}
|
||||
|
||||
func (b *ListItem) AddLine(indentation int, r Range) bool {
|
||||
isBlank := strings.TrimSpace(b.markdown[r.Position:r.End]) == ""
|
||||
if isBlank {
|
||||
b.hasTrailingBlankLine = true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *ListItem) HasTrailingBlankLine() bool {
|
||||
return b.hasTrailingBlankLine || (len(b.Children) > 0 && b.Children[len(b.Children)-1].HasTrailingBlankLine())
|
||||
}
|
||||
|
||||
func (b *ListItem) isLoose() bool {
|
||||
if b.hasBlankLineBetweenChildren {
|
||||
return true
|
||||
}
|
||||
for i, child := range b.Children {
|
||||
if i < len(b.Children)-1 && child.HasTrailingBlankLine() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type List struct {
|
||||
blockBase
|
||||
markdown string
|
||||
hasTrailingBlankLine bool
|
||||
hasBlankLineBetweenChildren bool
|
||||
|
||||
IsLoose bool
|
||||
IsOrdered bool
|
||||
OrderedStart int
|
||||
BulletOrDelimiter byte
|
||||
Children []*ListItem
|
||||
}
|
||||
|
||||
func (b *List) Continuation(indentation int, r Range) *continuation {
|
||||
s := b.markdown[r.Position:r.End]
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return &continuation{
|
||||
Remaining: r,
|
||||
}
|
||||
}
|
||||
return &continuation{
|
||||
Indentation: indentation,
|
||||
Remaining: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *List) AddChild(openBlocks []Block) []Block {
|
||||
if item, ok := openBlocks[0].(*ListItem); ok {
|
||||
b.Children = append(b.Children, item)
|
||||
if b.hasTrailingBlankLine {
|
||||
b.hasBlankLineBetweenChildren = true
|
||||
}
|
||||
b.hasTrailingBlankLine = false
|
||||
return openBlocks
|
||||
} else if list, ok := openBlocks[0].(*List); ok {
|
||||
if len(list.Children) == 1 && list.IsOrdered == b.IsOrdered && list.BulletOrDelimiter == b.BulletOrDelimiter {
|
||||
return b.AddChild(openBlocks[1:])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *List) AddLine(indentation int, r Range) bool {
|
||||
isBlank := strings.TrimSpace(b.markdown[r.Position:r.End]) == ""
|
||||
if isBlank {
|
||||
b.hasTrailingBlankLine = true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *List) HasTrailingBlankLine() bool {
|
||||
return b.hasTrailingBlankLine || (len(b.Children) > 0 && b.Children[len(b.Children)-1].HasTrailingBlankLine())
|
||||
}
|
||||
|
||||
func (b *List) isLoose() bool {
|
||||
if b.hasBlankLineBetweenChildren {
|
||||
return true
|
||||
}
|
||||
for i, child := range b.Children {
|
||||
if child.isLoose() || (i < len(b.Children)-1 && child.HasTrailingBlankLine()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *List) Close() {
|
||||
b.IsLoose = b.isLoose()
|
||||
}
|
||||
|
||||
func parseListMarker(markdown string, r Range) (success, isOrdered bool, orderedStart int, bulletOrDelimiter byte, markerWidth int, remaining Range) {
|
||||
digits := 0
|
||||
n := 0
|
||||
for i := r.Position; i < r.End && markdown[i] >= '0' && markdown[i] <= '9'; i++ {
|
||||
digits++
|
||||
n = n*10 + int(markdown[i]-'0')
|
||||
}
|
||||
if digits > 0 {
|
||||
if digits > 9 || r.Position+digits >= r.End {
|
||||
return
|
||||
}
|
||||
next := markdown[r.Position+digits]
|
||||
if next != '.' && next != ')' {
|
||||
return
|
||||
}
|
||||
return true, true, n, next, digits + 1, Range{r.Position + digits + 1, r.End}
|
||||
}
|
||||
if r.Position >= r.End {
|
||||
return
|
||||
}
|
||||
next := markdown[r.Position]
|
||||
if next != '-' && next != '+' && next != '*' {
|
||||
return
|
||||
}
|
||||
return true, false, 0, next, 1, Range{r.Position + 1, r.End}
|
||||
}
|
||||
|
||||
func listStart(markdown string, indent int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block {
|
||||
afterList := false
|
||||
if len(matchedBlocks) > 0 {
|
||||
_, afterList = matchedBlocks[len(matchedBlocks)-1].(*List)
|
||||
}
|
||||
if !afterList && indent > 3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
success, isOrdered, orderedStart, bulletOrDelimiter, markerWidth, remaining := parseListMarker(markdown, r)
|
||||
if !success {
|
||||
return nil
|
||||
}
|
||||
|
||||
isBlank := strings.TrimSpace(markdown[remaining.Position:remaining.End]) == ""
|
||||
if len(matchedBlocks) > 0 && len(unmatchedBlocks) == 0 {
|
||||
if _, ok := matchedBlocks[len(matchedBlocks)-1].(*Paragraph); ok {
|
||||
if isBlank || (isOrdered && orderedStart != 1) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
indentAfterMarker, indentBytesAfterMarker := countIndentation(markdown, remaining)
|
||||
if !isBlank && indentAfterMarker < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
remaining = Range{remaining.Position + indentBytesAfterMarker, remaining.End}
|
||||
consumedIndentAfterMarker := indentAfterMarker
|
||||
if isBlank || indentAfterMarker >= 5 {
|
||||
consumedIndentAfterMarker = 1
|
||||
}
|
||||
|
||||
listItem := &ListItem{
|
||||
markdown: markdown,
|
||||
Indentation: indent + markerWidth + consumedIndentAfterMarker,
|
||||
}
|
||||
list := &List{
|
||||
markdown: markdown,
|
||||
IsOrdered: isOrdered,
|
||||
OrderedStart: orderedStart,
|
||||
BulletOrDelimiter: bulletOrDelimiter,
|
||||
Children: []*ListItem{listItem},
|
||||
}
|
||||
ret := []Block{list, listItem}
|
||||
if descendants := blockStartOrParagraph(markdown, indentAfterMarker-consumedIndentAfterMarker, remaining, nil, nil); descendants != nil {
|
||||
listItem.Children = append(listItem.Children, descendants[0])
|
||||
ret = append(ret, descendants...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// This package implements a parser for the subset of the CommonMark spec necessary for us to do
|
||||
// server-side processing. It is not a full implementation and lacks many features. But it is
|
||||
// complete enough to efficiently and accurately allow us to do what we need to like rewrite image
|
||||
// URLs for proxying.
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func isEscapable(c rune) bool {
|
||||
return c > ' ' && (c < '0' || (c > '9' && (c < 'A' || (c > 'Z' && (c < 'a' || (c > 'z' && c <= '~'))))))
|
||||
}
|
||||
|
||||
func isEscapableByte(c byte) bool {
|
||||
return isEscapable(rune(c))
|
||||
}
|
||||
|
||||
func isWhitespace(c rune) bool {
|
||||
switch c {
|
||||
case ' ', '\t', '\n', '\u000b', '\u000c', '\r':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isWhitespaceByte(c byte) bool {
|
||||
return isWhitespace(rune(c))
|
||||
}
|
||||
|
||||
func isNumeric(c rune) bool {
|
||||
return c >= '0' && c <= '9'
|
||||
}
|
||||
|
||||
func isNumericByte(c byte) bool {
|
||||
return isNumeric(rune(c))
|
||||
}
|
||||
|
||||
func isHex(c rune) bool {
|
||||
return isNumeric(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
|
||||
}
|
||||
|
||||
func isHexByte(c byte) bool {
|
||||
return isHex(rune(c))
|
||||
}
|
||||
|
||||
func isAlphanumeric(c rune) bool {
|
||||
return isNumeric(c) || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||
}
|
||||
|
||||
func isAlphanumericByte(c byte) bool {
|
||||
return isAlphanumeric(rune(c))
|
||||
}
|
||||
|
||||
func nextNonWhitespace(markdown string, position int) int {
|
||||
for offset, c := range []byte(markdown[position:]) {
|
||||
if !isWhitespaceByte(c) {
|
||||
return position + offset
|
||||
}
|
||||
}
|
||||
return len(markdown)
|
||||
}
|
||||
|
||||
func nextLine(markdown string, position int) (linePosition int, skippedNonWhitespace bool) {
|
||||
for i := position; i < len(markdown); i++ {
|
||||
c := markdown[i]
|
||||
if c == '\r' {
|
||||
if i+1 < len(markdown) && markdown[i+1] == '\n' {
|
||||
return i + 2, skippedNonWhitespace
|
||||
}
|
||||
return i + 1, skippedNonWhitespace
|
||||
} else if c == '\n' {
|
||||
return i + 1, skippedNonWhitespace
|
||||
} else if !isWhitespaceByte(c) {
|
||||
skippedNonWhitespace = true
|
||||
}
|
||||
}
|
||||
return len(markdown), skippedNonWhitespace
|
||||
}
|
||||
|
||||
func countIndentation(markdown string, r Range) (spaces, bytes int) {
|
||||
for i := r.Position; i < r.End; i++ {
|
||||
if markdown[i] == ' ' {
|
||||
spaces++
|
||||
bytes++
|
||||
} else if markdown[i] == '\t' {
|
||||
spaces += 4
|
||||
bytes++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func trimLeftSpace(markdown string, r Range) Range {
|
||||
s := markdown[r.Position:r.End]
|
||||
trimmed := strings.TrimLeftFunc(s, isWhitespace)
|
||||
return Range{r.Position, r.End - (len(s) - len(trimmed))}
|
||||
}
|
||||
|
||||
func trimRightSpace(markdown string, r Range) Range {
|
||||
s := markdown[r.Position:r.End]
|
||||
trimmed := strings.TrimRightFunc(s, isWhitespace)
|
||||
return Range{r.Position, r.End - (len(s) - len(trimmed))}
|
||||
}
|
||||
|
||||
func relativeToAbsolutePosition(ranges []Range, position int) int {
|
||||
rem := position
|
||||
for _, r := range ranges {
|
||||
l := r.End - r.Position
|
||||
if rem < l {
|
||||
return r.Position + rem
|
||||
}
|
||||
rem -= l
|
||||
}
|
||||
if len(ranges) == 0 {
|
||||
return 0
|
||||
}
|
||||
return ranges[len(ranges)-1].End
|
||||
}
|
||||
|
||||
func trimBytesFromRanges(ranges []Range, bytes int) (result []Range) {
|
||||
rem := bytes
|
||||
for _, r := range ranges {
|
||||
if rem == 0 {
|
||||
result = append(result, r)
|
||||
continue
|
||||
}
|
||||
l := r.End - r.Position
|
||||
if rem < l {
|
||||
result = append(result, Range{r.Position + rem, r.End})
|
||||
rem = 0
|
||||
continue
|
||||
}
|
||||
rem -= l
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Parse(markdown string) (*Document, []*ReferenceDefinition) {
|
||||
lines := ParseLines(markdown)
|
||||
return ParseBlocks(markdown, lines)
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Paragraph struct {
|
||||
blockBase
|
||||
markdown string
|
||||
|
||||
Text []Range
|
||||
ReferenceDefinitions []*ReferenceDefinition
|
||||
}
|
||||
|
||||
func (b *Paragraph) ParseInlines(referenceDefinitions []*ReferenceDefinition) []Inline {
|
||||
return ParseInlines(b.markdown, b.Text, referenceDefinitions)
|
||||
}
|
||||
|
||||
func (b *Paragraph) Continuation(indentation int, r Range) *continuation {
|
||||
s := b.markdown[r.Position:r.End]
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return nil
|
||||
}
|
||||
return &continuation{
|
||||
Indentation: indentation,
|
||||
Remaining: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Paragraph) Close() {
|
||||
for {
|
||||
for i := 0; i < len(b.Text); i++ {
|
||||
b.Text[i] = trimLeftSpace(b.markdown, b.Text[i])
|
||||
if b.Text[i].Position < b.Text[i].End {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.Text) == 0 || b.Text[0].Position < b.Text[0].End && b.markdown[b.Text[0].Position] != '[' {
|
||||
break
|
||||
}
|
||||
|
||||
definition, remaining := parseReferenceDefinition(b.markdown, b.Text)
|
||||
if definition == nil {
|
||||
break
|
||||
}
|
||||
b.ReferenceDefinitions = append(b.ReferenceDefinitions, definition)
|
||||
b.Text = remaining
|
||||
}
|
||||
|
||||
for i := len(b.Text) - 1; i >= 0; i-- {
|
||||
b.Text[i] = trimRightSpace(b.markdown, b.Text[i])
|
||||
if b.Text[i].Position < b.Text[i].End {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newParagraph(markdown string, r Range) *Paragraph {
|
||||
s := markdown[r.Position:r.End]
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return nil
|
||||
}
|
||||
return &Paragraph{
|
||||
markdown: markdown,
|
||||
Text: []Range{r},
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
type ReferenceDefinition struct {
|
||||
RawDestination Range
|
||||
|
||||
markdown string
|
||||
rawLabel string
|
||||
rawTitle string
|
||||
}
|
||||
|
||||
func (d *ReferenceDefinition) Destination() string {
|
||||
return Unescape(d.markdown[d.RawDestination.Position:d.RawDestination.End])
|
||||
}
|
||||
|
||||
func (d *ReferenceDefinition) Label() string {
|
||||
return d.rawLabel
|
||||
}
|
||||
|
||||
func (d *ReferenceDefinition) Title() string {
|
||||
return Unescape(d.rawTitle)
|
||||
}
|
||||
|
||||
func parseReferenceDefinition(markdown string, ranges []Range) (*ReferenceDefinition, []Range) {
|
||||
raw := ""
|
||||
for _, r := range ranges {
|
||||
raw += markdown[r.Position:r.End]
|
||||
}
|
||||
|
||||
label, next, ok := parseLinkLabel(raw, 0)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
position := next
|
||||
|
||||
if position >= len(raw) || raw[position] != ':' {
|
||||
return nil, nil
|
||||
}
|
||||
position++
|
||||
|
||||
destination, next, ok := parseLinkDestination(raw, nextNonWhitespace(raw, position))
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
position = next
|
||||
|
||||
absoluteDestination := relativeToAbsolutePosition(ranges, destination.Position)
|
||||
ret := &ReferenceDefinition{
|
||||
RawDestination: Range{absoluteDestination, absoluteDestination + destination.End - destination.Position},
|
||||
markdown: markdown,
|
||||
rawLabel: raw[label.Position:label.End],
|
||||
}
|
||||
|
||||
if position < len(raw) && isWhitespaceByte(raw[position]) {
|
||||
title, next, ok := parseLinkTitle(raw, nextNonWhitespace(raw, position))
|
||||
if !ok {
|
||||
if nextLine, skippedNonWhitespace := nextLine(raw, position); !skippedNonWhitespace {
|
||||
return ret, trimBytesFromRanges(ranges, nextLine)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
if nextLine, skippedNonWhitespace := nextLine(raw, next); !skippedNonWhitespace {
|
||||
ret.rawTitle = raw[title.Position:title.End]
|
||||
return ret, trimBytesFromRanges(ranges, nextLine)
|
||||
}
|
||||
}
|
||||
|
||||
if nextLine, skippedNonWhitespace := nextLine(raw, position); !skippedNonWhitespace {
|
||||
return ret, trimBytesFromRanges(ranges, nextLine)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTextRanges(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
Markdown string
|
||||
ExpectedRanges []Range
|
||||
ExpectedValues []string
|
||||
}{
|
||||
"simple": {
|
||||
Markdown: "hello",
|
||||
ExpectedRanges: []Range{{0, 5}},
|
||||
ExpectedValues: []string{"hello"},
|
||||
},
|
||||
"simple2": {
|
||||
Markdown: "hello!",
|
||||
ExpectedRanges: []Range{{0, 6}},
|
||||
ExpectedValues: []string{"hello!"},
|
||||
},
|
||||
"multiline": {
|
||||
Markdown: "hello world\nfoobar",
|
||||
ExpectedRanges: []Range{{0, 11}, {12, 18}},
|
||||
ExpectedValues: []string{"hello world", "foobar"},
|
||||
},
|
||||
"code": {
|
||||
Markdown: "hello `code` world",
|
||||
ExpectedRanges: []Range{{0, 6}, {12, 18}},
|
||||
ExpectedValues: []string{"hello ", " world"},
|
||||
},
|
||||
"notcode": {
|
||||
Markdown: "hello ` world",
|
||||
ExpectedRanges: []Range{{0, 13}},
|
||||
ExpectedValues: []string{"hello ` world"},
|
||||
},
|
||||
"escape": {
|
||||
Markdown: "\\*hello\\*",
|
||||
ExpectedRanges: []Range{{1, 7}, {8, 9}},
|
||||
ExpectedValues: []string{"*hello", "*"},
|
||||
},
|
||||
"escapeescape": {
|
||||
Markdown: "\\\\",
|
||||
ExpectedRanges: []Range{{1, 2}},
|
||||
ExpectedValues: []string{"\\"},
|
||||
},
|
||||
"notescape": {
|
||||
Markdown: "foo\\x",
|
||||
ExpectedRanges: []Range{{0, 5}},
|
||||
ExpectedValues: []string{"foo\\x"},
|
||||
},
|
||||
"notlink": {
|
||||
Markdown: "[foo",
|
||||
ExpectedRanges: []Range{{0, 4}},
|
||||
ExpectedValues: []string{"[foo"},
|
||||
},
|
||||
"notlinkend": {
|
||||
Markdown: "[foo]",
|
||||
ExpectedRanges: []Range{{0, 5}},
|
||||
ExpectedValues: []string{"[foo]"},
|
||||
},
|
||||
"notimage": {
|
||||
Markdown: "![foo",
|
||||
ExpectedRanges: []Range{{0, 5}},
|
||||
ExpectedValues: []string{"![foo"},
|
||||
},
|
||||
"notimage2": {
|
||||
Markdown: "!foo",
|
||||
ExpectedRanges: []Range{{0, 4}},
|
||||
ExpectedValues: []string{"!foo"},
|
||||
},
|
||||
"charref": {
|
||||
Markdown: ""test",
|
||||
ExpectedRanges: []Range{{0, 1}, {6, 10}},
|
||||
ExpectedValues: []string{"\"", "test"},
|
||||
},
|
||||
"notcharref": {
|
||||
Markdown: "& test",
|
||||
ExpectedRanges: []Range{{0, 9}},
|
||||
ExpectedValues: []string{"& test"},
|
||||
},
|
||||
"notcharref2": {
|
||||
Markdown: "this is &mattermost;",
|
||||
ExpectedRanges: []Range{{0, 20}},
|
||||
ExpectedValues: []string{"this is &mattermost;"},
|
||||
},
|
||||
"standalone-ampersand": {
|
||||
Markdown: "Hello & World",
|
||||
ExpectedRanges: []Range{{0, 13}},
|
||||
ExpectedValues: []string{"Hello & World"},
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var ranges []Range
|
||||
var values []string
|
||||
Inspect(tc.Markdown, func(node any) bool {
|
||||
if textNode, ok := node.(*Text); ok {
|
||||
ranges = append(ranges, textNode.Range)
|
||||
values = append(values, textNode.Text)
|
||||
}
|
||||
return true
|
||||
})
|
||||
assert.Equal(t, tc.ExpectedRanges, ranges)
|
||||
assert.Equal(t, tc.ExpectedValues, values)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin/plugintest/mock"
|
||||
"github.com/mattermost/mattermost-server/server/v8/channels/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost-server/server/v8/plugin/plugintest/mock"
|
||||
)
|
||||
|
||||
func TestGenerateSecret(t *testing.T) {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package mlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// defaultLog manually encodes the log to STDERR, providing a basic, default logging implementation
|
||||
// before mlog is fully configured.
|
||||
func defaultLog(level Level, msg string, fields ...Field) {
|
||||
mFields := make(map[string]string)
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
for _, fld := range fields {
|
||||
buf.Reset()
|
||||
fld.ValueString(buf, shouldQuote)
|
||||
mFields[fld.Key] = buf.String()
|
||||
}
|
||||
|
||||
log := struct {
|
||||
Level string `json:"level"`
|
||||
Message string `json:"msg"`
|
||||
Fields map[string]string `json:"fields,omitempty"`
|
||||
}{
|
||||
level.Name,
|
||||
msg,
|
||||
mFields,
|
||||
}
|
||||
|
||||
if b, err := json.Marshal(log); err != nil {
|
||||
fmt.Fprintf(os.Stderr, `{"level":"error","msg":"failed to encode log message"}%s`, "\n")
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", b)
|
||||
}
|
||||
}
|
||||
|
||||
func defaultIsLevelEnabled(level Level) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func defaultCustomMultiLog(lvl []Level, msg string, fields ...Field) {
|
||||
for _, level := range lvl {
|
||||
defaultLog(level, msg, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
// shouldQuote returns true if val contains any characters that require quotations.
|
||||
func shouldQuote(val string) bool {
|
||||
for _, c := range val {
|
||||
if !((c >= '0' && c <= '9') ||
|
||||
(c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
c == '-' || c == '.' || c == '_' || c == '/' || c == '@' || c == '^' || c == '+') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Ссылка в новой задаче
Block a user