mirror of
https://github.com/anyproto/anytype-heart.git
synced 2025-06-10 18:10:49 +09:00
human readable filenames + numbered lists + filter deleted pages
This commit is contained in:
parent
fef51f1aa5
commit
283a32d8cc
13 changed files with 175 additions and 117 deletions
|
@ -3,19 +3,13 @@ package export
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"github.com/anytypeio/go-anytype-middleware/core/converter/dot"
|
||||
"github.com/anytypeio/go-anytype-middleware/core/converter/graphjson"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/anytypeio/go-anytype-middleware/app"
|
||||
"github.com/anytypeio/go-anytype-middleware/core/block"
|
||||
sb "github.com/anytypeio/go-anytype-middleware/core/block/editor/smartblock"
|
||||
"github.com/anytypeio/go-anytype-middleware/core/block/process"
|
||||
"github.com/anytypeio/go-anytype-middleware/core/converter"
|
||||
"github.com/anytypeio/go-anytype-middleware/core/converter/dot"
|
||||
"github.com/anytypeio/go-anytype-middleware/core/converter/graphjson"
|
||||
"github.com/anytypeio/go-anytype-middleware/core/converter/md"
|
||||
"github.com/anytypeio/go-anytype-middleware/core/converter/pbc"
|
||||
"github.com/anytypeio/go-anytype-middleware/core/converter/pbjson"
|
||||
|
@ -27,7 +21,15 @@ import (
|
|||
"github.com/anytypeio/go-anytype-middleware/pkg/lib/logging"
|
||||
"github.com/anytypeio/go-anytype-middleware/pkg/lib/pb/model"
|
||||
"github.com/anytypeio/go-anytype-middleware/util/pbtypes"
|
||||
"github.com/anytypeio/go-anytype-middleware/util/text"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"github.com/gogo/protobuf/types"
|
||||
"github.com/gosimple/slug"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const CName = "export"
|
||||
|
@ -71,7 +73,7 @@ func (e *export) Export(req pb.RpcExportRequest) (path string, succeed int, err
|
|||
}
|
||||
defer queue.Stop(err)
|
||||
|
||||
docIds, err := e.idsForExport(req.DocIds, req.IncludeNested)
|
||||
docs, err := e.docsForExport(req.DocIds, req.IncludeNested)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -96,24 +98,24 @@ func (e *export) Export(req pb.RpcExportRequest) (path string, succeed int, err
|
|||
format = dot.ExportFormatSVG
|
||||
}
|
||||
mc := dot.NewMultiConverter(format)
|
||||
mc.SetKnownLinks(docIds)
|
||||
mc.SetKnownDocs(docs)
|
||||
var werr error
|
||||
if succeed, werr = e.writeMultiDoc(mc, wr, docIds, queue); werr != nil {
|
||||
if succeed, werr = e.writeMultiDoc(mc, wr, docs, queue); werr != nil {
|
||||
log.Warnf("can't export docs: %v", werr)
|
||||
}
|
||||
} else if req.Format == pb.RpcExport_GRAPH_JSON {
|
||||
mc := graphjson.NewMultiConverter()
|
||||
mc.SetKnownLinks(docIds)
|
||||
mc.SetKnownDocs(docs)
|
||||
var werr error
|
||||
if succeed, werr = e.writeMultiDoc(mc, wr, docIds, queue); werr != nil {
|
||||
if succeed, werr = e.writeMultiDoc(mc, wr, docs, queue); werr != nil {
|
||||
log.Warnf("can't export docs: %v", werr)
|
||||
}
|
||||
} else {
|
||||
for _, docId := range docIds {
|
||||
for docId := range docs {
|
||||
did := docId
|
||||
if err = queue.Wait(func() {
|
||||
log.With("threadId", did).Debugf("write doc")
|
||||
if werr := e.writeDoc(req.Format, wr, docIds, queue, did, req.IncludeFiles); werr != nil {
|
||||
if werr := e.writeDoc(req.Format, wr, docs, queue, did, req.IncludeFiles); werr != nil {
|
||||
log.With("threadId", did).Warnf("can't export doc: %v", werr)
|
||||
} else {
|
||||
succeed++
|
||||
|
@ -132,7 +134,8 @@ func (e *export) Export(req pb.RpcExportRequest) (path string, succeed int, err
|
|||
return wr.Path(), succeed, nil
|
||||
}
|
||||
|
||||
func (e *export) idsForExport(reqIds []string, includeNested bool) (ids []string, err error) {
|
||||
func (e *export) docsForExport(reqIds []string, includeNested bool) (docs map[string]*types.Struct, err error) {
|
||||
docs = make(map[string]*types.Struct)
|
||||
if len(reqIds) == 0 {
|
||||
var res []*model.ObjectInfo
|
||||
res, _, err = e.a.ObjectStore().QueryObjectInfo(database.Query{
|
||||
|
@ -142,6 +145,11 @@ func (e *export) idsForExport(reqIds []string, includeNested bool) (ids []string
|
|||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Bool(false),
|
||||
},
|
||||
{
|
||||
RelationKey: bundle.RelationKeyIsDeleted.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Bool(false),
|
||||
},
|
||||
},
|
||||
}, []smartblock.SmartBlockType{
|
||||
smartblock.SmartBlockTypeHome,
|
||||
|
@ -153,17 +161,11 @@ func (e *export) idsForExport(reqIds []string, includeNested bool) (ids []string
|
|||
}
|
||||
|
||||
for _, r := range res {
|
||||
ids = append(ids, r.Id)
|
||||
docs[r.Id] = r.Details
|
||||
}
|
||||
return ids, nil
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
var m map[string]struct{}
|
||||
if includeNested {
|
||||
m = make(map[string]struct{}, len(reqIds)*10)
|
||||
} else {
|
||||
m = make(map[string]struct{}, len(reqIds))
|
||||
}
|
||||
var getNested func(id string)
|
||||
getNested = func(id string) {
|
||||
links, err := e.a.ObjectStore().GetOutboundLinksById(id)
|
||||
|
@ -172,7 +174,7 @@ func (e *export) idsForExport(reqIds []string, includeNested bool) (ids []string
|
|||
return
|
||||
}
|
||||
for _, link := range links {
|
||||
if _, exists := m[link]; !exists {
|
||||
if _, exists := docs[link]; !exists {
|
||||
sbt, err2 := smartblock.SmartBlockTypeFromID(link)
|
||||
if err2 != nil {
|
||||
log.Errorf("failed to get smartblocktype of id %s", link)
|
||||
|
@ -181,28 +183,54 @@ func (e *export) idsForExport(reqIds []string, includeNested bool) (ids []string
|
|||
if sbt != smartblock.SmartBlockTypePage && sbt != smartblock.SmartBlockTypeSet {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, link)
|
||||
m[link] = struct{}{}
|
||||
getNested(link)
|
||||
rec, _ := e.a.ObjectStore().QueryById(links)
|
||||
if len(rec) > 0 {
|
||||
docs[link] = rec[0].Details
|
||||
getNested(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range reqIds {
|
||||
if _, exists := m[id]; !exists {
|
||||
ids = append(ids, id)
|
||||
m[id] = struct{}{}
|
||||
if includeNested {
|
||||
if len(reqIds) > 0 {
|
||||
var res []*model.ObjectInfo
|
||||
res, _, err = e.a.ObjectStore().QueryObjectInfo(database.Query{
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyId.String(),
|
||||
Condition: model.BlockContentDataviewFilter_In,
|
||||
Value: pbtypes.StringList(reqIds),
|
||||
},
|
||||
{
|
||||
RelationKey: bundle.RelationKeyIsArchived.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Bool(false),
|
||||
},
|
||||
{
|
||||
RelationKey: bundle.RelationKeyIsDeleted.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Bool(false),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var ids []string
|
||||
for _, r := range res {
|
||||
docs[r.Id] = r.Details
|
||||
ids = append(ids, r.Id)
|
||||
}
|
||||
if includeNested {
|
||||
for _, id := range ids {
|
||||
getNested(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (e *export) writeMultiDoc(mw converter.MultiConverter, wr writer, docIds []string, queue process.Queue) (succeed int, err error) {
|
||||
for _, did := range docIds {
|
||||
func (e *export) writeMultiDoc(mw converter.MultiConverter, wr writer, docs map[string]*types.Struct, queue process.Queue) (succeed int, err error) {
|
||||
for did := range docs {
|
||||
if err = queue.Wait(func() {
|
||||
log.With("threadId", did).Debugf("write doc")
|
||||
werr := e.bs.Do(did, func(b sb.SmartBlock) error {
|
||||
|
@ -248,12 +276,14 @@ func (e *export) writeMultiDoc(mw converter.MultiConverter, wr writer, docIds []
|
|||
return
|
||||
}
|
||||
|
||||
func (e *export) writeDoc(format pb.RpcExportFormat, wr writer, docIds []string, queue process.Queue, docId string, exportFiles bool) (err error) {
|
||||
|
||||
func (e *export) writeDoc(format pb.RpcExportFormat, wr writer, docInfo map[string]*types.Struct, queue process.Queue, docId string, exportFiles bool) (err error) {
|
||||
return e.bs.Do(docId, func(b sb.SmartBlock) error {
|
||||
if pbtypes.GetBool(b.CombinedDetails(), bundle.RelationKeyIsArchived.String()) {
|
||||
return nil
|
||||
}
|
||||
if pbtypes.GetBool(b.CombinedDetails(), bundle.RelationKeyIsDeleted.String()) {
|
||||
return nil
|
||||
}
|
||||
var conv converter.Converter
|
||||
switch format {
|
||||
case pb.RpcExport_Markdown:
|
||||
|
@ -263,9 +293,17 @@ func (e *export) writeDoc(format pb.RpcExportFormat, wr writer, docIds []string,
|
|||
case pb.RpcExport_JSON:
|
||||
conv = pbjson.NewConverter(b)
|
||||
}
|
||||
conv.SetKnownLinks(docIds)
|
||||
conv.SetKnownDocs(docInfo)
|
||||
result := conv.Convert()
|
||||
filename := docId + conv.Ext()
|
||||
if format == pb.RpcExport_Markdown {
|
||||
s := b.NewState()
|
||||
name := pbtypes.GetString(s.Details(), bundle.RelationKeyName.String())
|
||||
if name == "" {
|
||||
name = s.Snippet()
|
||||
}
|
||||
filename = wr.Namer().Get("", docId, name, conv.Ext())
|
||||
}
|
||||
if docId == e.a.PredefinedBlocks().Home {
|
||||
filename = "index" + conv.Ext()
|
||||
}
|
||||
|
@ -304,7 +342,8 @@ func (e *export) saveFile(wr writer, hash string) (err error) {
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
filename := filepath.Join("files", wr.Namer().Get(hash, file.Meta().Name))
|
||||
origName := file.Meta().Name
|
||||
filename := wr.Namer().Get("files", hash, filepath.Base(origName), filepath.Ext(origName))
|
||||
rd, err := file.Reader()
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -321,7 +360,8 @@ func (e *export) saveImage(wr writer, hash string) (err error) {
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
filename := filepath.Join("files", wr.Namer().Get(hash, orig.Meta().Name))
|
||||
origName := orig.Meta().Name
|
||||
filename := wr.Namer().Get("files", hash, filepath.Base(origName), filepath.Ext(origName))
|
||||
rd, err := orig.Reader()
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -329,40 +369,34 @@ func (e *export) saveImage(wr writer, hash string) (err error) {
|
|||
return wr.WriteFile(filename, rd)
|
||||
}
|
||||
|
||||
func newNamer() *fileNamer {
|
||||
return &fileNamer{
|
||||
func newNamer() *namer {
|
||||
return &namer{
|
||||
names: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
type fileNamer struct {
|
||||
type namer struct {
|
||||
// id -> name and name -> id
|
||||
names map[string]string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (fn *fileNamer) Get(hash, title string) (name string) {
|
||||
const fileLenLimit = 30
|
||||
func (fn *namer) Get(path, hash, title, ext string) (name string) {
|
||||
const fileLenLimit = 48
|
||||
fn.mu.Lock()
|
||||
defer fn.mu.Unlock()
|
||||
var ok bool
|
||||
if name, ok = fn.names[hash]; ok {
|
||||
return name
|
||||
}
|
||||
if l := utf8.RuneCountInString(title); l > fileLenLimit {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
for i := l - fileLenLimit; i < l; i++ {
|
||||
buf.WriteRune([]rune(title)[i])
|
||||
}
|
||||
name = buf.String()
|
||||
} else {
|
||||
name = title
|
||||
}
|
||||
title = slug.Make(strings.TrimSuffix(title, ext))
|
||||
name = text.Truncate(title, fileLenLimit)
|
||||
name = strings.TrimSuffix(name, text.TruncateEllipsis)
|
||||
var (
|
||||
i = 0
|
||||
b = 36
|
||||
)
|
||||
gname := name
|
||||
gname := filepath.Join(path, name+ext)
|
||||
for {
|
||||
if _, ok = fn.names[gname]; !ok {
|
||||
fn.names[hash] = gname
|
||||
|
@ -371,6 +405,6 @@ func (fn *fileNamer) Get(hash, title string) (name string) {
|
|||
}
|
||||
i++
|
||||
n := int64(i * b)
|
||||
gname = strconv.FormatInt(rand.Int63n(n), b) + "_" + name
|
||||
gname = filepath.Join(path, name+"_"+strconv.FormatInt(rand.Int63n(n), b)+ext)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package export
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -11,15 +12,19 @@ func TestFileNamer_Get(t *testing.T) {
|
|||
fn := newNamer()
|
||||
names := make(map[string]bool)
|
||||
nl := []string{
|
||||
"some_long_name_12345678901234567890.jpg",
|
||||
"some_long_name_12345678901234567890.jpg",
|
||||
"files/some_long_name_12345678901234567890.jpg",
|
||||
"files/some_long_name_12345678901234567890.jpg",
|
||||
"some_long_name_12345678901234567890.jpg",
|
||||
"one.png",
|
||||
"two.png",
|
||||
"two.png",
|
||||
"сделай норм!.pdf",
|
||||
"some very long name maybe note or just unreal long title.md",
|
||||
"some very long name maybe note or just unreal long title.md",
|
||||
}
|
||||
for i, v := range nl {
|
||||
nm := fn.Get(fmt.Sprint(i), v)
|
||||
nm := fn.Get(filepath.Dir(v), fmt.Sprint(i), filepath.Base(v), filepath.Ext(v))
|
||||
t.Log(nm)
|
||||
names[nm] = true
|
||||
assert.NotEmpty(t, nm, v)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
type writer interface {
|
||||
Path() string
|
||||
Namer() *fileNamer
|
||||
Namer() *namer
|
||||
WriteFile(filename string, r io.Reader) (err error)
|
||||
Close() (err error)
|
||||
}
|
||||
|
@ -33,11 +33,11 @@ func newDirWriter(path string) (writer, error) {
|
|||
|
||||
type dirWriter struct {
|
||||
path string
|
||||
fn *fileNamer
|
||||
fn *namer
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func (d *dirWriter) Namer() *fileNamer {
|
||||
func (d *dirWriter) Namer() *namer {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.fn == nil {
|
||||
|
@ -85,10 +85,10 @@ type zipWriter struct {
|
|||
zw *zip.Writer
|
||||
f io.Closer
|
||||
m sync.Mutex
|
||||
fn *fileNamer
|
||||
fn *namer
|
||||
}
|
||||
|
||||
func (d *zipWriter) Namer() *fileNamer {
|
||||
func (d *zipWriter) Namer() *namer {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.fn == nil {
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package converter
|
||||
|
||||
import "github.com/anytypeio/go-anytype-middleware/core/block/editor/state"
|
||||
import (
|
||||
"github.com/anytypeio/go-anytype-middleware/core/block/editor/state"
|
||||
"github.com/gogo/protobuf/types"
|
||||
)
|
||||
|
||||
type Converter interface {
|
||||
Convert() (result []byte)
|
||||
SetKnownLinks(ids []string) Converter
|
||||
SetKnownDocs(docs map[string]*types.Struct) Converter
|
||||
FileHashes() []string
|
||||
ImageHashes() []string
|
||||
Ext() string
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
//go:build !gomobile && !windows && !nographviz
|
||||
// +build !gomobile,!windows,!nographviz
|
||||
|
||||
package dot
|
||||
|
@ -5,6 +6,7 @@ package dot
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/gogo/protobuf/types"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/goccy/go-graphviz"
|
||||
|
@ -16,7 +18,6 @@ import (
|
|||
"github.com/anytypeio/go-anytype-middleware/pkg/lib/core/smartblock"
|
||||
"github.com/anytypeio/go-anytype-middleware/pkg/lib/pb/model"
|
||||
"github.com/anytypeio/go-anytype-middleware/util/pbtypes"
|
||||
"github.com/anytypeio/go-anytype-middleware/util/slice"
|
||||
)
|
||||
|
||||
func NewMultiConverter(format graphviz.Format) converter.MultiConverter {
|
||||
|
@ -51,7 +52,7 @@ type linkInfo struct {
|
|||
type dot struct {
|
||||
graph *cgraph.Graph
|
||||
graphviz *graphviz.Graphviz
|
||||
knownIds []string
|
||||
knownDocs map[string]*types.Struct
|
||||
fileHashes []string
|
||||
imageHashes []string
|
||||
exportFormat graphviz.Format
|
||||
|
@ -59,8 +60,8 @@ type dot struct {
|
|||
linksByNode map[string][]linkInfo
|
||||
}
|
||||
|
||||
func (d *dot) SetKnownLinks(ids []string) converter.Converter {
|
||||
d.knownIds = ids
|
||||
func (d *dot) SetKnownDocs(docs map[string]*types.Struct) converter.Converter {
|
||||
d.knownDocs = docs
|
||||
return d
|
||||
}
|
||||
|
||||
|
@ -115,7 +116,7 @@ func (d *dot) Add(st *state.State) error {
|
|||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if slice.FindPos(d.knownIds, objId) == -1 {
|
||||
if _, ok := d.knownDocs[objId]; !ok {
|
||||
continue
|
||||
}
|
||||
if t != smartblock.SmartBlockTypeAnytypeProfile && t != smartblock.SmartBlockTypePage {
|
||||
|
@ -137,7 +138,7 @@ func (d *dot) Add(st *state.State) error {
|
|||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if slice.FindPos(d.knownIds, depId) == -1 {
|
||||
if _, ok := d.knownDocs[depId]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/anytypeio/go-anytype-middleware/pkg/lib/core/smartblock"
|
||||
"github.com/anytypeio/go-anytype-middleware/pkg/lib/pb/model"
|
||||
"github.com/anytypeio/go-anytype-middleware/util/pbtypes"
|
||||
"github.com/anytypeio/go-anytype-middleware/util/slice"
|
||||
"github.com/gogo/protobuf/types"
|
||||
)
|
||||
|
||||
func NewMultiConverter() converter.MultiConverter {
|
||||
|
@ -52,15 +52,15 @@ type Graph struct {
|
|||
}
|
||||
|
||||
type graphjson struct {
|
||||
knownIds []string
|
||||
knownDocs map[string]*types.Struct
|
||||
fileHashes []string
|
||||
imageHashes []string
|
||||
nodes map[string]*Node
|
||||
linksByNode map[string][]*Edge
|
||||
}
|
||||
|
||||
func (g *graphjson) SetKnownLinks(ids []string) converter.Converter {
|
||||
g.knownIds = ids
|
||||
func (g *graphjson) SetKnownDocs(docs map[string]*types.Struct) converter.Converter {
|
||||
g.knownDocs = docs
|
||||
return g
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ func (g *graphjson) Add(st *state.State) error {
|
|||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if slice.FindPos(g.knownIds, objId) == -1 {
|
||||
if _, ok := g.knownDocs[objId]; !ok {
|
||||
continue
|
||||
}
|
||||
if t != smartblock.SmartBlockTypeAnytypeProfile && t != smartblock.SmartBlockTypePage {
|
||||
|
@ -121,7 +121,7 @@ func (g *graphjson) Add(st *state.State) error {
|
|||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if slice.FindPos(g.knownIds, depId) == -1 {
|
||||
if _, ok := g.knownDocs[depId]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,11 @@ package md
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/anytypeio/go-anytype-middleware/pkg/lib/bundle"
|
||||
"github.com/gogo/protobuf/types"
|
||||
"html"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
|
@ -14,11 +17,10 @@ import (
|
|||
"github.com/anytypeio/go-anytype-middleware/pkg/lib/core"
|
||||
"github.com/anytypeio/go-anytype-middleware/pkg/lib/pb/model"
|
||||
"github.com/anytypeio/go-anytype-middleware/util/pbtypes"
|
||||
"github.com/anytypeio/go-anytype-middleware/util/slice"
|
||||
)
|
||||
|
||||
type FileNamer interface {
|
||||
Get(hash, title string) (name string)
|
||||
Get(path, hash, title, ext string) (name string)
|
||||
}
|
||||
|
||||
func NewMDConverter(a core.Service, s *state.State, fn FileNamer) converter.Converter {
|
||||
|
@ -33,9 +35,10 @@ type MD struct {
|
|||
fileHashes []string
|
||||
imageHashes []string
|
||||
|
||||
mw *marksWriter
|
||||
knownLinks []string
|
||||
fn FileNamer
|
||||
knownDocs map[string]*types.Struct
|
||||
|
||||
mw *marksWriter
|
||||
fn FileNamer
|
||||
}
|
||||
|
||||
func (h *MD) Convert() (result []byte) {
|
||||
|
@ -64,6 +67,7 @@ func (h *MD) Ext() string {
|
|||
type renderState struct {
|
||||
indent string
|
||||
listOpened bool
|
||||
listNumber int
|
||||
}
|
||||
|
||||
func (in renderState) AddNBSpace() *renderState {
|
||||
|
@ -71,7 +75,7 @@ func (in renderState) AddNBSpace() *renderState {
|
|||
}
|
||||
|
||||
func (in renderState) AddSpace() *renderState {
|
||||
return &renderState{indent: in.indent + " "}
|
||||
return &renderState{indent: in.indent + " "}
|
||||
}
|
||||
|
||||
func (h *MD) render(b *model.Block, in *renderState) {
|
||||
|
@ -123,6 +127,7 @@ func (h *MD) renderText(b *model.Block, in *renderState) {
|
|||
if in.listOpened && text.Style != model.BlockContentText_Marked && text.Style != model.BlockContentText_Numbered {
|
||||
h.buf.WriteString(" \n")
|
||||
in.listOpened = false
|
||||
in.listNumber = 0
|
||||
}
|
||||
|
||||
h.buf.WriteString(in.indent)
|
||||
|
@ -168,7 +173,8 @@ func (h *MD) renderText(b *model.Block, in *renderState) {
|
|||
h.renderChilds(b, in.AddSpace())
|
||||
in.listOpened = true
|
||||
case model.BlockContentText_Numbered:
|
||||
h.buf.WriteString(`1. `)
|
||||
in.listNumber++
|
||||
h.buf.WriteString(fmt.Sprintf(`%d. `, in.listNumber))
|
||||
renderText()
|
||||
h.renderChilds(b, in.AddSpace())
|
||||
in.listOpened = true
|
||||
|
@ -186,10 +192,10 @@ func (h *MD) renderFile(b *model.Block, in *renderState) {
|
|||
name := escape.MarkdownCharacters(html.EscapeString(file.Name))
|
||||
h.buf.WriteString(in.indent)
|
||||
if file.Type != model.BlockContentFile_Image {
|
||||
fmt.Fprintf(h.buf, "[%s](files/%s) \n", name, url.PathEscape(h.fn.Get(file.Hash, file.Name)))
|
||||
fmt.Fprintf(h.buf, "[%s](%s) \n", name, h.fn.Get("files", file.Hash, filepath.Base(file.Name), filepath.Ext(file.Name)))
|
||||
h.fileHashes = append(h.fileHashes, file.Hash)
|
||||
} else {
|
||||
fmt.Fprintf(h.buf, " \n", name, url.PathEscape(h.fn.Get(file.Hash, file.Name)))
|
||||
fmt.Fprintf(h.buf, " \n", name, h.fn.Get("files", file.Hash, filepath.Base(file.Name), filepath.Ext(file.Name)))
|
||||
h.imageHashes = append(h.imageHashes, file.Hash)
|
||||
}
|
||||
}
|
||||
|
@ -228,18 +234,12 @@ func (h *MD) renderLayout(b *model.Block, in *renderState) {
|
|||
|
||||
func (h *MD) renderLink(b *model.Block, in *renderState) {
|
||||
l := b.GetLink()
|
||||
h.a.ObjectStore().GetByIDs()
|
||||
if l != nil && l.TargetBlockId != "" && h.isKnownLink(l.TargetBlockId) {
|
||||
var title string
|
||||
ois, err := h.a.ObjectStore().GetByIDs(l.TargetBlockId)
|
||||
if err == nil && len(ois) > 0 {
|
||||
title = pbtypes.GetString(ois[0].Details, "name")
|
||||
if l != nil && l.TargetBlockId != "" {
|
||||
title, filename, ok := h.getLinkInfo(l.TargetBlockId)
|
||||
if ok {
|
||||
h.buf.WriteString(in.indent)
|
||||
fmt.Fprintf(h.buf, "[%s](%s) \n", escape.MarkdownCharacters(html.EscapeString(title)), filename)
|
||||
}
|
||||
if title == "" {
|
||||
title = l.TargetBlockId
|
||||
}
|
||||
h.buf.WriteString(in.indent)
|
||||
fmt.Fprintf(h.buf, "[%s](%s) \n", escape.MarkdownCharacters(html.EscapeString(title)), l.TargetBlockId+".md")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -260,13 +260,25 @@ func (h *MD) marksWriter(text *model.BlockContentText) *marksWriter {
|
|||
return h.mw.Init(text)
|
||||
}
|
||||
|
||||
func (h *MD) SetKnownLinks(ids []string) converter.Converter {
|
||||
h.knownLinks = ids
|
||||
func (h *MD) SetKnownDocs(docs map[string]*types.Struct) converter.Converter {
|
||||
h.knownDocs = docs
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *MD) isKnownLink(docId string) bool {
|
||||
return slice.FindPos(h.knownLinks, docId) != -1
|
||||
func (h *MD) getLinkInfo(docId string) (title, filename string, ok bool) {
|
||||
info, ok := h.knownDocs[docId]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
title = pbtypes.GetString(info, bundle.RelationKeyName.String())
|
||||
if title == "" {
|
||||
title = pbtypes.GetString(info, bundle.RelationKeySnippet.String())
|
||||
}
|
||||
if title == "" {
|
||||
title = docId
|
||||
}
|
||||
filename = h.fn.Get("", docId, title, h.Ext())
|
||||
return
|
||||
}
|
||||
|
||||
type marksWriter struct {
|
||||
|
@ -299,11 +311,12 @@ func (mw *marksWriter) writeMarks(pos int) {
|
|||
fmt.Fprintf(mw.h.buf, "](%s)", urlS)
|
||||
}
|
||||
case model.BlockContentTextMark_Mention, model.BlockContentTextMark_Object:
|
||||
if mw.h.isKnownLink(m.Param) {
|
||||
_, filename, ok := mw.h.getLinkInfo(m.Param)
|
||||
if ok {
|
||||
if start {
|
||||
mw.h.buf.WriteString("[")
|
||||
} else {
|
||||
fmt.Fprintf(mw.h.buf, "](%s)", m.Param+".md")
|
||||
fmt.Fprintf(mw.h.buf, "](%s)", filename)
|
||||
}
|
||||
}
|
||||
case model.BlockContentTextMark_Keyboard:
|
||||
|
|
|
@ -65,20 +65,14 @@ func TestMD_Convert(t *testing.T) {
|
|||
Range: &model.Range{12, 18},
|
||||
Type: model.BlockContentTextMark_Strikethrough,
|
||||
},
|
||||
{
|
||||
Range: &model.Range{21, 29},
|
||||
Type: model.BlockContentTextMark_Mention,
|
||||
Param: "some_page_id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
c := NewMDConverter(nil, s, nil)
|
||||
c.SetKnownLinks([]string{"some_page_id"})
|
||||
res := c.Convert()
|
||||
exp := "***[some](http://golang.org)*** [t](http://golang.org) [e](http://golang.org)xt **wi~~th m~~**~~ar~~ks [@mention](some_page_id.md) \n"
|
||||
exp := "***[some](http://golang.org)*** [t](http://golang.org) [e](http://golang.org)xt **wi~~th m~~**~~ar~~ks @mention \n"
|
||||
assert.Equal(t, exp, string(res))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"github.com/anytypeio/go-anytype-middleware/core/converter"
|
||||
"github.com/anytypeio/go-anytype-middleware/pb"
|
||||
"github.com/anytypeio/go-anytype-middleware/pkg/lib/pb/model"
|
||||
"github.com/gogo/protobuf/types"
|
||||
)
|
||||
|
||||
func NewConverter(s state.Doc) converter.Converter {
|
||||
|
@ -37,7 +38,7 @@ func (p *pbc) Ext() string {
|
|||
return ".pb"
|
||||
}
|
||||
|
||||
func (p *pbc) SetKnownLinks(ids []string) converter.Converter {
|
||||
func (p *pbc) SetKnownDocs(map[string]*types.Struct) converter.Converter {
|
||||
return p
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/anytypeio/go-anytype-middleware/pb"
|
||||
"github.com/anytypeio/go-anytype-middleware/pkg/lib/pb/model"
|
||||
"github.com/gogo/protobuf/jsonpb"
|
||||
"github.com/gogo/protobuf/types"
|
||||
)
|
||||
|
||||
func NewConverter(s state.Doc) converter.Converter {
|
||||
|
@ -39,7 +40,7 @@ func (p *pbj) Ext() string {
|
|||
return ".pb.json"
|
||||
}
|
||||
|
||||
func (p *pbj) SetKnownLinks(ids []string) converter.Converter {
|
||||
func (p *pbj) SetKnownDocs(map[string]*types.Struct) converter.Converter {
|
||||
return p
|
||||
}
|
||||
|
||||
|
|
1
go.mod
1
go.mod
|
@ -26,6 +26,7 @@ require (
|
|||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gosimple/slug v1.12.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645
|
||||
|
|
4
go.sum
4
go.sum
|
@ -514,6 +514,10 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA
|
|||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gosimple/slug v1.12.0 h1:xzuhj7G7cGtd34NXnW/yF0l+AGNfWqwgh/IXgFy7dnc=
|
||||
github.com/gosimple/slug v1.12.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
|
||||
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
|
||||
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
|
|
|
@ -5,8 +5,9 @@ import (
|
|||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const TruncateEllipsis = " …"
|
||||
|
||||
func Truncate(text string, length int) string {
|
||||
var ellipsis = " …"
|
||||
if utf8.RuneCountInString(text) <= length {
|
||||
return text
|
||||
}
|
||||
|
@ -27,7 +28,7 @@ func Truncate(text string, length int) string {
|
|||
endTextPos = lastWordIndex
|
||||
}
|
||||
out := text[0:endTextPos]
|
||||
return out + ellipsis
|
||||
return out + TruncateEllipsis
|
||||
}
|
||||
}
|
||||
return text
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue