Skip to content
14 changes: 14 additions & 0 deletions dbml-homepage/docs/syntax/enrichment-visualization.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,20 @@ TableGroup e_commerce [color: #3498DB] {
}
```

### Sticky note color

Use `color` on a sticky note to change its background color. Use `none` for transparent:

```text
Note reminder [color: #F4D03F] {
'This is a reminder'
}

Note no_color [color: none] {
'This note has no background color'
}
```

## Inactive Ref

Use `inactive` on a relationship to mark it as inactive. Inactive refs are displayed as a dotted line in the diagram, allowing you to document relationships that are not of immediate focus.
Expand Down
12 changes: 11 additions & 1 deletion packages/dbml-core/src/export/DbmlExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { addDoubleQuoteIfNeeded, formatRecordValue } from '@dbml/parse';
import { shouldPrintSchema } from './utils';
import { DEFAULT_SCHEMA_NAME } from '../model_structure/config';
import type { NormalizedModel, RecordValue } from '../../types/model_structure/database';
import type { NormalizedNote } from '../../types/model_structure/stickyNote';
import type { NormalizedTable } from '../../types/model_structure/table';
import type { NormalizedTableGroup } from '../../types/model_structure/tableGroup';

Expand Down Expand Up @@ -362,10 +363,19 @@ class DbmlExporter {
return tableGroupStrs.length ? tableGroupStrs.join('\n') : '';
}

static getStickyNoteSettings (note: NormalizedNote): string {
let settingStr = '';
if (note.color) {
settingStr += `color: ${note.color}`;
}
return settingStr ? ` [${settingStr}]` : '';
}

static exportStickyNotes (model: NormalizedModel): string {
return reduce(model.notes, (result, note) => {
const escapedContent = ` ${DbmlExporter.escapeNote(note.content)}`;
const stickyNote = `Note ${note.name} {\n${escapedContent}\n}\n`;
const settingStr = DbmlExporter.getStickyNoteSettings(note);
const stickyNote = `Note ${note.name}${settingStr} {\n${escapedContent}\n}\n`;

// Add a blank line between note elements
return result ? `${result}\n${stickyNote}` : stickyNote;
Expand Down
8 changes: 4 additions & 4 deletions packages/dbml-core/src/model_structure/stickyNote.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ class StickyNote extends Element {
* @param {import('../../types/model_structure/stickyNote').RawStickyNote} param0
*/
constructor ({
name, content, headerColor, token, database = {},
name, content, color, token, database = {},
} = {}) {
super(token);
/** @type {string} */
this.name = name;
/** @type {string} */
this.content = content;
/** @type {string} */
this.headerColor = headerColor;
/** @type {string | undefined} */
this.color = color;
/** @type {import('../../types/model_structure/database').default} */
this.database = database;
/** @type {import('../../types/model_structure/dbState').default} */
Expand All @@ -30,7 +30,7 @@ class StickyNote extends Element {
return {
name: this.name,
content: this.content,
headerColor: this.headerColor,
color: this.color,
};
}

Expand Down
10 changes: 5 additions & 5 deletions packages/dbml-core/types/model_structure/stickyNote.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,31 @@ export interface RawStickyNote {
content: string;
database: Database;
token: Token;
headerColor: string;
color?: string;
}

declare class StickyNote extends Element {
name: string;
content: string;
noteToken: Token;
headerColor: string;
color?: string;
database: Database;
dbState: DbState;
id: number;
constructor({ name, content, token, headerColor, database }: RawStickyNote);
constructor({ name, content, token, color, database }: RawStickyNote);
generateId(): void;
export(): {
name: string;
content: string;
headerColor: string;
color?: string;
};
normalize(model: NormalizedModel): void;
}
export interface NormalizedNote {
id: number;
name: string;
content: string;
headerColor: string | null;
color?: string;
}

export interface NormalizedNoteIdMap {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2104,6 +2104,46 @@ describe('[example] interpreter', () => {
// Should not crash
expect(result.getValue()).toBeDefined();
});

test('should interpret sticky note with color', () => {
const source = `
Note my_note [color: #FF5733] {
'A colored note'
}
`;
const db = interpret(source).getValue()!;
expect(db.notes[0].color).toBe('#FF5733');
});

test('should interpret sticky note with color none', () => {
const source = `
Note my_note [color: none] {
'A note without color'
}
`;
const db = interpret(source).getValue()!;
expect(db.notes[0].color).toBe('#00000000');
});

test('should interpret sticky note with double-quoted content', () => {
const source = `
Note my_note {
"A double-quoted note"
}
`;
const db = interpret(source).getValue()!;
expect(db.notes[0].content).toBe('A double-quoted note');
});

test('should interpret sticky note without color setting', () => {
const source = `
Note my_note {
'A plain note'
}
`;
const db = interpret(source).getValue()!;
expect(db.notes[0].color).toBeUndefined();
});
});

describe('multiple table verification', () => {
Expand Down
58 changes: 58 additions & 0 deletions packages/dbml-parse/__tests__/examples/validator/validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,64 @@ describe('[example] validator', () => {

expect(errors).toHaveLength(0);
});

test('should accept sticky note with color setting', () => {
const source = `
Note my_note [color: #FF5733] {
'A colored note'
}
`;
const errors = analyze(source).getErrors();

expect(errors).toHaveLength(0);
});

test('should accept sticky note with color none', () => {
const source = `
Note my_note [color: none] {
'A note without color'
}
`;
const errors = analyze(source).getErrors();

expect(errors).toHaveLength(0);
});

test('should reject unknown setting on sticky note', () => {
const source = `
Note my_note [unknown: value] {
'A note'
}
`;
const errors = analyze(source).getErrors();

expect(errors).toHaveLength(1);
expect(errors[0].code).toBe(CompileErrorCode.UNKNOWN_NOTE_SETTING);
});

test('should reject invalid color value on sticky note', () => {
const source = `
Note my_note [color: invalid] {
'A note'
}
`;
const errors = analyze(source).getErrors();

expect(errors).toHaveLength(1);
expect(errors[0].code).toBe(CompileErrorCode.INVALID_NOTE_SETTING_VALUE);
});

test('should reject duplicate color setting on sticky note', () => {
const source = `
Note my_note [color: #FF5733, color: #00FF00] {
'A note'
}
`;
const errors = analyze(source).getErrors();

expect(errors).toHaveLength(2);
expect(errors[0].code).toBe(CompileErrorCode.DUPLICATE_NOTE_SETTING);
});
});

describe('context validation', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Table users [headercolor: #3498DB] {
username varchar(255) [not null, unique]
}

Note nodeName [headercolor: #3457DB] {
Note nodeName [color: #3457DB] {
'''
Hello is that me you are looking for.
'''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Note "schema.note4" {
'''
}

Note "schema"."note5" [headercolor: #3457DB] {
Note "schema"."note5" [color: #3457DB] {
'''
# Title
body
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,4 @@
{
"errors": [
{
"code": "UNEXPECTED_SETTINGS",
"diagnostic": "A Note shouldn't have a setting list",
"filepath": "/main.dbml",
"level": "error",
"node": {
"context": {
"id": "node@<list-expression>@@[L5:C14, L5:C36]",
"snippet": "[headercol...: #3457DB]"
}
}
}
],
"program": {
"publicSchema": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,6 @@
"snippet": "Note schem...dy\n '''\n}"
}
}
},
{
"code": "UNEXPECTED_SETTINGS",
"diagnostic": "A Note shouldn't have a setting list",
"filepath": "/main.dbml",
"level": "error",
"node": {
"context": {
"id": "node@<list-expression>@@[L34:C22, L34:C44]",
"snippet": "[headercol...: #3457DB]"
}
}
}
],
"program": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Table users [headercolor: #3498DB] {
username varchar(255) [not null, unique]
}

Note note {
Note note [color: #FF5733] {
'One line note'
}

Expand All @@ -13,3 +13,7 @@ Note note2 {
body
'''
}

Note note3 [color: none] {
'Note without color'
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
"database": {
"notes": [
{
"color": "#FF5733",
"content": "One line note",
"name": "note",
"token": {
"end": {
"column": 2,
"line": 8,
"offset": 141
"offset": 158
},
"filepath": "/main.dbml",
"start": {
Expand All @@ -25,13 +26,31 @@
"end": {
"column": 2,
"line": 15,
"offset": 190
"offset": 207
},
"filepath": "/main.dbml",
"start": {
"column": 1,
"line": 10,
"offset": 143
"offset": 160
}
}
},
{
"color": "#00000000",
"content": "Note without color",
"name": "note3",
"token": {
"end": {
"column": 2,
"line": 19,
"offset": 260
},
"filepath": "/main.dbml",
"start": {
"column": 1,
"line": 17,
"offset": 209
}
}
}
Expand Down Expand Up @@ -104,8 +123,8 @@
"token": {
"end": {
"column": 1,
"line": 16,
"offset": 191
"line": 20,
"offset": 261
},
"filepath": "/main.dbml",
"start": {
Expand Down
13 changes: 10 additions & 3 deletions packages/dbml-parse/src/core/global_modules/note/interpret.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { get, partition } from 'lodash-es';
import { partition } from 'lodash-es';
import { aggregateSettingList } from '@/core/utils/validate';
import { CompileError, CompileErrorCode } from '@/core/types/errors';
import {
Expand All @@ -14,6 +14,8 @@ import {
getTokenPosition,
normalizeNote,
} from '@/core/utils/interpret';
import { isExpressionAnIdentifierNode } from '@/core/utils/validate';
import { extractQuotedStringToken } from '@/core/utils/expression';
import type {
Filepath,
NoteSymbol,
Expand Down Expand Up @@ -64,7 +66,12 @@ export class StickyNoteInterpreter {
private interpretSettingList (settings?: ListExpressionNode): CompileError[] {
const settingMap = aggregateSettingList(settings).getValue();

this.note.headerColor = settingMap.headercolor?.length ? extractColor(settingMap.headercolor?.at(0)?.value as any) : undefined;
if (settingMap.color?.length) {
const colorNode = settingMap.color.at(0)?.value;
const isNone = isExpressionAnIdentifierNode(colorNode) && colorNode.expression.variable.value.toLowerCase() === 'none';
// Transparent color #00000000
this.note.color = isNone ? '#00000000' : extractColor(colorNode as any);
}

return [];
}
Expand All @@ -87,7 +94,7 @@ export class StickyNoteInterpreter {
}

private interpretNote (note: FunctionApplicationNode): CompileError[] {
const noteContent = get(note, 'callee.expression.literal.value', '');
const noteContent = extractQuotedStringToken(note.callee) ?? '';

this.note.content = normalizeNote(noteContent);
return [];
Expand Down
Loading
Loading