Skip to content

Commit 5b21c51

Browse files
committed
refactor: Clean up Editor class, add docblocks and type hints.
1 parent 4bfa12b commit 5b21c51

File tree

1 file changed

+71
-18
lines changed

1 file changed

+71
-18
lines changed

src/Editor.js

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,26 @@ import 'ace-builds/src-noconflict/mode-yaml';
33
import 'ace-builds/src-noconflict/theme-github';
44
import Yaml from 'yaml';
55
import {Builder} from "./Builder.js";
6-
import {ImageEmbed} from "./ImageEmbed.js";
6+
import { ImageEmbed } from "./ImageEmbed.js";
77

8+
/**
9+
* Manages the Ace editor instance, handles YAML parsing,
10+
* updates the SVG preview, and manages content loading/saving.
11+
*/
812
export class Editor {
9-
13+
/** @type {string} The key used for storing YAML content in localStorage. */
1014
STORAGE_KEY = 'pcb-diagram-yaml';
1115

16+
/** @type {ace.Ace.Editor} The Ace editor instance. */
17+
ace;
18+
/** @type {HTMLElement} The HTML element where the SVG output is rendered. */
19+
output;
20+
21+
/**
22+
* Creates an Editor instance.
23+
* @param {string|HTMLElement} editor - The ID or HTML element for the Ace editor container.
24+
* @param {HTMLElement} output - The HTML element to render the SVG output into.
25+
*/
1226
constructor(editor, output) {
1327
this.output = output;
1428

@@ -27,25 +41,30 @@ export class Editor {
2741
tabSize: 2
2842
});
2943

30-
// Defer content loading and event binding
3144
this.initializeEditor();
32-
33-
// register download handler
3445
this.output.addEventListener('click', this.onDownloadClick);
3546
}
3647

48+
/**
49+
* Asynchronously initializes the editor by loading content
50+
* and setting up event listeners.
51+
* @private
52+
*/
3753
async initializeEditor() {
38-
// Load content first
3954
await this.loadInitialContent();
4055

41-
// Then register change handler and trigger initial processing
4256
this.ace.session.on('change', this.debounce(this.onChange.bind(this), 300));
43-
// Check if editor has content before triggering initial processing
4457
if (this.ace.getValue()) {
45-
this.onChange(); // Trigger initial processing only if content loaded
58+
this.onChange(); // Trigger initial processing only if content loaded
4659
}
4760
}
4861

62+
/**
63+
* Loads the initial content into the editor.
64+
* It prioritizes loading from a 'load' URL parameter,
65+
* falling back to localStorage if the parameter is not present.
66+
* @private
67+
*/
4968
async loadInitialContent() {
5069
const urlParams = new URLSearchParams(window.location.search);
5170
const loadUrl = urlParams.get('load');
@@ -60,27 +79,33 @@ export class Editor {
6079
this.ace.setValue(yamlContent, -1); // -1 moves cursor to the start
6180
console.log(`Loaded content from: ${loadUrl}`);
6281

63-
// Remove the 'load' parameter from the URL without reloading
6482
const currentUrlParams = new URLSearchParams(window.location.search);
6583
currentUrlParams.delete('load');
6684
const newRelativePathQuery = window.location.pathname + (currentUrlParams.toString() ? '?' + currentUrlParams.toString() : '');
6785
window.history.replaceState({ path: newRelativePathQuery }, '', newRelativePathQuery);
6886
} catch (error) {
6987
console.error('Failed to load content from URL:', loadUrl, error);
70-
// Display error to the user in the output div and editor
7188
const errorMessage = `# Failed to load content from: ${loadUrl}\n# Error: ${error.message}`;
7289
this.output.innerHTML = `<div class="error">Failed to load content from <a href="${loadUrl}" target="_blank">${loadUrl}</a>: ${error.message}</div>`;
7390
this.ace.setValue(errorMessage, -1);
7491
}
7592
} else {
76-
// Load initial content from localStorage if no loadUrl
7793
const savedYaml = localStorage.getItem(this.STORAGE_KEY);
7894
if (savedYaml) {
7995
this.ace.setValue(savedYaml, -1);
8096
}
8197
}
8298
}
8399

100+
/**
101+
* Creates a debounced version of a function that delays invoking the function
102+
* until after `wait` milliseconds have elapsed since the last time the
103+
* debounced function was invoked.
104+
* @param {Function} func The function to debounce.
105+
* @param {number} wait The number of milliseconds to delay.
106+
* @returns {Function} The debounced function.
107+
* @private
108+
*/
84109
debounce(func, wait) {
85110
let timeout;
86111
return function (...args) {
@@ -89,21 +114,25 @@ export class Editor {
89114
};
90115
}
91116

117+
/**
118+
* Handles the 'change' event from the Ace editor.
119+
* Parses the YAML content, saves valid content to localStorage,
120+
* triggers the SVG update, and displays parsing errors.
121+
* @private
122+
*/
92123
onChange() {
93124
const yaml = this.ace.getValue();
94125
try {
95126
const parsed = Yaml.parse(yaml, {
96127
prettyErrors: true,
97128
});
98-
// Save valid YAML to localStorage
99129
localStorage.setItem(this.STORAGE_KEY, yaml);
100130
this.onUpdate(parsed);
101131
this.clearErrors();
102132
} catch (e) {
103-
// Don't save invalid YAML
104133
if (e.linePos) {
105134
this.showError(
106-
e.linePos[0].line - 1,
135+
e.linePos[0].line - 1, // Ace lines are 0-indexed
107136
e.linePos[0].col - 1,
108137
e.message,
109138
e.name === 'YAMLWarning' ? 'error' : 'warning'
@@ -114,8 +143,15 @@ export class Editor {
114143
}
115144
}
116145

146+
/**
147+
* Called when the YAML content is successfully parsed.
148+
* Embeds images, builds the front and back SVG representations,
149+
* and updates the output element.
150+
* @param {object} setup - The parsed YAML configuration object.
151+
* @private
152+
*/
117153
async onUpdate(setup) {
118-
const embed = new ImageEmbed('pinouts');
154+
const embed = new ImageEmbed('pinouts'); // Assuming 'pinouts' is the base path for local images
119155
setup = await embed.embedImages(setup);
120156

121157
const builder = new Builder(setup);
@@ -129,8 +165,15 @@ export class Editor {
129165
this.output.appendChild(back);
130166
}
131167

168+
/**
169+
* Handles click events on the output area to trigger SVG download.
170+
* @param {MouseEvent} e - The click event object.
171+
* @private
172+
*/
132173
onDownloadClick(e) {
133174
const svg = e.target.closest('svg');
175+
if (!svg) return; // Click was not on an SVG or its child
176+
134177
const serializer = new XMLSerializer();
135178
const source = serializer.serializeToString(svg);
136179
const blob = new Blob([source], { type: 'image/svg+xml;charset=utf-8' });
@@ -146,9 +189,15 @@ export class Editor {
146189
URL.revokeObjectURL(url);
147190
}
148191

192+
/**
193+
* Displays an error or warning annotation in the Ace editor gutter.
194+
* @param {number} lineNumber - The 0-indexed line number for the annotation.
195+
* @param {number} column - The 0-indexed column number for the annotation.
196+
* @param {string} message - The error or warning message text.
197+
* @param {boolean} isWarning - True if the annotation is a warning, false for an error.
198+
* @private
199+
*/
149200
showError(lineNumber, column, message, isWarning) {
150-
console.error('ace', lineNumber, column, message);
151-
152201
this.ace.session.setAnnotations([{
153202
row: lineNumber,
154203
column: column,
@@ -157,6 +206,10 @@ export class Editor {
157206
}]);
158207
}
159208

209+
/**
210+
* Clears all annotations (errors and warnings) from the Ace editor.
211+
* @private
212+
*/
160213
clearErrors() {
161214
this.ace.session.clearAnnotations();
162215
}

0 commit comments

Comments
 (0)