Все требования реализованы без полного покрытия модульными тестами. (#3)
All checks were successful
drawio-export/pipeline/head This commit looks good
All checks were successful
drawio-export/pipeline/head This commit looks good
PR closes #1 Reviewed-on: #3
This commit is contained in:
parent
0d6f42f425
commit
78624641e4
@ -1,7 +1,31 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.mousesoft.ru/ms/drawio-exporter/pkg/drawio"
|
||||
)
|
||||
|
||||
var version string // Версия приложения
|
||||
|
||||
func main() {
|
||||
fmt.Println("Draw.io Exporter")
|
||||
flag.Parse()
|
||||
if flagHelp {
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
if flagVersion {
|
||||
fmt.Println("Draw.io Export cli util", version)
|
||||
os.Exit(0)
|
||||
}
|
||||
if flag.NArg() < 1 {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
exporter := drawio.NewWithOptions(&opts)
|
||||
if err := exporter.Export(flag.Args()...); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error:", err)
|
||||
}
|
||||
}
|
||||
|
75
cmd/drawio-export/options.go
Normal file
75
cmd/drawio-export/options.go
Normal file
@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"git.mousesoft.ru/ms/drawio-exporter/pkg/drawio"
|
||||
)
|
||||
|
||||
var (
|
||||
flagHelp bool // Вывести справку о приложении и выйти
|
||||
flagVersion bool // Вывести информацию о версии приложения и выйти
|
||||
opts = drawio.Options{} // Аргументы командной строки приложения
|
||||
)
|
||||
|
||||
func init() {
|
||||
// version
|
||||
flag.BoolVar(&flagVersion, "V", false, "Prints version information")
|
||||
flag.BoolVar(&flagVersion, "version", false, "Prints version information")
|
||||
// Application
|
||||
flag.StringVar(&opts.Application, "A", "", "Draw.io Desktop Application")
|
||||
flag.StringVar(&opts.Application, "application", "", "Draw.io Desktop Application")
|
||||
// Output
|
||||
flag.StringVar(&opts.Output, "o", "", "Exported folder name [default: export]")
|
||||
flag.StringVar(&opts.Output, "output", "", "Exported folder name [default: export]")
|
||||
// Format
|
||||
flag.Var(&opts.Format, "f",
|
||||
"Exported format [default: pdf] [possible values: pdf, png, jpg, svg, vsdx, xml]")
|
||||
flag.Var(&opts.Format, "format",
|
||||
"Exported format [default: pdf] [possible values: pdf, png, jpg, svg, vsdx, xml]")
|
||||
// Recursive
|
||||
flag.BoolVar(&opts.Recursive, "r", false,
|
||||
"For a folder input, recursively convert all files in sub-folders also")
|
||||
flag.BoolVar(&opts.Recursive, "recursive", false,
|
||||
"For a folder input, recursively convert all files in sub-folders also")
|
||||
// RemovePageSuffix
|
||||
flag.BoolVar(&opts.RemovePageSuffix, "remove-page-suffix", false,
|
||||
"Remove page suffix when possible (in case of single page file")
|
||||
// Quality
|
||||
flag.UintVar(&opts.Quality, "q", 0, "Output image quality for JPEG [default: 90]")
|
||||
flag.UintVar(&opts.Quality, "quality", 0, "Output image quality for JPEG [default: 90]")
|
||||
// Transparent
|
||||
flag.BoolVar(&opts.Transparent, "t", false, "Set transparent background for PNG")
|
||||
flag.BoolVar(&opts.Transparent, "transparent", false, "Set transparent background for PNG")
|
||||
// EmbedDiagram
|
||||
flag.BoolVar(&opts.EmbedDiagram, "e", false,
|
||||
"Includes a copy of the diagram for PDF, PNG, or SVG")
|
||||
flag.BoolVar(&opts.EmbedDiagram, "embed-diagram", false,
|
||||
"Includes a copy of the diagram for PDF, PNG, or SVG")
|
||||
// EmbedSvgImages
|
||||
flag.BoolVar(&opts.EmbedSvgImages, "embed-svg-images", false, "Embed Images in SVG file")
|
||||
// Border
|
||||
flag.UintVar(&opts.Border, "b", 0,
|
||||
"Sets the border width around the diagram [default: 0]")
|
||||
flag.UintVar(&opts.Border, "border", 0,
|
||||
"Sets the border width around the diagram [default: 0]")
|
||||
// Scale
|
||||
flag.UintVar(&opts.Scale, "s", 0, "Scales the diagram size")
|
||||
flag.UintVar(&opts.Scale, "scale", 0, "Scales the diagram size")
|
||||
// Width
|
||||
flag.UintVar(&opts.Width, "width", 0,
|
||||
"Fits the generated image/pdf into the specified width, preserves aspect ratio")
|
||||
// Height
|
||||
flag.UintVar(&opts.Height, "height", 0,
|
||||
"Fits the generated image/pdf into the specified height, preserves aspect ratio")
|
||||
// Crop
|
||||
flag.BoolVar(&opts.Crop, "crop", false, "Crops PDF to diagram size")
|
||||
// Uncompressed
|
||||
flag.BoolVar(&opts.Uncompressed, "u", false, "Uncompressed XML output")
|
||||
flag.BoolVar(&opts.Uncompressed, "uncompressed", false, "Uncompressed XML output")
|
||||
// EnablePlugins
|
||||
flag.BoolVar(&opts.EnablePlugins, "enable-plugins", false, "Enable Plugins")
|
||||
// help
|
||||
flag.BoolVar(&flagHelp, "h", false, "Prints help information")
|
||||
flag.BoolVar(&flagHelp, "help", false, "Prints help information")
|
||||
}
|
@ -2,12 +2,67 @@ package drawio
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
xmlparser "github.com/tamerh/xml-stream-parser"
|
||||
)
|
||||
|
||||
// Создать нового экспортёра диаграмм
|
||||
func New(opt ...Option) Exporter {
|
||||
options := &Options{
|
||||
Application: "drawio",
|
||||
Output: "export",
|
||||
Format: Format("pdf"),
|
||||
}
|
||||
for _, opt := range opt {
|
||||
opt.apply(options)
|
||||
}
|
||||
return NewWithOptions(options)
|
||||
}
|
||||
|
||||
// Создать нового экспортёра с параметрами
|
||||
func NewWithOptions(opts *Options) Exporter {
|
||||
return Exporter{opts: opts}
|
||||
}
|
||||
|
||||
// Экспортёр диаграмм
|
||||
type Exporter struct {
|
||||
opts *Options
|
||||
}
|
||||
|
||||
// Экспорт диаграмм из указанный файлов или папок
|
||||
func (exp Exporter) Export(fileOrDir ...string) error {
|
||||
var (
|
||||
err error
|
||||
errs = []error{}
|
||||
args = exp.opts.Args()
|
||||
info fs.FileInfo
|
||||
)
|
||||
for _, item := range fileOrDir {
|
||||
if info, err = os.Stat(item); err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
if info.IsDir() {
|
||||
err = exp.exportDir(item, args)
|
||||
} else {
|
||||
err = exp.exportFile(item, args)
|
||||
}
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// Разбирает данные и возвращает срез имён диаграмм в них
|
||||
func Diagrams(reader io.Reader) ([]string, error) {
|
||||
var (
|
||||
@ -21,7 +76,6 @@ func Diagrams(reader io.Reader) ([]string, error) {
|
||||
if xml.Err != nil {
|
||||
return result, xml.Err
|
||||
}
|
||||
fmt.Println(xml.Name)
|
||||
if items, ok := xml.Childs["diagram"]; ok {
|
||||
for _, item := range items {
|
||||
result = append(result, item.Attrs["name"])
|
||||
@ -30,3 +84,72 @@ func Diagrams(reader io.Reader) ([]string, error) {
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Экспорт диаграмм из всех файлов в папке
|
||||
func (exp Exporter) exportDir(dirPath string, args []string, subDir ...string) error {
|
||||
var (
|
||||
entries []fs.DirEntry
|
||||
err error
|
||||
errs []error
|
||||
)
|
||||
if entries, err = os.ReadDir(dirPath); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
outDir := make([]string, len(subDir), len(subDir)+1)
|
||||
outDir = append(outDir, name)
|
||||
if entry.IsDir() && exp.opts.Recursive {
|
||||
err = exp.exportDir(path.Join(dirPath, name), args, outDir...)
|
||||
} else {
|
||||
err = exp.exportFile(path.Join(dirPath, name), args, outDir...)
|
||||
}
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// Экспорт файла с диаграммами
|
||||
func (exp Exporter) exportFile(filePath string, args []string, subDir ...string) error {
|
||||
var (
|
||||
file *os.File
|
||||
diagrams []string
|
||||
err error
|
||||
)
|
||||
if file, err = os.Open(filePath); err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
if diagrams, err = Diagrams(file); err != nil {
|
||||
return err
|
||||
}
|
||||
outDirs := make([]string, 1, len(subDir)+1)
|
||||
outDirs[0] = exp.opts.Output
|
||||
outDirs = append(outDirs, subDir...)
|
||||
output := path.Join(outDirs...)
|
||||
errs := []error{}
|
||||
for i, name := range diagrams {
|
||||
outName := strings.TrimSuffix(path.Base(filePath), path.Ext(filePath))
|
||||
if !exp.opts.RemovePageSuffix || len(diagrams) > 1 {
|
||||
outName += "-" + name
|
||||
}
|
||||
outName += exp.opts.OutExt()
|
||||
drawioArgs := make([]string, len(args), len(args)+4)
|
||||
copy(drawioArgs, args)
|
||||
drawioArgs = append(drawioArgs,
|
||||
"--page-index", strconv.Itoa(i),
|
||||
"--output", path.Join(output, outName),
|
||||
"--export", filePath,
|
||||
)
|
||||
cmd := exec.Command(exp.opts.App(), drawioArgs...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
fmt.Println("Run command:", cmd.Path, cmd.Args)
|
||||
if err = cmd.Run(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
350
pkg/drawio/options.go
Normal file
350
pkg/drawio/options.go
Normal file
@ -0,0 +1,350 @@
|
||||
package drawio
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Интерфейс параметра экспортёра диаграмм
|
||||
type Option interface {
|
||||
apply(opts *Options) // Применить параметр к хранилищу параметров
|
||||
}
|
||||
|
||||
// Ошибка "неподдерживаемый формат"
|
||||
type ErrUnsupportedFormat struct {
|
||||
Format string
|
||||
}
|
||||
|
||||
// Описание ошибки
|
||||
func (e ErrUnsupportedFormat) Error() string {
|
||||
return fmt.Sprintf("unsupported format: '%s'", e.Format)
|
||||
}
|
||||
|
||||
var _ error = (*ErrUnsupportedFormat)(nil) // Проверка реализации интерфейса
|
||||
|
||||
type Format string // Формат экспортированного файла
|
||||
|
||||
// Строковое представление формата
|
||||
func (f Format) String() string {
|
||||
return string(f)
|
||||
}
|
||||
|
||||
// Загрузка формата из строки
|
||||
func (f *Format) Set(str string) error {
|
||||
for _, fmt := range SupportedFormats() {
|
||||
if str == string(fmt) {
|
||||
*f = Format(str)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return &ErrUnsupportedFormat{str}
|
||||
}
|
||||
|
||||
var _ flag.Value = (*Format)(nil) // Поверка реализации интерфейса
|
||||
|
||||
// Расширение файла заданного формата
|
||||
func (f Format) ext() string {
|
||||
return "." + string(f)
|
||||
}
|
||||
|
||||
// Параметры экспортёра диаграмм
|
||||
type Options struct {
|
||||
Application string // Путь к приложению drawio
|
||||
Output string // Путь к папке с экспортированными файлами
|
||||
Format Format // Формат экспортированных файлов
|
||||
Recursive bool // Рекурсивно сканировать вложенные папки с файлами
|
||||
RemovePageSuffix bool // Удалять суффикс страницы, если это возможно
|
||||
Quality uint // Качество экспортированного изображения (только для JPEG)
|
||||
Transparent bool // Прозрачный фона для PNG
|
||||
// Включать копию диаграммы в экспортированный файл для PDF, PNG и SVG
|
||||
EmbedDiagram bool
|
||||
EmbedSvgImages bool // Встраивать изображения в файл формата SVG
|
||||
Border uint // Ширина рамки вокруг диаграмм
|
||||
Scale uint // Масштаб в процентах размера экспортированных диаграмм
|
||||
Width uint // Ширина экспортированной диаграммы с сохранением масштаба
|
||||
Height uint // Высота экспортированной диаграммы с сохранением масштаба
|
||||
Crop bool // Обрезать результирующий PDF до размера диаграммы
|
||||
Uncompressed bool // Выводить несжатый XML
|
||||
EnablePlugins bool // Включить подключаемые модули
|
||||
}
|
||||
|
||||
// Путь к приложению drawio
|
||||
func (opts Options) App() string {
|
||||
app := opts.Application
|
||||
if len(app) == 0 {
|
||||
app = "drawio"
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
// Путь к папке с экспортированными файлами
|
||||
func (opts Options) OutDir() string {
|
||||
out := opts.Output
|
||||
if len(out) == 0 {
|
||||
out = "export"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Расширение экспортированных файлов
|
||||
func (opts Options) OutExt() string {
|
||||
fmt := opts.Format
|
||||
if len(fmt) == 0 {
|
||||
fmt = Format("pdf")
|
||||
}
|
||||
return fmt.ext()
|
||||
}
|
||||
|
||||
// Формирование аргументов командной строки
|
||||
func (opts Options) Args() []string {
|
||||
args := []string{}
|
||||
if len(opts.Format) > 0 {
|
||||
args = append(args, "--format", string(opts.Format))
|
||||
}
|
||||
if opts.Quality != 0 {
|
||||
args = append(args, "--quality", strconv.Itoa(int(opts.Quality)))
|
||||
}
|
||||
if opts.Transparent {
|
||||
args = append(args, "--transparent")
|
||||
}
|
||||
if opts.EmbedDiagram {
|
||||
args = append(args, "--embed-diagram")
|
||||
}
|
||||
if opts.EmbedSvgImages {
|
||||
args = append(args, "--embed-svg-images")
|
||||
}
|
||||
if opts.Border != 0 {
|
||||
args = append(args, "--border", strconv.Itoa(int(opts.Border)))
|
||||
}
|
||||
if opts.Scale != 0 {
|
||||
args = append(args, "--scale", strconv.Itoa(int(opts.Scale)))
|
||||
}
|
||||
if opts.Width != 0 {
|
||||
args = append(args, "--width", strconv.Itoa(int(opts.Width)))
|
||||
}
|
||||
if opts.Height != 0 {
|
||||
args = append(args, "--height", strconv.Itoa(int(opts.Height)))
|
||||
}
|
||||
if opts.Crop {
|
||||
args = append(args, "--crop")
|
||||
}
|
||||
if opts.Uncompressed {
|
||||
args = append(args, "--uncompressed")
|
||||
}
|
||||
if opts.EnablePlugins {
|
||||
args = append(args, "--enable-plugins")
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// Путь к приложению drawio
|
||||
func WithAppPath(path string) Option {
|
||||
return optionApplication(path)
|
||||
}
|
||||
|
||||
type optionApplication string
|
||||
|
||||
var _ Option = (*optionApplication)(nil) // Проверка реализации интерфейса
|
||||
|
||||
func (opt optionApplication) apply(opts *Options) {
|
||||
opts.Application = string(opt)
|
||||
}
|
||||
|
||||
// Путь к папке с экспортированными файлами
|
||||
func WithOutput(path string) Option {
|
||||
return optionOutput(path)
|
||||
}
|
||||
|
||||
type optionOutput string
|
||||
|
||||
var _ Option = (*optionOutput)(nil) // Проверка реализации интерфейса
|
||||
|
||||
func (opt optionOutput) apply(opts *Options) {
|
||||
opts.Output = string(opt)
|
||||
}
|
||||
|
||||
// Формат экспортированных файлов
|
||||
func WithFormat(format Format) Option {
|
||||
return format
|
||||
}
|
||||
|
||||
var _ Option = (*Format)(nil) // Проверка реализации интерфейса
|
||||
|
||||
func (opt Format) apply(opts *Options) {
|
||||
opts.Format = opt
|
||||
}
|
||||
|
||||
// Рекурсивно сканировать вложенные папки с файлами
|
||||
func WithRecursive() Option {
|
||||
return optionRecursive{}
|
||||
}
|
||||
|
||||
type optionRecursive struct{}
|
||||
|
||||
var _ Option = optionRecursive{} // Проверка реализации интерфейса
|
||||
|
||||
func (opt optionRecursive) apply(opts *Options) {
|
||||
opts.Recursive = true
|
||||
}
|
||||
|
||||
// Удалять суффикс страницы, если это возможно
|
||||
func WithRemovePageSuffix() Option {
|
||||
return optionRemovePageSuffix{}
|
||||
}
|
||||
|
||||
type optionRemovePageSuffix struct{}
|
||||
|
||||
var _ Option = optionRemovePageSuffix{} // Проверка реализации интерфейса
|
||||
|
||||
func (opt optionRemovePageSuffix) apply(opts *Options) {
|
||||
opts.RemovePageSuffix = true
|
||||
}
|
||||
|
||||
// Качество экспортированного изображения (только для JPEG)
|
||||
func WithQuality(q int) Option {
|
||||
return optionQuality(q)
|
||||
}
|
||||
|
||||
type optionQuality uint
|
||||
|
||||
var _ Option = (*optionQuality)(nil) // Проверка реализации интерфейса
|
||||
|
||||
func (opt optionQuality) apply(opts *Options) {
|
||||
opts.Quality = uint(opt)
|
||||
}
|
||||
|
||||
// Прозрачный фона для PNG
|
||||
func WithTransparent() Option {
|
||||
return optionTransparent{}
|
||||
}
|
||||
|
||||
type optionTransparent struct{}
|
||||
|
||||
var _ Option = optionTransparent{} // Проверка реализации интерфейса
|
||||
|
||||
func (opt optionTransparent) apply(opts *Options) {
|
||||
opts.Transparent = true
|
||||
}
|
||||
|
||||
// Включать копию диаграммы в экспортированный файл для PDF, PNG и SVG
|
||||
func WithEmbedDiagram() Option {
|
||||
return optionEmbedDiagram{}
|
||||
}
|
||||
|
||||
type optionEmbedDiagram struct{}
|
||||
|
||||
var _ Option = optionEmbedDiagram{} // Проверка реализации интерфейса
|
||||
|
||||
func (opt optionEmbedDiagram) apply(opts *Options) {
|
||||
opts.EmbedDiagram = true
|
||||
}
|
||||
|
||||
// Встраивать изображения в файл формата SVG
|
||||
func WithEmbedSvgImages() Option {
|
||||
return optionEmbedSvgImages{}
|
||||
}
|
||||
|
||||
type optionEmbedSvgImages struct{}
|
||||
|
||||
var _ Option = optionEmbedSvgImages{} // Проверка реализации интерфейса
|
||||
|
||||
func (opt optionEmbedSvgImages) apply(opts *Options) {
|
||||
opts.EmbedSvgImages = true
|
||||
}
|
||||
|
||||
// Ширина рамки вокруг диаграмм
|
||||
func WithBorder(border int) Option {
|
||||
return optionBorder(border)
|
||||
}
|
||||
|
||||
type optionBorder uint
|
||||
|
||||
var _ Option = (*optionBorder)(nil) // Проверка реализации интерфейса
|
||||
|
||||
func (opt optionBorder) apply(opts *Options) {
|
||||
opts.Border = uint(opt)
|
||||
}
|
||||
|
||||
// Масштаб в процентах размера экспортированных диаграмм
|
||||
func WithScale(scale int) Option {
|
||||
return optionScale(scale)
|
||||
}
|
||||
|
||||
type optionScale uint
|
||||
|
||||
var _ Option = (*optionScale)(nil) // Проверка реализации интерфейса
|
||||
|
||||
func (opt optionScale) apply(opts *Options) {
|
||||
opts.Scale = uint(opt)
|
||||
}
|
||||
|
||||
// Ширина экспортированной диаграммы с сохранением масштаба
|
||||
func WithWidth(width int) Option {
|
||||
return optionWidth(width)
|
||||
}
|
||||
|
||||
type optionWidth uint
|
||||
|
||||
var _ Option = (*optionWidth)(nil) // Проверка реализации интерфейса
|
||||
|
||||
func (opt optionWidth) apply(opts *Options) {
|
||||
opts.Width = uint(opt)
|
||||
}
|
||||
|
||||
// Высота экспортированной диаграммы с сохранением масштаба
|
||||
func WithHeight(height int) Option {
|
||||
return optionHeight(height)
|
||||
}
|
||||
|
||||
type optionHeight uint
|
||||
|
||||
var _ Option = (*optionHeight)(nil) // Проверка реализации интерфейса
|
||||
|
||||
func (opt optionHeight) apply(opts *Options) {
|
||||
opts.Height = uint(opt)
|
||||
}
|
||||
|
||||
// Обрезать результирующий PDF до размера диаграммы
|
||||
func WithCrop() Option {
|
||||
return optionCrop{}
|
||||
}
|
||||
|
||||
type optionCrop struct{}
|
||||
|
||||
var _ Option = optionCrop{} // Проверка реализации интерфейса
|
||||
|
||||
func (opt optionCrop) apply(opts *Options) {
|
||||
opts.Crop = true
|
||||
}
|
||||
|
||||
// Выводить несжатый XML
|
||||
func WithUncompressed() Option {
|
||||
return optionUncompressed{}
|
||||
}
|
||||
|
||||
type optionUncompressed struct{}
|
||||
|
||||
var _ Option = optionUncompressed{} // Проверка реализации интерфейса
|
||||
|
||||
func (opt optionUncompressed) apply(opts *Options) {
|
||||
opts.Uncompressed = true
|
||||
}
|
||||
|
||||
// Включить подключаемые модули
|
||||
func WithEnablePlugins() Option {
|
||||
return optionEnablePlugins{}
|
||||
}
|
||||
|
||||
type optionEnablePlugins struct{}
|
||||
|
||||
var _ Option = optionEnablePlugins{} // Проверка реализации интерфейса
|
||||
|
||||
func (opt optionEnablePlugins) apply(opts *Options) {
|
||||
opts.EnablePlugins = true
|
||||
}
|
||||
|
||||
// Список всех поддерживаемых форматов экспорта
|
||||
func SupportedFormats() []Format {
|
||||
return []Format{"pdf", "png", "jpg", "svg", "vsdx", "xml"}
|
||||
}
|
Loading…
Reference in New Issue
Block a user