@mat3ra/esse 2026.9.6-0 → 2026.9.6-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -15,5 +15,22 @@ export declare function parseFrontMatter(source: string): {
|
|
|
15
15
|
body: string;
|
|
16
16
|
};
|
|
17
17
|
export declare function expandFragments(body: string, graph: EntityGraph): string;
|
|
18
|
+
/** A heading the in-page table of contents can link to. */
|
|
19
|
+
export interface DocsHeading {
|
|
20
|
+
id: string;
|
|
21
|
+
text: string;
|
|
22
|
+
level: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Gives every `h2`/`h3` an id and returns them in document order.
|
|
26
|
+
*
|
|
27
|
+
* `marked` emits bare heading tags, so without this there is nothing for a table of
|
|
28
|
+
* contents — or an external deep link — to point at. Ids are de-duplicated because two
|
|
29
|
+
* sections may legitimately share a title.
|
|
30
|
+
*/
|
|
31
|
+
export declare function extractHeadings(html: string): {
|
|
32
|
+
html: string;
|
|
33
|
+
headings: DocsHeading[];
|
|
34
|
+
};
|
|
18
35
|
export declare function buildDocsPages(outputDir: string): DocsPage[];
|
|
19
36
|
export {};
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.parseFrontMatter = parseFrontMatter;
|
|
7
7
|
exports.expandFragments = expandFragments;
|
|
8
|
+
exports.extractHeadings = extractHeadings;
|
|
8
9
|
exports.buildDocsPages = buildDocsPages;
|
|
9
10
|
/**
|
|
10
11
|
* Renders the concept documentation in `docs/` to static HTML for the site.
|
|
@@ -229,7 +230,65 @@ function expandFragments(body, graph) {
|
|
|
229
230
|
function escapeHtml(value) {
|
|
230
231
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
231
232
|
}
|
|
232
|
-
function
|
|
233
|
+
function slugifyHeading(text) {
|
|
234
|
+
return text
|
|
235
|
+
.toLowerCase()
|
|
236
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
237
|
+
.replace(/^-+|-+$/g, "");
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Gives every `h2`/`h3` an id and returns them in document order.
|
|
241
|
+
*
|
|
242
|
+
* `marked` emits bare heading tags, so without this there is nothing for a table of
|
|
243
|
+
* contents — or an external deep link — to point at. Ids are de-duplicated because two
|
|
244
|
+
* sections may legitimately share a title.
|
|
245
|
+
*/
|
|
246
|
+
function extractHeadings(html) {
|
|
247
|
+
const headings = [];
|
|
248
|
+
const seen = new Map();
|
|
249
|
+
const withIds = html.replace(/<h([23])>([\s\S]*?)<\/h\1>/g, (_match, level, inner) => {
|
|
250
|
+
var _a;
|
|
251
|
+
const text = inner.replace(/<[^>]+>/g, "").trim();
|
|
252
|
+
const base = slugifyHeading(text) || `section-${headings.length + 1}`;
|
|
253
|
+
const count = (_a = seen.get(base)) !== null && _a !== void 0 ? _a : 0;
|
|
254
|
+
seen.set(base, count + 1);
|
|
255
|
+
const id = count === 0 ? base : `${base}-${count}`;
|
|
256
|
+
headings.push({ id, text, level: Number(level) });
|
|
257
|
+
return `<h${level} id="${id}">${inner}</h${level}>`;
|
|
258
|
+
});
|
|
259
|
+
return { html: withIds, headings };
|
|
260
|
+
}
|
|
261
|
+
function renderTableOfContents(headings) {
|
|
262
|
+
// One heading is the page title's own section; a list of one is noise.
|
|
263
|
+
if (headings.length < 2)
|
|
264
|
+
return "";
|
|
265
|
+
const items = headings
|
|
266
|
+
.map((heading) => `<a href="#${heading.id}" class="toc-h${heading.level}">` +
|
|
267
|
+
`${escapeHtml(heading.text)}</a>`)
|
|
268
|
+
.join("\n ");
|
|
269
|
+
return `<aside id="docs-toc">
|
|
270
|
+
<div class="nav-title">On this page</div>
|
|
271
|
+
<nav>
|
|
272
|
+
${items}
|
|
273
|
+
</nav>
|
|
274
|
+
</aside>`;
|
|
275
|
+
}
|
|
276
|
+
/** Sequential links, so the pages can be read in the order they are written in. */
|
|
277
|
+
function renderPageNav(page, pages) {
|
|
278
|
+
const index = pages.findIndex((item) => item.slug === page.slug);
|
|
279
|
+
const previous = index > 0 ? pages[index - 1] : undefined;
|
|
280
|
+
const next = index < pages.length - 1 ? pages[index + 1] : undefined;
|
|
281
|
+
const link = (target, direction, label) => target
|
|
282
|
+
? `<a class="page-nav-${direction}" href="${target.slug}.html">` +
|
|
283
|
+
`<span class="page-nav-label">${label}</span>` +
|
|
284
|
+
`<span class="page-nav-title">${escapeHtml(target.title)}</span></a>`
|
|
285
|
+
: `<span class="page-nav-${direction}"></span>`;
|
|
286
|
+
return `<nav class="page-nav">
|
|
287
|
+
${link(previous, "prev", "Previous")}
|
|
288
|
+
${link(next, "next", "Next")}
|
|
289
|
+
</nav>`;
|
|
290
|
+
}
|
|
291
|
+
function renderPage(page, pages, html, headings) {
|
|
233
292
|
const nav = pages
|
|
234
293
|
.map((item) => `<a href="${item.slug}.html"${item.slug === page.slug ? ' class="current"' : ""}>` +
|
|
235
294
|
`${escapeHtml(item.title)}</a>`)
|
|
@@ -257,16 +316,24 @@ function renderPage(page, pages, html) {
|
|
|
257
316
|
</div>
|
|
258
317
|
<div id="workspace">
|
|
259
318
|
<aside id="docs-nav">
|
|
260
|
-
<div class="nav-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
319
|
+
<div class="docs-nav-inner">
|
|
320
|
+
<div class="nav-title">Concepts</div>
|
|
321
|
+
<nav>
|
|
322
|
+
${nav}
|
|
323
|
+
</nav>
|
|
324
|
+
</div>
|
|
264
325
|
</aside>
|
|
265
326
|
<main id="docs-content">
|
|
266
327
|
<article>
|
|
267
328
|
${html}
|
|
268
329
|
</article>
|
|
330
|
+
${renderPageNav(page, pages)}
|
|
269
331
|
</main>
|
|
332
|
+
${renderTableOfContents(headings)}
|
|
333
|
+
</div>
|
|
334
|
+
<div id="statusbar">
|
|
335
|
+
<span id="status-page">${escapeHtml(page.summary || page.title)}</span>
|
|
336
|
+
<span id="status-position">Page ${pages.findIndex((item) => item.slug === page.slug) + 1} of ${pages.length}</span>
|
|
270
337
|
</div>
|
|
271
338
|
</body>
|
|
272
339
|
</html>
|
|
@@ -283,6 +350,8 @@ const DOCS_STYLESHEET = `/* Concept documentation — shares the explorer's pale
|
|
|
283
350
|
--text-muted: #858585;
|
|
284
351
|
--border: #3e3e3e;
|
|
285
352
|
--accent: #4ea1ff;
|
|
353
|
+
/* The Explorer and the Ontology map both paint their status bar in this blue. */
|
|
354
|
+
--accent-bar: #0078d4;
|
|
286
355
|
--code: #9cdcfe;
|
|
287
356
|
}
|
|
288
357
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -320,22 +389,29 @@ body {
|
|
|
320
389
|
#surfaces a.current { color: var(--text-primary); border-bottom: 2px solid var(--accent); }
|
|
321
390
|
#titlebar-tagline { margin-left: auto; color: var(--text-muted); white-space: nowrap; }
|
|
322
391
|
@media (max-width: 900px) { #titlebar-tagline { display: none; } }
|
|
323
|
-
#workspace { display: flex; flex: 1;
|
|
392
|
+
#workspace { display: flex; flex: 1; }
|
|
393
|
+
/*
|
|
394
|
+
* Two boxes, two jobs. The panel (#docs-nav) stretches for the full height of the
|
|
395
|
+
* workspace, so its background and border run the length of the page instead of stopping
|
|
396
|
+
* where the link list ends. The inner box is what sticks.
|
|
397
|
+
*
|
|
398
|
+
* Sticking the panel itself does not work: an element can only stick while it is shorter
|
|
399
|
+
* than the space it sticks within, so a full-height panel has no slack to hold a position
|
|
400
|
+
* in. Sizing the panel to its own content instead (align-self: flex-start) restores the
|
|
401
|
+
* sticking but leaves the panel visibly cut off part-way down the page. Separating the
|
|
402
|
+
* two concerns gets both.
|
|
403
|
+
*/
|
|
324
404
|
#docs-nav {
|
|
325
405
|
width: 250px;
|
|
326
406
|
flex-shrink: 0;
|
|
327
407
|
background: var(--bg-sidebar);
|
|
328
408
|
border-right: 1px solid var(--border);
|
|
329
|
-
|
|
409
|
+
}
|
|
410
|
+
.docs-nav-inner {
|
|
330
411
|
position: sticky;
|
|
331
412
|
top: 34px;
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
stick while it is shorter than the room it is sticking within. flex-start sizes
|
|
335
|
-
this to its own (short) nav-list content, so it actually stays pinned. The height
|
|
336
|
-
cap is a second guard: if the nav list ever grows past the viewport, it scrolls
|
|
337
|
-
internally instead of being clipped or pushing off-screen. */
|
|
338
|
-
align-self: flex-start;
|
|
413
|
+
padding: 16px 0;
|
|
414
|
+
/* If the list ever outgrows the viewport it scrolls itself rather than being clipped. */
|
|
339
415
|
max-height: calc(100vh - 34px);
|
|
340
416
|
overflow-y: auto;
|
|
341
417
|
}
|
|
@@ -362,7 +438,8 @@ body {
|
|
|
362
438
|
border-left-color: var(--accent);
|
|
363
439
|
}
|
|
364
440
|
#docs-content { flex: 1; min-width: 0; padding: 32px 40px 80px; }
|
|
365
|
-
|
|
441
|
+
/* A fixed measure, centred: on a wide screen the text should not hug the sidebar. */
|
|
442
|
+
article { max-width: 68ch; margin: 0 auto; }
|
|
366
443
|
article h1 { font-size: 27px; font-weight: 600; margin-bottom: 18px; text-wrap: balance; }
|
|
367
444
|
article h2 {
|
|
368
445
|
font-size: 19px;
|
|
@@ -413,17 +490,88 @@ article th { background: var(--bg-sidebar); font-weight: 600; }
|
|
|
413
490
|
article td:not(:first-child) { font-variant-numeric: tabular-nums; }
|
|
414
491
|
article hr { border: none; border-top: 1px solid var(--border); margin: 26px 0; }
|
|
415
492
|
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
493
|
+
|
|
494
|
+
/* In-page contents. Mirrors the concept nav opposite it, one weight lighter. */
|
|
495
|
+
#docs-toc {
|
|
496
|
+
width: 220px;
|
|
497
|
+
flex-shrink: 0;
|
|
498
|
+
padding: 16px 0;
|
|
499
|
+
position: sticky;
|
|
500
|
+
top: 34px;
|
|
501
|
+
align-self: flex-start;
|
|
502
|
+
max-height: calc(100vh - 34px);
|
|
503
|
+
overflow-y: auto;
|
|
504
|
+
}
|
|
505
|
+
#docs-toc nav { display: flex; flex-direction: column; }
|
|
506
|
+
#docs-toc a {
|
|
507
|
+
padding: 4px 16px;
|
|
508
|
+
color: var(--text-muted);
|
|
509
|
+
text-decoration: none;
|
|
510
|
+
font-size: 12px;
|
|
511
|
+
line-height: 1.4;
|
|
512
|
+
border-left: 2px solid transparent;
|
|
513
|
+
}
|
|
514
|
+
#docs-toc a:hover { color: var(--text-primary); border-left-color: var(--border); }
|
|
515
|
+
#docs-toc a.toc-h3 { padding-left: 28px; font-size: 11.5px; }
|
|
516
|
+
|
|
517
|
+
/* The pages are written as a sequence; give the reader a way to follow it. */
|
|
518
|
+
.page-nav {
|
|
519
|
+
display: flex;
|
|
520
|
+
gap: 12px;
|
|
521
|
+
max-width: 68ch;
|
|
522
|
+
margin: 40px auto 0;
|
|
523
|
+
padding-top: 20px;
|
|
524
|
+
border-top: 1px solid var(--border);
|
|
525
|
+
}
|
|
526
|
+
.page-nav > * { flex: 1 1 0; min-width: 0; }
|
|
527
|
+
.page-nav a {
|
|
528
|
+
display: flex;
|
|
529
|
+
flex-direction: column;
|
|
530
|
+
gap: 3px;
|
|
531
|
+
padding: 10px 14px;
|
|
532
|
+
border: 1px solid var(--border);
|
|
533
|
+
border-radius: 3px;
|
|
534
|
+
text-decoration: none;
|
|
535
|
+
color: var(--text-primary);
|
|
536
|
+
}
|
|
537
|
+
.page-nav a:hover { background: var(--bg-hover); border-color: var(--accent); }
|
|
538
|
+
.page-nav-next { text-align: right; }
|
|
539
|
+
.page-nav-label { font-size: 11px; color: var(--text-muted); }
|
|
540
|
+
.page-nav-title { font-size: 13px; }
|
|
541
|
+
|
|
542
|
+
/* Matches the Explorer's and the map's, so the three surfaces end the same way. */
|
|
543
|
+
#statusbar {
|
|
544
|
+
height: 22px;
|
|
545
|
+
background: var(--accent-bar);
|
|
546
|
+
color: #fff;
|
|
547
|
+
display: flex;
|
|
548
|
+
align-items: center;
|
|
549
|
+
padding: 0 14px;
|
|
550
|
+
font-size: 12px;
|
|
551
|
+
flex-shrink: 0;
|
|
552
|
+
gap: 16px;
|
|
553
|
+
position: sticky;
|
|
554
|
+
bottom: 0;
|
|
555
|
+
}
|
|
556
|
+
#status-page { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
557
|
+
|
|
558
|
+
/* The contents column is the first thing to go when it stops earning its width. */
|
|
559
|
+
@media (max-width: 1100px) { #docs-toc { display: none; } }
|
|
416
560
|
@media (max-width: 760px) {
|
|
417
561
|
#workspace { flex-direction: column; }
|
|
418
562
|
#docs-nav {
|
|
419
563
|
width: 100%;
|
|
420
|
-
position: static;
|
|
421
564
|
border-right: none;
|
|
422
565
|
border-bottom: 1px solid var(--border);
|
|
566
|
+
}
|
|
567
|
+
.docs-nav-inner {
|
|
568
|
+
position: static;
|
|
423
569
|
max-height: none;
|
|
424
570
|
overflow-y: visible;
|
|
425
571
|
}
|
|
426
572
|
#docs-content { padding: 20px; }
|
|
573
|
+
.page-nav { flex-direction: column; }
|
|
574
|
+
.page-nav-next { text-align: left; }
|
|
427
575
|
}
|
|
428
576
|
`;
|
|
429
577
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
@@ -451,11 +599,12 @@ function buildDocsPages(outputDir) {
|
|
|
451
599
|
// should fail the build outright, not leave a half-written site behind.
|
|
452
600
|
const rendered = pages.map((page) => {
|
|
453
601
|
const expanded = expandFragments(page.body, graph);
|
|
454
|
-
const
|
|
602
|
+
const parsed = marked_1.marked.parse(expanded, { async: false })
|
|
455
603
|
// Wide tables scroll inside their own container rather than the page body.
|
|
456
604
|
.replace(/<table>/g, '<div class="table-wrap"><table>')
|
|
457
605
|
.replace(/<\/table>/g, "</table></div>");
|
|
458
|
-
|
|
606
|
+
const { html, headings } = extractHeadings(parsed);
|
|
607
|
+
return { page, html: renderPage(page, pages, html, headings) };
|
|
459
608
|
});
|
|
460
609
|
const targetDir = path_1.default.join(outputDir, "docs");
|
|
461
610
|
fs_1.default.mkdirSync(targetDir, { recursive: true });
|
package/package.json
CHANGED