Generator Specyfikacji Markdown

Twórz przejrzyste dokumentacje techniczne w kilka minut!
Potrzebujesz szybko przygotować specyfikację projektu, dokument API albo opis funkcjonalny aplikacji? Ten generator zrobi to za Ciebie — szybko, prosto i w czytelnym formacie Markdown.
Jak to działa?
Aplikacja prowadzi Cię krok po kroku przez najważniejsze elementy specyfikacji:
-
Wpisujesz nazwę projektu Krótki tytuł, który pojawi się jako główny nagłówek.
-
Określasz wymagania funkcjonalne Wypisz, co system ma robić — punkt po punkcie.
-
Dodajesz endpointy API Podaj metodę, ścieżkę, opis, parametry i odpowiedzi — aplikacja wygeneruje czytelny opis REST API.
-
Definiujesz modele danych Opisz struktury obiektów lub JSON — idealne do dokumentacji backendu.
-
Wpisujesz przypadki brzegowe i błędy Dzięki temu specyfikacja uwzględni sytuacje wyjątkowe i walidacje.
- Dodajesz kryteria akceptacji To świetna sekcja dla QA, Product Ownerów i programistów.
Na końcu jednym kliknięciem generujesz kompletny plik Markdown, gotowy do użycia w repozytorium, w komunikacji z zespołem lub jako część dokumentacji projektowej.
Dlaczego warto korzystać?
- Oszczędność czasu — zamiast budować dokument od zera, po prostu uzupełniasz pola.
- Spójność — każda specyfikacja ma uporządkowaną strukturę.
- Czytelność — Markdown wygląda świetnie w GitHubie, GitLabie, Notion czy VS Code.
- Porządek w projekcie — ułatwia komunikację między programistami, testerami i klientem.
- Elastyczność — możesz wygenerować minimalną specyfikację lub bardzo rozbudowaną.
Jak korzystać?
Dokumentacja techniczna jest kluczowym elementem każdego projektu — niezależnie od tego, czy tworzysz API, aplikację webową, mikroserwis, czy moduł wewnętrzny. Niestety przygotowanie spójnej, czytelnej i poprawnie sformatowanej specyfikacji wymaga czasu. Generator Specyfikacji Markdown (GSM) automatyzuje ten proces, prowadząc użytkownika przez wszystkie istotne sekcje i generując gotowy dokument w standardzie Markdown (.md).
Ten rozdział wyjaśni:
- jak działa aplikacja,
- jak wypełniać poszczególne sekcje,
- co otrzymasz w wyniku,
- oraz jak najlepiej wykorzystać wygenerowaną specyfikację.
1. Architektura działania aplikacji
Generator działa w całości po stronie przeglądarki (HTML + Bootstrap 5.3 + JavaScript). Oznacza to:
- brak backendu,
- brak wysyłania danych na serwer,
- pełna prywatność — cała specyfikacja generowana jest lokalnie.
Logika aplikacji polega na:
- Pobieraniu danych z formularza.
- Dynamicznym dodawaniu i usuwaniu sekcji (endpointów, modeli, warunków).
- Łączeniu tych danych w spójną strukturę Markdown.
- Renderowaniu podglądu w czasie rzeczywistym.
- Generowaniu finalnego tekstu, który można skopiować lub zapisać.
2. Struktura specyfikacji generowanej przez aplikację
Końcowy dokument Markdown składa się z uporządkowanych sekcji:
- Tytuł projektu
- Wymagania funkcjonalne
- Endpoints REST API
- Modele danych
- Przypadki brzegowe i błędy
- Kryteria akceptacji
- (Opcjonalnie) sekcja stworzona samodzielnie
Każda z nich ma swoją funkcję i jest tworzona automatycznie na podstawie pól formularza.
3. Omówienie sekcji formularza
3.1. Nazwa projektu
Najprostszy element — tytuł dokumentu. Powinien jasno określać, czego specyfikacja dotyczy, np.:
User Management Service
Shopping Cart API
Internal Order Processing Module
3.2. Wymagania funkcjonalne
To opis tego co system ma robić, bez wchodzenia w szczegóły implementacyjne.
Przykłady:
- „Użytkownik może utworzyć konto za pomocą adresu e-mail.”
- „Aplikacja waliduje wszystkie żądania przychodzące.”
- „System zapisuje logi wszystkich operacji administracyjnych.”
W aplikacji każde wymaganie to osobna linia listy Markdown.
3.3. Endpoints API
To jedna z najważniejszych sekcji, szczególnie w projektach backendowych.
Dla każdego endpointu uzupełniasz:
- Metodę HTTP (GET/POST/PUT/DELETE itd.)
- Ścieżkę (np.
/users/{id}) - Opis działania
- Parametry wejściowe (opcjonalne)
- Przykładowy payload (opcjonalny, JSON)
- Możliwe odpowiedzi z kodami HTTP i opisami
Przykład generowanego fragmentu:
### POST /users
**Opis:** Tworzy nowego użytkownika.
**Payload:**
{
"email": "string",
"password": "string"
}
Odpowiedzi:
201 Created– użytkownik utworzony400 Bad Request– błędne dane wejściowe
3.4. Modele danych
Służą do opisania struktur obiektów używanych przez API, mikroserwis lub moduł logiki.
Zazwyczaj zawierają:
- nazwę modelu,
- pola i typy,
- zależności między encjami (opcjonalnie),
- przykładowy JSON.
Fragment wygenerowany:
### User
{
"id": "number",
"email": "string",
"createdAt": "string (ISO date)"
}
3.5. Przypadki brzegowe i błędy
To kluczowa sekcja dla testerów i QA.
Wpisujesz tu:
- błędy walidacji,
- sytuacje wyjątkowe,
- specyficzne reguły biznesowe.
Np:
- „Wysłanie żądania bez nagłówka
Authorizationzwraca401 Unauthorized.” - „Dla pola
agewartość < 0 zwraca400 Bad Request.”
3.6. Kryteria akceptacji
Są to mierzalne warunki, które muszą zostać spełnione, by funkcjonalność uznać za gotową.
Np:
- „API zwraca odpowiedź w < 500 ms.”
- „Endpoint
/loginobsługuje 100 równoczesnych zapytań.”
4. Generowanie specyfikacji
Po wypełnieniu formularza kliknij „Generuj Markdown”.
Aplikacja tworzy finalny dokument na podstawie wpisanych danych.
Możesz:
- skopiować całość jednym kliknięciem,
- wkleić do IDE lub repozytorium,
- zapisać jako
.md, - użyć w dokumentacji API, w projektach SCRUM, lub podczas planowania sprintów.
5. Wskazówki techniczne i dobre praktyki
Używaj przykładów JSON tam, gdzie to możliwe
Ułatwia to backendowi i frontendowi zrozumienie kontraktu.
Unikaj ogólnych opisów
Zamiast:
„System waliduje dane.”
napisz:
„Pole
Jeden endpoint = jeden cel
Dobra specyfikacja zmniejsza ryzyko błędów implementacyjnych.
Traktuj generator jako szablon
Możesz go używać dla:
- API REST
- dokumentacji mikroserwisów
- usług wewnętrznych
- analiz wymagań
- front-endowych kontraktów z backendem
Dla kogo jest ta aplikacja?
- programistów
- analityków
- product ownerów
- QA i testerów
- studentów informatyki
- zespołów tworzących API lub mikroserwisy
Generator Specyfikacji Markdown to narzędzie, które znacząco przyspiesza i ułatwia tworzenie dokumentacji technicznej.
Nie musisz znać składni Markdown — aplikacja generuje wszystko za Ciebie na podstawie przejrzystego formularza.
Dzięki temu:
- dokumentacja jest kompletna,
- spójna,
- łatwa do używania w projektach programistycznych,
- i gotowa do wykorzystania w dowolnym repozytorium.
Generator Specyfikacji Markdown — REST API
Wypełnij pola → kliknij Generuj Markdown. Możesz kopiować lub pobrać plik .md.
Informacje ogólne
Wymagania funkcjonalne
Endpoints (API)
Dodane endpointy
Modele danych
1. Wpisz nazwę pola oraz wybierz jego typ.
2. (Opcjonalnie) dodaj opis i oznacz jako wymagane.
3. Kliknij „Dodaj pole” — pojawi się ono na liście roboczej.
4. Powtarzaj aż dodasz wszystkie potrzebne pola.
5. Na końcu kliknij „Utwórz model”, aby zapisać całość.
Błędy i przypadki brzegowe
Kryteria akceptacji
Dodatkowe informacje
Podgląd Markdown
Kliknij "Generuj Markdown" aby zobaczyć wynik...
Kod po stronie przeglądarki
<link type="text/css" href="http://www.dariuszrorat.ugu.pl/assets/css/bootstrap/wcag-outline.min.css" rel="stylesheet">
<div id="app">
<h2 class="mb-3">Generator Specyfikacji Markdown — REST API</h2>
<p class="small text-muted">Wypełnij pola → kliknij <strong>Generuj Markdown</strong>. Możesz kopiować lub pobrać plik .md.</p>
<!-- Project Info -->
<div class="card mb-3">
<div class="card-body">
<h3 class="h5 card-title">Informacje ogólne</h3>
<div class="row g-3">
<div class="col-md-6">
<label for="proj-name" class="form-label">Nazwa projektu / komponentu</label>
<input id="proj-name" class="form-control" placeholder="Task Manager API" />
</div>
<div class="col-md-6">
<label for="proj-desc" class="form-label">Krótki opis</label>
<input id="proj-desc" class="form-control" placeholder="API do zarządzania zadaniami" />
</div>
<div class="col-12">
<label for="proj-goal" class="form-label">Cel projektu (why?)</label>
<textarea id="proj-goal" class="form-control" rows="2" placeholder="Dlaczego ten projekt powstaje?"></textarea>
</div>
<div class="col-md-6">
<label for="proj-scope" class="form-label">Zakres (scope)</label>
<textarea id="proj-scope" class="form-control" rows="2" placeholder="Co będzie w MVP?"></textarea>
</div>
<div class="col-md-6">
<label for="proj-out" class="form-label">Poza zakresem (out of scope)</label>
<textarea id="proj-out" class="form-control" rows="2" placeholder="Czego nie obejmuje MVP?"></textarea>
</div>
</div>
</div>
</div>
<!-- Functional Requirements -->
<div class="card mb-3">
<div class="card-body">
<h3 class="h5 card-title d-flex justify-content-between align-items-center">
Wymagania funkcjonalne
<button id="add-req-btn" class="btn btn-sm btn-primary">Dodaj wymaganie</button>
</h3>
<div class="mb-2">
<input id="req-input" class="form-control" placeholder="Np. Użytkownik może tworzyć zadania..." aria-label="Opis wymagania"/>
</div>
<ul id="req-list" class="list-group"></ul>
</div>
</div>
<!-- API Endpoints -->
<div class="card mb-3">
<div class="card-body">
<h3 class="h5 card-title">Endpoints (API)</h3>
<div class="mb-3">
<div class="row g-2">
<div class="col-md-2">
<label for="ep-method" class="form-label">Metoda</label>
<select id="ep-method" class="form-select">
<option>GET</option><option>POST</option><option>PUT</option><option>PATCH</option><option>DELETE</option>
</select>
</div>
<div class="col-md-4">
<label for="ep-path" class="form-label">Ścieżka</label>
<input id="ep-path" class="form-control" placeholder="/tasks or /tasks/{id}" />
</div>
<div class="col-md-6">
<label for="ep-desc" class="form-label">Krótki opis</label>
<input id="ep-desc" class="form-control" placeholder="Zwraca listę zadań" />
</div>
</div>
</div>
<div class="mb-2">
<div class="row g-2">
<div class="col-md-6">
<label for="ep-query" class="form-label">Query params (JSON array) — opcjonalnie</label>
<textarea id="ep-query" class="form-control json" placeholder='[{"name":"status","type":"string","desc":"todo|in-progress|done","required":false}]'></textarea>
</div>
<div class="col-md-6">
<label for="ep-pathparams" class="form-label">Path params (JSON array) — opcjonalnie</label>
<textarea id="ep-pathparams" class="form-control json" placeholder='[{"name":"id","type":"uuid","desc":"Task ID","required":true}]'></textarea>
</div>
</div>
</div>
<div class="mb-2">
<div class="row g-2">
<div class="col-md-6">
<label for="ep-request" class="form-label">Request body (JSON) — opcjonalnie</label>
<textarea id="ep-request" class="form-control json" placeholder='{ "title": "string", "description": "string" }'></textarea>
</div>
<div class="col-md-6">
<label for="ep-response" class="form-label">Response 200 (JSON)</label>
<textarea id="ep-response" class="form-control json" placeholder='{ "items": [ { "id": "uuid", "title": "string" } ], "count": 1 }'></textarea>
</div>
</div>
</div>
<div class="mb-3">
<label for="ep-other" class="form-label">Inne kody odpowiedzi (newline-separated, np. "400: Bad Request")</label>
<textarea id="ep-other" class="form-control" rows="2" placeholder="400: Bad Request\n404: Not Found"></textarea>
</div>
<div class="d-flex gap-2">
<button id="ep-add-btn" class="btn btn-success">Dodaj endpoint</button>
<button id="ep-clear-btn" class="btn btn-outline-secondary">Wyczyść pola</button>
</div>
<hr />
<h4 class="h6">Dodane endpointy</h4>
<ul id="endpoints-list" class="list-group"></ul>
</div>
</div>
<!-- Data Models -->
<div class="card mb-3">
<div class="card-body">
<h3 class="h5 card-title">Modele danych</h3>
<div class="alert alert-secondary small mt-3">
<strong>Jak tworzyć model?</strong><br>
1. Wpisz nazwę pola oraz wybierz jego typ.<br>
2. (Opcjonalnie) dodaj opis i oznacz jako wymagane.<br>
3. Kliknij <em>„Dodaj pole”</em> — pojawi się ono na liście roboczej.<br>
4. Powtarzaj aż dodasz wszystkie potrzebne pola.<br>
5. Na końcu kliknij <strong>„Utwórz model”</strong>, aby zapisać całość.
</div>
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label for="model-name" class="form-label">Nazwa modelu</label>
<input id="model-name" class="form-control" placeholder="Task" />
</div>
<div class="col-md-3">
<label for="model-field-name" class="form-label">Pole (name)</label>
<input id="model-field-name" class="form-control" placeholder="title" />
</div>
<div class="col-md-2">
<label for="model-field-type" class="form-label">Typ</label>
<select id="model-field-type" class="form-select">
<option>string</option><option>integer</option><option>boolean</option><option>uuid</option><option>array[string]</option><option>object</option>
</select>
</div>
<div class="col-md-2">
<label for="model-field-required" class="form-label">Wymagane?</label>
<div><input id="model-field-required" type="checkbox" /></div>
</div>
<div class="col-md-1">
<button id="model-add-field" class="btn btn-primary">Dodaj</button>
</div>
<div class="col-12 mt-2">
<label for="model-field-desc" class="form-label">Opis pola (opcjonalny)</label>
<input id="model-field-desc" class="form-control" placeholder="Krótki opis pola" />
</div>
</div>
<div class="mt-3 d-flex gap-2">
<button id="model-create" class="btn btn-success">Utwórz model</button>
<button id="model-clear" class="btn btn-outline-secondary">Wyczyść pola</button>
</div>
<hr />
<div id="models-container"></div>
</div>
</div>
<!-- Errors / Edge cases -->
<div class="card mb-3">
<div class="card-body">
<h3 class="h5 card-title">Błędy i przypadki brzegowe</h3>
<div class="row g-2">
<div class="col-md-3">
<label for="err-code" class="form-label">Kod błędu</label>
<input id="err-code" class="form-control" placeholder="VALIDATION_ERROR" />
</div>
<div class="col-md-2">
<label for="err-status" class="form-label">HTTP Status</label>
<input id="err-status" class="form-control" placeholder="400" />
</div>
<div class="col-md-7">
<label for="err-msg" class="form-label">Opis</label>
<input id="err-msg" class="form-control" placeholder="Title is too short" />
</div>
</div>
<div class="mt-2">
<label for="err-example" class="form-label">Przykładowy JSON odpowiedzi</label>
<textarea id="err-example" class="form-control json" placeholder='{ "error": { "code":"VALIDATION_ERROR", "message":"Title is too short" } }'></textarea>
</div>
<div class="mt-2">
<button id="err-add" class="btn btn-primary">Dodaj przypadek</button>
</div>
<hr />
<ul id="errors-list" class="list-group"></ul>
</div>
</div>
<!-- Acceptance -->
<div class="card mb-3">
<div class="card-body">
<h3 class="h5 card-title d-flex justify-content-between align-items-center">
Kryteria akceptacji
<button id="accept-add-btn" class="btn btn-sm btn-primary">Dodaj punkt</button>
</h3>
<div class="mb-2">
<input id="accept-input" class="form-control" placeholder="Np. Endpoint POST /tasks zwraca 201" aria-label="Kryterium akceptacji" />
</div>
<ul id="accept-list" class="list-group"></ul>
</div>
</div>
<!-- Additional notes -->
<div class="card mb-3">
<div class="card-body">
<h3 class="h5 card-title">Dodatkowe informacje</h3>
<div class="mb-2">
<label for="proj-notes" class="form-label">Założenia, ograniczenia, technologie</label>
<textarea id="proj-notes" class="form-control" rows="3" placeholder="Np. DB: PostgreSQL, ISO timestamps"></textarea>
</div>
</div>
</div>
<!-- Generate / Preview -->
<div class="card mb-3">
<div class="card-body">
<div class="d-flex gap-2 mb-3">
<button id="generate-btn" class="btn btn-lg btn-primary">Generuj Markdown</button>
<button id="download-btn" class="btn btn-lg btn-success" disabled>Pobierz .md</button>
<button id="clear-all-btn" class="btn btn-lg btn-danger ms-auto">Wyczyść wszystko</button>
</div>
<h4 class="h6">Podgląd Markdown</h4>
<ul class="nav nav-tabs mt-3">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="tab-mdview-tab" data-bs-toggle="tab" data-bs-target="#tab-mdview" type="button" role="tab">Markdown</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-htmlview-tab" data-bs-toggle="tab" data-bs-target="#tab-htmlview" type="button" role="tab">HTML</button>
</li>
</ul>
<div class="tab-content">
<div id="tab-mdview" class="tab-pane fade show active" role="tabpanel">
<pre><code id="md-preview" class="language-markdown">Kliknij "Generuj Markdown" aby zobaczyć wynik...</code></pre>
</div>
<div id="tab-htmlview" class="tab-pane fade" role="tabpanel">
<div id="html-preview" class="p-3"></div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
// prosty helper
function id(i) {
return document.getElementById(i);
}
// requirements
id('add-req-btn').addEventListener('click', function() {
addReq();
});
id('req-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter') addReq();
});
function addReq() {
var v = id('req-input').value.trim();
if (!v) return;
var li = document.createElement('li');
li.className = 'list-group-item d-flex justify-content-between align-items-start';
var left = document.createElement('div');
left.textContent = v;
var btnWrap = document.createElement('div');
var btn = document.createElement('button');
btn.className = 'btn btn-sm btn-outline-danger';
btn.textContent = 'Usuń';
btn.onclick = function() {
li.remove();
};
btnWrap.appendChild(btn);
li.appendChild(left);
li.appendChild(btnWrap);
id('req-list').appendChild(li);
id('req-input').value = '';
}
// endpoints storage
var endpoints = [];
function renderEndpoints() {
var ul = id('endpoints-list');
ul.innerHTML = '';
endpoints.forEach(function(ep, idx) {
var li = document.createElement('li');
li.className = 'list-group-item';
var top = document.createElement('div');
top.className = 'd-flex w-100 justify-content-between align-items-start';
var left = document.createElement('div');
var badge = document.createElement('span');
badge.className = 'badge bg-secondary badge-method me-2';
badge.textContent = ep.method;
var strong = document.createElement('strong');
strong.textContent = ep.path;
var small = document.createElement('div');
small.className = 'small text-muted';
small.textContent = ep.desc || '';
left.appendChild(badge);
left.appendChild(strong);
left.appendChild(small);
var right = document.createElement('div');
var del = document.createElement('button');
del.className = 'btn btn-sm btn-outline-danger';
del.textContent = 'Usuń';
del.onclick = function() {
endpoints.splice(idx, 1);
renderEndpoints();
};
right.appendChild(del);
top.appendChild(left);
top.appendChild(right);
li.appendChild(top);
ul.appendChild(li);
});
}
id('ep-add-btn').addEventListener('click', function() {
var method = id('ep-method').value;
var path = id('ep-path').value.trim();
if (!path) {
alert('Podaj ścieżkę endpointu');
return;
}
var desc = id('ep-desc').value.trim();
var query = parseJSONSafe(id('ep-query').value);
var pathparams = parseJSONSafe(id('ep-pathparams').value);
var request = id('ep-request').value.trim();
var response = id('ep-response').value.trim();
var other = id('ep-other').value.trim();
endpoints.push({
method: method,
path: path,
desc: desc,
query: query,
pathparams: pathparams,
request: request,
response: response,
other: other
});
renderEndpoints();
clearEndpointInputs();
});
id('ep-clear-btn').addEventListener('click', clearEndpointInputs);
function clearEndpointInputs() {
id('ep-path').value = '';
id('ep-desc').value = '';
id('ep-query').value = '';
id('ep-pathparams').value = '';
id('ep-request').value = '';
id('ep-response').value = '';
id('ep-other').value = '';
}
// MODELS – poprawiona obsługa pól
var tempFields = [];
function renderTempFields() {
var container = document.querySelector('.model-fields-temp');
if (!container) return;
container.innerHTML = '';
tempFields.forEach(function(f, i) {
var row = document.createElement('div');
row.className = 'd-flex gap-2 align-items-center mb-2 p-2 border rounded';
var left = document.createElement('div');
left.className = 'flex-grow-1';
var strong = document.createElement('strong');
strong.textContent = f.name;
var span = document.createElement('span');
span.className = 'text-muted';
span.textContent = ' (' + f.type + ')';
left.appendChild(strong);
left.appendChild(span);
if (f.required) {
var badge = document.createElement('span');
badge.className = 'badge bg-info text-dark ms-1';
badge.textContent = 'required';
left.appendChild(badge);
}
if (f.desc) {
var small = document.createElement('div');
small.className = 'small text-muted';
small.textContent = f.desc;
left.appendChild(small);
}
var delBtn = document.createElement('button');
delBtn.className = 'btn btn-sm btn-outline-danger';
delBtn.textContent = 'Usuń';
// poprawka: bezpieczne usuwanie po indeksie
delBtn.addEventListener('click', function() {
tempFields.splice(i, 1);
renderTempFields();
});
row.appendChild(left);
row.appendChild(delBtn);
container.appendChild(row);
});
}
id('model-add-field').addEventListener('click', function() {
var name = id('model-field-name').value.trim();
if (!name) {
alert('Podaj nazwę pola');
return;
}
var field = {
name: name,
type: id('model-field-type').value,
desc: id('model-field-desc').value.trim(),
required: id('model-field-required').checked
};
tempFields.push(field);
id('model-field-name').value = '';
id('model-field-desc').value = '';
id('model-field-required').checked = false;
renderTempFields();
});
id('model-create').addEventListener('click', function() {
var modelName = id('model-name').value.trim();
if (!modelName) {
alert('Podaj nazwę modelu');
return;
}
var container = id('models-container');
var card = document.createElement('div');
card.className = 'card mb-2';
var cardBody = document.createElement('div');
cardBody.className = 'card-body';
var header = document.createElement('div');
header.className = 'd-flex justify-content-between align-items-center mb-2';
var h = document.createElement('h4');
h.className = 'h6 m-0';
h.textContent = modelName;
var delModelBtn = document.createElement('button');
delModelBtn.className = 'btn btn-sm btn-outline-danger';
delModelBtn.textContent = 'Usuń model';
delModelBtn.onclick = function() {
card.remove();
};
header.appendChild(h);
header.appendChild(delModelBtn);
cardBody.appendChild(header);
// tabela
var table = document.createElement('table');
table.className = 'table table-sm';
var thead = document.createElement('thead');
thead.innerHTML = '<tr><th>Field</th><th>Type</th><th>Required</th><th>Description</th></tr>';
var tbody = document.createElement('tbody');
tempFields.forEach(function(f) {
var tr = document.createElement('tr');
tr.innerHTML =
'<td>' + escapeHtml(f.name) + '</td>' +
'<td>' + escapeHtml(f.type) + '</td>' +
'<td>' + (f.required ? 'yes' : 'no') + '</td>' +
'<td>' + escapeHtml(f.desc || '') + '</td>';
tbody.appendChild(tr);
});
table.appendChild(thead);
table.appendChild(tbody);
cardBody.appendChild(table);
card.appendChild(cardBody);
card.dataset.model = JSON.stringify({
name: modelName,
fields: tempFields
});
container.appendChild(card);
// reset
tempFields = [];
renderTempFields();
id('model-name').value = '';
});
id('model-clear').addEventListener('click', function() {
tempFields = [];
renderTempFields();
});
// UTWORZENIE TEMP BOXA (poprawione)
(function makeTempBox() {
var container = id('models-container');
var box = document.createElement('div');
box.className = 'temp-box mb-3';
box.innerHTML =
'<div class="card">' +
'<div class="card-body">' +
'<h4 class="h6 mb-1">Aktualny model — pola robocze</h4>' +
'<p class="small text-muted mb-2">Dodawaj pola jedno po drugim. Lista poniżej pokazuje aktualnie dodane pola, które zostaną zapisane dopiero po kliknięciu „Utwórz model”.</p>' +
'<div class="model-fields-temp"></div>' +
'</div>' +
'</div>';
// 🔥 wstawiamy DO środka, na początek
container.prepend(box);
renderTempFields();
})();
// errors
id('err-add').addEventListener('click', function() {
var code = id('err-code').value.trim();
var status = id('err-status').value.trim();
var msg = id('err-msg').value.trim();
var example = id('err-example').value.trim();
if (!code || !status || !msg) {
alert('Uzupełnij kod, status, opis błędu');
return;
}
var ul = id('errors-list');
var li = document.createElement('li');
li.className = 'list-group-item';
var header = document.createElement('div');
header.className = 'd-flex justify-content-between';
var left = document.createElement('div');
var strong = document.createElement('strong');
strong.textContent = code;
var statusSpan = document.createElement('span');
statusSpan.className = 'text-muted';
statusSpan.textContent = ' HTTP ' + status;
var small = document.createElement('div');
small.className = 'small text-muted';
small.textContent = msg;
left.appendChild(strong);
left.appendChild(statusSpan);
left.appendChild(small);
var right = document.createElement('div');
var del = document.createElement('button');
del.className = 'btn btn-sm btn-outline-danger';
del.textContent = 'Usuń';
del.onclick = function() {
li.remove();
};
right.appendChild(del);
header.appendChild(left);
header.appendChild(right);
li.appendChild(header);
if (example) {
var pre = document.createElement('pre');
pre.className = 'mt-2';
pre.textContent = example;
li.appendChild(pre);
}
ul.appendChild(li);
id('err-code').value = '';
id('err-status').value = '';
id('err-msg').value = '';
id('err-example').value = '';
});
// acceptance
id('accept-add-btn').addEventListener('click', addAccept);
id('accept-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter') addAccept();
});
function addAccept() {
var v = id('accept-input').value.trim();
if (!v) return;
var li = document.createElement('li');
li.className = 'list-group-item d-flex justify-content-between align-items-center';
var left = document.createElement('div');
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'form-check-input me-2';
left.appendChild(cb);
left.appendChild(document.createTextNode(v));
var del = document.createElement('button');
del.className = 'btn btn-sm btn-outline-danger';
del.textContent = 'Usuń';
del.onclick = function() {
li.remove();
};
li.appendChild(left);
li.appendChild(del);
id('accept-list').appendChild(li);
id('accept-input').value = '';
}
// generate markdown
id('generate-btn').addEventListener('click', function() {
var md = buildMarkdown();
id('md-preview').textContent = md;
id('html-preview').innerHTML = marked.parse(md);
const el = id('md-preview');
if (el) {
if (el.hasAttribute('data-highlighted'))
el.removeAttribute('data-highlighted');
if (el.hasAttribute('data-highlighter'))
el.removeAttribute('data-highlighter');
if (el.classList.contains('hljs'))
el.classList.remove(...Array.from(el.classList).filter(cls => cls.startsWith('hljs')));
el.innerHTML = escapeHtml(el.textContent);
hljs.highlightElement(el);
}
id('download-btn').disabled = false;
id('download-btn').onclick = function() {
download(md, (id('proj-name').value || 'spec').replace(/\s+/g, '_') + '.md');
};
});
id('clear-all-btn').addEventListener('click', function() {
if (!confirm('Czy na pewno wyczyścić wszystkie pola?')) return;
document.querySelectorAll('input, textarea').forEach(function(i) {
if (i.type !== 'checkbox') i.value = '';
else i.checked = false;
});
document.querySelectorAll('.list-group').forEach(function(ul) {
ul.innerHTML = '';
});
document.getElementById('models-container').innerHTML = '';
tempFields = [];
renderTempFields();
endpoints = [];
renderEndpoints();
id('md-preview').textContent = 'Kliknij "Generuj Markdown" aby zobaczyć wynik...';
id('download-btn').disabled = true;
});
// build markdown safely
function buildMarkdown() {
var lines = [];
function add(s) {
lines.push(s);
}
var name = id('proj-name').value.trim() || 'Projekt';
var desc = id('proj-desc').value.trim();
var goal = id('proj-goal').value.trim();
var scope = id('proj-scope').value.trim();
var out = id('proj-out').value.trim();
var notes = id('proj-notes').value.trim();
add('# ' + mdEscape(name));
if (desc) add('');
if (desc) add('**Opis:** ' + mdEscape(desc));
if (goal) {
add('');
add('## Cel projektu');
add(mdEscape(goal));
}
if (scope) {
add('');
add('## Zakres');
add(mdEscape(scope));
}
if (out) {
add('');
add('## Poza zakresem');
add(mdEscape(out));
}
// requirements
add('');
add('## Wymagania funkcjonalne');
var reqs = Array.prototype.slice.call(id('req-list').children).map(function(li) {
return li.firstChild.textContent.trim();
});
if (reqs.length === 0) add('- *(brak wpisanych wymagań)*');
else reqs.forEach(function(r) {
add('- ' + mdEscape(r));
});
// endpoints
add('');
add('## API / Endpoints');
if (endpoints.length === 0) add('*Brak zdefiniowanych endpointów*');
else {
endpoints.forEach(function(ep) {
add('');
add('### ' + ep.method + ' ' + ep.path);
if (ep.desc) add('**Opis:** ' + mdEscape(ep.desc));
if (ep.pathparams && Array.isArray(ep.pathparams) && ep.pathparams.length) {
add('');
add('**Path params:**');
ep.pathparams.forEach(function(p) {
add('- ' + mdEscape(JSON.stringify(p)));
});
}
if (ep.query && Array.isArray(ep.query) && ep.query.length) {
add('');
add('**Query params:**');
ep.query.forEach(function(q) {
add('- ' + mdEscape(JSON.stringify(q)));
});
}
if (ep.request) {
add('');
add('**Request body:**');
add('```json');
add(ep.request);
add('```');
}
if (ep.response) {
add('');
add('**Response 200:**');
add('```json');
add(ep.response);
add('```');
}
if (ep.other) {
add('');
add('**Inne kody odpowiedzi:**');
ep.other.split('\\n').filter(Boolean).forEach(function(o) {
add('- ' + mdEscape(o));
});
}
});
}
// models
add('');
add('## Modele danych');
var modelCards = document.querySelectorAll('#models-container .card');
console.log(modelCards);
if (modelCards.length === 1) add('*Brak zdefiniowanych modeli*');
else {
modelCards.forEach(function(card) {
try {
var m = JSON.parse(card.dataset.model);
add('');
add('### ' + mdEscape(m.name));
add('');
add('| Pole | Typ | Wymagane | Opis |');
add('|------|-----:|:--------:|------|');
m.fields.forEach(function(f) {
add('| ' + mdEscape(f.name) + ' | ' + mdEscape(f.type) + ' | ' + (f.required ? 'yes' : 'no') + ' | ' + mdEscape(f.desc || '') + ' |');
});
} catch (e) {}
});
}
// errors
add('');
add('## Przypadki brzegowe i błędy');
var errorLis = Array.prototype.slice.call(id('errors-list').children);
if (errorLis.length === 0) add('*Brak zdefiniowanych przypadków błędów*');
else {
add('Każdy błąd zwraca strukturę:');
add('```json');
add('{ "error": { "code": "string", "message": "string", "details": {} } }');
add('```');
add('');
errorLis.forEach(function(li) {
var code = li.querySelector('strong') ? li.querySelector('strong').textContent : '';
var status = li.querySelector('.text-muted') ? li.querySelector('.text-muted').textContent : '';
var msg = li.querySelector('.small') ? li.querySelector('.small').textContent : '';
add('### ' + mdEscape(code) + ' — ' + mdEscape(status));
if (msg) add(mdEscape(msg));
var pre = li.querySelector('pre');
if (pre) {
add('Przykład:');
add('```json');
add(pre.textContent);
add('```');
}
});
}
// acceptance
add('');
add('## Kryteria akceptacji');
var acc = Array.prototype.slice.call(id('accept-list').children).map(function(li) {
return li.firstChild.textContent.trim();
});
if (acc.length === 0) add('- *(brak punktów akceptacji)*');
else acc.forEach(function(a) {
add('- [ ] ' + mdEscape(a));
});
if (notes) {
add('');
add('## Dodatkowe informacje');
add(mdEscape(notes));
}
add('');
add('_Wygenerowano za pomocą Generatora Specyfikacji Markdown._');
return lines.join('\n');
}
// download helper
function download(text, filename) {
var blob = new Blob([text], {
type: 'text/markdown;charset=utf-8'
});
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename || 'spec.md';
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
// safe JSON parse
function parseJSONSafe(s) {
if (!s || !s.trim()) return [];
try {
return JSON.parse(s);
} catch (e) {
alert('Niepoprawny JSON: ' + e.message);
return [];
}
}
// simple escapes
function escapeHtml(s) {
if (!s) return '';
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
}
function mdEscape(s) {
if (s === null || s === undefined) return '';
return String(s)
.replace(/\r/g, '')
.replace(/\n/g, '\n') // <-- zamiast '\\n'
.replace(/\|/g, '\\|'); // <-- zostaje, bo chroni tabelki
}
</script>
Kod po stronie serwera
Brak kodu serwera
Ta aplikacja działa wyłącznie w przeglądarce i nie korzysta z kodu po stronie serwera.