feat: spec graph
This commit is contained in:
117
public/app.js
Normal file
117
public/app.js
Normal file
@@ -0,0 +1,117 @@
|
||||
(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();
|
||||
})();
|
||||
Reference in New Issue
Block a user