Files
specgraph/public/app.js
Patrick Balsiger c0706c3a2b feat: spec graph
2025-10-31 11:05:59 +01:00

118 lines
3.1 KiB
JavaScript

(function() {
const textarea = document.getElementById('yaml-input');
const errorEl = document.getElementById('error');
const metaEl = document.getElementById('meta');
const renderBtn = document.getElementById('render-btn');
const editor = CodeMirror.fromTextArea(textarea, {
mode: 'yaml',
lineNumbers: true,
lineWrapping: true,
tabSize: 2,
indentUnit: 2,
});
const sample = `openapi: 3.0.0
info:
title: Sample API
version: 1.0.0
paths:
/api/test/bla:
get:
summary: Get bla
responses:
'200':
description: OK
/api/test/foo:
post:
summary: Create foo
responses:
'201':
description: Created
/api/users/{id}:
get:
summary: Get user
responses:
'200': { description: OK }
`;
editor.setValue(sample);
function selectWholeLine(lineNumber) {
const lineText = editor.getLine(lineNumber) || '';
const from = { line: lineNumber, ch: 0 };
const to = { line: lineNumber, ch: lineText.length };
editor.setSelection(from, to);
editor.scrollIntoView({ from, to }, 100);
editor.focus();
}
function jumpToEndpointInYaml(path) {
const lineCount = editor.lineCount();
const target = `${path}:`;
for (let i = 0; i < lineCount; i++) {
const line = editor.getLine(i);
if (!line) continue;
if (line.trimStart().startsWith(target)) {
selectWholeLine(i);
return true;
}
}
return false;
}
async function parseAndRender() {
errorEl.textContent = '';
metaEl.textContent = '';
const yamlText = editor.getValue();
try {
const res = await fetch('/api/parse', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ yaml: yamlText })
});
const data = await res.json();
if (!res.ok) {
throw new Error(data && data.error ? data.error : 'Failed to parse');
}
const { graph, info } = data;
if (info && (info.title || info.version)) {
const title = info.title || 'Untitled';
const version = info.version ? ` v${info.version}` : '';
metaEl.textContent = `${title}${version}`;
}
const endpoints = graph.endpoints || [];
function onNodeClick(segmentId) {
const match = endpoints.find(ep => {
const parts = (ep.path || '').split('/').filter(Boolean);
return parts.includes(segmentId);
});
if (match) {
if (!jumpToEndpointInYaml(match.path)) {
// Fallback: best-effort contains check
const lineCount = editor.lineCount();
for (let i = 0; i < lineCount; i++) {
const line = editor.getLine(i) || '';
if (line.includes(match.path)) {
selectWholeLine(i);
break;
}
}
}
}
}
renderGraph('#graph', graph, { onNodeClick });
} catch (e) {
errorEl.textContent = e.message || String(e);
}
}
renderBtn.addEventListener('click', parseAndRender);
// initial render
parseAndRender();
})();