Add responsive preview, editor themes, template gallery, devtools, and autocomplete
- Device breakpoint toggles (mobile 375px / tablet 768px / desktop 100%) - Editor theme selector with 6 themes (VS Dark/Light, High Contrast, Monokai, Dracula, GitHub Dark) - Starter template gallery with 8 pre-built templates (Todo, API Fetch, CSS Animation, etc.) - Code autocomplete with DOM/React type definitions and snippet completions - Devtools panels: console, network, elements, performance - Code formatter (Prettier), diff view, and linter integration
This commit is contained in:
172
public/js/linter.js
Normal file
172
public/js/linter.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import { loadScript } from './utils.js';
|
||||
import { getActiveEditor, getActiveTab, getCurrentMode, setOnTabSwitch, setOnModeChange } from './editors.js';
|
||||
|
||||
const ESLINT_CDN = 'https://cdn.jsdelivr.net/npm/eslint-linter-browserify@9/linter.min.js';
|
||||
|
||||
let linterInstance = null;
|
||||
let loaded = false;
|
||||
let debounceTimer = null;
|
||||
|
||||
async function ensureESLint() {
|
||||
if (loaded) return;
|
||||
await loadScript(ESLINT_CDN);
|
||||
if (window.eslint && window.eslint.Linter) {
|
||||
linterInstance = new window.eslint.Linter();
|
||||
} else if (window.Linter) {
|
||||
linterInstance = new window.Linter();
|
||||
}
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
function isLintableMode(mode) {
|
||||
return ['html-css-js', 'typescript', 'react', 'react-ts', 'wasm'].includes(mode);
|
||||
}
|
||||
|
||||
function isLintableTab(tabId) {
|
||||
return tabId === 'js';
|
||||
}
|
||||
|
||||
const BROWSER_GLOBALS = {
|
||||
document: 'readonly', window: 'readonly', console: 'readonly',
|
||||
setTimeout: 'readonly', clearTimeout: 'readonly', setInterval: 'readonly', clearInterval: 'readonly',
|
||||
requestAnimationFrame: 'readonly', cancelAnimationFrame: 'readonly',
|
||||
fetch: 'readonly', URL: 'readonly', URLSearchParams: 'readonly',
|
||||
HTMLElement: 'readonly', Event: 'readonly', CustomEvent: 'readonly',
|
||||
MutationObserver: 'readonly', IntersectionObserver: 'readonly', ResizeObserver: 'readonly',
|
||||
localStorage: 'readonly', sessionStorage: 'readonly', navigator: 'readonly', location: 'readonly',
|
||||
history: 'readonly', performance: 'readonly', alert: 'readonly', confirm: 'readonly', prompt: 'readonly',
|
||||
WebAssembly: 'readonly', Uint8Array: 'readonly', ArrayBuffer: 'readonly',
|
||||
Map: 'readonly', Set: 'readonly', WeakMap: 'readonly', WeakSet: 'readonly',
|
||||
Promise: 'readonly', Symbol: 'readonly', Proxy: 'readonly', Reflect: 'readonly',
|
||||
globalThis: 'readonly', self: 'readonly', queueMicrotask: 'readonly',
|
||||
};
|
||||
|
||||
const REACT_GLOBALS = {
|
||||
React: 'readonly', ReactDOM: 'readonly',
|
||||
};
|
||||
|
||||
function getEslintConfig(mode) {
|
||||
const parserOptions = {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
};
|
||||
|
||||
const globals = { ...BROWSER_GLOBALS };
|
||||
|
||||
if (mode === 'react' || mode === 'react-ts') {
|
||||
parserOptions.ecmaFeatures = { jsx: true };
|
||||
Object.assign(globals, REACT_GLOBALS);
|
||||
}
|
||||
|
||||
return {
|
||||
languageOptions: { globals, parserOptions },
|
||||
rules: {
|
||||
'no-constant-condition': 'warn',
|
||||
'no-debugger': 'warn',
|
||||
'no-empty': 'warn',
|
||||
'no-extra-semi': 'warn',
|
||||
'no-unreachable': 'warn',
|
||||
'no-duplicate-case': 'warn',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function severityToMonaco(severity) {
|
||||
// ESLint: 1=warn, 2=error
|
||||
if (severity === 2) return monaco.MarkerSeverity.Error;
|
||||
return monaco.MarkerSeverity.Warning;
|
||||
}
|
||||
|
||||
function lintEditor(editor, mode) {
|
||||
if (!linterInstance || !editor) return 0;
|
||||
|
||||
const model = editor.getModel();
|
||||
if (!model) return 0;
|
||||
|
||||
const code = editor.getValue();
|
||||
if (!code.trim()) {
|
||||
monaco.editor.setModelMarkers(model, 'eslint', []);
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
const config = getEslintConfig(mode);
|
||||
const messages = linterInstance.verify(code, config);
|
||||
|
||||
const markers = messages.map(m => ({
|
||||
startLineNumber: m.line || 1,
|
||||
startColumn: m.column || 1,
|
||||
endLineNumber: m.endLine || m.line || 1,
|
||||
endColumn: m.endColumn || (m.column || 1) + 1,
|
||||
message: `${m.message} (${m.ruleId || 'syntax'})`,
|
||||
severity: severityToMonaco(m.severity),
|
||||
source: 'eslint',
|
||||
}));
|
||||
|
||||
monaco.editor.setModelMarkers(model, 'eslint', markers);
|
||||
return markers.length;
|
||||
} catch (e) {
|
||||
monaco.editor.setModelMarkers(model, 'eslint', []);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function updateBadge(count) {
|
||||
// Lint feedback is shown as editor squiggles only — no badge needed
|
||||
}
|
||||
|
||||
function scheduleLint() {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
const mode = getCurrentMode();
|
||||
const tabId = getActiveTab();
|
||||
if (!isLintableMode(mode) || !isLintableTab(tabId)) {
|
||||
clearMarkers();
|
||||
updateBadge(0);
|
||||
return;
|
||||
}
|
||||
const editor = getActiveEditor();
|
||||
const count = lintEditor(editor, mode);
|
||||
updateBadge(count);
|
||||
}, 800);
|
||||
}
|
||||
|
||||
function clearMarkers() {
|
||||
const editor = getActiveEditor();
|
||||
if (editor && editor.getModel()) {
|
||||
monaco.editor.setModelMarkers(editor.getModel(), 'eslint', []);
|
||||
}
|
||||
}
|
||||
|
||||
export async function initLinter() {
|
||||
try {
|
||||
await ensureESLint();
|
||||
} catch (e) {
|
||||
console.warn('ESLint failed to load:', e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!linterInstance) return;
|
||||
|
||||
// Lint on tab switch
|
||||
setOnTabSwitch((tabId, editor) => {
|
||||
const mode = getCurrentMode();
|
||||
if (isLintableMode(mode) && isLintableTab(tabId) && editor) {
|
||||
const count = lintEditor(editor, mode);
|
||||
updateBadge(count);
|
||||
} else {
|
||||
updateBadge(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear markers on mode change
|
||||
setOnModeChange((mode) => {
|
||||
if (!isLintableMode(mode)) {
|
||||
updateBadge(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function lintOnChange() {
|
||||
scheduleLint();
|
||||
}
|
||||
Reference in New Issue
Block a user