Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

d2fmt: lowercase reserved keywords #2098

Merged
merged 2 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- Vars: Variable definitions can now refer to other variables in the current scope [#2052](https://github.com/terrastruct/d2/pull/2052)
- Composition: Imported boards can use underscores to reference boards beyond its own
scope (e.g. to a sibling board at the scope its imported to) [#2075](https://github.com/terrastruct/d2/pull/2075)
- Autoformat: Reserved keywords are formatted to be lowercase [#2098](https://github.com/terrastruct/d2/pull/2098)

#### Improvements 🧹

Expand Down
201 changes: 201 additions & 0 deletions d2ast/keywords.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package d2ast

import "oss.terrastruct.com/d2/lib/label"

// All reserved keywords. See init below.
var ReservedKeywords map[string]struct{}

// Non Style/Holder keywords.
var SimpleReservedKeywords = map[string]struct{}{
"label": {},
"desc": {},
"shape": {},
"icon": {},
"constraint": {},
"tooltip": {},
"link": {},
"near": {},
"width": {},
"height": {},
"direction": {},
"top": {},
"left": {},
"grid-rows": {},
"grid-columns": {},
"grid-gap": {},
"vertical-gap": {},
"horizontal-gap": {},
"class": {},
"vars": {},
}

// ReservedKeywordHolders are reserved keywords that are meaningless on its own and must hold composites
var ReservedKeywordHolders = map[string]struct{}{
"style": {},
"source-arrowhead": {},
"target-arrowhead": {},
}

// CompositeReservedKeywords are reserved keywords that can hold composites
var CompositeReservedKeywords = map[string]struct{}{
"classes": {},
"constraint": {},
"label": {},
"icon": {},
}

// StyleKeywords are reserved keywords which cannot exist outside of the "style" keyword
var StyleKeywords = map[string]struct{}{
"opacity": {},
"stroke": {},
"fill": {},
"fill-pattern": {},
"stroke-width": {},
"stroke-dash": {},
"border-radius": {},

// Only for text
"font": {},
"font-size": {},
"font-color": {},
"bold": {},
"italic": {},
"underline": {},
"text-transform": {},

// Only for shapes
"shadow": {},
"multiple": {},
"double-border": {},

// Only for squares
"3d": {},

// Only for edges
"animated": {},
"filled": {},
}

// TODO maybe autofmt should allow other values, and transform them to conform
// e.g. left-center becomes center-left
var NearConstantsArray = []string{
"top-left",
"top-center",
"top-right",

"center-left",
"center-right",

"bottom-left",
"bottom-center",
"bottom-right",
}
var NearConstants map[string]struct{}

// LabelPositionsArray are the values that labels and icons can set `near` to
var LabelPositionsArray = []string{
"top-left",
"top-center",
"top-right",

"center-left",
"center-center",
"center-right",

"bottom-left",
"bottom-center",
"bottom-right",

"outside-top-left",
"outside-top-center",
"outside-top-right",

"outside-left-top",
"outside-left-center",
"outside-left-bottom",

"outside-right-top",
"outside-right-center",
"outside-right-bottom",

"outside-bottom-left",
"outside-bottom-center",
"outside-bottom-right",
}
var LabelPositions map[string]struct{}

var LabelPositionsMapping = map[string]label.Position{
"top-left": label.InsideTopLeft,
"top-center": label.InsideTopCenter,
"top-right": label.InsideTopRight,

"center-left": label.InsideMiddleLeft,
"center-center": label.InsideMiddleCenter,
"center-right": label.InsideMiddleRight,

"bottom-left": label.InsideBottomLeft,
"bottom-center": label.InsideBottomCenter,
"bottom-right": label.InsideBottomRight,

"outside-top-left": label.OutsideTopLeft,
"outside-top-center": label.OutsideTopCenter,
"outside-top-right": label.OutsideTopRight,

"outside-left-top": label.OutsideLeftTop,
"outside-left-center": label.OutsideLeftMiddle,
"outside-left-bottom": label.OutsideLeftBottom,

"outside-right-top": label.OutsideRightTop,
"outside-right-center": label.OutsideRightMiddle,
"outside-right-bottom": label.OutsideRightBottom,

"outside-bottom-left": label.OutsideBottomLeft,
"outside-bottom-center": label.OutsideBottomCenter,
"outside-bottom-right": label.OutsideBottomRight,
}

var FillPatterns = []string{
"none",
"dots",
"lines",
"grain",
"paper",
}

var TextTransforms = []string{"none", "uppercase", "lowercase", "capitalize"}

// BoardKeywords contains the keywords that create new boards.
var BoardKeywords = map[string]struct{}{
"layers": {},
"scenarios": {},
"steps": {},
}

func init() {
ReservedKeywords = make(map[string]struct{})
for k, v := range SimpleReservedKeywords {
ReservedKeywords[k] = v
}
for k, v := range StyleKeywords {
ReservedKeywords[k] = v
}
for k, v := range ReservedKeywordHolders {
CompositeReservedKeywords[k] = v
}
for k, v := range BoardKeywords {
CompositeReservedKeywords[k] = v
}
for k, v := range CompositeReservedKeywords {
ReservedKeywords[k] = v
}

NearConstants = make(map[string]struct{}, len(NearConstantsArray))
for _, k := range NearConstantsArray {
NearConstants[k] = struct{}{}
}

LabelPositions = make(map[string]struct{}, len(LabelPositionsArray))
for _, k := range LabelPositionsArray {
LabelPositions[k] = struct{}{}
}
}
30 changes: 15 additions & 15 deletions d2compiler/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {
if f.Name == "shape" {
continue
}
if _, ok := d2graph.BoardKeywords[f.Name]; ok {
if _, ok := d2ast.BoardKeywords[f.Name]; ok {
continue
}
c.compileField(obj, f)
Expand All @@ -293,12 +293,12 @@ func (c *compiler) compileMap(obj *d2graph.Object, m *d2ir.Map) {

func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
keyword := strings.ToLower(f.Name)
_, isStyleReserved := d2graph.StyleKeywords[keyword]
_, isStyleReserved := d2ast.StyleKeywords[keyword]
if isStyleReserved {
c.errorf(f.LastRef().AST(), "%v must be style.%v", f.Name, f.Name)
return
}
_, isReserved := d2graph.SimpleReservedKeywords[keyword]
_, isReserved := d2ast.SimpleReservedKeywords[keyword]
if f.Name == "classes" {
if f.Map() != nil {
if len(f.Map().Edges) > 0 {
Expand All @@ -309,7 +309,7 @@ func (c *compiler) compileField(obj *d2graph.Object, f *d2ir.Field) {
continue
}
for _, cf := range classesField.Map().Fields {
if _, ok := d2graph.ReservedKeywords[cf.Name]; !ok {
if _, ok := d2ast.ReservedKeywords[cf.Name]; !ok {
c.errorf(cf.LastRef().AST(), "%s is an invalid class field, must be reserved keyword", cf.Name)
}
if cf.Name == "class" {
Expand Down Expand Up @@ -440,7 +440,7 @@ func (c *compiler) compilePosition(attrs *d2graph.Attributes, f *d2ir.Field) {
case *d2ast.Null:
attrs.LabelPosition = nil
default:
if _, ok := d2graph.LabelPositions[scalar.ScalarString()]; !ok {
if _, ok := d2ast.LabelPositions[scalar.ScalarString()]; !ok {
c.errorf(f.LastPrimaryKey(), `invalid "near" field`)
} else {
switch name {
Expand Down Expand Up @@ -686,7 +686,7 @@ func (c *compiler) compileStyle(attrs *d2graph.Attributes, m *d2ir.Map) {
}

func (c *compiler) compileStyleField(attrs *d2graph.Attributes, f *d2ir.Field) {
if _, ok := d2graph.StyleKeywords[strings.ToLower(f.Name)]; !ok {
if _, ok := d2ast.StyleKeywords[strings.ToLower(f.Name)]; !ok {
c.errorf(f.LastRef().AST(), `invalid style keyword: "%s"`, f.Name)
return
}
Expand Down Expand Up @@ -814,7 +814,7 @@ func (c *compiler) compileEdgeMap(edge *d2graph.Edge, m *d2ir.Map) {
}
}
for _, f := range m.Fields {
_, ok := d2graph.ReservedKeywords[f.Name]
_, ok := d2ast.ReservedKeywords[f.Name]
if !ok {
c.errorf(f.References[0].AST(), `edge map keys must be reserved keywords`)
continue
Expand All @@ -825,12 +825,12 @@ func (c *compiler) compileEdgeMap(edge *d2graph.Edge, m *d2ir.Map) {

func (c *compiler) compileEdgeField(edge *d2graph.Edge, f *d2ir.Field) {
keyword := strings.ToLower(f.Name)
_, isStyleReserved := d2graph.StyleKeywords[keyword]
_, isStyleReserved := d2ast.StyleKeywords[keyword]
if isStyleReserved {
c.errorf(f.LastRef().AST(), "%v must be style.%v", f.Name, f.Name)
return
}
_, isReserved := d2graph.SimpleReservedKeywords[keyword]
_, isReserved := d2ast.SimpleReservedKeywords[keyword]
if isReserved {
c.compileReserved(&edge.Attributes, f)
return
Expand Down Expand Up @@ -868,7 +868,7 @@ func (c *compiler) compileArrowheads(edge *d2graph.Edge, f *d2ir.Field) {
if f.Map() != nil {
for _, f2 := range f.Map().Fields {
keyword := strings.ToLower(f2.Name)
_, isReserved := d2graph.SimpleReservedKeywords[keyword]
_, isReserved := d2ast.SimpleReservedKeywords[keyword]
if isReserved {
c.compileReserved(attrs, f2)
continue
Expand Down Expand Up @@ -985,7 +985,7 @@ func (c *compiler) compileSQLTable(obj *d2graph.Object) {

func (c *compiler) validateKeys(obj *d2graph.Object, m *d2ir.Map) {
for _, f := range m.Fields {
if _, ok := d2graph.BoardKeywords[f.Name]; ok {
if _, ok := d2ast.BoardKeywords[f.Name]; ok {
continue
}
c.validateKey(obj, f)
Expand All @@ -994,7 +994,7 @@ func (c *compiler) validateKeys(obj *d2graph.Object, m *d2ir.Map) {

func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
keyword := strings.ToLower(f.Name)
_, isReserved := d2graph.ReservedKeywords[keyword]
_, isReserved := d2ast.ReservedKeywords[keyword]
if isReserved {
switch obj.Shape.Value {
case d2target.ShapeCircle, d2target.ShapeSquare:
Expand Down Expand Up @@ -1067,7 +1067,7 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
for _, obj := range g.Objects {
if obj.NearKey != nil {
nearObj, isKey := g.Root.HasChild(d2graph.Key(obj.NearKey))
_, isConst := d2graph.NearConstants[d2graph.Key(obj.NearKey)[0]]
_, isConst := d2ast.NearConstants[d2graph.Key(obj.NearKey)[0]]
if isKey {
// Doesn't make sense to set near to an ancestor or descendant
nearIsAncestor := false
Expand Down Expand Up @@ -1097,7 +1097,7 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
continue
}
if nearObj.NearKey != nil {
_, nearObjNearIsConst := d2graph.NearConstants[d2graph.Key(nearObj.NearKey)[0]]
_, nearObjNearIsConst := d2ast.NearConstants[d2graph.Key(nearObj.NearKey)[0]]
if nearObjNearIsConst {
c.errorf(obj.NearKey, "near keys cannot be set to an object with a constant near key")
continue
Expand All @@ -1117,7 +1117,7 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
continue
}
} else {
c.errorf(obj.NearKey, "near key %#v must be the absolute path to a shape or one of the following constants: %s", d2format.Format(obj.NearKey), strings.Join(d2graph.NearConstantsArray, ", "))
c.errorf(obj.NearKey, "near key %#v must be the absolute path to a shape or one of the following constants: %s", d2format.Format(obj.NearKey), strings.Join(d2ast.NearConstantsArray, ", "))
continue
}
}
Expand Down
6 changes: 6 additions & 0 deletions d2format/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ func (p *printer) interpolationBoxes(boxes []d2ast.InterpolationBox, isDoubleStr
}
b.StringRaw = &s
}
if !isDoubleString {
if _, ok := d2ast.ReservedKeywords[strings.ToLower(*b.StringRaw)]; ok {
s := strings.ToLower(*b.StringRaw)
b.StringRaw = &s
}
}
p.sb.WriteString(*b.StringRaw)
}
}
Expand Down
30 changes: 29 additions & 1 deletion d2format/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ x -> y
exp: `x -> y
`,
},

{
name: "complex",
in: `
Expand Down Expand Up @@ -853,6 +852,35 @@ jeremy: {
!&shape: rectangle
label: I'm not a rectangle
}
`,
},
{
name: "lowercase-reserved",
in: `jacob: {
SHAPE: circle
}
jeremy.SHAPE: rectangle
alice.STYLE.fill: red
bob.style.FILL: red
carmen.STYLE.FILL: red
coop: {
STYLE: {
FILL: blue
}
}
`,
exp: `jacob: {
shape: circle
}
jeremy.shape: rectangle
alice.style.fill: red
bob.style.fill: red
carmen.style.fill: red
coop: {
style: {
fill: blue
}
}
`,
},
}
Expand Down
Loading
Loading