canvas_erd 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,6 +25,7 @@
25
25
  paragraph: Object.freeze({ fontFamily: "Arial", fontSize: 15, fontWeight: "normal", lineHeight: 1.25 }),
26
26
  code: Object.freeze({ fontFamily: "Courier New", fontSize: 14, fontWeight: "normal", lineHeight: 1.2 })
27
27
  });
28
+ const CODE_FONT_SIZES = Object.freeze({ small: 12, medium: 14, large: 18 });
28
29
  const DEFAULT_TABLE_HEADER_COLOR = "#243b63";
29
30
  const DEFAULT_TABLE_HEADER_COLORS = DEFAULT_FOREGROUND_COLORS;
30
31
  const DEFAULT_ARROW_FORMATTING = Object.freeze({
@@ -305,6 +306,115 @@
305
306
  };
306
307
  }
307
308
 
309
+ function codeFontSize(block) {
310
+ return CODE_FONT_SIZES[block.size] || CODE_FONT_SIZES.medium;
311
+ }
312
+
313
+ function codeCanvasGeometry(block) {
314
+ const style = NOTE_PARAGRAPH_STYLES.code;
315
+ return {
316
+ left: block.x,
317
+ top: block.y,
318
+ width: block.width,
319
+ angle: block.angle ?? 0,
320
+ fontFamily: style.fontFamily,
321
+ fontSize: codeFontSize(block),
322
+ lineHeight: style.lineHeight
323
+ };
324
+ }
325
+
326
+ function codeStateGeometry(object) {
327
+ return {
328
+ x: object.left,
329
+ y: object.top,
330
+ width: object.width * object.scaleX,
331
+ angle: object.angle,
332
+ code: object.text
333
+ };
334
+ }
335
+
336
+ function codeTokenStyles(runs) {
337
+ const styles = {};
338
+ let line = 0;
339
+ let character = 0;
340
+ runs.forEach(([text, style]) => {
341
+ text.split("\n").forEach((segment, index) => {
342
+ if (index > 0) {
343
+ line += 1;
344
+ character = 0;
345
+ }
346
+ Array.from(segment).forEach(() => {
347
+ styles[line] = styles[line] || {};
348
+ styles[line][character] = { ...style };
349
+ character += 1;
350
+ });
351
+ });
352
+ });
353
+ return styles;
354
+ }
355
+
356
+ function sourceFromFields(path, startLine, endLine) {
357
+ const trimmed = String(path || "").trim();
358
+ if (!trimmed) return null;
359
+ const lineNumber = (value) => {
360
+ const number = Number.parseInt(value, 10);
361
+ return number > 0 ? number : null;
362
+ };
363
+ return { path: trimmed, startLine: lineNumber(startLine), endLine: lineNumber(endLine) };
364
+ }
365
+
366
+ function codeLineLabels(block, lineCount) {
367
+ if (block.source?.model) {
368
+ return Array.from({ length: lineCount }, (_, index) => block.lineNumbers?.[index] ?? null);
369
+ }
370
+ const first = block.source?.startLine || 1;
371
+ return Array.from({ length: lineCount }, (_, index) => first + index);
372
+ }
373
+
374
+ function codeGutterWidth(labels, fontSize) {
375
+ const digits = Math.max(1, ...labels.filter((label) => label !== null).map((label) => String(label).length));
376
+ return (digits + 2) * fontSize * 0.6;
377
+ }
378
+
379
+ function codeRowAt(rowHeights, y) {
380
+ let top = 0;
381
+ for (let index = 0; index < rowHeights.length; index += 1) {
382
+ if (y >= top && y < top + rowHeights[index]) return index;
383
+ top += rowHeights[index];
384
+ }
385
+ return -1;
386
+ }
387
+
388
+ function toggleHighlightedLine(lines = [], label) {
389
+ if (lines.includes(label)) return lines.filter((line) => line !== label);
390
+ return [...lines, label].sort((first, second) => first - second);
391
+ }
392
+
393
+ function highlightLineRange(lines = [], labels, anchor, label) {
394
+ const start = labels.indexOf(anchor);
395
+ const end = labels.indexOf(label);
396
+ if (start < 0 || end < 0) return toggleHighlightedLine(lines, label);
397
+
398
+ const range = labels.slice(Math.min(start, end), Math.max(start, end) + 1).filter((line) => line !== null);
399
+ if (!lines.includes(anchor)) return lines.filter((line) => !range.includes(line));
400
+ return [...new Set([...lines, ...range])].sort((first, second) => first - second);
401
+ }
402
+
403
+ function modelDetailBlock(codeBlocks, entityId, detail) {
404
+ return codeBlocks.find((block) => block.source?.model === entityId && block.source.detail === detail) || null;
405
+ }
406
+
407
+ function modelDetailOrigin(tablePosition) {
408
+ return { x: tablePosition.x + CARD_WIDTH + 80, y: tablePosition.y };
409
+ }
410
+
411
+ function sourceReferenceLabel(source) {
412
+ if (source.model) return `${source.model} ${source.detail}`;
413
+ if (!source.startLine && !source.endLine) return source.path;
414
+ if (source.startLine === source.endLine) return `${source.path}:${source.startLine}`;
415
+ return `${source.path}:${source.startLine || 1}-${source.endLine || "end"}`;
416
+ }
417
+
308
418
  function erdFilename(value) {
309
419
  const basename = String(value).trim().replace(/(?:\.erd)?\.png$/i, "");
310
420
  return `${basename || "diagram"}.erd.png`;
@@ -330,6 +440,7 @@
330
440
 
331
441
  return {
332
442
  n: "addNote",
443
+ c: "addCode",
333
444
  a: "arrow",
334
445
  t: "toggleTables",
335
446
  l: "layoutTables",
@@ -398,6 +509,7 @@
398
509
  function arrowAttachment(target) {
399
510
  if (target?.canvasErdType === "entity") return { type: "entity", id: target.entityId };
400
511
  if (target?.canvasErdType === "note") return { type: "note", id: target.noteId };
512
+ if (target?.canvasErdType === "code") return { type: "code", id: target.codeBlockId };
401
513
  return null;
402
514
  }
403
515
 
@@ -482,6 +594,7 @@
482
594
  tableHeaderColors: {},
483
595
  frames: [],
484
596
  notes: [],
597
+ codeBlocks: [],
485
598
  arrows: []
486
599
  };
487
600
  }
@@ -544,6 +657,10 @@
544
657
  tableHeaderColors: { ...(state.tableHeaderColors || {}) },
545
658
  frames: (state.frames || []).map((frame) => ({ ...frame })),
546
659
  notes: state.notes.map((note) => ({ ...note })),
660
+ codeBlocks: (state.codeBlocks || []).map((block) => ({
661
+ ...block,
662
+ source: block.source ? { ...block.source } : null
663
+ })),
547
664
  arrows: (state.arrows || []).map((arrow) => ({
548
665
  ...arrow,
549
666
  start: { ...arrow.start, attachment: arrow.start.attachment ? { ...arrow.start.attachment } : null },
@@ -834,6 +951,20 @@
834
951
  frameLabelCanvasStyle,
835
952
  noteCanvasGeometry,
836
953
  noteStateGeometry,
954
+ CODE_FONT_SIZES,
955
+ codeFontSize,
956
+ codeCanvasGeometry,
957
+ codeStateGeometry,
958
+ codeTokenStyles,
959
+ sourceFromFields,
960
+ sourceReferenceLabel,
961
+ codeLineLabels,
962
+ codeGutterWidth,
963
+ codeRowAt,
964
+ toggleHighlightedLine,
965
+ highlightLineRange,
966
+ modelDetailBlock,
967
+ modelDetailOrigin,
837
968
  erdFilename,
838
969
  diagramName,
839
970
  confirmDiscardChanges,
@@ -66,6 +66,7 @@
66
66
  <div><dt>New</dt><dd><kbd>⌘/Ctrl</kbd> <kbd>N</kbd></dd></div>
67
67
  <div><dt>Save</dt><dd><kbd>⌘/Ctrl</kbd> <kbd>S</kbd></dd></div>
68
68
  <div><dt>Add note at cursor</dt><dd><kbd>N</kbd></dd></div>
69
+ <div><dt>Add code at cursor</dt><dd><kbd>C</kbd></dd></div>
69
70
  <div><dt>Bold note</dt><dd><kbd>⌘/Ctrl</kbd> <kbd>B</kbd></dd></div>
70
71
  <div><dt>Italic note</dt><dd><kbd>⌘/Ctrl</kbd> <kbd>I</kbd></dd></div>
71
72
  <div><dt>Underline note</dt><dd><kbd>⌘/Ctrl</kbd> <kbd>U</kbd></dd></div>
@@ -225,6 +226,61 @@
225
226
  </select>
226
227
  </label>
227
228
  </div>
229
+ <div id="code-formatting" class="code-formatting" role="toolbar" aria-label="Code formatting" hidden>
230
+ <label class="note-format-field">
231
+ <span>Language</span>
232
+ <select id="code-language" aria-label="Code language"></select>
233
+ </label>
234
+ <label class="note-format-field">
235
+ <span>Size</span>
236
+ <select id="code-size" aria-label="Code font size">
237
+ <option value="small">Small</option>
238
+ <option value="medium" selected>Medium</option>
239
+ <option value="large">Large</option>
240
+ </select>
241
+ </label>
242
+ <button id="code-line-numbers"class="format-button" type="button" aria-label="Line numbers" title="Line numbers" aria-pressed="false">#</button>
243
+ <span id="code-source" class="code-source" hidden></span>
244
+ <div id="code-source-fields" class="code-source-fields" hidden>
245
+ <label class="note-format-field">
246
+ <span>Path</span>
247
+ <input id="code-source-path" class="source-path-input" type="text" list="source-file-paths" autocomplete="off" aria-label="Source file path">
248
+ </label>
249
+ <label class="note-format-field">
250
+ <span>Lines</span>
251
+ <input id="code-source-start" class="line-input" type="number" min="1" placeholder="1" aria-label="Start line">
252
+ </label>
253
+ <label class="note-format-field">
254
+ <span>to</span>
255
+ <input id="code-source-end" class="line-input" type="number" min="1" placeholder="end" aria-label="End line">
256
+ </label>
257
+ </div>
258
+ </div>
259
+ <datalist id="source-file-paths"></datalist>
260
+ <dialog id="source-dialog" class="source-dialog" aria-labelledby="source-dialog-title">
261
+ <form id="source-form">
262
+ <h2 id="source-dialog-title">Add source file</h2>
263
+ <label class="source-field">
264
+ <span>Path</span>
265
+ <input id="source-path" class="source-path-input" type="text" list="source-file-paths" autocomplete="off" placeholder="app/models/book.rb" required>
266
+ </label>
267
+ <div class="source-lines">
268
+ <label class="source-field">
269
+ <span>Start line</span>
270
+ <input id="source-start" class="line-input" type="number" min="1" placeholder="1">
271
+ </label>
272
+ <label class="source-field">
273
+ <span>End line</span>
274
+ <input id="source-end" class="line-input" type="number" min="1" placeholder="end">
275
+ </label>
276
+ </div>
277
+ <p id="source-error" class="source-error" role="alert" hidden></p>
278
+ <div class="source-actions">
279
+ <button id="source-cancel" type="button">Cancel</button>
280
+ <button id="source-submit" type="submit">Add</button>
281
+ </div>
282
+ </form>
283
+ </dialog>
228
284
  <div class="canvas-tools" aria-label="Canvas tools">
229
285
  <button id="toggle-tables" class="icon-button" type="button" aria-label="Show tables" title="Show tables (T)" aria-expanded="false">
230
286
  <svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" color="#000000" aria-hidden="true" focusable="false"><path d="M3 20.4V3.6C3 3.26863 3.26863 3 3.6 3H20.4C20.7314 3 21 3.26863 21 3.6V20.4C21 20.7314 20.7314 21 20.4 21H3.6C3.26863 21 3 20.7314 3 20.4Z" stroke="#000000" stroke-width="1.5"></path><path d="M3 16.5H21" stroke="#000000" stroke-width="1.5"></path><path d="M3 12H21" stroke="#000000" stroke-width="1.5"></path><path d="M21 7.5H3" stroke="#000000" stroke-width="1.5"></path><path d="M12 21V3" stroke="#000000" stroke-width="1.5"></path></svg>
@@ -238,6 +294,12 @@
238
294
  <button id="add-note" class="icon-button" type="button" aria-label="Add note" title="Add note (N)">
239
295
  <svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" color="#000000" aria-hidden="true" focusable="false"><path d="M12 8L12 16M12 8H8M12 8H16" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M21 13.5V19C21 20.1046 20.1046 21 19 21H5C3.89543 21 3 20.1046 3 19V13.5M21 10.5V5C21 3.89543 20.1046 3 19 3H5C3.89543 3 3 3.89543 3 5V10.5" stroke="#000000" stroke-width="1.5" stroke-linejoin="round"></path><path d="M19.5 13.5V10.5H22.5V13.5H19.5Z" stroke="#000000" stroke-width="1.5" stroke-linejoin="round"></path><path d="M1.5 13.5V10.5H4.5V13.5H1.5Z" stroke="#000000" stroke-width="1.5" stroke-linejoin="round"></path></svg>
240
296
  </button>
297
+ <button id="add-code" class="icon-button" type="button" aria-label="Add code" title="Add code (C)">
298
+ <svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" color="#000000" aria-hidden="true" focusable="false"><path d="M13.5 6L10 18.5" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M6.5 8.5L3 12L6.5 15.5" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M17.5 8.5L21 12L17.5 15.5" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
299
+ </button>
300
+ <button id="add-source" class="icon-button" type="button" aria-label="Add source file" title="Add source file">
301
+ <svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" color="#000000" aria-hidden="true" focusable="false"><path d="M4 21.4V2.6C4 2.26863 4.26863 2 4.6 2H16.2515C16.4106 2 16.5632 2.06321 16.6757 2.17574L19.8243 5.32426C19.9368 5.43679 20 5.5894 20 5.74853V21.4C20 21.7314 19.7314 22 19.4 22H4.6C4.26863 22 4 21.7314 4 21.4Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M8 10L16 10" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M8 18L16 18" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M8 14L12 14" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M16 2V5.4C16 5.73137 16.2686 6 16.6 6H20" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
302
+ </button>
241
303
  <button id="add-arrow" class="icon-button" type="button" aria-label="Arrow tool" title="Arrow tool (A) · Select an arrow to edit endpoints · Hold Ctrl for a free endpoint" aria-pressed="false">
242
304
  <svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" color="#000000" aria-hidden="true" focusable="false"><path d="M6.00005 19L19 5.99996M19 5.99996V18.48M19 5.99996H6.52005" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
243
305
  </button>
@@ -324,7 +324,8 @@ select:focus-visible {
324
324
  .note-formatting,
325
325
  .table-formatting,
326
326
  .arrow-formatting,
327
- .frame-formatting {
327
+ .frame-formatting,
328
+ .code-formatting {
328
329
  position: absolute;
329
330
  bottom: 4rem;
330
331
  left: 50%;
@@ -515,7 +516,9 @@ select:focus-visible {
515
516
  padding: 0.2rem 0.35rem;
516
517
  }
517
518
 
518
- #arrow-stroke-width {
519
+ #arrow-stroke-width,
520
+ .source-path-input,
521
+ .line-input {
519
522
  width: 4rem;
520
523
  min-height: 1.9rem;
521
524
  border: 1px solid #cbd3df;
@@ -645,3 +648,87 @@ kbd {
645
648
  border-color: #fecaca;
646
649
  background: #fff7f7;
647
650
  }
651
+
652
+ .code-source {
653
+ color: #526277;
654
+ padding: 0 0.35rem;
655
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
656
+ font-size: 0.72rem;
657
+ }
658
+
659
+ .detail-toggle {
660
+ min-height: 1.4rem;
661
+ margin-left: auto;
662
+ border-radius: 999px;
663
+ color: #526277;
664
+ padding: 0 0.5rem;
665
+ font-size: 0.7rem;
666
+ }
667
+
668
+ .detail-toggle[aria-pressed="true"] {
669
+ border-color: #2563eb;
670
+ color: #1d4ed8;
671
+ background: #eff6ff;
672
+ }
673
+
674
+ .code-source-fields {
675
+ display: flex;
676
+ align-items: center;
677
+ }
678
+
679
+ .note-format-field .source-path-input {
680
+ width: 18rem;
681
+ }
682
+
683
+ .source-dialog {
684
+ width: min(32rem, calc(100% - 2rem));
685
+ border: 1px solid rgba(203, 213, 225, 0.9);
686
+ border-radius: 0.75rem;
687
+ color: #263447;
688
+ padding: 1rem;
689
+ box-shadow: 0 10px 30px rgba(15, 23, 42, 0.2);
690
+ }
691
+
692
+ .source-dialog::backdrop {
693
+ background: rgba(15, 23, 42, 0.2);
694
+ }
695
+
696
+ .source-dialog h2 {
697
+ margin: 0 0 0.8rem;
698
+ color: #172554;
699
+ font-size: 0.95rem;
700
+ }
701
+
702
+ .source-field {
703
+ display: flex;
704
+ flex-direction: column;
705
+ gap: 0.3rem;
706
+ color: #526277;
707
+ font-size: 0.75rem;
708
+ }
709
+
710
+ .source-dialog .source-path-input,
711
+ .source-dialog .line-input {
712
+ width: 100%;
713
+ min-height: 2.25rem;
714
+ }
715
+
716
+ .source-lines {
717
+ display: grid;
718
+ grid-template-columns: 1fr 1fr;
719
+ gap: 0.75rem;
720
+ margin-top: 0.75rem;
721
+ }
722
+
723
+ .source-error {
724
+ margin: 0.75rem 0 0;
725
+ color: #b91c1c;
726
+ font-size: 0.8rem;
727
+ }
728
+
729
+ .source-actions {
730
+ display: flex;
731
+ justify-content: flex-end;
732
+ gap: 0.5rem;
733
+ margin-top: 1rem;
734
+ }
data/lib/canvas_erd.rb CHANGED
@@ -5,6 +5,9 @@ require "canvas_erd/diagram"
5
5
  require "canvas_erd/schema_provider"
6
6
  require "canvas_erd/png_document"
7
7
  require "canvas_erd/diagram_store"
8
+ require "canvas_erd/highlighter"
9
+ require "canvas_erd/source_files"
10
+ require "canvas_erd/model_details"
8
11
  require "canvas_erd/application_loader"
9
12
  require "canvas_erd/browser"
10
13
  require "canvas_erd/server"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canvas_erd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marc Heiligers
@@ -43,6 +43,26 @@ dependencies:
43
43
  - - "<"
44
44
  - !ruby/object:Gem::Version
45
45
  version: '4'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rouge
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '4.0'
53
+ - - "<"
54
+ - !ruby/object:Gem::Version
55
+ version: '6'
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '4.0'
63
+ - - "<"
64
+ - !ruby/object:Gem::Version
65
+ version: '6'
46
66
  - !ruby/object:Gem::Dependency
47
67
  name: minitest
48
68
  requirement: !ruby/object:Gem::Requirement
@@ -110,9 +130,12 @@ files:
110
130
  - lib/canvas_erd/cli.rb
111
131
  - lib/canvas_erd/diagram.rb
112
132
  - lib/canvas_erd/diagram_store.rb
133
+ - lib/canvas_erd/highlighter.rb
134
+ - lib/canvas_erd/model_details.rb
113
135
  - lib/canvas_erd/png_document.rb
114
136
  - lib/canvas_erd/schema_provider.rb
115
137
  - lib/canvas_erd/server.rb
138
+ - lib/canvas_erd/source_files.rb
116
139
  - lib/canvas_erd/version.rb
117
140
  - lib/canvas_erd/web/app.js
118
141
  - lib/canvas_erd/web/document.js
@@ -142,14 +165,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
142
165
  requirements:
143
166
  - - ">="
144
167
  - !ruby/object:Gem::Version
145
- version: '3.1'
168
+ version: '3.3'
146
169
  required_rubygems_version: !ruby/object:Gem::Requirement
147
170
  requirements:
148
171
  - - ">="
149
172
  - !ruby/object:Gem::Version
150
173
  version: '0'
151
174
  requirements: []
152
- rubygems_version: 4.0.16
175
+ rubygems_version: 4.0.20
153
176
  specification_version: 4
154
177
  summary: Edit Rails ERD diagrams on an interactive canvas
155
178
  test_files: []