#!/usr/bin/env node
/*
* Balinyaar product-docs generator (zero dependency).
* cd product && node build-docs.mjs
* Walks every .md file, renders a brand-styled, cross-linked .html beside it,
* builds the sidebar from NAV below, and rewrites internal `…/foo.md` links to `…/foo.html`.
* Markdown is the source of truth — never hand-edit the generated .html.
*/
import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join, relative, posix } from 'node:path';
const ROOT = dirname(fileURLToPath(import.meta.url));
const SEP = String.fromCharCode(0); // collision-proof placeholder delimiter for code spans
/* ---- Sidebar manifest. Add/rename .md files here so they show in the nav. ---- */
const NAV = [
{ label: 'Start here', items: [
{ path: 'index.md', title: 'Docs home' },
{ path: 'overview/platform-summary.md', title: 'Platform summary & ground truths' },
]},
{ label: 'Business requirements', items: [
{ path: 'business/index.md', title: 'Overview & MVP scope' },
{ path: 'business/01-actors-and-onboarding.md', title: '1. Actors & onboarding' },
{ path: 'business/02-nurse-verification.md', title: '2. Nurse verification' },
{ path: 'business/03-service-catalog-and-pricing.md', title: '3. Service catalog & pricing' },
{ path: 'business/04-search-and-matching.md', title: '4. Search & matching' },
{ path: 'business/05-booking-and-scheduling.md', title: '5. Booking & scheduling' },
{ path: 'business/06-evv-and-service-delivery.md', title: '6. EVV / service delivery' },
{ path: 'business/07-cancellation-and-refunds.md', title: '7. Cancellation & refunds' },
{ path: 'business/08-payments-and-escrow.md', title: '8. Payments & escrow' },
{ path: 'business/09-installments-bnpl.md', title: '9. Installments / BNPL' },
{ path: 'business/10-payouts.md', title: '10. Payouts to nurses' },
{ path: 'business/11-reviews-trust-and-safety.md', title: '11. Reviews, trust & safety' },
{ path: 'business/12-messaging-and-emergencies.md', title: '12. Messaging & emergencies' },
{ path: 'business/13-tax-invoicing-and-legal.md', title: '13. Tax, invoicing & legal' },
{ path: 'business/14-notifications-and-admin.md', title: '14. Notifications & admin' },
]},
{ label: 'Database model', items: [
{ path: 'data-model/index.md', title: 'Overview & decisions' },
{ path: 'data-model/diagrams.md', title: 'Diagrams' },
{ path: 'data-model/01-identity-and-access.md', title: '1. Identity & access' },
{ path: 'data-model/02-geography.md', title: '2. Geography' },
{ path: 'data-model/03-services-and-pricing.md', title: '3. Services & pricing' },
{ path: 'data-model/04-verification-and-credentials.md', title: '4. Verification & credentials' },
{ path: 'data-model/05-booking-and-scheduling.md', title: '5. Booking & scheduling' },
{ path: 'data-model/06-payments-ledger-and-refunds.md', title: '6. Payments, ledger & refunds' },
{ path: 'data-model/07-payouts.md', title: '7. Payouts' },
{ path: 'data-model/08-bnpl.md', title: '8. BNPL / installments' },
{ path: 'data-model/09-messaging.md', title: '9. Messaging' },
{ path: 'data-model/10-reviews-and-records.md', title: '10. Reviews & records' },
{ path: 'data-model/11-notifications.md', title: '11. Notifications' },
{ path: 'data-model/12-audit-config-and-reference.md', title: '12. Audit, config & reference' },
{ path: 'data-model/13-partner-centers-and-future.md', title: '13. Partner centers & future' },
]},
{ label: 'Payments deep-dive', items: [
{ path: 'payments/index.md', title: 'Overview & exec summary' },
{ path: 'payments/iranian-payment-reality.md', title: 'Iranian payment reality' },
{ path: 'payments/escrow-ledger.md', title: 'Escrow as a ledger' },
{ path: 'payments/bnpl-landscape.md', title: 'BNPL landscape & finding' },
{ path: 'payments/cancellation-and-payout.md', title: 'Cancellation & nurse payout' },
{ path: 'payments/integration-notes.md', title: 'Integration & schema touchpoints' },
{ path: 'payments/sources.md', title: 'Recommendations & sources' },
]},
{ label: 'Research & strategy', items: [
{ path: 'research/index.md', title: 'Overview & exec summary' },
{ path: 'research/market-and-competitors.md', title: 'Market & competitors' },
{ path: 'research/problems-and-risks.md', title: 'Problems & risks' },
{ path: 'research/verification.md', title: 'Verification (research)' },
{ path: 'research/legal-landscape.md', title: 'Legal landscape' },
{ path: 'research/go-to-market.md', title: 'Go-to-market & sources' },
]},
{ label: 'Notes & more', items: [
{ path: 'notes/open-questions.md', title: 'Open questions' },
{ path: 'notes/future-ideas.md', title: 'Future ideas' },
{ path: 'wireframes/index.html', title: 'Wireframes' },
{ path: 'fa/index.html', title: 'Farsi documents' },
]},
];
/* ---------------------------- tiny markdown renderer ---------------------------- */
const esc = (s) => s.replace(/&/g, '&').replace(//g, '>');
const escAttr = (s) => esc(s).replace(/"/g, '"');
function slug(text) {
return text.toLowerCase().trim()
.replace(/[`*~]/g, '')
.replace(/[^\w-ۿ\s-]/g, '')
.replace(/\s+/g, '-').replace(/-+/g, '-');
}
function rewriteHref(url) {
if (/^(https?:|mailto:|#|\/\/)/.test(url)) return url;
return url.replace(/\.md(#.*)?$/i, '.html$1');
}
// Inline: protect code spans with NUL-delimited placeholders, escape, then links/emphasis/strike.
function inline(text) {
const codes = [];
text = text.replace(/`([^`]+)`/g, (_, c) => { codes.push(c); return SEP + (codes.length - 1) + SEP; });
text = esc(text);
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) =>
'' + label + '');
text = text.replace(/\*\*([^*]+?)\*\*/g, '$1');
text = text.replace(/(^|[^*])\*([^*\n]+?)\*(?!\*)/g, '$1$2');
text = text.replace(/~~([^~]+?)~~/g, '$1');
const restore = new RegExp(SEP + '(\\d+)' + SEP, 'g');
text = text.replace(restore, (_, i) => '' + esc(codes[+i]) + '');
return text;
}
function renderList(lines, i, indent) {
const reItem = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/;
let html = '';
let ordered = null;
while (i < lines.length) {
const m = lines[i].match(reItem);
if (!m) break;
const ind = m[1].length;
if (ind < indent) break;
if (ind > indent) break;
const thisOrdered = /\d/.test(m[2]);
if (ordered === null) ordered = thisOrdered;
let itemHtml = inline(m[3]);
i++;
if (i < lines.length) {
const mn = lines[i].match(reItem);
if (mn && mn[1].length > indent) {
const [sub, ni] = renderList(lines, i, mn[1].length);
itemHtml += sub;
i = ni;
}
}
html += '
' + body + '\n' : '
' + body + '\n';
continue;
}
const h = line.match(/^(#{1,6})\s+(.*)$/);
if (h) {
const lvl = h[1].length;
const txt = inline(h[2].replace(/\s+#+\s*$/, ''));
const id = slug(h[2]);
const anchor = lvl >= 2 && lvl <= 3 ? ' #' : '';
out += '' + mdToHtml(buf.join('\n')) + '\n'; continue; } if (reItem.test(line)) { const ind = line.match(reItem)[1].length; const [html, ni] = renderList(lines, i, ind); out += html; i = ni; continue; } const para = []; while (i < lines.length && lines[i].trim() !== '' && !/^(#{1,6}\s|```|\s*>|\s*(-{3,}|\*{3,}|_{3,})\s*$)/.test(lines[i]) && !reItem.test(lines[i]) && !(lines[i].includes('|') && i + 1 < lines.length && /^\s*\|?\s*:?-{2,}/.test(lines[i + 1]))) { para.push(lines[i]); i++; } if (para.length) out += '
' + inline(para.join(' ')) + '
\n'; } return out; } /* ---------------------------- page template ---------------------------- */ function relHref(fromPath, toPath) { const rel = posix.relative(posix.dirname(fromPath), toPath); return rel || posix.basename(toPath); } function firstH1(md) { const m = md.match(/^#\s+(.+)$/m); return m ? m[1].replace(/[`*~]/g, '').trim() : 'Balinyaar'; } function sidebar(currentMdPath) { const currentHtml = currentMdPath.replace(/\.md$/, '.html'); let html = ''; for (const group of NAV) { html += '