initial
This commit is contained in:
896
converter.go
Normal file
896
converter.go
Normal file
@@ -0,0 +1,896 @@
|
||||
package gliffy2drawio
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"math"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GliffyDiagramConverter struct {
|
||||
diagramString string
|
||||
gliffyDiagram Diagram
|
||||
drawioDiagram *MxGraph
|
||||
vertices map[string]*GliffyObject
|
||||
layers map[string]*GliffyLayer
|
||||
|
||||
rotationPattern *regexp.Regexp
|
||||
pageIDPattern *regexp.Regexp
|
||||
namePattern *regexp.Regexp
|
||||
|
||||
report strings.Builder
|
||||
translator *StencilTranslator
|
||||
}
|
||||
|
||||
func NewGliffyDiagramConverter(gliffyDiagramString string) (*GliffyDiagramConverter, error) {
|
||||
c := &GliffyDiagramConverter{
|
||||
diagramString: gliffyDiagramString,
|
||||
drawioDiagram: NewMxGraph(),
|
||||
vertices: make(map[string]*GliffyObject),
|
||||
layers: make(map[string]*GliffyLayer),
|
||||
rotationPattern: regexp.MustCompile(`rotation=(\-?\w+)`),
|
||||
pageIDPattern: regexp.MustCompile(`pageId=([^&]+)`),
|
||||
namePattern: regexp.MustCompile(`name=([^&]+)`),
|
||||
translator: NewStencilTranslator(""),
|
||||
}
|
||||
if err := c.start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *GliffyDiagramConverter) start() error {
|
||||
if err := json.Unmarshal([]byte(c.diagramString), &c.gliffyDiagram); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Support newer Gliffy files that wrap content in pages/scene.
|
||||
if len(c.gliffyDiagram.Stage.Objects) == 0 && len(c.gliffyDiagram.Pages) > 0 {
|
||||
selected := c.gliffyDiagram.Pages[0]
|
||||
if c.gliffyDiagram.DefaultPage != "" {
|
||||
for _, p := range c.gliffyDiagram.Pages {
|
||||
if p.ID == c.gliffyDiagram.DefaultPage {
|
||||
selected = p
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
scene := selected.Scene
|
||||
c.gliffyDiagram.Stage = Stage{
|
||||
Background: scene.Background,
|
||||
Width: scene.Width,
|
||||
Height: scene.Height,
|
||||
AutoFit: scene.AutoFit,
|
||||
GridOn: scene.GridOn,
|
||||
DrawingGuidesOn: scene.DrawingGuidesOn,
|
||||
Objects: scene.Objects,
|
||||
Layers: scene.Layers,
|
||||
TextStyles: scene.TextStyles,
|
||||
}
|
||||
}
|
||||
|
||||
c.collectLayersAndConvert(c.layers, c.gliffyDiagram.Stage.Layers)
|
||||
c.collectVerticesAndConvert(c.vertices, c.gliffyDiagram.Stage.Objects, nil)
|
||||
|
||||
sortObjectsByOrder(c.gliffyDiagram.Stage.Objects)
|
||||
|
||||
c.importLayers()
|
||||
for _, obj := range c.gliffyDiagram.Stage.Objects {
|
||||
if err := c.importObject(obj, obj.Parent); err != nil {
|
||||
c.report.WriteString("-- Warning, Object " + obj.ID + " cannot be transformed.\n")
|
||||
}
|
||||
}
|
||||
// Shift everything to origin to avoid bottom-right offset in draw.io.
|
||||
c.drawioDiagram.NormalizeOrigin()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GliffyDiagramConverter) importLayers() {
|
||||
if len(c.gliffyDiagram.Stage.Layers) == 0 {
|
||||
return
|
||||
}
|
||||
sortLayersByOrder(c.gliffyDiagram.Stage.Layers)
|
||||
for i, layer := range c.gliffyDiagram.Stage.Layers {
|
||||
// Avoid duplicating the default layer (id=1 already in root).
|
||||
if i == 0 && layer.MxObject != nil && layer.MxObject.ID == "1" && c.drawioDiagram.HasCell("1") {
|
||||
continue
|
||||
}
|
||||
c.drawioDiagram.AddCell(layer.MxObject, c.drawioDiagram.root)
|
||||
if i == 0 {
|
||||
c.drawioDiagram.SetDefaultParent(layer.MxObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GliffyDiagramConverter) importObject(obj *GliffyObject, gliffyParent *GliffyObject) error {
|
||||
var parent *MxCell
|
||||
if gliffyParent != nil {
|
||||
parent = gliffyParent.MxObject
|
||||
}
|
||||
if parent == nil && obj.LayerID != nil {
|
||||
if layer, ok := c.layers[*obj.LayerID]; ok && layer != nil {
|
||||
parent = layer.MxObject
|
||||
}
|
||||
}
|
||||
|
||||
c.drawioDiagram.AddCell(obj.MxObject, parent)
|
||||
|
||||
if obj.hasChildren() {
|
||||
if obj.isSwimlane() {
|
||||
if obj.Rotation != 0 {
|
||||
reverse(obj.Children)
|
||||
}
|
||||
} else {
|
||||
sortObjectsByOrder(obj.Children)
|
||||
}
|
||||
for _, child := range obj.Children {
|
||||
if err := c.importObject(child, obj); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if obj.isLine() {
|
||||
startTerminal := c.getTerminalCell(obj, true)
|
||||
endTerminal := c.getTerminalCell(obj, false)
|
||||
if startTerminal != nil {
|
||||
obj.MxObject.Source = startTerminal.ID
|
||||
}
|
||||
if endTerminal != nil {
|
||||
obj.MxObject.Target = endTerminal.ID
|
||||
}
|
||||
c.setWaypoints(obj, startTerminal, endTerminal)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GliffyDiagramConverter) getTerminalCell(edge *GliffyObject, start bool) *MxCell {
|
||||
cons := edge.GetConstraints()
|
||||
if cons == nil {
|
||||
return nil
|
||||
}
|
||||
var con *Constraint
|
||||
if start {
|
||||
con = cons.StartConstraint
|
||||
} else {
|
||||
con = cons.EndConstraint
|
||||
}
|
||||
if con == nil {
|
||||
return nil
|
||||
}
|
||||
var data *ConstraintData
|
||||
if start {
|
||||
data = con.StartPositionConstraint
|
||||
} else {
|
||||
data = con.EndPositionConstraint
|
||||
}
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
terminal := c.vertices[data.NodeID]
|
||||
if terminal == nil {
|
||||
return nil
|
||||
}
|
||||
return terminal.MxObject
|
||||
}
|
||||
|
||||
func (c *GliffyDiagramConverter) collectLayersAndConvert(layerMap map[string]*GliffyLayer, layers []*GliffyLayer) {
|
||||
if len(layers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure only a single default layer (id=1, parent=0) exists.
|
||||
if len(c.drawioDiagram.Model.Root.Cells) > 2 {
|
||||
trimmed := []*MxCell{}
|
||||
for _, cell := range c.drawioDiagram.Model.Root.Cells {
|
||||
if cell == nil {
|
||||
continue
|
||||
}
|
||||
if cell.ID == "0" || cell.ID == "1" {
|
||||
trimmed = append(trimmed, cell)
|
||||
}
|
||||
}
|
||||
if len(trimmed) < 2 {
|
||||
trimmed = append(trimmed, &MxCell{ID: "1", Parent: c.drawioDiagram.root.ID})
|
||||
}
|
||||
c.drawioDiagram.Model.Root.Cells = trimmed
|
||||
}
|
||||
|
||||
// Reuse existing default layer (id 1, parent 0) for the first Gliffy layer to match draw.io expectations.
|
||||
var defaultLayer *MxCell
|
||||
if len(c.drawioDiagram.Model.Root.Cells) >= 2 {
|
||||
if cell := c.drawioDiagram.Model.Root.Cells[1]; cell != nil && cell.Parent == c.drawioDiagram.root.ID {
|
||||
defaultLayer = cell
|
||||
}
|
||||
}
|
||||
if defaultLayer == nil {
|
||||
defaultLayer = &MxCell{ID: "1", Parent: c.drawioDiagram.root.ID}
|
||||
c.drawioDiagram.Model.Root.Cells = append(c.drawioDiagram.Model.Root.Cells, defaultLayer)
|
||||
}
|
||||
|
||||
for i, layer := range layers {
|
||||
var cell *MxCell
|
||||
if i == 0 {
|
||||
cell = defaultLayer
|
||||
} else {
|
||||
cell = &MxCell{}
|
||||
c.drawioDiagram.AddCell(cell, c.drawioDiagram.root)
|
||||
}
|
||||
cell.Value = layer.Name
|
||||
if layer.Locked {
|
||||
cell.Style = joinStyle(cell.Style, "locked=1")
|
||||
}
|
||||
if !layer.Visible {
|
||||
cell.Style = joinStyle(cell.Style, "visible=0")
|
||||
}
|
||||
layer.MxObject = cell
|
||||
layerMap[layer.GUID] = layer
|
||||
if i == 0 {
|
||||
c.drawioDiagram.SetDefaultParent(cell)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GliffyDiagramConverter) collectVerticesAndConvert(vertices map[string]*GliffyObject, objects []*GliffyObject, parent *GliffyObject) {
|
||||
for _, obj := range objects {
|
||||
obj.Parent = parent
|
||||
obj.MxObject = c.convertGliffyObject(obj, parent)
|
||||
if !obj.isLine() {
|
||||
vertices[obj.ID] = obj
|
||||
}
|
||||
if obj.isGroup() || obj.isSelection() || (obj.isLine() && obj.hasChildren()) {
|
||||
c.collectVerticesAndConvert(vertices, obj.Children, obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GliffyDiagramConverter) GraphXML() (string, error) {
|
||||
grid := c.gliffyDiagram.Stage.GridOn
|
||||
guides := c.gliffyDiagram.Stage.DrawingGuidesOn
|
||||
w := c.gliffyDiagram.Stage.Width
|
||||
h := c.gliffyDiagram.Stage.Height
|
||||
if w == 0 {
|
||||
w = 1200
|
||||
}
|
||||
if h == 0 {
|
||||
h = 900
|
||||
}
|
||||
xml, err := c.drawioDiagram.ToXML("default-style2", c.gliffyDiagram.Stage.Background, grid, guides, w, h)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name := c.diagramName()
|
||||
return fmt.Sprintf(`<mxfile><diagram name="%s">%s</diagram></mxfile>`, html.EscapeString(name), xml), nil
|
||||
}
|
||||
|
||||
func (c *GliffyDiagramConverter) diagramName() string {
|
||||
if c.gliffyDiagram.Title != "" {
|
||||
return c.gliffyDiagram.Title
|
||||
}
|
||||
if len(c.gliffyDiagram.Pages) > 0 {
|
||||
selected := c.gliffyDiagram.Pages[0]
|
||||
if c.gliffyDiagram.DefaultPage != "" {
|
||||
for _, p := range c.gliffyDiagram.Pages {
|
||||
if p.ID == c.gliffyDiagram.DefaultPage {
|
||||
selected = p
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if selected.Title != "" {
|
||||
return selected.Title
|
||||
}
|
||||
}
|
||||
if c.gliffyDiagram.Metadata.Title != "" {
|
||||
return c.gliffyDiagram.Metadata.Title
|
||||
}
|
||||
return "Page-1"
|
||||
}
|
||||
|
||||
func (c *GliffyDiagramConverter) convertGliffyObject(obj *GliffyObject, parent *GliffyObject) *MxCell {
|
||||
cell := &MxCell{}
|
||||
if obj.ID != "" {
|
||||
cell.ID = obj.ID
|
||||
}
|
||||
if obj.IsUnrecognizedGraphicType() {
|
||||
return cell
|
||||
}
|
||||
|
||||
var style strings.Builder
|
||||
geo := &MxGeometry{X: obj.X, Y: obj.Y, Width: obj.Width, Height: obj.Height}
|
||||
obj.adjustGeo(geo)
|
||||
cell.Geometry = geo
|
||||
|
||||
var textObject *GliffyObject
|
||||
var link string
|
||||
graphic := obj.GraphicOrChildGraphic()
|
||||
translatedStyle := c.translator.Translate(obj.UID, obj.TID)
|
||||
|
||||
if obj.isGroup() {
|
||||
if graphic == nil || translatedStyle == "" {
|
||||
style.WriteString("group;")
|
||||
}
|
||||
cell.Vertex = true
|
||||
} else {
|
||||
textObject = obj.TextObject()
|
||||
}
|
||||
|
||||
if graphic != nil {
|
||||
link = obj.AdjustedLink()
|
||||
switch graphic.GetType() {
|
||||
case GraphicTypeShape, GraphicTypeMindmap:
|
||||
shape := graphic.Shape
|
||||
if shape == nil {
|
||||
shape = &GliffyShape{}
|
||||
}
|
||||
cell.Vertex = true
|
||||
isChevron := strings.Contains(obj.UID, "chevron")
|
||||
if translatedStyle != "" {
|
||||
style.WriteString("shape=" + translatedStyle + ";")
|
||||
}
|
||||
if !strings.Contains(style.String(), "shadow=") {
|
||||
style.WriteString("shadow=" + intToString(boolToInt(shape.DropShadow)) + ";")
|
||||
}
|
||||
if !strings.Contains(style.String(), "strokeWidth") {
|
||||
style.WriteString("strokeWidth=" + intToString(shape.StrokeWidthValue()) + ";")
|
||||
if shape.StrokeWidthValue() == 0 && !isChevron {
|
||||
style.WriteString("strokeColor=none;")
|
||||
}
|
||||
}
|
||||
if !strings.Contains(style.String(), "fillColor") {
|
||||
if shape.NoFill() && !isChevron {
|
||||
style.WriteString("fillColor=none;")
|
||||
} else {
|
||||
style.WriteString("fillColor=" + shape.FillColor + ";")
|
||||
}
|
||||
if shape.FillColor == "none" {
|
||||
style.WriteString("pointerEvents=0;")
|
||||
}
|
||||
}
|
||||
if !strings.Contains(style.String(), "strokeColor") && !shape.NoFill() {
|
||||
stroke := shape.StrokeColor
|
||||
if obj.IsUseFillColorForStroke() {
|
||||
stroke = shape.FillColor
|
||||
}
|
||||
style.WriteString("strokeColor=" + stroke + ";")
|
||||
}
|
||||
if !strings.Contains(style.String(), "gradient") && shape.Gradient && !obj.GradientIgnored() {
|
||||
style.WriteString("gradientColor=" + obj.GradientColor() + ";gradientDirection=north;")
|
||||
}
|
||||
if !obj.isVennCircle() && !strings.Contains(style.String(), "opacity") {
|
||||
style.WriteString("opacity=" + floatToString(shape.Opacity*100) + ";")
|
||||
}
|
||||
style.WriteString(DashStyleMapping(shape.DashStyle, 1))
|
||||
if obj.IsSubRoutine() && obj.Width != 0 {
|
||||
style.WriteString("size=" + floatToString(10/obj.Width) + ";")
|
||||
}
|
||||
if fragment := obj.UMLSequenceCombinedFragmentText(); fragment != "" && len(obj.Children) > 0 {
|
||||
cell.Value = fragment
|
||||
obj.Children = obj.Children[1:]
|
||||
}
|
||||
case GraphicTypeLine:
|
||||
if graphic.Line == nil {
|
||||
break
|
||||
}
|
||||
line := graphic.Line
|
||||
cell.Edge = true
|
||||
style.WriteString("shape=filledEdge;")
|
||||
style.WriteString("strokeWidth=" + intToString(line.StrokeWidthValue()) + ";")
|
||||
style.WriteString("strokeColor=" + line.StrokeColor + ";")
|
||||
style.WriteString("fillColor=" + line.FillColor + ";")
|
||||
style.WriteString(ArrowMapping(line.StartArrow).ToString(true))
|
||||
style.WriteString(ArrowMapping(line.EndArrow).ToString(false))
|
||||
if line.CornerRadius != nil {
|
||||
style.WriteString("rounded=1;")
|
||||
} else {
|
||||
style.WriteString("rounded=0;")
|
||||
}
|
||||
style.WriteString(DashStyleMapping(line.DashStyle, line.StrokeWidthValue()))
|
||||
style.WriteString(LineMapping(line.Interpolation))
|
||||
cell.Geometry.X = 0
|
||||
cell.Geometry.Y = 0
|
||||
case GraphicTypeText:
|
||||
textObject = obj
|
||||
cell.Vertex = true
|
||||
style.WriteString("text;html=1;nl2Br=0;")
|
||||
cell.Value = obj.TextHTML()
|
||||
if obj.Parent != nil && !obj.Parent.isGroup() {
|
||||
parentGeo := obj.Parent.MxObject.Geometry
|
||||
if obj.Parent.isLine() {
|
||||
mxGeo := &MxGeometry{X: 0, Y: 0, Width: 0, Height: 0}
|
||||
lineT := 0.0
|
||||
if graphic.Text != nil {
|
||||
lineT = graphic.Text.LineTValue*2 - 1
|
||||
}
|
||||
mxGeo.X = lineT
|
||||
var lblY, lblX float64
|
||||
if graphic.Text != nil && graphic.Text.LinePerpValue != nil {
|
||||
control := obj.Parent.Graphic.Line.ControlPath
|
||||
if len(control) >= 2 {
|
||||
i1 := 0
|
||||
i2 := len(control) - 1
|
||||
noCardinal := false
|
||||
switch graphic.Text.CardinalityType {
|
||||
case "begin":
|
||||
i2 = 1
|
||||
case "end":
|
||||
i1 = len(control) - 2
|
||||
default:
|
||||
noCardinal = true
|
||||
}
|
||||
if noCardinal || control[i1][1] == control[i2][1] {
|
||||
lblY = *graphic.Text.LinePerpValue
|
||||
if control[i1][0]-control[i2][0] > 0 {
|
||||
lblY = -lblY
|
||||
}
|
||||
} else {
|
||||
lblX = *graphic.Text.LinePerpValue
|
||||
if control[i1][1]-control[i2][1] < 0 {
|
||||
lblX = -lblX
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mxGeo.SourcePoint = &MxPoint{X: lblX, Y: lblY}
|
||||
setRelative(mxGeo, true)
|
||||
cell.Geometry = mxGeo
|
||||
style.WriteString("labelBackgroundColor=" + c.gliffyDiagram.Stage.Background + ";")
|
||||
if graphic.Text != nil {
|
||||
graphic.Text.SetHAlign("")
|
||||
}
|
||||
} else {
|
||||
cell.Geometry = &MxGeometry{X: 0, Y: 0, Width: parentGeo.Width, Height: parentGeo.Height}
|
||||
setRelative(cell.Geometry, true)
|
||||
}
|
||||
}
|
||||
case GraphicTypeImage:
|
||||
img := graphic.Image
|
||||
cell.Vertex = true
|
||||
style.WriteString("shape=" + c.translator.Translate(obj.UID, obj.TID) + ";")
|
||||
style.WriteString("image=" + img.CleanURL() + ";")
|
||||
case GraphicTypeSVG:
|
||||
svg := graphic.Svg
|
||||
cell.Vertex = true
|
||||
style.WriteString("shape=image;imageAspect=0;")
|
||||
if svg != nil && svg.EmbeddedResourceID != nil {
|
||||
if res, ok := c.gliffyDiagram.EmbeddedResources.Get(*svg.EmbeddedResourceID); ok {
|
||||
svgUtil := SVGImporterUtils{}
|
||||
data := svgUtil.SetViewBox(res.Data)
|
||||
style.WriteString("image=data:image/svg+xml," + EmbeddedResource{Data: data}.Base64EncodedData() + ";")
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Fallback: render as image if possible or apply translated style if found.
|
||||
if graphic.Image != nil && graphic.Image.URL != "" {
|
||||
cell.Vertex = true
|
||||
style.WriteString("shape=image;imageAspect=0;image=" + graphic.Image.CleanURL() + ";")
|
||||
} else if graphic.Svg != nil && graphic.Svg.EmbeddedResourceID != nil {
|
||||
cell.Vertex = true
|
||||
style.WriteString("shape=image;imageAspect=0;")
|
||||
if res, ok := c.gliffyDiagram.EmbeddedResources.Get(*graphic.Svg.EmbeddedResourceID); ok {
|
||||
svgUtil := SVGImporterUtils{}
|
||||
data := svgUtil.SetViewBox(res.Data)
|
||||
style.WriteString("image=data:image/svg+xml," + EmbeddedResource{Data: data}.Base64EncodedData() + ";")
|
||||
}
|
||||
} else if translatedStyle != "" {
|
||||
cell.Vertex = true
|
||||
style.WriteString("shape=" + translatedStyle + ";")
|
||||
}
|
||||
}
|
||||
} else if obj.isSwimlane() && len(obj.Children) > 0 {
|
||||
cell.Vertex = true
|
||||
style.WriteString(c.translator.Translate(obj.UID, "") + ";")
|
||||
if obj.Rotation == 0 {
|
||||
style.WriteString("childLayout=stackLayout;resizeParent=1;resizeParentMax=0;")
|
||||
}
|
||||
header := obj.Children[0]
|
||||
shape := header.Graphic.Shape
|
||||
style.WriteString("strokeWidth=" + intToString(shape.StrokeWidthValue()) + ";")
|
||||
style.WriteString("shadow=" + intToString(boolToInt(shape.DropShadow)) + ";")
|
||||
style.WriteString("fillColor=" + shape.FillColor + ";")
|
||||
style.WriteString("strokeColor=" + shape.StrokeColor + ";")
|
||||
style.WriteString("startSize=" + floatToString(header.Height) + ";")
|
||||
style.WriteString("whiteSpace=wrap;")
|
||||
|
||||
for i := 1; i < len(obj.Children); i++ {
|
||||
gLane := obj.Children[i]
|
||||
gLane.Parent = obj
|
||||
gs := gLane.Graphic.Shape
|
||||
var laneStyle strings.Builder
|
||||
laneStyle.WriteString("swimlane;collapsible=0;swimlaneLine=0;")
|
||||
laneStyle.WriteString("strokeWidth=" + intToString(gs.StrokeWidthValue()) + ";")
|
||||
laneStyle.WriteString("shadow=" + intToString(boolToInt(gs.DropShadow)) + ";")
|
||||
laneStyle.WriteString("fillColor=" + gs.FillColor + ";")
|
||||
laneStyle.WriteString("strokeColor=" + gs.StrokeColor + ";")
|
||||
laneStyle.WriteString("whiteSpace=wrap;html=1;fontStyle=0;")
|
||||
|
||||
childGeometry := &MxGeometry{X: gLane.X, Y: gLane.Y, Width: gLane.Width, Height: gLane.Height}
|
||||
if obj.Rotation != 0 {
|
||||
if obj.Rotation == 270 {
|
||||
laneStyle.WriteString("horizontal=0;")
|
||||
w := childGeometry.Width
|
||||
childGeometry.Width = childGeometry.Height
|
||||
childGeometry.Height = w
|
||||
x := childGeometry.X
|
||||
childGeometry.X = childGeometry.Y
|
||||
childGeometry.Y = obj.Width - w - x
|
||||
} else {
|
||||
laneStyle.WriteString("rotation=" + floatToString(obj.Rotation) + ";")
|
||||
rotateGeometry(childGeometry, obj.Rotation, obj.Width/2, obj.Height/2)
|
||||
}
|
||||
}
|
||||
mxLane := &MxCell{Vertex: true}
|
||||
laneTxt := gLane.Children[0]
|
||||
mxLane.Value = laneTxt.TextHTML()
|
||||
if laneTxt.Graphic != nil && laneTxt.Graphic.Text != nil {
|
||||
laneStyle.WriteString(laneTxt.Graphic.Text.GetStyle(0, 0))
|
||||
}
|
||||
laneStyle.WriteString("gliffyId=" + gLane.ID + ";")
|
||||
mxLane.Style = laneStyle.String()
|
||||
mxLane.Geometry = childGeometry
|
||||
gLane.MxObject = mxLane
|
||||
cell.Geometry = geo
|
||||
}
|
||||
} else if obj.isMindmap() && len(obj.Children) > 0 {
|
||||
rectangle := obj.Children[0]
|
||||
if rectangle.Graphic == nil || rectangle.Graphic.Mindmap == nil {
|
||||
obj.MxObject = cell
|
||||
return cell
|
||||
}
|
||||
mindmap := rectangle.Graphic.Mindmap
|
||||
style.WriteString("shape=" + c.translator.Translate(obj.UID, "") + ";")
|
||||
style.WriteString("shadow=" + intToString(boolToInt(mindmap.DropShadow)) + ";")
|
||||
style.WriteString("strokeWidth=" + intToString(mindmap.StrokeWidthValue()) + ";")
|
||||
style.WriteString("fillColor=" + mindmap.FillColor + ";")
|
||||
style.WriteString("strokeColor=" + mindmap.StrokeColor + ";")
|
||||
style.WriteString(DashStyleMapping(mindmap.DashStyle, 1))
|
||||
if mindmap.Gradient {
|
||||
style.WriteString("gradientColor=#FFFFFF;gradientDirection=north;")
|
||||
}
|
||||
cell.Vertex = true
|
||||
}
|
||||
|
||||
if !obj.isLine() {
|
||||
if strings.Contains(style.String(), "rotation") {
|
||||
if m := c.rotationPattern.FindStringSubmatch(style.String()); len(m) > 1 {
|
||||
initial, _ := strconv.ParseFloat(m[1], 64)
|
||||
rotation := initial + obj.Rotation
|
||||
styleStr := c.rotationPattern.ReplaceAllString(style.String(), "rotation="+floatToString(rotation))
|
||||
style.Reset()
|
||||
style.WriteString(styleStr)
|
||||
}
|
||||
} else if obj.Rotation != 0 {
|
||||
if strings.Contains(style.String(), "swimlane;collapsible=0;") && obj.Rotation == 270 {
|
||||
w := geo.Width
|
||||
h := geo.Height
|
||||
geo.X = geo.X + (w-h)/2
|
||||
geo.Y = geo.Y + (h-w)/2
|
||||
geo.Width = h
|
||||
geo.Height = w
|
||||
style.WriteString("childLayout=stackLayout;resizeParent=1;resizeParentMax=0;horizontal=0;horizontalStack=0;")
|
||||
} else {
|
||||
if obj.isGroup() {
|
||||
for _, child := range obj.Children {
|
||||
c.rotateGroupedObject(obj, child)
|
||||
}
|
||||
}
|
||||
style.WriteString("rotation=" + floatToString(obj.Rotation) + ";")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if textObject != nil {
|
||||
style.WriteString("html=1;nl2Br=0;")
|
||||
if !obj.isLine() && textObject.Graphic != nil && textObject.Graphic.Text != nil {
|
||||
txt := textObject.Graphic.Text
|
||||
if obj.isSwimlane() {
|
||||
txt.SetForceTopPaddingShift(true)
|
||||
txt.SetVAlign("middle")
|
||||
}
|
||||
cell.Value = textObject.TextHTML()
|
||||
obj.adjustTextPos(textObject)
|
||||
if obj.ContainsTextBracket() {
|
||||
c.fixFrameTextBorder(obj, &style)
|
||||
style.WriteString(strings.ReplaceAll(txt.GetStyle(0, 0), "verticalAlign=middle", "verticalAlign=top"))
|
||||
} else {
|
||||
isChevron := strings.Contains(obj.UID, "chevron")
|
||||
if textObject == obj || isChevron {
|
||||
style.WriteString(txt.GetStyle(0, 0))
|
||||
} else {
|
||||
style.WriteString(txt.GetStyle(textObject.X, textObject.Y))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
popup := c.getGliffyPopup(obj)
|
||||
if link != "" || popup != nil {
|
||||
userObj := &UserObject{}
|
||||
if link != "" {
|
||||
if lb := c.extractLightboxDataFromGliffyUrl(link); lb != nil {
|
||||
link = "/plugins/drawio/lightbox.action?ceoId=" + intToString(int(lb.Key)) + "&diagramName=" + lb.Value + ".drawio"
|
||||
}
|
||||
userObj.Link = link
|
||||
if textObject != nil {
|
||||
userObj.Label = textObject.TextHTML()
|
||||
}
|
||||
}
|
||||
if popup != nil && popup.Graphic != nil && popup.Graphic.PopupNote != nil {
|
||||
userObj.Tooltip = popup.Graphic.PopupNote.Text
|
||||
}
|
||||
cell.UserObject = userObj
|
||||
}
|
||||
|
||||
style.WriteString("gliffyId=" + obj.ID + ";")
|
||||
cell.Style = style.String()
|
||||
obj.MxObject = cell
|
||||
return cell
|
||||
}
|
||||
|
||||
func (c *GliffyDiagramConverter) getGliffyPopup(obj *GliffyObject) *GliffyObject {
|
||||
if !obj.hasChildren() {
|
||||
return nil
|
||||
}
|
||||
for _, child := range obj.Children {
|
||||
if child.Graphic != nil && child.Graphic.GetType() == GraphicTypePopup {
|
||||
return child
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GliffyDiagramConverter) rotateGroupedObject(group, child *GliffyObject) {
|
||||
pivot := &MxPoint{X: group.Width/2 - child.Width/2, Y: group.Height/2 - child.Height/2}
|
||||
temp := &MxPoint{X: child.X, Y: child.Y}
|
||||
if group.Rotation != 0 {
|
||||
rads := group.Rotation * math.Pi / 180
|
||||
cos := math.Cos(rads)
|
||||
sin := math.Sin(rads)
|
||||
temp = &MxPoint{
|
||||
X: temp.X*cos - temp.Y*sin + pivot.X,
|
||||
Y: temp.X*sin + temp.Y*cos + pivot.Y,
|
||||
}
|
||||
child.X = temp.X
|
||||
child.Y = temp.Y
|
||||
child.Rotation += group.Rotation
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GliffyDiagramConverter) fixFrameTextBorder(obj *GliffyObject, style *strings.Builder) {
|
||||
if obj.TextObject() == nil {
|
||||
return
|
||||
}
|
||||
wrong := "labelX=32"
|
||||
correct := "labelX=" + floatToString(obj.TextObject().Width*1.1)
|
||||
styleStr := strings.Replace(style.String(), wrong, correct, 1)
|
||||
style.Reset()
|
||||
style.WriteString(styleStr)
|
||||
}
|
||||
|
||||
func (c *GliffyDiagramConverter) addConstraint(obj *GliffyObject, terminal *MxCell, source bool, orthogonal bool) bool {
|
||||
cons := obj.GetConstraints()
|
||||
if cons == nil {
|
||||
return orthogonal
|
||||
}
|
||||
var con *Constraint
|
||||
if source {
|
||||
con = cons.StartConstraint
|
||||
} else {
|
||||
con = cons.EndConstraint
|
||||
}
|
||||
if con == nil {
|
||||
return orthogonal
|
||||
}
|
||||
var data *ConstraintData
|
||||
if source {
|
||||
data = con.StartPositionConstraint
|
||||
} else {
|
||||
data = con.EndPositionConstraint
|
||||
}
|
||||
if data != nil {
|
||||
direction := getStyleValue(terminal, "direction", "east")
|
||||
temp := &MxPoint{X: data.PX, Y: data.PY}
|
||||
rotation := 0
|
||||
switch strings.ToLower(direction) {
|
||||
case "south":
|
||||
rotation = 270
|
||||
case "west":
|
||||
rotation = 180
|
||||
case "north":
|
||||
rotation = 90
|
||||
}
|
||||
if rotation != 0 {
|
||||
rad := float64(rotation) * math.Pi / 180
|
||||
temp = &MxPoint{
|
||||
X: temp.X*math.Cos(rad) - temp.Y*math.Sin(rad),
|
||||
Y: temp.X*math.Sin(rad) + temp.Y*math.Cos(rad),
|
||||
}
|
||||
}
|
||||
if !orthogonal || (temp.X == 0.5 && temp.Y == 0.5) || obj.ForceConstraints() {
|
||||
cell := obj.MxObject
|
||||
if source {
|
||||
cell.Style += "exitX=" + floatToString(temp.X) + ";exitY=" + floatToString(temp.Y) + ";exitPerimeter=0;"
|
||||
} else {
|
||||
cell.Style += "entryX=" + floatToString(temp.X) + ";entryY=" + floatToString(temp.Y) + ";entryPerimeter=0;"
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return orthogonal
|
||||
}
|
||||
|
||||
func (c *GliffyDiagramConverter) setWaypoints(obj *GliffyObject, start, end *MxCell) {
|
||||
if obj.Graphic == nil || obj.Graphic.Line == nil {
|
||||
return
|
||||
}
|
||||
cell := obj.MxObject
|
||||
geo := cell.Geometry
|
||||
if geo == nil {
|
||||
geo = &MxGeometry{}
|
||||
cell.Geometry = geo
|
||||
}
|
||||
setRelative(geo, true)
|
||||
|
||||
points := obj.Graphic.Line.ControlPath
|
||||
if len(points) < 2 {
|
||||
return
|
||||
}
|
||||
mxPoints := make([]*MxPoint, 0, len(points))
|
||||
pivot := &MxPoint{X: obj.X + obj.Width/2, Y: obj.Y + obj.Height/2}
|
||||
for _, p := range points {
|
||||
wp := &MxPoint{X: p[0] + obj.X, Y: p[1] + obj.Y}
|
||||
if obj.Rotation != 0 {
|
||||
rads := obj.Rotation * math.Pi / 180
|
||||
cos := math.Cos(rads)
|
||||
sin := math.Sin(rads)
|
||||
wp = &MxPoint{
|
||||
X: pivot.X + (wp.X-pivot.X)*cos - (wp.Y-pivot.Y)*sin,
|
||||
Y: pivot.Y + (wp.X-pivot.X)*sin + (wp.Y-pivot.Y)*cos,
|
||||
}
|
||||
}
|
||||
mxPoints = append(mxPoints, wp)
|
||||
}
|
||||
|
||||
orthogonal := true
|
||||
last := mxPoints[0]
|
||||
for _, p := range mxPoints[1:] {
|
||||
orthogonal = orthogonal && (last.X == p.X || last.Y == p.Y)
|
||||
last = p
|
||||
}
|
||||
|
||||
p0 := mxPoints[0]
|
||||
pe := mxPoints[len(mxPoints)-1]
|
||||
if start == nil {
|
||||
geo.SourcePoint = &MxPoint{X: p0.X, Y: p0.Y, As: "sourcePoint"}
|
||||
mxPoints = mxPoints[1:]
|
||||
} else {
|
||||
if c.addConstraint(obj, start, true, orthogonal) {
|
||||
// keep point
|
||||
}
|
||||
}
|
||||
if end == nil {
|
||||
geo.TargetPoint = &MxPoint{X: pe.X, Y: pe.Y, As: "targetPoint"}
|
||||
mxPoints = mxPoints[:len(mxPoints)-1]
|
||||
} else {
|
||||
if c.addConstraint(obj, end, false, orthogonal) {
|
||||
// keep
|
||||
}
|
||||
}
|
||||
|
||||
if orthogonal {
|
||||
cell.Style += "edgeStyle=orthogonalEdgeStyle;"
|
||||
result := make([]*MxPoint, 0, len(mxPoints))
|
||||
var prev *MxPoint
|
||||
for _, p := range mxPoints {
|
||||
if prev == nil || prev.X != p.X || prev.Y != p.Y {
|
||||
result = append(result, p)
|
||||
prev = p
|
||||
}
|
||||
}
|
||||
if len(result) == 0 && ((start == nil && end != nil) || (start != nil && end == nil)) {
|
||||
center := &MxPoint{X: p0.X + (pe.X-p0.X)/2, Y: p0.Y + (pe.Y-p0.Y)/2}
|
||||
result = []*MxPoint{center, center}
|
||||
}
|
||||
mxPoints = result
|
||||
}
|
||||
|
||||
if len(mxPoints) > 0 {
|
||||
geo.Points = &MxPointArray{Points: mxPoints}
|
||||
}
|
||||
cell.Geometry = geo
|
||||
}
|
||||
|
||||
type lightboxInfo struct {
|
||||
Key int64
|
||||
Value string
|
||||
}
|
||||
|
||||
func (c *GliffyDiagramConverter) extractLightboxDataFromGliffyUrl(link string) *lightboxInfo {
|
||||
pagem := c.pageIDPattern.FindStringSubmatch(link)
|
||||
namem := c.namePattern.FindStringSubmatch(link)
|
||||
if len(pagem) > 1 {
|
||||
oldPageID, err := strconv.ParseInt(pagem[1], 10, 64)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if len(namem) > 1 {
|
||||
return &lightboxInfo{Key: oldPageID, Value: namem[1]}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStyleValue(cell *MxCell, key, defaultValue string) string {
|
||||
if cell == nil {
|
||||
return defaultValue
|
||||
}
|
||||
style := cell.Style
|
||||
if style == "" {
|
||||
return defaultValue
|
||||
}
|
||||
pairs := strings.Split(style, ";")
|
||||
for _, p := range pairs {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(p, "=", 2)
|
||||
if len(parts) == 2 && strings.EqualFold(parts[0], key) {
|
||||
return parts[1]
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func sortObjectsByOrder(objs []*GliffyObject) {
|
||||
sort.SliceStable(objs, func(i, j int) bool {
|
||||
o1 := objs[i].Order.String()
|
||||
o2 := objs[j].Order.String()
|
||||
if o1 == "" && o2 == "" {
|
||||
return false
|
||||
}
|
||||
if o1 == "" {
|
||||
return false
|
||||
}
|
||||
if o2 == "" {
|
||||
return true
|
||||
}
|
||||
f1, err1 := strconv.ParseFloat(o1, 64)
|
||||
f2, err2 := strconv.ParseFloat(o2, 64)
|
||||
if err1 == nil && err2 == nil {
|
||||
return f1 < f2
|
||||
}
|
||||
return o1 < o2
|
||||
})
|
||||
}
|
||||
|
||||
func sortLayersByOrder(layers []*GliffyLayer) {
|
||||
sort.SliceStable(layers, func(i, j int) bool {
|
||||
return layers[i].Order < layers[j].Order
|
||||
})
|
||||
}
|
||||
|
||||
func reverse(objs []*GliffyObject) {
|
||||
for i, j := 0, len(objs)-1; i < j; i, j = i+1, j-1 {
|
||||
objs[i], objs[j] = objs[j], objs[i]
|
||||
}
|
||||
}
|
||||
|
||||
func boolToInt(v bool) int {
|
||||
if v {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Convenience entry point for users wanting a single call.
|
||||
func ConvertGliffyJSONToDrawioXML(gliffyJSON string) (string, error) {
|
||||
converter, err := NewGliffyDiagramConverter(gliffyJSON)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return converter.GraphXML()
|
||||
}
|
||||
176
mapping.go
Normal file
176
mapping.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package gliffy2drawio
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const DefaultTranslationPath = "java/gliffy/importer/gliffyTranslation.properties"
|
||||
|
||||
type StencilTranslator struct {
|
||||
once sync.Once
|
||||
path string
|
||||
translations map[string]string
|
||||
customFile string
|
||||
}
|
||||
|
||||
func NewStencilTranslator(path string) *StencilTranslator {
|
||||
return &StencilTranslator{path: path}
|
||||
}
|
||||
|
||||
// WithCustomMapping allows specifying an optional JSON file (map[string]string) for extra translations.
|
||||
func (s *StencilTranslator) WithCustomMapping(jsonPath string) *StencilTranslator {
|
||||
s.customFile = jsonPath
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StencilTranslator) Translate(uid, tid string) string {
|
||||
s.once.Do(s.load)
|
||||
if s.translations == nil {
|
||||
return ""
|
||||
}
|
||||
if v, ok := s.translations[uid]; ok {
|
||||
return v
|
||||
}
|
||||
if tid != "" {
|
||||
if v, ok := s.translations[tid]; ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *StencilTranslator) load() {
|
||||
path := s.path
|
||||
if path == "" {
|
||||
path = DefaultTranslationPath
|
||||
}
|
||||
file, err := os.Open(filepath.Clean(path))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
s.translations = make(map[string]string)
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
s.translations[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
// Hard-coded additions for shapes absent from properties.
|
||||
for k, v := range map[string]string{
|
||||
"gshape.aws_v3.database.Amazon_RDS_PostgreSQL_instance_light_bg": "mxgraph.aws4.rds_postgresql_instance;fillColor=#3334B9",
|
||||
"gstencil.aws_v3.database.Amazon_RDS_PostgreSQL_instance_light_bg": "mxgraph.aws4.rds_postgresql_instance;fillColor=#3334B9",
|
||||
} {
|
||||
if _, ok := s.translations[k]; !ok {
|
||||
s.translations[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Load optional custom JSON mapping file if provided.
|
||||
if s.customFile != "" {
|
||||
if data, err := os.ReadFile(filepath.Clean(s.customFile)); err == nil {
|
||||
var extra map[string]string
|
||||
if err := json.Unmarshal(data, &extra); err == nil {
|
||||
for k, v := range extra {
|
||||
if _, exists := s.translations[k]; !exists {
|
||||
s.translations[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DashStyleMapping(value string, width int) string {
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(value, ",")
|
||||
for i, p := range parts {
|
||||
if f, err := strconv.ParseFloat(strings.TrimSpace(p), 64); err == nil {
|
||||
parts[i] = floatToString(f * float64(width))
|
||||
}
|
||||
}
|
||||
return "dashed=1;fixDash=1;dashPattern=" + strings.Join(parts, " ") + ";"
|
||||
}
|
||||
|
||||
var lineMapping = map[string]string{
|
||||
"linear": "",
|
||||
"orthogonal": "edgeStyle=orthogonal;",
|
||||
"quadratic": "curved=1;edgeStyle=orthogonalEdgeStyle;",
|
||||
}
|
||||
|
||||
func LineMapping(style string) string {
|
||||
return lineMapping[style]
|
||||
}
|
||||
|
||||
type ArrowStyle struct {
|
||||
Name string
|
||||
Fill bool
|
||||
Size int
|
||||
Spacing int
|
||||
}
|
||||
|
||||
func (a ArrowStyle) ToString(start bool) string {
|
||||
fill := 0
|
||||
if a.Fill {
|
||||
fill = 1
|
||||
}
|
||||
if start {
|
||||
return "startArrow=" + a.Name + ";startFill=" + strconv.Itoa(fill) + ";startSize=" + strconv.Itoa(a.Size) + optionalSpacing("sourcePerimeterSpacing", a.Spacing) + ";"
|
||||
}
|
||||
return "endArrow=" + a.Name + ";endFill=" + strconv.Itoa(fill) + ";endSize=" + strconv.Itoa(a.Size) + optionalSpacing("targetPerimeterSpacing", a.Spacing) + ";"
|
||||
}
|
||||
|
||||
func optionalSpacing(key string, v int) string {
|
||||
if v == 0 {
|
||||
return ""
|
||||
}
|
||||
return ";" + key + "=" + strconv.Itoa(v) + ";"
|
||||
}
|
||||
|
||||
var arrowMapping = map[int]ArrowStyle{
|
||||
0: {Name: "none", Fill: false, Size: 6},
|
||||
1: {Name: "open", Fill: false, Size: 6},
|
||||
2: {Name: "block", Fill: true, Size: 6},
|
||||
3: {Name: "block", Fill: false, Size: 6},
|
||||
4: {Name: "block", Fill: false, Size: 10},
|
||||
5: {Name: "diamond", Fill: false, Size: 12},
|
||||
6: {Name: "open", Fill: false, Size: 10},
|
||||
7: {Name: "diamond", Fill: true, Size: 12},
|
||||
8: {Name: "classic", Fill: true, Size: 6},
|
||||
9: {Name: "ERzeroToMany", Fill: true, Size: 10},
|
||||
10: {Name: "ERoneToMany", Fill: true, Size: 10},
|
||||
11: {Name: "ERmandOne", Fill: true, Size: 10},
|
||||
12: {Name: "ERone", Fill: true, Size: 10},
|
||||
13: {Name: "ERzeroToOne", Fill: true, Size: 10},
|
||||
14: {Name: "ERmany", Fill: true, Size: 10},
|
||||
15: {Name: "oval", Fill: false, Size: 10, Spacing: 6},
|
||||
16: {Name: "dash", Fill: false, Size: 6},
|
||||
17: {Name: "block", Fill: true, Size: 6},
|
||||
18: {Name: "classic", Fill: true, Size: 6},
|
||||
19: {Name: "openAsync", Fill: false, Size: 10},
|
||||
}
|
||||
|
||||
func ArrowMapping(id *int) ArrowStyle {
|
||||
if id == nil {
|
||||
return arrowMapping[0]
|
||||
}
|
||||
if a, ok := arrowMapping[*id]; ok {
|
||||
return a
|
||||
}
|
||||
return arrowMapping[0]
|
||||
}
|
||||
961
models.go
Normal file
961
models.go
Normal file
@@ -0,0 +1,961 @@
|
||||
package gliffy2drawio
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Diagram is the root Gliffy document.
|
||||
type Diagram struct {
|
||||
ContentType string `json:"contentType"`
|
||||
Version string `json:"version"`
|
||||
Title string `json:"title"`
|
||||
DefaultPage string `json:"defaultPage"`
|
||||
Stage Stage `json:"stage"`
|
||||
Pages []GliffyPage `json:"pages"`
|
||||
Metadata Metadata `json:"metadata"`
|
||||
EmbeddedResources EmbeddedResources `json:"embeddedResources"`
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
type Stage struct {
|
||||
Background string `json:"background"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
AutoFit bool `json:"autoFit"`
|
||||
GridOn bool `json:"gridOn"`
|
||||
DrawingGuidesOn bool `json:"drawingGuidesOn"`
|
||||
Objects []*GliffyObject `json:"objects"`
|
||||
Layers []*GliffyLayer `json:"layers"`
|
||||
TextStyles TextStyles `json:"textStyles"`
|
||||
}
|
||||
|
||||
type GliffyPage struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Index int `json:"index"`
|
||||
Scene Scene `json:"scene"`
|
||||
}
|
||||
|
||||
type Scene struct {
|
||||
Objects []*GliffyObject `json:"objects"`
|
||||
Layers []*GliffyLayer `json:"layers"`
|
||||
Background string `json:"background"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
AutoFit bool `json:"autoFit"`
|
||||
GridOn bool `json:"gridOn"`
|
||||
DrawingGuidesOn bool `json:"drawingGuidesOn"`
|
||||
TextStyles TextStyles `json:"textStyles"`
|
||||
}
|
||||
|
||||
type GliffyLayer struct {
|
||||
GUID string `json:"guid"`
|
||||
Order int `json:"order"`
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
Locked bool `json:"locked"`
|
||||
Visible bool `json:"visible"`
|
||||
NodeIndex int `json:"nodeIndex"`
|
||||
|
||||
MxObject *MxCell `json:"-"`
|
||||
}
|
||||
|
||||
type TextStyles struct {
|
||||
Global GlobalTextStyles `json:"global"`
|
||||
}
|
||||
|
||||
type GlobalTextStyles struct {
|
||||
Size string `json:"size"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
func (g GlobalTextStyles) SizeValue() string {
|
||||
if g.Size == "" {
|
||||
return ""
|
||||
}
|
||||
idx := strings.Index(g.Size, "px")
|
||||
if idx == -1 {
|
||||
return g.Size
|
||||
}
|
||||
return g.Size[:idx]
|
||||
}
|
||||
|
||||
type GraphicType string
|
||||
|
||||
const (
|
||||
GraphicTypeSVG GraphicType = "Svg"
|
||||
GraphicTypeLine GraphicType = "Line"
|
||||
GraphicTypeShape GraphicType = "Shape"
|
||||
GraphicTypeText GraphicType = "Text"
|
||||
GraphicTypeImage GraphicType = "Image"
|
||||
GraphicTypeLink GraphicType = "Link"
|
||||
GraphicTypeMindmap GraphicType = "Mindmap"
|
||||
GraphicTypePopup GraphicType = "PopupNote"
|
||||
GraphicTypeUnknown GraphicType = "Unwknown"
|
||||
)
|
||||
|
||||
type Graphic struct {
|
||||
Type GraphicType `json:"type"`
|
||||
Text *GliffyText `json:"Text"`
|
||||
Line *GliffyLine `json:"Line"`
|
||||
Link *GliffyLink `json:"Link"`
|
||||
Shape *GliffyShape `json:"Shape"`
|
||||
Image *GliffyImage `json:"Image"`
|
||||
Svg *GliffySvg `json:"Svg"`
|
||||
Mindmap *GliffyMindmap `json:"Mindmap"`
|
||||
PopupNote *GliffyPopupNote `json:"PopupNote"`
|
||||
}
|
||||
|
||||
func (g *Graphic) GetType() GraphicType {
|
||||
if g == nil {
|
||||
return GraphicTypeUnknown
|
||||
}
|
||||
if g.Type == "" {
|
||||
return GraphicTypeUnknown
|
||||
}
|
||||
return g.Type
|
||||
}
|
||||
|
||||
type GliffyAbstractShape struct {
|
||||
StrokeWidth float64 `json:"strokeWidth"`
|
||||
StrokeColor string `json:"strokeColor"`
|
||||
FillColor string `json:"fillColor"`
|
||||
DashStyle string `json:"dashStyle"`
|
||||
}
|
||||
|
||||
func (s GliffyAbstractShape) StrokeWidthValue() int {
|
||||
return int(math.Round(s.StrokeWidth))
|
||||
}
|
||||
|
||||
type GliffyLine struct {
|
||||
GliffyAbstractShape
|
||||
StartArrow *int `json:"startArrow"`
|
||||
EndArrow *int `json:"endArrow"`
|
||||
Interpolation string `json:"interpolationType"`
|
||||
CornerRadius *int `json:"cornerRadius"`
|
||||
ControlPath [][]float64 `json:"controlPath"`
|
||||
}
|
||||
|
||||
type GliffyShape struct {
|
||||
GliffyAbstractShape
|
||||
TID string `json:"tid"`
|
||||
Gradient bool `json:"gradient"`
|
||||
DropShadow bool `json:"dropShadow"`
|
||||
State int `json:"state"`
|
||||
ShadowX float64 `json:"shadowX"`
|
||||
ShadowY float64 `json:"shadowY"`
|
||||
Opacity float64 `json:"opacity"`
|
||||
}
|
||||
|
||||
func (s GliffyShape) NoFill() bool {
|
||||
return s.TID != "" && strings.Contains(s.TID, "no_fill")
|
||||
}
|
||||
|
||||
type GliffyImage struct {
|
||||
GliffyShape
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func (g GliffyImage) CleanURL() string {
|
||||
return strings.ReplaceAll(g.URL, ";base64", "")
|
||||
}
|
||||
|
||||
type GliffySvg struct {
|
||||
GliffyShape
|
||||
EmbeddedResourceID *int `json:"embeddedResourceId"`
|
||||
}
|
||||
|
||||
type GliffyMindmap struct {
|
||||
GliffyShape
|
||||
}
|
||||
|
||||
type GliffyPopupNote struct {
|
||||
GliffyShape
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type GliffyLink struct {
|
||||
Href string `json:"href"`
|
||||
RenderIcon bool `json:"renderIcon"`
|
||||
}
|
||||
|
||||
type EmbeddedResource struct {
|
||||
ID int `json:"id"`
|
||||
MimeType string `json:"mimeType"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
func (r EmbeddedResource) Base64EncodedData() string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(r.Data))
|
||||
}
|
||||
|
||||
type EmbeddedResources struct {
|
||||
Resources []EmbeddedResource `json:"resources"`
|
||||
resourceMap map[int]EmbeddedResource
|
||||
}
|
||||
|
||||
func (e *EmbeddedResources) Get(id int) (EmbeddedResource, bool) {
|
||||
if e == nil {
|
||||
return EmbeddedResource{}, false
|
||||
}
|
||||
if e.resourceMap == nil {
|
||||
e.resourceMap = make(map[int]EmbeddedResource, len(e.Resources))
|
||||
for _, r := range e.Resources {
|
||||
e.resourceMap[r.ID] = r
|
||||
}
|
||||
}
|
||||
r, ok := e.resourceMap[id]
|
||||
return r, ok
|
||||
}
|
||||
|
||||
type Constraints struct {
|
||||
Constraints []*Constraint `json:"constraints"`
|
||||
StartConstraint *Constraint `json:"startConstraint"`
|
||||
EndConstraint *Constraint `json:"endConstraint"`
|
||||
}
|
||||
|
||||
type ConstraintType string
|
||||
|
||||
const (
|
||||
ConstraintTypeStart ConstraintType = "StartPositionConstraint"
|
||||
ConstraintTypeEnd ConstraintType = "EndPositionConstraint"
|
||||
ConstraintTypeHeight ConstraintType = "HeightConstraint"
|
||||
)
|
||||
|
||||
type Constraint struct {
|
||||
Type ConstraintType `json:"type"`
|
||||
StartPositionConstraint *ConstraintData `json:"StartPositionConstraint"`
|
||||
EndPositionConstraint *ConstraintData `json:"EndPositionConstraint"`
|
||||
}
|
||||
|
||||
type ConstraintData struct {
|
||||
NodeID string `json:"nodeId"`
|
||||
PX float64 `json:"px"`
|
||||
PY float64 `json:"py"`
|
||||
}
|
||||
|
||||
// GliffyText mirrors Gliffy's text object.
|
||||
type GliffyText struct {
|
||||
HTML string `json:"html"`
|
||||
Valign string `json:"valign"`
|
||||
Halign string `json:"-"`
|
||||
VPosition string `json:"vposition"`
|
||||
HPosition string `json:"hposition"`
|
||||
PaddingLeft int `json:"paddingLeft"`
|
||||
PaddingRight int `json:"paddingRight"`
|
||||
PaddingBottom int `json:"paddingBottom"`
|
||||
PaddingTop int `json:"paddingTop"`
|
||||
LineTValue float64 `json:"lineTValue"`
|
||||
LinePerpValue *float64 `json:"linePerpValue"`
|
||||
CardinalityType string `json:"cardinalityType"`
|
||||
Overflow string `json:"overflow"`
|
||||
|
||||
forceTopPaddingShift bool
|
||||
}
|
||||
|
||||
func (t *GliffyText) UnmarshalJSON(data []byte) error {
|
||||
type raw GliffyText
|
||||
var r raw
|
||||
if err := json.Unmarshal(data, &r); err != nil {
|
||||
return err
|
||||
}
|
||||
*t = GliffyText(r)
|
||||
if t.LineTValue == 0 {
|
||||
t.LineTValue = 0.5
|
||||
}
|
||||
t.postDeserialize()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *GliffyText) postDeserialize() {
|
||||
t.Halign = t.extractHorizontalAlignment()
|
||||
t.convertTextAlign()
|
||||
t.replaceParagraphWithDiv()
|
||||
}
|
||||
|
||||
func (t *GliffyText) GetStyle(x, y float64) string {
|
||||
var sb strings.Builder
|
||||
topPaddingShift := 7
|
||||
|
||||
switch t.VPosition {
|
||||
case "above":
|
||||
sb.WriteString("verticalLabelPosition=top;verticalAlign=bottom;")
|
||||
case "below":
|
||||
sb.WriteString("verticalLabelPosition=bottom;verticalAlign=top;")
|
||||
case "none":
|
||||
sb.WriteString("verticalAlign=" + t.Valign + ";")
|
||||
if !t.forceTopPaddingShift && strings.EqualFold(t.Valign, "middle") {
|
||||
topPaddingShift = 0
|
||||
}
|
||||
}
|
||||
|
||||
switch t.HPosition {
|
||||
case "left":
|
||||
sb.WriteString("labelPosition=left;align=right;")
|
||||
case "right":
|
||||
sb.WriteString("labelPosition=right;align=left;")
|
||||
case "none":
|
||||
if t.Halign != "" {
|
||||
sb.WriteString("align=" + t.Halign + ";")
|
||||
if strings.EqualFold(t.Halign, "right") {
|
||||
x = 0
|
||||
}
|
||||
} else {
|
||||
sb.WriteString("align=center;")
|
||||
}
|
||||
}
|
||||
|
||||
t.PaddingLeft = maxInt(0, t.PaddingLeft-2)
|
||||
t.PaddingRight = maxInt(0, t.PaddingRight-2)
|
||||
|
||||
sb.WriteString("spacingLeft=" + floatToString(float64(t.PaddingLeft)+x) + ";")
|
||||
sb.WriteString("spacingRight=" + intToString(t.PaddingRight) + ";")
|
||||
|
||||
if t.forceTopPaddingShift || !strings.EqualFold(t.Valign, "middle") {
|
||||
sb.WriteString("spacingTop=" + floatToString(float64(t.PaddingTop-topPaddingShift)+y) + ";")
|
||||
sb.WriteString("spacingBottom=" + intToString(t.PaddingBottom) + ";")
|
||||
}
|
||||
|
||||
if t.Overflow == "none" {
|
||||
sb.WriteString("whiteSpace=wrap;")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (t *GliffyText) SetHAlign(value string) {
|
||||
t.Halign = value
|
||||
}
|
||||
|
||||
func (t *GliffyText) SetVAlign(value string) {
|
||||
t.Valign = value
|
||||
}
|
||||
|
||||
func (t *GliffyText) SetForceTopPaddingShift(v bool) {
|
||||
t.forceTopPaddingShift = v
|
||||
}
|
||||
|
||||
var (
|
||||
spanPattern = regexp.MustCompile(`<span style="(.*?)">`)
|
||||
textAlign = regexp.MustCompile(`.*(text-align: ?(left|center|right);).*`)
|
||||
textAlignToDraw = regexp.MustCompile(`style="text-align:\s?(left|center|right);"`)
|
||||
lineHeight = regexp.MustCompile(`.*(line-height: .*px;).*`)
|
||||
)
|
||||
|
||||
func (t *GliffyText) replaceParagraphWithDiv() {
|
||||
m := spanPattern.FindAllStringSubmatchIndex(t.HTML, -1)
|
||||
if len(m) == 0 {
|
||||
t.HTML = strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(t.HTML, "<p ", "<div "), "<p>", "<div>"), "</p>", "</div>")
|
||||
return
|
||||
}
|
||||
|
||||
var mod strings.Builder
|
||||
last := 0
|
||||
for _, match := range m {
|
||||
_, end := match[0], match[1]
|
||||
styleStart, styleEnd := match[2], match[3]
|
||||
span := t.HTML[last:end]
|
||||
style := t.HTML[styleStart:styleEnd]
|
||||
if style != "" {
|
||||
if !lineHeight.MatchString(style) {
|
||||
if strings.HasPrefix(strings.ToLower(t.HTML[end:]), "<span") {
|
||||
span = span[:styleEnd-last] + " line-height: 0;" + span[styleEnd-last:]
|
||||
} else {
|
||||
span = span[:styleEnd-last] + " line-height: normal;" + span[styleEnd-last:]
|
||||
}
|
||||
}
|
||||
}
|
||||
mod.WriteString(span)
|
||||
last = end
|
||||
}
|
||||
mod.WriteString(t.HTML[last:])
|
||||
t.HTML = mod.String()
|
||||
t.HTML = strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(t.HTML, "<p ", "<div "), "<p>", "<div>"), "</p>", "</div>")
|
||||
}
|
||||
|
||||
func (t *GliffyText) extractHorizontalAlignment() string {
|
||||
m := textAlign.FindStringSubmatch(t.HTML)
|
||||
if len(m) == 0 {
|
||||
return ""
|
||||
}
|
||||
return m[2]
|
||||
}
|
||||
|
||||
func (t *GliffyText) convertTextAlign() {
|
||||
t.HTML = textAlignToDraw.ReplaceAllString(t.HTML, `align="$1"`)
|
||||
}
|
||||
|
||||
// GliffyObject represents an element on the canvas.
|
||||
type GliffyObject struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
ID string `json:"id"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
Rotation float64 `json:"rotation"`
|
||||
UID string `json:"uid"`
|
||||
TID string `json:"tid"`
|
||||
Order OrderValue `json:"order"`
|
||||
LockShape bool `json:"lockshape"`
|
||||
LayerID *string `json:"layerId"`
|
||||
Graphic *Graphic `json:"graphic"`
|
||||
Children []*GliffyObject `json:"children"`
|
||||
Constraints *Constraints `json:"constraints"`
|
||||
|
||||
MxObject *MxCell `json:"-"`
|
||||
Parent *GliffyObject `json:"-"`
|
||||
}
|
||||
|
||||
// OrderValue supports both numeric and string orders found in Gliffy files.
|
||||
type OrderValue string
|
||||
|
||||
func (o *OrderValue) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if data[0] == '"' {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
*o = OrderValue(s)
|
||||
return nil
|
||||
}
|
||||
var num json.Number
|
||||
if err := json.Unmarshal(data, &num); err == nil {
|
||||
*o = OrderValue(num.String())
|
||||
return nil
|
||||
}
|
||||
*o = OrderValue(strings.Trim(string(data), "\""))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o OrderValue) String() string {
|
||||
return string(o)
|
||||
}
|
||||
|
||||
func (o *GliffyObject) UnmarshalJSON(data []byte) error {
|
||||
type raw GliffyObject
|
||||
var r raw
|
||||
if err := json.Unmarshal(data, &r); err != nil {
|
||||
return err
|
||||
}
|
||||
*o = GliffyObject(r)
|
||||
o.postDeserialize()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *GliffyObject) postDeserialize() {
|
||||
if o.isGroup() && o.hasChildren() {
|
||||
o.normalizeChildrenCoordinates()
|
||||
o.adjustZOrder()
|
||||
}
|
||||
}
|
||||
|
||||
func (o *GliffyObject) hasChildren() bool {
|
||||
return len(o.Children) > 0
|
||||
}
|
||||
|
||||
func (o *GliffyObject) GraphicOrChildGraphic() *Graphic {
|
||||
if o.Graphic != nil {
|
||||
return o.Graphic
|
||||
}
|
||||
if o.isUml() || graphicalessShapes[o.UID] {
|
||||
return o.firstChildGraphic()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *GliffyObject) firstChildGraphic() *Graphic {
|
||||
if len(o.Children) == 0 {
|
||||
return nil
|
||||
}
|
||||
return o.Children[0].Graphic
|
||||
}
|
||||
|
||||
func (o *GliffyObject) TextObject() *GliffyObject {
|
||||
return o.textObjectWithOffset(0, 0)
|
||||
}
|
||||
|
||||
func (o *GliffyObject) textObjectWithOffset(x, y float64) *GliffyObject {
|
||||
if o.isText() {
|
||||
return o
|
||||
}
|
||||
if len(o.Children) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, child := range o.Children {
|
||||
if g := child.GraphicOrChildGraphic(); g != nil && g.GetType() == GraphicTypeText {
|
||||
child.X += x
|
||||
child.Y += y
|
||||
return child
|
||||
}
|
||||
if txt := child.textObjectWithOffset(child.X, child.Y); txt != nil {
|
||||
return txt
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *GliffyObject) TextHTML() string {
|
||||
if o.Graphic == nil || o.Graphic.Text == nil {
|
||||
return ""
|
||||
}
|
||||
text := o.Graphic.Text
|
||||
widthDiff := -3
|
||||
if text.Overflow != "none" {
|
||||
widthDiff = 6
|
||||
}
|
||||
return "<div style='width: " + floatToString(o.Width+float64(widthDiff)) + "px;height:auto;word-break: break-word;'>" + text.HTML + "</div>"
|
||||
}
|
||||
|
||||
func (o *GliffyObject) isGroup() bool {
|
||||
if !o.hasChildren() {
|
||||
return false
|
||||
}
|
||||
if o.UID != "" && (groupShapes[o.UID] || strings.HasPrefix(o.UID, "com.gliffy.shape.table")) {
|
||||
return true
|
||||
}
|
||||
if o.UID == "" && len(o.Children) > 0 && !o.Children[0].isText() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (o *GliffyObject) isSelection() bool {
|
||||
return o.UID != "" && strings.Contains(o.UID, "default.selection")
|
||||
}
|
||||
|
||||
func (o *GliffyObject) isMindmap() bool {
|
||||
return o.UID != "" && mindmapShapes[o.UID]
|
||||
}
|
||||
|
||||
func (o *GliffyObject) isLine() bool {
|
||||
return o.Graphic != nil && o.Graphic.GetType() == GraphicTypeLine
|
||||
}
|
||||
|
||||
func (o *GliffyObject) isLink() bool {
|
||||
return o.Graphic != nil && o.Graphic.GetType() == GraphicTypeLink
|
||||
}
|
||||
|
||||
func (o *GliffyObject) isUml() bool {
|
||||
return strings.HasPrefix(o.UID, "com.gliffy.shape.uml.uml")
|
||||
}
|
||||
|
||||
func (o *GliffyObject) isShape() bool {
|
||||
if o.Graphic != nil {
|
||||
t := o.Graphic.GetType()
|
||||
return t == GraphicTypeShape || t == GraphicTypeMindmap
|
||||
}
|
||||
g := o.firstChildGraphic()
|
||||
return g != nil && g.GetType() == GraphicTypeShape
|
||||
}
|
||||
|
||||
func (o *GliffyObject) isSvg() bool {
|
||||
return o.Graphic != nil && o.Graphic.GetType() == GraphicTypeSVG
|
||||
}
|
||||
|
||||
func (o *GliffyObject) isSwimlane() bool {
|
||||
return o.UID != "" && strings.Contains(o.UID, "com.gliffy.shape.swimlanes")
|
||||
}
|
||||
|
||||
func (o *GliffyObject) isText() bool {
|
||||
return o.Graphic != nil && o.Graphic.GetType() == GraphicTypeText
|
||||
}
|
||||
|
||||
func (o *GliffyObject) isImage() bool {
|
||||
return o.Graphic != nil && o.Graphic.GetType() == GraphicTypeImage
|
||||
}
|
||||
|
||||
func (o *GliffyObject) isVennCircle() bool {
|
||||
return strings.HasPrefix(o.UID, "com.gliffy.shape.venn")
|
||||
}
|
||||
|
||||
func (o *GliffyObject) isDeeplyNestedLink() bool {
|
||||
return o.UID != "" && deeplyNestedLinks[o.UID]
|
||||
}
|
||||
|
||||
func (o *GliffyObject) GradientColor() string {
|
||||
gradientColor := "#FFFFFF"
|
||||
if o.Graphic != nil && o.Graphic.Shape != nil && o.UID != "" && !strings.HasPrefix(o.UID, "com.gliffy.shape.radial") {
|
||||
hex := o.Graphic.Shape.FillColor
|
||||
if len(hex) == 7 && strings.HasPrefix(hex, "#") {
|
||||
clr, err := parseHex(hex[1:])
|
||||
if err == nil {
|
||||
r := math.Min(0xFF0000, float64(clr&0xFF0000)+0xAA0000)
|
||||
g := math.Min(0x00FF00, float64(clr&0x00FF00)+0x00AA00)
|
||||
b := math.Min(0x0000FF, float64(clr&0x0000FF)+0x0000AA)
|
||||
gradientColor = "#" + strings.ToUpper(hexInt(int(r+g+b)))
|
||||
}
|
||||
}
|
||||
}
|
||||
return gradientColor
|
||||
}
|
||||
|
||||
func (o *GliffyObject) GradientIgnored() bool {
|
||||
return o.UID != "" && (strings.HasPrefix(o.UID, "com.gliffy.shape.venn.outline") || strings.HasPrefix(o.UID, "com.gliffy.shape.venn.flat"))
|
||||
}
|
||||
|
||||
func (o *GliffyObject) IsSubRoutine() bool {
|
||||
return o.UID == "com.gliffy.shape.flowchart.flowchart_v1.default.subroutine"
|
||||
}
|
||||
|
||||
func (o *GliffyObject) IsUnrecognizedGraphicType() bool {
|
||||
return o.Graphic != nil && o.Graphic.Type == ""
|
||||
}
|
||||
|
||||
func (o *GliffyObject) GetConstraints() *Constraints {
|
||||
return o.Constraints
|
||||
}
|
||||
|
||||
func (o *GliffyObject) String() string {
|
||||
if o.UID != "" {
|
||||
return o.UID
|
||||
}
|
||||
return o.TID
|
||||
}
|
||||
|
||||
func (o *GliffyObject) normalizeChildrenCoordinates() {
|
||||
if !o.hasChildren() {
|
||||
return
|
||||
}
|
||||
sort.Slice(o.Children, func(i, j int) bool { return o.Children[i].X < o.Children[j].X })
|
||||
xMin := o.Children[0].X
|
||||
if xMin < 0 {
|
||||
o.Width += -xMin
|
||||
o.X += xMin
|
||||
for _, c := range o.Children {
|
||||
c.X += -xMin
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(o.Children, func(i, j int) bool { return o.Children[i].Y < o.Children[j].Y })
|
||||
yMin := o.Children[0].Y
|
||||
if yMin < 0 {
|
||||
o.Height += -yMin
|
||||
o.Y += yMin
|
||||
for _, c := range o.Children {
|
||||
c.Y += -yMin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *GliffyObject) adjustZOrder() {
|
||||
var maxOrder *int
|
||||
for _, c := range o.Children {
|
||||
if c.UID == "com.gliffy.shape.basic.basic_v1.default.rectangle" && c.X == 0 && c.Y == 0 && c.Width == o.Width && c.Height == o.Height {
|
||||
if val, err := strconv.Atoi(c.Order.String()); err == nil {
|
||||
if maxOrder == nil || val > *maxOrder {
|
||||
maxOrder = &val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if maxOrder != nil {
|
||||
o.Order = OrderValue(intToString(*maxOrder))
|
||||
}
|
||||
}
|
||||
|
||||
func (o *GliffyObject) adjustGeo(geo *MxGeometry) {
|
||||
arr := shapeCoordFix[o.UID]
|
||||
if arr == nil && o.Graphic != nil && o.Graphic.Shape != nil {
|
||||
arr = shapeCoordFix[o.Graphic.Shape.TID]
|
||||
}
|
||||
if arr == nil {
|
||||
return
|
||||
}
|
||||
x, y, w, h := geo.X, geo.Y, geo.Width, geo.Height
|
||||
shifts := o.getAdjustShifts(arr, x, y, w, h)
|
||||
geo.X = x + shifts.X
|
||||
geo.Y = y + shifts.Y
|
||||
geo.Width = w + shifts.Width
|
||||
geo.Height = h + shifts.Height
|
||||
}
|
||||
|
||||
func (o *GliffyObject) adjustTextPos(textObj *GliffyObject) {
|
||||
arr := shapeCoordFix[o.UID]
|
||||
if arr == nil || len(arr) != 4 {
|
||||
return
|
||||
}
|
||||
shifts := o.getAdjustShifts(arr, o.X, o.Y, o.Width, o.Height)
|
||||
textObj.X -= shifts.X
|
||||
textObj.Y -= shifts.Y
|
||||
}
|
||||
|
||||
func (o *GliffyObject) getAdjustShifts(arr []float64, x, y, w, h float64) *MxGeometry {
|
||||
xShift := relativeShift(arr[0], w)
|
||||
yShift := relativeShift(arr[1], h)
|
||||
wShift := relativeShift(arr[2], w)
|
||||
hShift := relativeShift(arr[3], h)
|
||||
mod := &MxGeometry{X: x + xShift, Y: y + yShift, Width: w + wShift, Height: h + hShift}
|
||||
if o.Rotation > 0 {
|
||||
orig := &MxGeometry{X: x, Y: y, Width: w, Height: h}
|
||||
rotateGeometry(orig, o.Rotation, 0, 0)
|
||||
rotateGeometry(mod, o.Rotation, 0, 0)
|
||||
xShift += mod.X - orig.X
|
||||
yShift += mod.Y - orig.Y
|
||||
}
|
||||
mod.X = xShift
|
||||
mod.Y = yShift
|
||||
mod.Width = wShift
|
||||
mod.Height = hShift
|
||||
return mod
|
||||
}
|
||||
|
||||
func (o *GliffyObject) AdjustedLink() string {
|
||||
if len(o.Children) == 0 {
|
||||
return ""
|
||||
}
|
||||
if o.isDeeplyNestedLink() {
|
||||
var sb strings.Builder
|
||||
o.collectDeepLink(&sb)
|
||||
return sb.String()
|
||||
}
|
||||
for _, c := range o.Children {
|
||||
if c.isLink() && c.Graphic != nil && c.Graphic.Link != nil {
|
||||
return c.Graphic.Link.Href
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (o *GliffyObject) collectDeepLink(sb *strings.Builder) {
|
||||
for _, c := range o.Children {
|
||||
if c.isLink() && c.Graphic != nil && c.Graphic.Link != nil {
|
||||
sb.WriteString(c.Graphic.Link.Href)
|
||||
return
|
||||
}
|
||||
if len(c.Children) > 0 {
|
||||
c.collectDeepLink(sb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *GliffyObject) IsUseFillColorForStroke() bool {
|
||||
key := o.UID
|
||||
if key == "" && o.Graphic != nil && o.Graphic.Shape != nil {
|
||||
key = o.Graphic.Shape.TID
|
||||
}
|
||||
return fillColorIsStroke[key]
|
||||
}
|
||||
|
||||
func (o *GliffyObject) ContainsTextBracket() bool {
|
||||
return o.UID != "" && strings.Contains(o.UID, "com.gliffy.shape.uml.uml_v2.activity.frame")
|
||||
}
|
||||
|
||||
func (o *GliffyObject) ForceConstraints() bool {
|
||||
return forceConstraintsShapes[o.UID]
|
||||
}
|
||||
|
||||
func (o *GliffyObject) UMLSequenceCombinedFragmentText() string {
|
||||
switch o.UID {
|
||||
case "com.gliffy.shape.uml.uml_v2.sequence.interaction_use":
|
||||
return "ref"
|
||||
case "com.gliffy.shape.uml.uml_v2.sequence.opt_combined_fragment":
|
||||
return "opt"
|
||||
case "com.gliffy.shape.uml.uml_v2.sequence.loop_combined_fragment":
|
||||
return "loop"
|
||||
case "com.gliffy.shape.uml.uml_v2.sequence.alt_combined_fragment":
|
||||
return "alt"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func relativeShift(v, size float64) float64 {
|
||||
if math.Abs(v) < 1 {
|
||||
return size * v
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func parseHex(s string) (int64, error) {
|
||||
return strconv.ParseInt(s, 16, 64)
|
||||
}
|
||||
|
||||
func hexInt(v int) string {
|
||||
return fmt.Sprintf("%06X", v&0xFFFFFF)
|
||||
}
|
||||
|
||||
// to avoid fmt in hot paths.
|
||||
func floatToString(v float64) string {
|
||||
return strings.TrimRight(strings.TrimRight(strconv.FormatFloat(v, 'f', 4, 64), "0"), ".")
|
||||
}
|
||||
|
||||
func intToString(v int) string {
|
||||
return strconv.Itoa(v)
|
||||
}
|
||||
|
||||
func maxInt(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Static shape metadata replicated from Java implementation.
|
||||
var graphicalessShapes = map[string]bool{
|
||||
"com.gliffy.shape.uml.uml_v1.default.package": true,
|
||||
"com.gliffy.shape.uml.uml_v1.default.class": true,
|
||||
"com.gliffy.shape.uml.uml_v1.default.simple_class": true,
|
||||
"com.gliffy.shape.uml.uml_v1.default.object_timeline": true,
|
||||
"com.gliffy.shape.uml.uml_v1.default.lifeline": true,
|
||||
"com.gliffy.shape.uml.uml_v1.default.use_case": true,
|
||||
"com.gliffy.shape.uml.uml_v1.default.actor": true,
|
||||
"com.gliffy.shape.uml.uml_v1.default.message": true,
|
||||
"com.gliffy.shape.uml.uml_v1.default.activation": true,
|
||||
"com.gliffy.shape.uml.uml_v1.default.dependency": true,
|
||||
"com.gliffy.shape.uml.uml_v1.default.composition": true,
|
||||
"com.gliffy.shape.uml.uml_v1.default.aggregation": true,
|
||||
"com.gliffy.shape.uml.uml_v1.default.association": true,
|
||||
"com.gliffy.shape.uml.uml_v2.class.package": true,
|
||||
"com.gliffy.shape.uml.uml_v2.class.simple_class": true,
|
||||
"com.gliffy.shape.uml.uml_v2.class.class": true,
|
||||
"com.gliffy.shape.uml.uml_v2.class.class2": true,
|
||||
"com.gliffy.shape.uml.uml_v2.class.interface": true,
|
||||
"com.gliffy.shape.uml.uml_v2.class.enumeration": true,
|
||||
"com.gliffy.shape.uml.uml_v2.sequence.lifeline": true,
|
||||
"com.gliffy.shape.uml.uml_v2.sequence.boundary_lifeline": true,
|
||||
"com.gliffy.shape.uml.uml_v2.sequence.control_lifeline": true,
|
||||
"com.gliffy.shape.uml.uml_v2.sequence.entity_lifeline": true,
|
||||
"com.gliffy.shape.uml.uml_v2.deployment.package": true,
|
||||
"com.gliffy.shape.uml.uml_v2.component.package": true,
|
||||
"com.gliffy.shape.uml.uml_v2.use_case.package": true,
|
||||
"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes": true,
|
||||
"com.gliffy.shape.erd.erd_v1.default.entity_with_multiple_attributes": true,
|
||||
"com.gliffy.shape.bpmn.bpmn_v1.data_artifacts.annotation": true,
|
||||
"com.gliffy.shape.ui.ui_v3.navigation.navbar": true,
|
||||
"com.gliffy.shape.ui.ui_v3.forms_controls.combo_box": true,
|
||||
"com.gliffy.shape.ui.ui_v3.containers_content.tooltip_top": true,
|
||||
"com.gliffy.shape.ui.ui_v3.containers_content.tooltip_bottom": true,
|
||||
"com.gliffy.shape.ui.ui_v3.containers_content.tooltip_left": true,
|
||||
"com.gliffy.shape.ui.ui_v3.containers_content.tooltip_right": true,
|
||||
"com.gliffy.shape.ui.ui_v3.containers_content.popover_top": true,
|
||||
"com.gliffy.shape.ui.ui_v3.containers_content.popover_bottom": true,
|
||||
"com.gliffy.shape.ui.ui_v3.forms_controls.selector": true,
|
||||
"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_left": true,
|
||||
"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_right": true,
|
||||
"com.gliffy.shape.ui.ui_v3.icon_symbols.annotate_top": true,
|
||||
"com.gliffy.shape.ui.ui_v3.containers_content.speech_bubble_right": true,
|
||||
"com.gliffy.shape.ui.ui_v3.containers_content.speech_bubble_left": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.page": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.home": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.gliffy": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.form": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.shopping_cart": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.text": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.video": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.upload": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.slideshow": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.sitemap": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.settings": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.search": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.script": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.print": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.pricing": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.photo": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.map": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.login": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.game": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.gallery": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.download": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.document": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.chat": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.calendar": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.audio": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.profile": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.error": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.success": true,
|
||||
"com.gliffy.shape.sitemap.sitemap_v2.cloud": true,
|
||||
}
|
||||
|
||||
var groupShapes = map[string]bool{
|
||||
"com.gliffy.shape.basic.basic_v1.default.group": true,
|
||||
"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes": true,
|
||||
"com.gliffy.shape.erd.erd_v1.default.entity_with_multiple_attributes": true,
|
||||
"com.gliffy.shape.uml.uml_v2.sequence.interaction_use": true,
|
||||
"com.gliffy.shape.uml.uml_v2.sequence.opt_combined_fragment": true,
|
||||
"com.gliffy.shape.uml.uml_v2.sequence.loop_combined_fragment": true,
|
||||
"com.gliffy.shape.uml.uml_v2.sequence.alt_combined_fragment": true,
|
||||
"com.gliffy.shape.uml.uml_v2.class.object": true,
|
||||
"com.gliffy.shape.uml.uml_v2.class.enumeration": true,
|
||||
"com.gliffy.shape.uml.uml_v2.class.interface": true,
|
||||
"com.gliffy.shape.uml.uml_v2.class.class2": true,
|
||||
"com.gliffy.shape.uml.uml_v2.class.class": true,
|
||||
"com.gliffy.shape.uml.uml_v2.class.data_type": true,
|
||||
"com.gliffy.shape.uml.uml_v2.state_machine.composite_state": true,
|
||||
"com.gliffy.shape.uml.uml_v2.state_machine.orthoganal_state": true,
|
||||
"com.gliffy.shape.uml.uml_v2.class.package": true,
|
||||
"com.gliffy.shape.uml.uml_v2.sequence.boundary_lifeline": true,
|
||||
"com.gliffy.shape.uml.uml_v2.sequence.lifeline": true,
|
||||
"com.gliffy.shape.uml.uml_v2.sequence.entity_lifeline": true,
|
||||
"com.gliffy.shape.uml.uml_v2.sequence.control_lifeline": true,
|
||||
"com.gliffy.shape.uml.uml_v1.default.object_timeline": true,
|
||||
"com.gliffy.shape.uml.uml_v1.default.class": true,
|
||||
"com.gliffy.shape.uml.uml_v1.default.object": true,
|
||||
"com.gliffy.shape.iphone.iphone_ios7.containers_content.table": true,
|
||||
"com.gliffy.shape.iphone.iphone_ios7.forms_controls.button_stack": true,
|
||||
"com.gliffy.shape.iphone.iphone_ios7.forms_controls.alert_2options": true,
|
||||
"com.gliffy.shape.iphone.iphone_ios7.forms_controls.alert": true,
|
||||
"com.gliffy.shape.iphone.iphone_ios7.navigation.contextual_menu": true,
|
||||
"com.gliffy.shape.iphone.iphone_ios7.navigation.nav_3tabs": true,
|
||||
"com.gliffy.shape.iphone.iphone_ios7.containers_content.title_bar": true,
|
||||
"com.gliffy.shape.iphone.iphone_ios7.navigation.tab_bar": true,
|
||||
"com.gliffy.shape.iphone.iphone_ios7.forms_controls.search": true,
|
||||
"com.gliffy.shape.android.android_v1.general.dialog": true,
|
||||
"com.gliffy.shape.android.android_v1.general.list_1line": true,
|
||||
"com.gliffy.shape.android.android_v1.general.list_2lines": true,
|
||||
"com.gliffy.shape.android.android_v1.general.tabs01": true,
|
||||
"com.gliffy.shape.android.android_v1.general.tabs02": true,
|
||||
"com.gliffy.shape.network.network_v3.business.user_group": true,
|
||||
"com.gliffy.shape.ui.ui_v3.navigation.navbar": true,
|
||||
"com.gliffy.shape.ui.ui_v3.navigation.navbar_vertical": true,
|
||||
"com.gliffy.shape.ui.ui_v3.forms_controls.dropdown": true,
|
||||
"com.gliffy.shape.uml.uml_v2.sequence.recursive_message": true,
|
||||
}
|
||||
|
||||
var mindmapShapes = map[string]bool{
|
||||
"com.gliffy.shape.mindmap.mindmap_v1.default.main_topic": true,
|
||||
"com.gliffy.shape.mindmap.mindmap_v1.default.subtopic": true,
|
||||
"com.gliffy.shape.mindmap.mindmap_v1.default.child_node": true,
|
||||
}
|
||||
|
||||
var fillColorIsStroke = map[string]bool{
|
||||
"com.gliffy.stencil.rectangle.no_fill_line_bottom_2px_off": true,
|
||||
}
|
||||
|
||||
var deeplyNestedLinks = map[string]bool{
|
||||
"com.gliffy.shape.basic.basic_v1.default.chevron_box_right": true,
|
||||
"com.gliffy.shape.basic.basic_v1.default.chevron_box_left": true,
|
||||
"com.gliffy.shape.basic.basic_v1.default.chevron_tail_right": true,
|
||||
"com.gliffy.shape.basic.basic_v1.default.chevron_tail_left": true,
|
||||
}
|
||||
|
||||
var shapeCoordFix = map[string][]float64{
|
||||
"com.gliffy.shape.flowchart.flowchart_v1.default.paper_tape": {0, -0.1, 0, 0.2},
|
||||
"com.gliffy.shape.uml.uml_v1.default.node": {0, -10, 10, 10},
|
||||
"com.gliffy.shape.uml.uml_v2.deployment.node": {0, -10, 10, 10},
|
||||
"com.gliffy.shape.uml.uml_v2.deployment.device_node": {0, -10, 10, 10},
|
||||
"com.gliffy.shape.uml.uml_v2.deployment.execution_environment_node": {0, -10, 10, 10},
|
||||
"com.gliffy.shape.flowchart.flowchart_v1.default.data_storage": {0, 0, 0.115, 0},
|
||||
"com.gliffy.shape.flowchart.flowchart_v1.default.database": {0, 0, 0, 0.15},
|
||||
"com.gliffy.stencil.entity_lifeline.uml_v2": {10, 0, -20, 0},
|
||||
"com.gliffy.stencil.boundary_lifeline.uml_v2": {35, 0, -70, 0},
|
||||
"com.gliffy.stencil.control_lifeline.uml_v2": {10, 0, -20, 0},
|
||||
"com.gliffy.shape.ui.ui_v3.containers_content.browser": {0, -40, 0, 40, 1},
|
||||
}
|
||||
|
||||
var forceConstraintsShapes = map[string]bool{
|
||||
"com.gliffy.shape.uml.uml_v2.class.association": true,
|
||||
}
|
||||
463
mxgraph.go
Normal file
463
mxgraph.go
Normal file
@@ -0,0 +1,463 @@
|
||||
package gliffy2drawio
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MxGraph struct {
|
||||
Model *MxGraphModel
|
||||
nextID int
|
||||
root *MxCell
|
||||
defaultParent *MxCell
|
||||
}
|
||||
|
||||
func NewMxGraph() *MxGraph {
|
||||
root := &MxCell{ID: "0"}
|
||||
layer := &MxCell{ID: "1", Parent: "0"}
|
||||
graph := &MxGraph{
|
||||
Model: &MxGraphModel{
|
||||
Root: &mxRoot{Cells: []*MxCell{root, layer}},
|
||||
},
|
||||
nextID: 2,
|
||||
root: root,
|
||||
defaultParent: layer,
|
||||
}
|
||||
return graph
|
||||
}
|
||||
|
||||
func (g *MxGraph) SetDefaultParent(cell *MxCell) {
|
||||
g.defaultParent = cell
|
||||
}
|
||||
|
||||
func (g *MxGraph) NextID() string {
|
||||
id := g.nextID
|
||||
g.nextID++
|
||||
return intToString(id)
|
||||
}
|
||||
|
||||
func (g *MxGraph) AddCell(cell *MxCell, parent *MxCell) {
|
||||
if cell == nil {
|
||||
return
|
||||
}
|
||||
// Allow preassigned IDs; otherwise generate.
|
||||
if cell.ID == "" {
|
||||
cell.ID = g.NextID()
|
||||
}
|
||||
if parent == nil {
|
||||
parent = g.defaultParent
|
||||
}
|
||||
cell.Parent = parent.ID
|
||||
g.Model.Root.Cells = append(g.Model.Root.Cells, cell)
|
||||
}
|
||||
|
||||
// HasCell returns true if a cell with the given ID exists in the root.
|
||||
func (g *MxGraph) HasCell(id string) bool {
|
||||
for _, cell := range g.Model.Root.Cells {
|
||||
if cell != nil && cell.ID == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type MxGraphModel struct {
|
||||
XMLName xml.Name `xml:"mxGraphModel"`
|
||||
Style string `xml:"style,attr,omitempty"`
|
||||
Background string `xml:"background,attr,omitempty"`
|
||||
Grid int `xml:"grid,attr,omitempty"`
|
||||
GridSize int `xml:"gridSize,attr,omitempty"`
|
||||
Guides int `xml:"guides,attr,omitempty"`
|
||||
Dx int `xml:"dx,attr,omitempty"`
|
||||
Dy int `xml:"dy,attr,omitempty"`
|
||||
Connect int `xml:"connect,attr,omitempty"`
|
||||
Arrows int `xml:"arrows,attr,omitempty"`
|
||||
Fold int `xml:"fold,attr,omitempty"`
|
||||
Page int `xml:"page,attr,omitempty"`
|
||||
PageScale int `xml:"pageScale,attr,omitempty"`
|
||||
PageWidth int `xml:"pageWidth,attr,omitempty"`
|
||||
PageHeight int `xml:"pageHeight,attr,omitempty"`
|
||||
Tooltips int `xml:"tooltips,attr,omitempty"`
|
||||
Math int `xml:"math,attr,omitempty"`
|
||||
Shadow int `xml:"shadow,attr,omitempty"`
|
||||
Root *mxRoot `xml:"root"`
|
||||
}
|
||||
|
||||
type mxRoot struct {
|
||||
Cells []*MxCell
|
||||
}
|
||||
|
||||
type UserObject struct {
|
||||
Label string
|
||||
Link string
|
||||
Tooltip string
|
||||
}
|
||||
|
||||
type MxCell struct {
|
||||
ID string
|
||||
Value string
|
||||
Style string
|
||||
Vertex bool
|
||||
Edge bool
|
||||
Parent string
|
||||
Source string
|
||||
Target string
|
||||
|
||||
Geometry *MxGeometry
|
||||
UserObject *UserObject
|
||||
}
|
||||
|
||||
type MxGeometry struct {
|
||||
X float64 `xml:"x,attr,omitempty"`
|
||||
Y float64 `xml:"y,attr,omitempty"`
|
||||
Width float64 `xml:"width,attr,omitempty"`
|
||||
Height float64 `xml:"height,attr,omitempty"`
|
||||
Relative int `xml:"relative,attr,omitempty"`
|
||||
As string `xml:"as,attr,omitempty"`
|
||||
|
||||
Points *MxPointArray `xml:"Array,omitempty"`
|
||||
SourcePoint *MxPoint `xml:"-"`
|
||||
TargetPoint *MxPoint `xml:"-"`
|
||||
}
|
||||
|
||||
type MxPoint struct {
|
||||
X float64 `xml:"x,attr,omitempty"`
|
||||
Y float64 `xml:"y,attr,omitempty"`
|
||||
As string `xml:"as,attr,omitempty"`
|
||||
}
|
||||
|
||||
type MxPointArray struct {
|
||||
As string `xml:"as,attr,omitempty"`
|
||||
Points []*MxPoint `xml:"mxPoint"`
|
||||
}
|
||||
|
||||
// MarshalXML customizes geometry output to allow multiple mxPoint children.
|
||||
func (g *MxGeometry) MarshalXML(enc *xml.Encoder, start xml.StartElement) error {
|
||||
if start.Name.Local == "" {
|
||||
start.Name.Local = "mxGeometry"
|
||||
}
|
||||
as := g.As
|
||||
if as == "" {
|
||||
as = "geometry"
|
||||
}
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "as"}, Value: as})
|
||||
if g.X != 0 {
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "x"}, Value: floatToString(g.X)})
|
||||
}
|
||||
if g.Y != 0 {
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "y"}, Value: floatToString(g.Y)})
|
||||
}
|
||||
if g.Width != 0 {
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "width"}, Value: floatToString(g.Width)})
|
||||
}
|
||||
if g.Height != 0 {
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "height"}, Value: floatToString(g.Height)})
|
||||
}
|
||||
if g.Relative != 0 {
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "relative"}, Value: intToString(g.Relative)})
|
||||
}
|
||||
|
||||
if err := enc.EncodeToken(start); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if g.Points != nil && len(g.Points.Points) > 0 {
|
||||
asVal := g.Points.As
|
||||
if asVal == "" {
|
||||
asVal = "points"
|
||||
}
|
||||
arrStart := xml.StartElement{Name: xml.Name{Local: "Array"}, Attr: []xml.Attr{{Name: xml.Name{Local: "as"}, Value: asVal}}}
|
||||
if err := enc.EncodeToken(arrStart); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range g.Points.Points {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
if err := enc.EncodeElement(p, xml.StartElement{Name: xml.Name{Local: "mxPoint"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := enc.EncodeToken(xml.EndElement{Name: arrStart.Name}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if g.SourcePoint != nil {
|
||||
p := *g.SourcePoint
|
||||
if p.As == "" {
|
||||
p.As = "sourcePoint"
|
||||
}
|
||||
if err := enc.EncodeElement(p, xml.StartElement{Name: xml.Name{Local: "mxPoint"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if g.TargetPoint != nil {
|
||||
p := *g.TargetPoint
|
||||
if p.As == "" {
|
||||
p.As = "targetPoint"
|
||||
}
|
||||
if err := enc.EncodeElement(p, xml.StartElement{Name: xml.Name{Local: "mxPoint"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return enc.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
|
||||
// MarshalXML customizes how the mxRoot emits cells, handling UserObject wrappers.
|
||||
func (r *mxRoot) MarshalXML(enc *xml.Encoder, start xml.StartElement) error {
|
||||
start.Name.Local = "root"
|
||||
if err := enc.EncodeToken(start); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, cell := range r.Cells {
|
||||
if cell == nil {
|
||||
continue
|
||||
}
|
||||
if cell.UserObject != nil {
|
||||
uoStart := xml.StartElement{Name: xml.Name{Local: "UserObject"}}
|
||||
if cell.UserObject.Label != "" {
|
||||
uoStart.Attr = append(uoStart.Attr, xml.Attr{Name: xml.Name{Local: "label"}, Value: cell.UserObject.Label})
|
||||
}
|
||||
if cell.UserObject.Link != "" {
|
||||
uoStart.Attr = append(uoStart.Attr, xml.Attr{Name: xml.Name{Local: "link"}, Value: cell.UserObject.Link})
|
||||
}
|
||||
if cell.UserObject.Tooltip != "" {
|
||||
uoStart.Attr = append(uoStart.Attr, xml.Attr{Name: xml.Name{Local: "tooltip"}, Value: cell.UserObject.Tooltip})
|
||||
}
|
||||
if err := enc.EncodeToken(uoStart); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := enc.EncodeElement(cell.alias(), xml.StartElement{Name: xml.Name{Local: "mxCell"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := enc.EncodeToken(xml.EndElement{Name: uoStart.Name}); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := enc.EncodeElement(cell.alias(), xml.StartElement{Name: xml.Name{Local: "mxCell"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return enc.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
|
||||
func (c *MxCell) alias() cellAlias {
|
||||
return cellAlias{
|
||||
ID: c.ID,
|
||||
Value: c.Value,
|
||||
Style: c.Style,
|
||||
Vertex: boolToAttr(c.Vertex),
|
||||
Edge: boolToAttr(c.Edge),
|
||||
Parent: c.Parent,
|
||||
Source: c.Source,
|
||||
Target: c.Target,
|
||||
Geometry: c.geometryWithDefault(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MxCell) geometryWithDefault() *MxGeometry {
|
||||
if c.Geometry == nil {
|
||||
return nil
|
||||
}
|
||||
g := *c.Geometry
|
||||
if g.As == "" {
|
||||
g.As = "geometry"
|
||||
}
|
||||
if g.Points != nil && g.Points.As == "" {
|
||||
g.Points.As = "points"
|
||||
}
|
||||
return &g
|
||||
}
|
||||
|
||||
type cellAlias struct {
|
||||
ID string `xml:"id,attr"`
|
||||
Value string `xml:"value,attr,omitempty"`
|
||||
Style string `xml:"style,attr,omitempty"`
|
||||
Vertex string `xml:"vertex,attr,omitempty"`
|
||||
Edge string `xml:"edge,attr,omitempty"`
|
||||
Parent string `xml:"parent,attr,omitempty"`
|
||||
Source string `xml:"source,attr,omitempty"`
|
||||
Target string `xml:"target,attr,omitempty"`
|
||||
Geometry *MxGeometry `xml:"mxGeometry,omitempty"`
|
||||
}
|
||||
|
||||
func boolToAttr(v bool) string {
|
||||
if v {
|
||||
return "1"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// rotateGeometry rotates a rectangle around the given pivot.
|
||||
func rotateGeometry(geo *MxGeometry, rotation, cx, cy float64) {
|
||||
if geo == nil || rotation == 0 {
|
||||
return
|
||||
}
|
||||
rads := rotation * math.Pi / 180
|
||||
cos := math.Cos(rads)
|
||||
sin := math.Sin(rads)
|
||||
x := geo.X - cx
|
||||
y := geo.Y - cy
|
||||
geo.X = x*cos - y*sin + cx
|
||||
geo.Y = x*sin + y*cos + cy
|
||||
}
|
||||
|
||||
// ToXML renders the graph model to draw.io compatible XML.
|
||||
func (g *MxGraph) ToXML(style, background string, grid, guides bool, width, height float64) (string, error) {
|
||||
model := g.Model
|
||||
model.Style = style
|
||||
model.Background = background
|
||||
if grid {
|
||||
model.Grid = 1
|
||||
}
|
||||
if guides {
|
||||
model.Guides = 1
|
||||
}
|
||||
model.GridSize = 10
|
||||
model.Connect = 1
|
||||
model.Arrows = 1
|
||||
model.Fold = 1
|
||||
model.Page = 1
|
||||
model.PageScale = 1
|
||||
model.Dx = 0
|
||||
model.Dy = 0
|
||||
if width == 0 {
|
||||
width = 850
|
||||
}
|
||||
if height == 0 {
|
||||
height = 1100
|
||||
}
|
||||
model.PageWidth = int(width)
|
||||
model.PageHeight = int(height)
|
||||
model.Tooltips = 1
|
||||
model.Math = 0
|
||||
model.Shadow = 0
|
||||
|
||||
output, err := xml.Marshal(model)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// NormalizeOrigin shifts all absolute coordinates so the top-left-most point is at (0,0).
|
||||
func (g *MxGraph) NormalizeOrigin() {
|
||||
minX, minY := math.MaxFloat64, math.MaxFloat64
|
||||
|
||||
for _, cell := range g.Model.Root.Cells {
|
||||
if cell == nil || cell.Geometry == nil {
|
||||
continue
|
||||
}
|
||||
geo := cell.Geometry
|
||||
if geo.Relative == 0 {
|
||||
if geo.X < minX {
|
||||
minX = geo.X
|
||||
}
|
||||
if geo.Y < minY {
|
||||
minY = geo.Y
|
||||
}
|
||||
}
|
||||
if geo.Points != nil {
|
||||
for _, p := range geo.Points.Points {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
if p.X < minX {
|
||||
minX = p.X
|
||||
}
|
||||
if p.Y < minY {
|
||||
minY = p.Y
|
||||
}
|
||||
}
|
||||
}
|
||||
if geo.SourcePoint != nil {
|
||||
if geo.SourcePoint.X < minX {
|
||||
minX = geo.SourcePoint.X
|
||||
}
|
||||
if geo.SourcePoint.Y < minY {
|
||||
minY = geo.SourcePoint.Y
|
||||
}
|
||||
}
|
||||
if geo.TargetPoint != nil {
|
||||
if geo.TargetPoint.X < minX {
|
||||
minX = geo.TargetPoint.X
|
||||
}
|
||||
if geo.TargetPoint.Y < minY {
|
||||
minY = geo.TargetPoint.Y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if minX == math.MaxFloat64 || minY == math.MaxFloat64 {
|
||||
return
|
||||
}
|
||||
|
||||
dx, dy := -minX, -minY
|
||||
|
||||
for _, cell := range g.Model.Root.Cells {
|
||||
if cell == nil || cell.Geometry == nil {
|
||||
continue
|
||||
}
|
||||
geo := cell.Geometry
|
||||
if geo.Relative == 0 {
|
||||
geo.X += dx
|
||||
geo.Y += dy
|
||||
}
|
||||
if geo.Points != nil {
|
||||
for _, p := range geo.Points.Points {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
p.X += dx
|
||||
p.Y += dy
|
||||
}
|
||||
}
|
||||
if geo.SourcePoint != nil {
|
||||
geo.SourcePoint.X += dx
|
||||
geo.SourcePoint.Y += dy
|
||||
}
|
||||
if geo.TargetPoint != nil {
|
||||
geo.TargetPoint.X += dx
|
||||
geo.TargetPoint.Y += dy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helper to ensure geometry has relative flag set to 1.
|
||||
func setRelative(geo *MxGeometry, v bool) {
|
||||
if geo == nil {
|
||||
return
|
||||
}
|
||||
if v {
|
||||
geo.Relative = 1
|
||||
} else {
|
||||
geo.Relative = 0
|
||||
}
|
||||
}
|
||||
|
||||
// linearly interpolated center used for rotations.
|
||||
func midpoint(p1, p2 *MxPoint) *MxPoint {
|
||||
return &MxPoint{
|
||||
X: (p1.X + p2.X) / 2,
|
||||
Y: (p1.Y + p2.Y) / 2,
|
||||
}
|
||||
}
|
||||
|
||||
// joinStyle appends style fragments with trailing semicolon when needed.
|
||||
func joinStyle(parts ...string) string {
|
||||
var sb strings.Builder
|
||||
for _, p := range parts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(p, ";") {
|
||||
p += ";"
|
||||
}
|
||||
sb.WriteString(p)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
22
svg_utils.go
Normal file
22
svg_utils.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package gliffy2drawio
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// SVGImporterUtils mirrors the Java helper but keeps a lightweight implementation.
|
||||
// It currently preserves the original SVG and only updates the viewBox attribute
|
||||
// if one is present.
|
||||
type SVGImporterUtils struct {
|
||||
}
|
||||
|
||||
var svgPattern = regexp.MustCompile(`viewBox="(.*?)"`)
|
||||
|
||||
func (s SVGImporterUtils) SetViewBox(svg string) string {
|
||||
m := svgPattern.FindStringSubmatch(svg)
|
||||
if len(m) == 0 {
|
||||
return svg
|
||||
}
|
||||
// No bounding-box calculation available here; keep the existing viewBox.
|
||||
return svg
|
||||
}
|
||||
Reference in New Issue
Block a user