")
347 | for i, heading := range headings {
348 | level := strconv.Itoa(heading.HeadingLevel)
349 | spaces := (heading.HeadingLevel - 1) * 2
350 | r.WriteString(strings.Repeat(" ", spaces))
351 | r.WriteString("
")
352 | r.WriteString("" + heading.Text() + "")
353 | }
354 | r.WriteString("
\n\n")
355 | }
356 | return ast.WalkContinue
357 | }
358 |
359 | func (r *DocxRenderer) headings() (ret []*ast.Node) {
360 | for n := r.Tree.Root.FirstChild; nil != n; n = n.Next {
361 | r.headings0(n, &ret)
362 | }
363 | return
364 | }
365 |
366 | func (r *DocxRenderer) headings0(n *ast.Node, headings *[]*ast.Node) {
367 | if ast.NodeHeading == n.Type {
368 | *headings = append(*headings, n)
369 | return
370 | }
371 | if ast.NodeList == n.Type || ast.NodeListItem == n.Type || ast.NodeBlockquote == n.Type {
372 | for c := n.FirstChild; nil != c; c = c.Next {
373 | r.headings0(c, headings)
374 | }
375 | }
376 | }
377 |
378 | func (r *DocxRenderer) RenderFootnotesDefs(context *parse.Context) []byte {
379 | //if r.needRenderFootnotesDef {
380 | // return nil
381 | //}
382 | //
383 | //r.addPage()
384 | //r.renderThematicBreak(nil, false)
385 | //for i, def := range context.FootnotesDefs {
386 | // r.pdf.SetAnchor(string(def.Tokens))
387 | // r.WriteString(fmt.Sprint(i+1) + ". ")
388 | // tree := &parse.Tree{Name: "", Context: context}
389 | // tree.Context.Tree = tree
390 | // tree.Root = &ast.Node{Type: ast.NodeDocument}
391 | // tree.Root.AppendChild(def)
392 | // r.Tree = tree
393 | // r.needRenderFootnotesDef = true
394 | // r.Render()
395 | // r.Newline()
396 | //}
397 | //r.needRenderFootnotesDef = false
398 | //r.renderFooter()
399 | return nil
400 | }
401 |
402 | func (r *DocxRenderer) renderFootnotesRef(node *ast.Node, entering bool) ast.WalkStatus {
403 | //x := r.pdf.GetX() + 1
404 | //r.pdf.SetX(x)
405 | //y := r.pdf.GetY()
406 | //r.pdf.SetFont("regular", "R", 8)
407 | //r.pdf.SetTextColor(66, 133, 244)
408 | //
409 | //idx := string(node.Tokens)
410 | //width, _ := r.pdf.MeasureTextWidth(idx[1:])
411 | //r.pdf.SetY(y - 4)
412 | //r.pdf.Cell(nil, idx[1:])
413 | //r.pdf.AddInternalLink(idx, x-3, y-9, width+4, r.lineHeight)
414 | //
415 | //x += width
416 | //r.pdf.SetX(x)
417 | //r.pdf.SetY(y)
418 | //font := r.peekFont()
419 | //r.pdf.SetFont(font.family, font.style, font.size)
420 | //textColor := r.peekTextColor()
421 | //r.pdf.SetTextColor(textColor.R, textColor.G, textColor.B)
422 | return ast.WalkContinue
423 | }
424 |
425 | func (r *DocxRenderer) renderFootnotesDef(node *ast.Node, entering bool) ast.WalkStatus {
426 | if !r.needRenderFootnotesDef {
427 | return ast.WalkContinue
428 | }
429 | return ast.WalkContinue
430 | }
431 |
432 | func (r *DocxRenderer) renderCodeBlock(node *ast.Node, entering bool) ast.WalkStatus {
433 | if entering {
434 | if !node.IsFencedCodeBlock {
435 | // 缩进代码块处理
436 | r.renderCodeBlockLike(node.Tokens)
437 | return ast.WalkContinue
438 | }
439 | }
440 | return ast.WalkContinue
441 | }
442 |
443 | // renderCodeBlockCode 进行代码块 HTML 渲染,实现语法高亮。
444 | func (r *DocxRenderer) renderCodeBlockCode(node *ast.Node, entering bool) ast.WalkStatus {
445 | if entering {
446 | r.renderCodeBlockLike(node.Tokens)
447 | }
448 | return ast.WalkContinue
449 | }
450 |
451 | func (r *DocxRenderer) renderCodeBlockLike(content []byte) {
452 | para := r.doc.AddParagraph()
453 | r.pushPara(¶)
454 | run := para.AddRun()
455 | run.Properties().SetStyle("CodeBlock")
456 | r.pushRun(&run)
457 | r.WriteString(util.BytesToStr(content))
458 | run.AddBreak()
459 | r.popRun()
460 | r.reRun()
461 | r.popPara()
462 | }
463 |
464 | func (r *DocxRenderer) renderCodeSpanLike(content []byte) {
465 | para := r.peekPara()
466 | run := para.AddRun()
467 | run.Properties().SetStyle("Code")
468 | r.pushRun(&run)
469 | r.WriteString(util.BytesToStr(content))
470 | r.popRun()
471 | r.reRun()
472 | }
473 |
474 | func (r *DocxRenderer) reRun() {
475 | if nil != r.peekRun() {
476 | // 如果链接之前有输出的话需要先结束掉,然后重新开一个
477 | r.popRun()
478 | para := r.peekPara()
479 | run := para.AddRun()
480 | r.pushRun(&run)
481 | }
482 | }
483 |
484 | func (r *DocxRenderer) renderCodeBlockCloseMarker(node *ast.Node, entering bool) ast.WalkStatus {
485 | return ast.WalkContinue
486 | }
487 |
488 | func (r *DocxRenderer) renderCodeBlockInfoMarker(node *ast.Node, entering bool) ast.WalkStatus {
489 | return ast.WalkContinue
490 | }
491 |
492 | func (r *DocxRenderer) renderCodeBlockOpenMarker(node *ast.Node, entering bool) ast.WalkStatus {
493 | return ast.WalkContinue
494 | }
495 |
496 | func (r *DocxRenderer) renderEmojiAlias(node *ast.Node, entering bool) ast.WalkStatus {
497 | return ast.WalkContinue
498 | }
499 |
500 | func (r *DocxRenderer) renderEmojiImg(node *ast.Node, entering bool) ast.WalkStatus {
501 | return ast.WalkContinue
502 | }
503 |
504 | func (r *DocxRenderer) renderEmojiUnicode(node *ast.Node, entering bool) ast.WalkStatus {
505 | return ast.WalkContinue
506 | }
507 |
508 | func (r *DocxRenderer) renderEmoji(node *ast.Node, entering bool) ast.WalkStatus {
509 | // 暂不渲染 Emoji,字体似乎有问题
510 | return ast.WalkContinue
511 | }
512 |
513 | func (r *DocxRenderer) renderInlineMathCloseMarker(node *ast.Node, entering bool) ast.WalkStatus {
514 | return ast.WalkContinue
515 | }
516 |
517 | func (r *DocxRenderer) renderInlineMathContent(node *ast.Node, entering bool) ast.WalkStatus {
518 | if entering {
519 | r.renderCodeSpanLike(node.Tokens)
520 | }
521 | return ast.WalkContinue
522 | }
523 |
524 | func (r *DocxRenderer) renderInlineMathOpenMarker(node *ast.Node, entering bool) ast.WalkStatus {
525 | return ast.WalkContinue
526 | }
527 |
528 | func (r *DocxRenderer) renderInlineMath(node *ast.Node, entering bool) ast.WalkStatus {
529 | return ast.WalkContinue
530 | }
531 |
532 | func (r *DocxRenderer) renderMathBlockCloseMarker(node *ast.Node, entering bool) ast.WalkStatus {
533 | return ast.WalkContinue
534 | }
535 |
536 | func (r *DocxRenderer) renderMathBlockContent(node *ast.Node, entering bool) ast.WalkStatus {
537 | if entering {
538 | r.renderCodeBlockLike(node.Tokens)
539 | }
540 | return ast.WalkContinue
541 | }
542 |
543 | func (r *DocxRenderer) renderMathBlockOpenMarker(node *ast.Node, entering bool) ast.WalkStatus {
544 | return ast.WalkContinue
545 | }
546 |
547 | func (r *DocxRenderer) renderMathBlock(node *ast.Node, entering bool) ast.WalkStatus {
548 | return ast.WalkContinue
549 | }
550 |
551 | func (r *DocxRenderer) renderTableCell(node *ast.Node, entering bool) ast.WalkStatus {
552 | //if entering {
553 | // // TODO: table align
554 | // //var attrs [][]string
555 | // //switch node.TableCellAlign {
556 | // //case 1:
557 | // // attrs = append(attrs, []string{"align", "left"})
558 | // //case 2:
559 | // // attrs = append(attrs, []string{"align", "center"})
560 | // //case 3:
561 | // // attrs = append(attrs, []string{"align", "right"})
562 | // //}
563 | // x := r.pdf.GetX()
564 | // cols := float64(r.tableCols(node))
565 | // maxWidth := (r.pageSize.W - r.margin*2) / cols
566 | // if node.Parent.FirstChild != node {
567 | // prevWidth, _ := r.pdf.MeasureTextWidth(util.BytesToStr(node.Previous.TableCellContent))
568 | // x += maxWidth - prevWidth
569 | // r.pdf.SetX(x)
570 | // }
571 | // // TODO: table border
572 | // // r.pdf.RectFromUpperLeftWithStyle(x, r.pdf.GetY(), maxWidth, r.lineHeight, "D")
573 | // r.pdf.SetX(r.pdf.GetX() + 4)
574 | // r.pdf.SetY(r.pdf.GetY() + 4)
575 | //} else {
576 | // r.pdf.SetX(r.pdf.GetX() - 4)
577 | // r.pdf.SetY(r.pdf.GetY() - 4)
578 | //}
579 | return ast.WalkContinue
580 | }
581 |
582 | func (r *DocxRenderer) tableCols(cell *ast.Node) int {
583 | for parent := cell.Parent; nil != parent; parent = parent.Parent {
584 | if nil != parent.TableAligns {
585 | return len(parent.TableAligns)
586 | }
587 | }
588 | return 0
589 | }
590 |
591 | func (r *DocxRenderer) renderTableRow(node *ast.Node, entering bool) ast.WalkStatus {
592 | r.Newline()
593 | return ast.WalkContinue
594 | }
595 |
596 | func (r *DocxRenderer) renderTableHead(node *ast.Node, entering bool) ast.WalkStatus {
597 | //if entering {
598 | // r.pushPara(&Font{"bold", "B", r.fontSize})
599 | //} else {
600 | // r.popFont()
601 | //}
602 | return ast.WalkContinue
603 | }
604 |
605 | func (r *DocxRenderer) renderTable(node *ast.Node, entering bool) ast.WalkStatus {
606 | //if entering {
607 | // r.pdf.SetY(r.pdf.GetY() + 6)
608 | //} else {
609 | // r.pdf.SetY(r.pdf.GetY() + 6)
610 | // r.Newline()
611 | //}
612 | return ast.WalkContinue
613 | }
614 |
615 | func (r *DocxRenderer) renderStrikethrough(node *ast.Node, entering bool) ast.WalkStatus {
616 | if entering {
617 | run := r.peekPara().AddRun()
618 | r.pushRun(&run)
619 | run.Properties().SetStrikeThrough(true)
620 | } else {
621 | r.popRun()
622 | r.reRun()
623 | }
624 | return ast.WalkContinue
625 | }
626 |
627 | func (r *DocxRenderer) renderStrikethrough1OpenMarker(node *ast.Node, entering bool) ast.WalkStatus {
628 | return ast.WalkContinue
629 | }
630 |
631 | func (r *DocxRenderer) renderStrikethrough1CloseMarker(node *ast.Node, entering bool) ast.WalkStatus {
632 | return ast.WalkContinue
633 | }
634 |
635 | func (r *DocxRenderer) renderStrikethrough2OpenMarker(node *ast.Node, entering bool) ast.WalkStatus {
636 | return ast.WalkContinue
637 | }
638 |
639 | func (r *DocxRenderer) renderStrikethrough2CloseMarker(node *ast.Node, entering bool) ast.WalkStatus {
640 | return ast.WalkContinue
641 | }
642 |
643 | func (r *DocxRenderer) renderLinkTitle(node *ast.Node, entering bool) ast.WalkStatus {
644 | return ast.WalkContinue
645 | }
646 |
647 | func (r *DocxRenderer) renderLinkDest(node *ast.Node, entering bool) ast.WalkStatus {
648 | return ast.WalkContinue
649 | }
650 |
651 | func (r *DocxRenderer) renderLinkSpace(node *ast.Node, entering bool) ast.WalkStatus {
652 | return ast.WalkContinue
653 | }
654 |
655 | func (r *DocxRenderer) renderLinkText(node *ast.Node, entering bool) ast.WalkStatus {
656 | if entering {
657 | if ast.NodeImage != node.Parent.Type {
658 | r.Write(node.Tokens)
659 | }
660 | }
661 | return ast.WalkContinue
662 | }
663 |
664 | func (r *DocxRenderer) renderCloseParen(node *ast.Node, entering bool) ast.WalkStatus {
665 | return ast.WalkContinue
666 | }
667 |
668 | func (r *DocxRenderer) renderOpenParen(node *ast.Node, entering bool) ast.WalkStatus {
669 | return ast.WalkContinue
670 | }
671 |
672 | func (r *DocxRenderer) renderCloseBracket(node *ast.Node, entering bool) ast.WalkStatus {
673 | return ast.WalkContinue
674 | }
675 |
676 | func (r *DocxRenderer) renderOpenBracket(node *ast.Node, entering bool) ast.WalkStatus {
677 | return ast.WalkContinue
678 | }
679 |
680 | func (r *DocxRenderer) renderBang(node *ast.Node, entering bool) ast.WalkStatus {
681 | return ast.WalkContinue
682 | }
683 |
684 | func (r *DocxRenderer) renderImage(node *ast.Node, entering bool) ast.WalkStatus {
685 | if entering {
686 | if 0 == r.DisableTags {
687 | destTokens := node.ChildByType(ast.NodeLinkDest).Tokens
688 | src := util.BytesToStr(destTokens)
689 | src, ok, isTemp := r.downloadImg(src)
690 | if ok {
691 | img, _ := common.ImageFromFile(src)
692 | imgRef, _ := r.doc.AddImage(img)
693 | inline, _ := r.peekRun().AddDrawingInline(imgRef)
694 | width, height := r.getImgSize(src)
695 | inline.SetSize(measurement.Distance(width), measurement.Distance(height))
696 | if isTemp {
697 | r.images = append(r.images, src)
698 | }
699 | }
700 | }
701 | r.DisableTags++
702 | return ast.WalkContinue
703 | }
704 |
705 | r.DisableTags--
706 | if 0 == r.DisableTags {
707 | //r.WriteString("\"")
708 | //if title := node.ChildByType(ast.NodeLinkTitle); nil != title && nil != title.Tokens {
709 | // r.WriteString(" title=\"")
710 | // r.Write(title.Tokens)
711 | // r.WriteString("\"")
712 | //}
713 | //r.WriteString(" />")
714 | }
715 | return ast.WalkContinue
716 | }
717 |
718 | func (r *DocxRenderer) renderLink(node *ast.Node, entering bool) ast.WalkStatus {
719 | if entering {
720 | dest := node.ChildByType(ast.NodeLinkDest)
721 | destTokens := dest.Tokens
722 | destTokens = r.RelativePath(destTokens)
723 | para := r.peekPara()
724 | link := para.AddHyperLink()
725 | link.SetTarget(util.BytesToStr(destTokens))
726 | run := link.AddRun()
727 | run.Properties().SetStyle("Hyperlink")
728 | r.pushRun(&run)
729 | } else {
730 | r.popRun()
731 | r.reRun()
732 | }
733 | return ast.WalkContinue
734 | }
735 |
736 | func (r *DocxRenderer) renderHTML(node *ast.Node, entering bool) ast.WalkStatus {
737 | if entering {
738 | r.renderCodeBlockLike(node.Tokens)
739 | }
740 | return ast.WalkContinue
741 | }
742 |
743 | func (r *DocxRenderer) renderInlineHTML(node *ast.Node, entering bool) ast.WalkStatus {
744 | if entering {
745 | r.renderCodeSpanLike(node.Tokens)
746 | }
747 | return ast.WalkContinue
748 | }
749 |
750 | func (r *DocxRenderer) renderDocument(node *ast.Node, entering bool) ast.WalkStatus {
751 | return ast.WalkContinue
752 | }
753 |
754 | func (r *DocxRenderer) Save(docxPath string) {
755 | err := r.doc.SaveToFile(docxPath)
756 | for _, img := range r.images {
757 | os.Remove(img)
758 | }
759 | if nil != err {
760 | logger.Fatal(err)
761 | }
762 | }
763 |
764 | func (r *DocxRenderer) renderParagraph(node *ast.Node, entering bool) ast.WalkStatus {
765 | inList := false
766 | grandparent := node.Parent.Parent
767 | inTightList := false
768 | if nil != grandparent && ast.NodeList == grandparent.Type {
769 | inList = true
770 | inTightList = grandparent.Tight
771 | }
772 |
773 | if inTightList {
774 | if entering {
775 | para := r.peekPara()
776 | run := para.AddRun()
777 | r.pushRun(&run)
778 | } else {
779 | r.popRun()
780 | }
781 | return ast.WalkContinue
782 | }
783 |
784 | isFirstParaInList := false
785 | if inList {
786 | isFirstParaInList = node.Parent.FirstChild == node
787 | }
788 |
789 | if entering {
790 | if !inList {
791 | para := r.doc.AddParagraph()
792 | r.pushPara(¶)
793 | run := para.AddRun()
794 | r.pushRun(&run)
795 | } else {
796 | if inTightList {
797 | para := r.peekPara()
798 | run := para.AddRun()
799 | r.pushRun(&run)
800 | } else {
801 | if isFirstParaInList {
802 | para := r.peekPara()
803 | run := para.AddRun()
804 | r.pushRun(&run)
805 | } else {
806 | para := r.doc.AddParagraph()
807 | r.pushPara(¶)
808 | run := para.AddRun()
809 | r.pushRun(&run)
810 | }
811 | }
812 | }
813 | } else {
814 | if !inList {
815 | r.peekRun().AddBreak()
816 | r.popRun()
817 | r.popPara()
818 | } else {
819 | if inTightList {
820 | r.popRun()
821 | } else {
822 | if isFirstParaInList {
823 | r.popRun()
824 | } else {
825 | r.popRun()
826 | r.popPara()
827 | }
828 | }
829 | }
830 | }
831 | return ast.WalkContinue
832 | }
833 |
834 | func (r *DocxRenderer) renderText(node *ast.Node, entering bool) ast.WalkStatus {
835 | if entering {
836 | text := util.BytesToStr(node.Tokens)
837 | r.WriteString(text)
838 | }
839 | return ast.WalkContinue
840 | }
841 |
842 | func (r *DocxRenderer) renderCodeSpan(node *ast.Node, entering bool) ast.WalkStatus {
843 | return ast.WalkContinue
844 | }
845 |
846 | func (r *DocxRenderer) renderCodeSpanOpenMarker(node *ast.Node, entering bool) ast.WalkStatus {
847 | return ast.WalkContinue
848 | }
849 |
850 | func (r *DocxRenderer) renderCodeSpanContent(node *ast.Node, entering bool) ast.WalkStatus {
851 | if entering {
852 | r.renderCodeSpanLike(node.Tokens)
853 | }
854 | return ast.WalkContinue
855 | }
856 |
857 | func (r *DocxRenderer) renderCodeSpanCloseMarker(node *ast.Node, entering bool) ast.WalkStatus {
858 | return ast.WalkContinue
859 | }
860 |
861 | func (r *DocxRenderer) renderEmphasis(node *ast.Node, entering bool) ast.WalkStatus {
862 | if entering {
863 | run := r.peekPara().AddRun()
864 | r.pushRun(&run)
865 | run.Properties().SetItalic(true)
866 | } else {
867 | r.popRun()
868 | r.reRun()
869 | }
870 | return ast.WalkContinue
871 | }
872 |
873 | func (r *DocxRenderer) renderEmAsteriskOpenMarker(node *ast.Node, entering bool) ast.WalkStatus {
874 | return ast.WalkContinue
875 | }
876 |
877 | func (r *DocxRenderer) renderEmAsteriskCloseMarker(node *ast.Node, entering bool) ast.WalkStatus {
878 | return ast.WalkContinue
879 | }
880 |
881 | func (r *DocxRenderer) renderEmUnderscoreOpenMarker(node *ast.Node, entering bool) ast.WalkStatus {
882 | return ast.WalkContinue
883 | }
884 |
885 | func (r *DocxRenderer) renderEmUnderscoreCloseMarker(node *ast.Node, entering bool) ast.WalkStatus {
886 | return ast.WalkContinue
887 | }
888 |
889 | func (r *DocxRenderer) renderStrong(node *ast.Node, entering bool) ast.WalkStatus {
890 | if entering {
891 | run := r.peekPara().AddRun()
892 | r.pushRun(&run)
893 | run.Properties().SetBold(true)
894 | } else {
895 | r.popRun()
896 | r.reRun()
897 | }
898 | return ast.WalkContinue
899 | }
900 |
901 | func (r *DocxRenderer) renderStrongA6kOpenMarker(node *ast.Node, entering bool) ast.WalkStatus {
902 | return ast.WalkContinue
903 | }
904 |
905 | func (r *DocxRenderer) renderStrongA6kCloseMarker(node *ast.Node, entering bool) ast.WalkStatus {
906 | return ast.WalkContinue
907 | }
908 |
909 | func (r *DocxRenderer) renderStrongU8eOpenMarker(node *ast.Node, entering bool) ast.WalkStatus {
910 | return ast.WalkContinue
911 | }
912 |
913 | func (r *DocxRenderer) renderStrongU8eCloseMarker(node *ast.Node, entering bool) ast.WalkStatus {
914 | return ast.WalkContinue
915 | }
916 |
917 | func (r *DocxRenderer) renderBlockquote(node *ast.Node, entering bool) ast.WalkStatus {
918 | return ast.WalkContinue
919 | }
920 |
921 | func (r *DocxRenderer) renderBlockquoteMarker(node *ast.Node, entering bool) ast.WalkStatus {
922 | return ast.WalkContinue
923 | }
924 |
925 | func (r *DocxRenderer) renderHeading(node *ast.Node, entering bool) ast.WalkStatus {
926 | if entering {
927 | para := r.doc.AddParagraph()
928 | r.pushPara(¶)
929 | run := para.AddRun()
930 | r.pushRun(&run)
931 | props := run.Properties()
932 | props.SetBold(true)
933 |
934 | switch node.HeadingLevel {
935 | case 1:
936 | props.SetSize(measurement.Distance(r.heading1Size))
937 | props.SetStyle("Heading1")
938 | case 2:
939 | props.SetSize(measurement.Distance(r.heading2Size))
940 | props.SetStyle("Heading2")
941 | case 3:
942 | props.SetSize(measurement.Distance(r.heading3Size))
943 | props.SetStyle("Heading3")
944 | case 4:
945 | props.SetSize(measurement.Distance(r.heading4Size))
946 | props.SetStyle("Heading4")
947 | case 5:
948 | props.SetSize(measurement.Distance(r.heading5Size))
949 | props.SetStyle("Heading5")
950 | case 6:
951 | props.SetSize(measurement.Distance(r.heading6Size))
952 | props.SetStyle("Heading6")
953 | default:
954 | props.SetSize(measurement.Distance(r.heading3Size))
955 | props.SetStyle("Heading3")
956 | }
957 | } else {
958 | r.popPara()
959 | r.popRun()
960 | r.Newline()
961 | }
962 | return ast.WalkContinue
963 | }
964 |
965 | func (r *DocxRenderer) renderHeadingC8hMarker(node *ast.Node, entering bool) ast.WalkStatus {
966 | return ast.WalkContinue
967 | }
968 |
969 | func (r *DocxRenderer) renderList(node *ast.Node, entering bool) ast.WalkStatus {
970 | if !entering {
971 | r.Newline()
972 | }
973 | return ast.WalkContinue
974 | }
975 |
976 | func (r *DocxRenderer) renderListItem(node *ast.Node, entering bool) ast.WalkStatus {
977 | if entering {
978 | paragraph := r.doc.AddParagraph()
979 | r.pushPara(¶graph)
980 |
981 | nestedLevel := r.countParentContainerBlocks(node) - 1
982 | indent := float64(nestedLevel * 22)
983 |
984 | if 3 == node.ListData.Typ && "" != r.Options.GFMTaskListItemClass &&
985 | nil != node.FirstChild && nil != node.FirstChild.FirstChild && ast.NodeTaskListItemMarker == node.FirstChild.FirstChild.Type {
986 | r.WriteString(fmt.Sprintf("%s", node.ListData.Marker))
987 | } else {
988 | definition := r.doc.Numbering.AddDefinition()
989 | level := definition.AddLevel()
990 | level.Properties().SetLeftIndent(measurement.Distance(indent))
991 | if 0 != node.BulletChar {
992 | level.SetFormat(wml.ST_NumberFormatBullet)
993 | level.RunProperties().SetSize(6)
994 | level.SetText("●")
995 | } else {
996 | level.Properties().SetStartIndent(30)
997 | level.SetFormat(wml.ST_NumberFormatDecimal)
998 | level.SetText(fmt.Sprint(node.Num) + ".")
999 | }
1000 | paragraph.SetNumberingDefinition(definition)
1001 | }
1002 | } else {
1003 | r.popPara()
1004 | }
1005 | return ast.WalkContinue
1006 | }
1007 |
1008 | func (r *DocxRenderer) renderTaskListItemMarker(node *ast.Node, entering bool) ast.WalkStatus {
1009 | if entering {
1010 | var attrs [][]string
1011 | if node.TaskListItemChecked {
1012 | attrs = append(attrs, []string{"checked", ""})
1013 | }
1014 | attrs = append(attrs, []string{"disabled", ""}, []string{"type", "checkbox"})
1015 | //r.tag("input", attrs, true)
1016 | }
1017 | return ast.WalkContinue
1018 | }
1019 |
1020 | func (r *DocxRenderer) renderThematicBreak(node *ast.Node, entering bool) ast.WalkStatus {
1021 | //r.Newline()
1022 | //r.pdf.SetY(r.pdf.GetY() + 14)
1023 | //r.pdf.SetStrokeColor(106, 115, 125)
1024 | //r.pdf.Line(r.pdf.GetX()+float64(r.fontSize), r.pdf.GetY(), r.pageSize.W-r.margin-float64(r.fontSize), r.pdf.GetY())
1025 | //r.pdf.SetY(r.pdf.GetY() + 12)
1026 | //r.pdf.SetStrokeColor(0, 0, 0)
1027 | //r.Newline()
1028 | return ast.WalkContinue
1029 | }
1030 |
1031 | func (r *DocxRenderer) renderHardBreak(node *ast.Node, entering bool) ast.WalkStatus {
1032 | if entering {
1033 | r.Newline()
1034 | }
1035 | return ast.WalkContinue
1036 | }
1037 |
1038 | func (r *DocxRenderer) renderSoftBreak(node *ast.Node, entering bool) ast.WalkStatus {
1039 | if entering {
1040 | r.Newline()
1041 | }
1042 | return ast.WalkContinue
1043 | }
1044 |
1045 | func (r *DocxRenderer) pushPara(para *document.Paragraph) {
1046 | r.paragraphs = append(r.paragraphs, para)
1047 | }
1048 |
1049 | func (r *DocxRenderer) popPara() *document.Paragraph {
1050 | ret := r.paragraphs[len(r.paragraphs)-1]
1051 | r.paragraphs = r.paragraphs[:len(r.paragraphs)-1]
1052 | return ret
1053 | }
1054 |
1055 | func (r *DocxRenderer) peekPara() *document.Paragraph {
1056 | return r.paragraphs[len(r.paragraphs)-1]
1057 | }
1058 |
1059 | func (r *DocxRenderer) pushRun(run *document.Run) {
1060 | r.runs = append(r.runs, run)
1061 | }
1062 |
1063 | func (r *DocxRenderer) popRun() *document.Run {
1064 | ret := r.runs[len(r.runs)-1]
1065 | r.runs = r.runs[:len(r.runs)-1]
1066 | return ret
1067 | }
1068 |
1069 | func (r *DocxRenderer) peekRun() *document.Run {
1070 | if 1 > len(r.runs) {
1071 | return nil
1072 | }
1073 |
1074 | return r.runs[len(r.runs)-1]
1075 | }
1076 |
1077 | func (r *DocxRenderer) countParentContainerBlocks(n *ast.Node) (ret int) {
1078 | for parent := n.Parent; nil != parent; parent = parent.Parent {
1079 | if ast.NodeBlockquote == parent.Type || ast.NodeList == parent.Type {
1080 | ret++
1081 | }
1082 | }
1083 | return
1084 | }
1085 |
1086 | // WriteByte 输出一个字节 c。
1087 | func (r *DocxRenderer) WriteByte(c byte) {
1088 | r.WriteString(string(c))
1089 | }
1090 |
1091 | // Write 输出指定的字节数组 content。
1092 | func (r *DocxRenderer) Write(content []byte) {
1093 | r.WriteString(util.BytesToStr(content))
1094 | }
1095 |
1096 | // WriteString 输出指定的字符串 content。
1097 | func (r *DocxRenderer) WriteString(content string) {
1098 | if length := len(content); 0 < length {
1099 | run := r.peekRun()
1100 | run.AddText(content)
1101 | r.LastOut = content[length-1]
1102 | }
1103 | }
1104 |
1105 | // Newline 会在最新内容不是换行符 \n 时输出一个换行符。
1106 | func (r *DocxRenderer) Newline() {
1107 | if lex.ItemNewline != r.LastOut {
1108 | r.doc.AddParagraph()
1109 | r.LastOut = lex.ItemNewline
1110 | }
1111 | }
1112 |
1113 | func (r *DocxRenderer) downloadImg(src string) (localPath string, ok, isTemp bool) {
1114 | if strings.HasPrefix(src, "//") {
1115 | src = "https:" + src
1116 | }
1117 |
1118 | u, err := url.Parse(src)
1119 | if nil != err {
1120 | logger.Infof("image src [%s] is not an valid URL, treat it as local path", src)
1121 | return src, true, false
1122 | }
1123 |
1124 | if !strings.HasPrefix(u.Scheme, "http") {
1125 | logger.Infof("image src [%s] scheme is not [http] or [https], treat it as local path", src)
1126 | return src, true, false
1127 | }
1128 |
1129 | src = r.qiniuImgProcessing(src)
1130 | u, _ = url.Parse(src)
1131 |
1132 | client := http.Client{
1133 | Timeout: 5 * time.Second,
1134 | }
1135 | req := &http.Request{
1136 | Header: http.Header{
1137 | "User-Agent": []string{"Lute-DOCX; +https://github.com/88250/lute-docx"},
1138 | },
1139 | URL: u,
1140 | }
1141 | resp, err := client.Do(req)
1142 | if nil != err {
1143 | logger.Warnf("download image [%s] failed: %s", src, err)
1144 | return src, false, false
1145 | }
1146 | defer resp.Body.Close()
1147 | if 200 != resp.StatusCode {
1148 | logger.Warnf("download image [%s] failed, status code is [%d]", src, resp.StatusCode)
1149 | return src, false, false
1150 | }
1151 |
1152 | data, err := ioutil.ReadAll(resp.Body)
1153 | file, err := ioutil.TempFile("", "lute-docx.img.")
1154 | if nil != err {
1155 | logger.Warnf("create temp image [%s] failed: %s", src, err)
1156 | return src, false, false
1157 | }
1158 | _, err = file.Write(data)
1159 | if nil != err {
1160 | logger.Warnf("write temp image [%s] failed: %s", src, err)
1161 | return src, false, false
1162 | }
1163 | file.Close()
1164 | return file.Name(), true, true
1165 | }
1166 |
1167 | // qiniuImgProcessing 七牛云图片样式处理。
1168 | func (r *DocxRenderer) qiniuImgProcessing(src string) string {
1169 | if !strings.Contains(src, "img.hacpai.com") && !strings.Contains(src, "b3logfile.com") && !strings.Contains(src, "imageView") {
1170 | return src
1171 | }
1172 |
1173 | if 0 < strings.Index(src, "?") {
1174 | src = src[:strings.Index(src, "?")]
1175 | }
1176 |
1177 | //maxWidth := int(math.Round(r.pageSize.W-r.margin*2) * 128 / 72)
1178 | //style := "imageView2/2/w/%d/interlace/1/format/jpg"
1179 | //style = fmt.Sprintf(style, maxWidth)
1180 | //src += "?" + style
1181 | return src
1182 | }
1183 |
1184 | func (r *DocxRenderer) getImgSize(imgPath string) (width, height float64) {
1185 | file, err := os.Open(imgPath)
1186 | if nil != err {
1187 | logger.Fatal(err)
1188 | }
1189 | img, _, err := image.Decode(file)
1190 | if nil != err {
1191 | logger.Fatal(err)
1192 | }
1193 | file.Close()
1194 |
1195 | imageRect := img.Bounds()
1196 | k := 1
1197 | w := -128
1198 | h := -128
1199 | if w < 0 {
1200 | w = -imageRect.Dx() * 72 / w / k
1201 | }
1202 | if h < 0 {
1203 | h = -imageRect.Dy() * 72 / h / k
1204 | }
1205 | if w == 0 {
1206 | w = h * imageRect.Dx() / imageRect.Dy()
1207 | }
1208 | if h == 0 {
1209 | h = w * imageRect.Dy() / imageRect.Dx()
1210 | }
1211 | return float64(w), float64(h)
1212 | }
1213 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/88250/lute-docx
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/88250/gulu v1.1.2
7 | github.com/88250/lute v1.7.1-0.20201227150112-460780f34e08
8 | github.com/unidoc/unioffice v1.4.0
9 | golang.org/x/text v0.3.4 // indirect
10 | )
11 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/88250/gulu v1.1.2 h1:zgnftJgG4knQ97EPwDhQRKCgos40qRIafTWgGcP5EG4=
2 | github.com/88250/gulu v1.1.2/go.mod h1:a2POIziN+QFeNT4Mj7FHH2lz1HEaFlMRF6wPE0NHM4U=
3 | github.com/88250/lute v1.6.7-0.20201123095407-ba1f0facb9a0 h1:IFzR9Dg7q+i4qK8wd0MzekUsxid6e+tnbvNzsOECChU=
4 | github.com/88250/lute v1.6.7-0.20201123095407-ba1f0facb9a0/go.mod h1:APx6IhxzrUGJqmIalKYomjVLrO4yw/PzEBUtYxpMrfk=
5 | github.com/88250/lute v1.7.1-0.20201227150112-460780f34e08 h1:QcG/1nAcBO1W1dKwZFZhPnXdkS8E5BVA/9EO9j9kG8g=
6 | github.com/88250/lute v1.7.1-0.20201227150112-460780f34e08/go.mod h1:APx6IhxzrUGJqmIalKYomjVLrO4yw/PzEBUtYxpMrfk=
7 | github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
8 | github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
9 | github.com/alecthomas/chroma v0.8.2 h1:x3zkuE2lUk/RIekyAJ3XRqSCP4zwWDfcw/YJCuCAACg=
10 | github.com/alecthomas/chroma v0.8.2/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
11 | github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
12 | github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk=
13 | github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
14 | github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
15 | github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
16 | github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c h1:MVVbswUlqicyj8P/JljoocA7AyCo62gzD0O7jfvrhtE=
17 | github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
18 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
19 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
20 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
21 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
22 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
23 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24 | github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
25 | github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
26 | github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
27 | github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
28 | github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
29 | github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
30 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
31 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
32 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
33 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
34 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
35 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
36 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
37 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
38 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
39 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
40 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
41 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
42 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
43 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
44 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
45 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
46 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
47 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
48 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
49 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
50 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
51 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
52 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
53 | github.com/unidoc/unioffice v1.4.0 h1:yl+TbZJu2GTVYAYvu51wppj0R+fPC67xzVcy91qgrzI=
54 | github.com/unidoc/unioffice v1.4.0/go.mod h1:7wl8btOkZW1TfqfpDWoujRXkUpowwisGRYDo7COHBiI=
55 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
56 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
57 | golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
58 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8=
59 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
60 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
61 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
62 | golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
63 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
64 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
65 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
66 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
67 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
68 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
69 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
70 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
71 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
72 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
73 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
74 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // Lute DOCX - 一款将 Markdown 文本转换为 Word 文档 (.docx) 的小工具
2 | // Copyright (c) 2020-present, b3log.org
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU Affero General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU Affero General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU Affero General Public License
15 | // along with this program. If not, see