浏览代码

Merge branch 'master' of https://github.com/gsw945/mindoc into master

gsw945 3 年之前
父节点
当前提交
8ff4c28927
共有 12 个文件被更改,包括 690 次插入37 次删除
  1. 3 3
      README.md
  2. 1 1
      conf/lang/zh-cn.ini
  3. 8 4
      controllers/BookController.go
  4. 25 22
      docker-compose.yml
  5. 1 0
      go.mod
  6. 4 0
      go.sum
  7. 73 1
      models/BookModel.go
  8. 2 2
      static/css/kancloud.css
  9. 571 0
      utils/docx2md.go
  10. 0 1
      views/blog/index.tpl
  11. 2 2
      views/book/index.tpl
  12. 0 1
      views/document/default_read.tpl

+ 3 - 3
README.md

@@ -11,9 +11,9 @@ MinDoc 的前身是 [SmartWiki](https://github.com/lifei6671/SmartWiki) 文档
 
 可以用来储存日常接口文档,数据库字典,手册说明等文档。内置项目管理,用户管理,权限管理等功能,能够满足大部分中小团队的文档管理需求。
 
-##### 演示站点:
-- [https://www.iminho.me/wiki/](https://www.iminho.me/wiki/)
-- https://doc.gsw945.com/
+##### 演示站点&文档:
+- https://www.iminho.me/wiki/docs/mindoc/
+- https://doc.gsw945.com/docs/mindoc-docs/
 
 ---
 

+ 1 - 1
conf/lang/zh-cn.ini

@@ -127,7 +127,7 @@ project_id_existed = 文档标识已被使用
 project_id_error = 项目标识有误
 project_id_length = 项目标识必须小于50字符
 import_file_empty = 请选择需要上传的文件
-file_type_placeholder = 请选择Zip文件
+file_type_placeholder = 请选择Zip或Docx文件
 publish_to_queue = 发布任务已推送到任务队列,稍后将在后台执行。
 team_name_empty = 团队名称不能为空
 operate_failed = 操作失败

+ 8 - 4
controllers/BookController.go

@@ -340,7 +340,7 @@ func (c *BookController) UploadCover() {
 	fileName := "cover_" + strconv.FormatInt(time.Now().UnixNano(), 16)
 
 	//附件路径按照项目组织
-// 	filePath := filepath.Join("uploads", book.Identify, "images", fileName+ext)
+	// 	filePath := filepath.Join("uploads", book.Identify, "images", fileName+ext)
 	filePath := filepath.Join(conf.WorkingDirectory, "uploads", book.Identify, "images", fileName+ext)
 
 	path := filepath.Dir(filePath)
@@ -571,7 +571,7 @@ func (c *BookController) Copy() {
 	}
 }
 
-//导入zip压缩包
+// 导入zip压缩包或docx
 func (c *BookController) Import() {
 
 	file, moreFile, err := c.GetFile("import-file")
@@ -608,7 +608,7 @@ func (c *BookController) Import() {
 
 	ext := filepath.Ext(moreFile.Filename)
 
-	if !strings.EqualFold(ext, ".zip") {
+	if !strings.EqualFold(ext, ".zip") && !strings.EqualFold(ext, ".docx") {
 		c.JsonResult(6004, "不支持的文件类型")
 	}
 
@@ -643,7 +643,11 @@ func (c *BookController) Import() {
 	book.Editor = "markdown"
 	book.Theme = "default"
 
-	go book.ImportBook(tempPath, c.Lang)
+	if strings.EqualFold(ext, ".zip") {
+		go book.ImportBook(tempPath, c.Lang)
+	} else if strings.EqualFold(ext, ".docx") {
+		go book.ImportWordBook(tempPath, c.Lang)
+	}
 
 	logs.Info("用户[", c.Member.Account, "]导入了项目 ->", book)
 

+ 25 - 22
docker-compose.yml

@@ -1,22 +1,25 @@
-MinDoc_New:
-  image: registry.cn-hangzhou.aliyuncs.com/mindoc/mindoc:v2.0-beta.5
-  privileged: false
-  restart: always
-  ports:
-    - 8181:8181
-  volumes:
-    - /var/www/mindoc://mindoc-sync-host
-  environment:
-    - MINDOC_RUN_MODE=prod
-    - MINDOC_DB_ADAPTER=sqlite3
-    - MINDOC_DB_DATABASE=./database/mindoc.db
-    - MINDOC_CACHE=true
-    - MINDOC_CACHE_PROVIDER=file
-    - MINDOC_ENABLE_EXPORT=false
-    - MINDOC_BASE_URL=
-    - MINDOC_CDN_IMG_URL=
-    - MINDOC_CDN_CSS_URL=
-    - MINDOC_CDN_JS_URL=
-  dns:
-    - 223.5.5.5
-    - 223.6.6.6
+version: "3"
+services:
+  mindoc:
+    image: registry.cn-hangzhou.aliyuncs.com/mindoc-org/mindoc:v2.1-beta.5
+    container_name: mindoc
+    privileged: false
+    restart: always
+    ports:
+      - 8181:8181
+    volumes:
+      - /var/www/mindoc://mindoc-sync-host
+    environment:
+      - MINDOC_RUN_MODE=prod
+      - MINDOC_DB_ADAPTER=sqlite3
+      - MINDOC_DB_DATABASE=./database/mindoc.db
+      - MINDOC_CACHE=true
+      - MINDOC_CACHE_PROVIDER=file
+      - MINDOC_ENABLE_EXPORT=false
+      - MINDOC_BASE_URL=
+      - MINDOC_CDN_IMG_URL=
+      - MINDOC_CDN_CSS_URL=
+      - MINDOC_CDN_JS_URL=
+    dns:
+      - 223.5.5.5
+      - 223.6.6.6

+ 1 - 0
go.mod

@@ -14,6 +14,7 @@ require (
 	github.com/kardianos/service v1.1.0
 	github.com/lib/pq v1.7.0 // indirect
 	github.com/lifei6671/gocaptcha v0.1.1
+	github.com/mattn/go-runewidth v0.0.13
 	github.com/mattn/go-sqlite3 v2.0.3+incompatible
 	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
 	github.com/russross/blackfriday/v2 v2.1.0

+ 4 - 0
go.sum

@@ -126,6 +126,8 @@ github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY=
 github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lifei6671/gocaptcha v0.1.1 h1:5cvU3w0bK8eJm1P6AiQoPuicoZVAgKKpREBxXF9IaHo=
 github.com/lifei6671/gocaptcha v0.1.1/go.mod h1:6QlTU2WzFhzqylAJWSo3OANfKCraGccJwbK01P5fFmI=
+github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
+github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
 github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
@@ -174,6 +176,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8=
 github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=

+ 73 - 1
models/BookModel.go

@@ -680,7 +680,7 @@ func (book *Book) ResetDocumentNumber(bookId int) {
 	}
 }
 
-//导入项目
+// 导入zip项目
 func (book *Book) ImportBook(zipPath string, lang string) error {
 	if !filetil.FileExists(zipPath) {
 		return errors.New("文件不存在 => " + zipPath)
@@ -978,6 +978,78 @@ func (book *Book) ImportBook(zipPath string, lang string) error {
 	return err
 }
 
+// 导入docx项目
+func (book *Book) ImportWordBook(docxPath string, lang string) (err error) {
+	if !filetil.FileExists(docxPath) {
+		return errors.New("文件不存在")
+	}
+	docxPath = strings.Replace(docxPath, "\\", "/", -1)
+
+	o := orm.NewOrm()
+
+	o.Insert(book)
+	relationship := NewRelationship()
+	relationship.BookId = book.BookId
+	relationship.RoleId = 0
+	relationship.MemberId = book.MemberId
+	err = relationship.Insert()
+	if err != nil {
+		logs.Error("插入项目与用户关联 -> ", err)
+		return err
+	}
+
+	doc := NewDocument()
+	doc.BookId = book.BookId
+	doc.MemberId = book.MemberId
+	docIdentify := strings.Replace(strings.TrimPrefix(docxPath, os.TempDir()+"/"), "/", "-", -1)
+
+	if ok, err := regexp.MatchString(`[a-z]+[a-zA-Z0-9_.\-]*$`, docIdentify); !ok || err != nil {
+		docIdentify = "import-" + docIdentify
+	}
+
+	doc.Identify = docIdentify
+
+	if doc.Markdown, err = utils.Docx2md(docxPath, false); err != nil {
+		logs.Error("导入doc项目转换异常 => ", err)
+		return err
+	}
+
+	// fmt.Println("===doc.Markdown===")
+	// fmt.Println(doc.Markdown)
+	// fmt.Println("==================")
+
+	doc.Content = string(blackfriday.Run([]byte(doc.Markdown)))
+
+	// fmt.Println("===doc.Content===")
+	// fmt.Println(doc.Content)
+	// fmt.Println("==================")
+
+	doc.Version = time.Now().Unix()
+
+	var docName string
+	for _, line := range strings.Split(doc.Markdown, "\n") {
+		if strings.HasPrefix(line, "#") {
+			docName = strings.TrimLeft(line, "#")
+			break
+		}
+	}
+
+	doc.DocumentName = strings.TrimSpace(docName)
+
+	doc.DocumentId = book.MemberId
+
+	if err := doc.InsertOrUpdate("document_name", "book_id", "markdown", "content"); err != nil {
+		logs.Error(doc.DocumentId, err)
+	}
+	if err != nil {
+		logs.Error("导入项目异常 => ", err)
+		book.Description = "【项目导入存在错误:" + err.Error() + "】"
+	}
+	logs.Info("项目导入完毕 => ", book.BookName)
+	book.ReleaseContent(book.BookId, lang)
+	return err
+}
+
 func (book *Book) FindForRoleId(bookId, memberId int) (conf.BookRole, error) {
 	o := orm.NewOrm()
 

+ 2 - 2
static/css/kancloud.css

@@ -265,7 +265,7 @@ table>tbody>tr:hover{
     right: 0;
     overflow-y: auto;
     background-color: #fafafa;
-    margin-bottom: 35px;
+    margin-bottom: 60px;
 }
 
 .m-manual .manual-tab .tab-item.active {
@@ -1173,4 +1173,4 @@ table>tbody>tr:hover{
         opacity: .3;
         z-index: 3000
     }
-}
+}

+ 571 - 0
utils/docx2md.go

@@ -0,0 +1,571 @@
+// https://github.com/mattn/docx2md
+// License MIT
+package utils
+
+import (
+	"archive/zip"
+	"bytes"
+	"encoding/base64"
+	"encoding/xml"
+	"errors"
+	_ "flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"path"
+	"path/filepath"
+	_ "runtime"
+	"strconv"
+	"strings"
+
+	"github.com/mattn/go-runewidth"
+)
+
+// Relationship is
+type Relationship struct {
+	Text       string `xml:",chardata"`
+	ID         string `xml:"Id,attr"`
+	Type       string `xml:"Type,attr"`
+	Target     string `xml:"Target,attr"`
+	TargetMode string `xml:"TargetMode,attr"`
+}
+
+// Relationships is
+type Relationships struct {
+	XMLName      xml.Name       `xml:"Relationships"`
+	Text         string         `xml:",chardata"`
+	Xmlns        string         `xml:"xmlns,attr"`
+	Relationship []Relationship `xml:"Relationship"`
+}
+
+// TextVal is
+type TextVal struct {
+	Text string `xml:",chardata"`
+	Val  string `xml:"val,attr"`
+}
+
+// NumberingLvl is
+type NumberingLvl struct {
+	Text      string  `xml:",chardata"`
+	Ilvl      string  `xml:"ilvl,attr"`
+	Tplc      string  `xml:"tplc,attr"`
+	Tentative string  `xml:"tentative,attr"`
+	Start     TextVal `xml:"start"`
+	NumFmt    TextVal `xml:"numFmt"`
+	LvlText   TextVal `xml:"lvlText"`
+	LvlJc     TextVal `xml:"lvlJc"`
+	PPr       struct {
+		Text string `xml:",chardata"`
+		Ind  struct {
+			Text    string `xml:",chardata"`
+			Left    string `xml:"left,attr"`
+			Hanging string `xml:"hanging,attr"`
+		} `xml:"ind"`
+	} `xml:"pPr"`
+	RPr struct {
+		Text string `xml:",chardata"`
+		U    struct {
+			Text string `xml:",chardata"`
+			Val  string `xml:"val,attr"`
+		} `xml:"u"`
+		RFonts struct {
+			Text string `xml:",chardata"`
+			Hint string `xml:"hint,attr"`
+		} `xml:"rFonts"`
+	} `xml:"rPr"`
+}
+
+// Numbering is
+type Numbering struct {
+	XMLName     xml.Name `xml:"numbering"`
+	Text        string   `xml:",chardata"`
+	Wpc         string   `xml:"wpc,attr"`
+	Cx          string   `xml:"cx,attr"`
+	Cx1         string   `xml:"cx1,attr"`
+	Mc          string   `xml:"mc,attr"`
+	O           string   `xml:"o,attr"`
+	R           string   `xml:"r,attr"`
+	M           string   `xml:"m,attr"`
+	V           string   `xml:"v,attr"`
+	Wp14        string   `xml:"wp14,attr"`
+	Wp          string   `xml:"wp,attr"`
+	W10         string   `xml:"w10,attr"`
+	W           string   `xml:"w,attr"`
+	W14         string   `xml:"w14,attr"`
+	W15         string   `xml:"w15,attr"`
+	W16se       string   `xml:"w16se,attr"`
+	Wpg         string   `xml:"wpg,attr"`
+	Wpi         string   `xml:"wpi,attr"`
+	Wne         string   `xml:"wne,attr"`
+	Wps         string   `xml:"wps,attr"`
+	Ignorable   string   `xml:"Ignorable,attr"`
+	AbstractNum []struct {
+		Text                       string         `xml:",chardata"`
+		AbstractNumID              string         `xml:"abstractNumId,attr"`
+		RestartNumberingAfterBreak string         `xml:"restartNumberingAfterBreak,attr"`
+		Nsid                       TextVal        `xml:"nsid"`
+		MultiLevelType             TextVal        `xml:"multiLevelType"`
+		Tmpl                       TextVal        `xml:"tmpl"`
+		Lvl                        []NumberingLvl `xml:"lvl"`
+	} `xml:"abstractNum"`
+	Num []struct {
+		Text          string  `xml:",chardata"`
+		NumID         string  `xml:"numId,attr"`
+		AbstractNumID TextVal `xml:"abstractNumId"`
+	} `xml:"num"`
+}
+
+type file struct {
+	rels  Relationships
+	num   Numbering
+	r     *zip.ReadCloser
+	embed bool
+	list  map[string]int
+	name  string
+}
+
+// Node is
+type Node struct {
+	XMLName xml.Name
+	Attrs   []xml.Attr `xml:"-"`
+	Content []byte     `xml:",innerxml"`
+	Nodes   []Node     `xml:",any"`
+}
+
+// UnmarshalXML is
+func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+	n.Attrs = start.Attr
+	type node Node
+
+	return d.DecodeElement((*node)(n), &start)
+}
+
+func escape(s, set string) string {
+	replacer := []string{}
+	for _, r := range []rune(set) {
+		rs := string(r)
+		replacer = append(replacer, rs, `\`+rs)
+	}
+	return strings.NewReplacer(replacer...).Replace(s)
+}
+
+func (zf *file) extract(rel *Relationship, w io.Writer) error {
+	err := os.MkdirAll(
+		filepath.Join("uploads",
+			strings.TrimSuffix(zf.name, ".docx"),
+			filepath.Dir(rel.Target)),
+		0755)
+	if err != nil {
+		return err
+	}
+	for _, f := range zf.r.File {
+		if f.Name != "word/"+rel.Target {
+			continue
+		}
+		rc, err := f.Open()
+		if err != nil {
+			return err
+		}
+		defer rc.Close()
+
+		b := make([]byte, f.UncompressedSize64)
+		n, err := rc.Read(b)
+		if err != nil && err != io.EOF {
+			return err
+		}
+		if zf.embed {
+			fmt.Fprintf(w, "![](data:image/png;base64,%s)",
+				base64.StdEncoding.EncodeToString(b[:n]))
+		} else {
+			err = ioutil.WriteFile(
+				filepath.Join("uploads",
+					strings.TrimSuffix(zf.name, ".docx"),
+					rel.Target),
+				b, 0644)
+			if err != nil {
+				return err
+			}
+			fmt.Fprintf(w, "![](%s)", "/"+filepath.Join(
+				"uploads",
+				strings.TrimSuffix(zf.name, ".docx"),
+				escape(rel.Target, "()")))
+		}
+		break
+	}
+	return nil
+}
+
+func attr(attrs []xml.Attr, name string) (string, bool) {
+	for _, attr := range attrs {
+		if attr.Name.Local == name {
+			return attr.Value, true
+		}
+	}
+	return "", false
+}
+
+func (zf *file) walk(node *Node, w io.Writer) error {
+	switch node.XMLName.Local {
+	case "hyperlink":
+		fmt.Fprint(w, "[")
+		var cbuf bytes.Buffer
+		for _, n := range node.Nodes {
+			if err := zf.walk(&n, &cbuf); err != nil {
+				return err
+			}
+		}
+		fmt.Fprint(w, escape(cbuf.String(), "[]"))
+		fmt.Fprint(w, "]")
+
+		fmt.Fprint(w, "(")
+		if id, ok := attr(node.Attrs, "id"); ok {
+			for _, rel := range zf.rels.Relationship {
+				if id == rel.ID {
+					fmt.Fprint(w, escape(rel.Target, "()"))
+					break
+				}
+			}
+		}
+		fmt.Fprint(w, ")")
+	case "t":
+		fmt.Fprint(w, string(node.Content))
+	case "pPr":
+		code := false
+		for _, n := range node.Nodes {
+			switch n.XMLName.Local {
+			case "ind":
+				if left, ok := attr(n.Attrs, "left"); ok {
+					if i, err := strconv.Atoi(left); err == nil && i > 0 {
+						fmt.Fprint(w, strings.Repeat("  ", i/360))
+					}
+				}
+			case "pStyle":
+				if val, ok := attr(n.Attrs, "val"); ok {
+					if strings.HasPrefix(val, "Heading") {
+						if i, err := strconv.Atoi(val[7:]); err == nil && i > 0 {
+							fmt.Fprint(w, strings.Repeat("#", i)+" ")
+						}
+					} else if val == "Code" {
+						code = true
+					} else {
+						if i, err := strconv.Atoi(val); err == nil && i > 0 {
+							fmt.Fprint(w, strings.Repeat("#", i)+" ")
+						}
+					}
+				}
+			case "numPr":
+				numID := ""
+				ilvl := ""
+				numFmt := ""
+				start := 1
+				ind := 0
+				for _, nn := range n.Nodes {
+					if nn.XMLName.Local == "numId" {
+						if val, ok := attr(nn.Attrs, "val"); ok {
+							numID = val
+						}
+					}
+					if nn.XMLName.Local == "ilvl" {
+						if val, ok := attr(nn.Attrs, "val"); ok {
+							ilvl = val
+						}
+					}
+				}
+				for _, num := range zf.num.Num {
+					if numID != num.NumID {
+						continue
+					}
+					for _, abnum := range zf.num.AbstractNum {
+						if abnum.AbstractNumID != num.AbstractNumID.Val {
+							continue
+						}
+						for _, ablvl := range abnum.Lvl {
+							if ablvl.Ilvl != ilvl {
+								continue
+							}
+							if i, err := strconv.Atoi(ablvl.Start.Val); err == nil {
+								start = i
+							}
+							if i, err := strconv.Atoi(ablvl.PPr.Ind.Left); err == nil {
+								ind = i / 360
+							}
+							numFmt = ablvl.NumFmt.Val
+							break
+						}
+						break
+					}
+					break
+				}
+
+				fmt.Fprint(w, strings.Repeat("  ", ind))
+				switch numFmt {
+				case "decimal", "aiueoFullWidth":
+					key := fmt.Sprintf("%s:%d", numID, ind)
+					cur, ok := zf.list[key]
+					if !ok {
+						zf.list[key] = start
+					} else {
+						zf.list[key] = cur + 1
+					}
+					fmt.Fprintf(w, "%d. ", zf.list[key])
+				case "bullet":
+					fmt.Fprint(w, "* ")
+				}
+			}
+		}
+		if code {
+			fmt.Fprint(w, "`")
+		}
+		for _, n := range node.Nodes {
+			if err := zf.walk(&n, w); err != nil {
+				return err
+			}
+		}
+		if code {
+			fmt.Fprint(w, "`")
+		}
+	case "tbl":
+		var rows [][]string
+		for _, tr := range node.Nodes {
+			if tr.XMLName.Local != "tr" {
+				continue
+			}
+			var cols []string
+			for _, tc := range tr.Nodes {
+				if tc.XMLName.Local != "tc" {
+					continue
+				}
+				var cbuf bytes.Buffer
+				if err := zf.walk(&tc, &cbuf); err != nil {
+					return err
+				}
+				cols = append(cols, strings.Replace(cbuf.String(), "\n", "", -1))
+			}
+			rows = append(rows, cols)
+		}
+		maxcol := 0
+		for _, cols := range rows {
+			if len(cols) > maxcol {
+				maxcol = len(cols)
+			}
+		}
+		widths := make([]int, maxcol)
+		for _, row := range rows {
+			for i := 0; i < maxcol; i++ {
+				if i < len(row) {
+					width := runewidth.StringWidth(row[i])
+					if widths[i] < width {
+						widths[i] = width
+					}
+				}
+			}
+		}
+		for i, row := range rows {
+			if i == 0 {
+				for j := 0; j < maxcol; j++ {
+					fmt.Fprint(w, "|")
+					fmt.Fprint(w, strings.Repeat(" ", widths[j]))
+				}
+				fmt.Fprint(w, "|\n")
+				for j := 0; j < maxcol; j++ {
+					fmt.Fprint(w, "|")
+					fmt.Fprint(w, strings.Repeat("-", widths[j]))
+				}
+				fmt.Fprint(w, "|\n")
+			}
+			for j := 0; j < maxcol; j++ {
+				fmt.Fprint(w, "|")
+				if j < len(row) {
+					width := runewidth.StringWidth(row[j])
+					fmt.Fprint(w, escape(row[j], "|"))
+					fmt.Fprint(w, strings.Repeat(" ", widths[j]-width))
+				} else {
+					fmt.Fprint(w, strings.Repeat(" ", widths[j]))
+				}
+			}
+			fmt.Fprint(w, "|\n")
+		}
+		fmt.Fprint(w, "\n")
+	case "r":
+		bold := false
+		italic := false
+		strike := false
+		for _, n := range node.Nodes {
+			if n.XMLName.Local != "rPr" {
+				continue
+			}
+			for _, nn := range n.Nodes {
+				switch nn.XMLName.Local {
+				case "b":
+					bold = true
+				case "i":
+					italic = true
+				case "strike":
+					strike = true
+				}
+			}
+		}
+		if strike {
+			fmt.Fprint(w, "~~")
+		}
+		if bold {
+			fmt.Fprint(w, "**")
+		}
+		if italic {
+			fmt.Fprint(w, "*")
+		}
+		var cbuf bytes.Buffer
+		for _, n := range node.Nodes {
+			if err := zf.walk(&n, &cbuf); err != nil {
+				return err
+			}
+		}
+		fmt.Fprint(w, escape(cbuf.String(), `*~\`))
+		if italic {
+			fmt.Fprint(w, "*")
+		}
+		if bold {
+			fmt.Fprint(w, "**")
+		}
+		if strike {
+			fmt.Fprint(w, "~~")
+		}
+	case "p":
+		for _, n := range node.Nodes {
+			if err := zf.walk(&n, w); err != nil {
+				return err
+			}
+		}
+		fmt.Fprintln(w)
+	case "blip":
+		if id, ok := attr(node.Attrs, "embed"); ok {
+			for _, rel := range zf.rels.Relationship {
+				if id != rel.ID {
+					continue
+				}
+				if err := zf.extract(&rel, w); err != nil {
+					return err
+				}
+			}
+		}
+	case "Fallback":
+	case "txbxContent":
+		var cbuf bytes.Buffer
+		for _, n := range node.Nodes {
+			if err := zf.walk(&n, &cbuf); err != nil {
+				return err
+			}
+		}
+		fmt.Fprintln(w, "\n```\n"+cbuf.String()+"```")
+	default:
+		for _, n := range node.Nodes {
+			if err := zf.walk(&n, w); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func readFile(f *zip.File) (*Node, error) {
+	rc, err := f.Open()
+	defer rc.Close()
+
+	b, _ := ioutil.ReadAll(rc)
+	if err != nil {
+		return nil, err
+	}
+
+	var node Node
+	err = xml.Unmarshal(b, &node)
+	if err != nil {
+		return nil, err
+	}
+	return &node, nil
+}
+
+func findFile(files []*zip.File, target string) *zip.File {
+	for _, f := range files {
+		if ok, _ := path.Match(target, f.Name); ok {
+			return f
+		}
+	}
+	return nil
+}
+
+func Docx2md(arg string, embed bool) (string, error) {
+	r, err := zip.OpenReader(arg)
+	if err != nil {
+		return "", err
+	}
+	defer r.Close()
+
+	var rels Relationships
+	var num Numbering
+
+	for _, f := range r.File {
+		switch f.Name {
+		case "word/_rels/document.xml.rels":
+			rc, err := f.Open()
+			defer rc.Close()
+
+			b, _ := ioutil.ReadAll(rc)
+			if err != nil {
+				return "", err
+			}
+
+			err = xml.Unmarshal(b, &rels)
+			if err != nil {
+				return "", err
+			}
+		case "word/numbering.xml":
+			rc, err := f.Open()
+			defer rc.Close()
+
+			b, _ := ioutil.ReadAll(rc)
+			if err != nil {
+				return "", err
+			}
+
+			err = xml.Unmarshal(b, &num)
+			if err != nil {
+				return "", err
+			}
+		}
+	}
+
+	f := findFile(r.File, "word/document*.xml")
+	if f == nil {
+		return "", errors.New("incorrect document")
+	}
+	node, err := readFile(f)
+	if err != nil {
+		return "", err
+	}
+
+	fileNames := strings.Split(arg, "/")
+	fileName := fileNames[len(fileNames)-1]
+	// make sure the file name
+	if !strings.HasSuffix(fileName, ".docx") {
+		log.Fatal("File name must end with .docx")
+	}
+
+	var buf bytes.Buffer
+	zf := &file{
+		r:     r,
+		rels:  rels,
+		num:   num,
+		embed: embed,
+		list:  make(map[string]int),
+		name:  fileName,
+	}
+	err = zf.walk(node, &buf)
+	if err != nil {
+		return "", err
+	}
+
+	return buf.String(), nil
+}

+ 0 - 1
views/blog/index.tpl

@@ -15,7 +15,6 @@
     <link href="{{cdncss "/static/bootstrap/css/bootstrap.min.css"}}" rel="stylesheet">
 
     <link href="{{cdncss "/static/font-awesome/css/font-awesome.min.css"}}" rel="stylesheet">
-    <link href="{{cdncss "/static/editor.md/lib/mermaid/mermaid.css"}}" rel="stylesheet">
     <link href="{{cdncss "/static/editor.md/lib/sequence/sequence-diagram-min.css"}}" rel="stylesheet">
     <link href="{{cdncss "/static/css/kancloud.css" "version"}}" rel="stylesheet">
     <link href="{{cdncss "/static/editor.md/css/editormd.preview.css"}}" rel="stylesheet">

+ 2 - 2
views/book/index.tpl

@@ -248,7 +248,7 @@
                         </div>
                         <div class="form-group">
                             <div class="file-loading">
-                                <input id="import-book-upload" name="import-file" type="file" accept=".zip">
+                                <input id="import-book-upload" name="import-file" type="file" accept=".zip,.docx">
                             </div>
                             <div id="kartik-file-errors"></div>
                         </div>
@@ -465,7 +465,7 @@
                 'required': true,
                 'validateInitialCount': true,
                 "language" : "{{i18n $.Lang "common.upload_lang"}}",
-                'allowedFileExtensions': ['zip'],
+                'allowedFileExtensions': ['zip', 'docx'],
                 'msgPlaceholder' : '{{i18n $.Lang "message.file_type_placeholder"}}',
                 'elErrorContainer' : "#import-book-form-error-message",
                 'uploadExtraData' : function () {

+ 0 - 1
views/document/default_read.tpl

@@ -21,7 +21,6 @@
     <link href="{{cdncss "/static/nprogress/nprogress.css"}}" rel="stylesheet">
     <link href="{{cdncss "/static/css/kancloud.css" "version"}}" rel="stylesheet">
     <link href="{{cdncss "/static/css/jstree.css"}}" rel="stylesheet">
-    <link href="{{cdncss "/static/editor.md/lib/mermaid/mermaid.css" "version"}}" rel="stylesheet">
     <link href="{{cdncss "/static/editor.md/lib/sequence/sequence-diagram-min.css" "version"}}" rel="stylesheet">
     <link href="{{cdncss "/static/editor.md/css/editormd.preview.css" "version"}}" rel="stylesheet">
     <link href="{{cdncss "/static/css/markdown.preview.css" "version"}}" rel="stylesheet">