@enhansome/core 1.7.1 → 1.8.0
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.
- package/dist/github.d.ts +1 -0
- package/dist/github.js +1 -0
- package/dist/markdown.d.ts +2 -1
- package/dist/markdown.js +181 -62
- package/package.json +1 -1
package/dist/github.d.ts
CHANGED
package/dist/github.js
CHANGED
package/dist/markdown.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface SortOptions {
|
|
|
17
17
|
}
|
|
18
18
|
export interface RepoInfo {
|
|
19
19
|
archived: boolean;
|
|
20
|
+
id: number;
|
|
20
21
|
language: null | string;
|
|
21
22
|
last_commit: null | string;
|
|
22
23
|
owner: string;
|
|
@@ -29,7 +30,7 @@ export interface JsonItem {
|
|
|
29
30
|
children: JsonNode[];
|
|
30
31
|
description: null | string;
|
|
31
32
|
node_type: 'item';
|
|
32
|
-
repo_info
|
|
33
|
+
repo_info: RepoInfo;
|
|
33
34
|
title: string;
|
|
34
35
|
}
|
|
35
36
|
export interface JsonGroup {
|
package/dist/markdown.js
CHANGED
|
@@ -10,6 +10,7 @@ import { consoleLog } from './logger.js';
|
|
|
10
10
|
export function toRepoInfo(details) {
|
|
11
11
|
return {
|
|
12
12
|
archived: details.archived,
|
|
13
|
+
id: details.id,
|
|
13
14
|
language: details.language,
|
|
14
15
|
last_commit: details.pushed_at,
|
|
15
16
|
owner: details.owner,
|
|
@@ -282,10 +283,10 @@ function processListRecursively(listNode, repoInfoMap, sortOptions, isNested = f
|
|
|
282
283
|
if (!isNested && itemsWithGitHubLinks.length < sortOptions.minLinks) {
|
|
283
284
|
return [];
|
|
284
285
|
}
|
|
285
|
-
// Zip each item with its JSON
|
|
286
|
-
// rendered AST and the emitted JSON. `
|
|
287
|
-
// leaves (no own link, no nested GitHub children): kept in the
|
|
288
|
-
// from JSON.
|
|
286
|
+
// Zip each item with its emitted JSON nodes and repo info so one sort orders
|
|
287
|
+
// both the rendered AST and the emitted JSON. `emitted` is empty for
|
|
288
|
+
// non-GitHub leaves (no own link, no nested GitHub children): kept in the
|
|
289
|
+
// AST, dropped from JSON.
|
|
289
290
|
const entries = [];
|
|
290
291
|
for (const itemNode of listNode.children) {
|
|
291
292
|
const githubUrl = findOwnGitHubLink(itemNode);
|
|
@@ -314,37 +315,41 @@ function processListRecursively(listNode, repoInfoMap, sortOptions, isNested = f
|
|
|
314
315
|
// `repo_info`, that's the identity-borrowing bug. No-own-link, no-child
|
|
315
316
|
// items are non-GitHub leaves: kept in markdown, dropped from JSON.
|
|
316
317
|
// TODO(future): preserve non-GitHub leaves in a separate shape.
|
|
317
|
-
let
|
|
318
|
-
if (githubUrl) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
318
|
+
let emitted = [];
|
|
319
|
+
if (githubUrl && repoInfo) {
|
|
320
|
+
emitted = [
|
|
321
|
+
{
|
|
322
|
+
node_type: 'item',
|
|
323
|
+
title,
|
|
324
|
+
description: description || null,
|
|
325
|
+
children: childrenJson,
|
|
326
|
+
repo_info: toRepoInfo(repoInfo),
|
|
327
|
+
},
|
|
328
|
+
];
|
|
329
|
+
}
|
|
330
|
+
else if (githubUrl) {
|
|
331
|
+
// Dead target: the item itself is not emitted; its children lift to this
|
|
332
|
+
// list's level — the nearest live parent.
|
|
333
|
+
emitted = childrenJson;
|
|
329
334
|
}
|
|
330
335
|
else if (childrenJson.length > 0) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
336
|
+
emitted = [
|
|
337
|
+
{
|
|
338
|
+
node_type: 'group',
|
|
339
|
+
title,
|
|
340
|
+
description: description || null,
|
|
341
|
+
children: childrenJson,
|
|
342
|
+
},
|
|
343
|
+
];
|
|
337
344
|
}
|
|
338
|
-
entries.push({
|
|
345
|
+
entries.push({ emitted, node: itemNode, repoInfo });
|
|
339
346
|
}
|
|
340
347
|
if (sortOptions.by) {
|
|
341
348
|
entries.sort((a, b) => compareByRepoInfo(sortOptions.by, a.repoInfo, b.repoInfo));
|
|
342
349
|
}
|
|
343
350
|
// Reorder the AST to match the sort so rendered markdown and JSON agree.
|
|
344
351
|
listNode.children = entries.map(entry => entry.node);
|
|
345
|
-
return entries
|
|
346
|
-
.map(entry => entry.json)
|
|
347
|
-
.filter((json) => json !== null);
|
|
352
|
+
return entries.flatMap(entry => entry.emitted);
|
|
348
353
|
}
|
|
349
354
|
const INVALID_TITLE_PATTERNS = [
|
|
350
355
|
/^contributing/i,
|
|
@@ -389,6 +394,19 @@ function isValidTitle(title) {
|
|
|
389
394
|
}
|
|
390
395
|
return !INVALID_TITLE_PATTERNS.some(pattern => pattern.test(title.trim()));
|
|
391
396
|
}
|
|
397
|
+
// Headings that mirror structure rather than delimit it. Unlike
|
|
398
|
+
// INVALID_TITLE_PATTERNS (a title-detection aid — "## Tools" is a perfectly
|
|
399
|
+
// good content section), a TOC heading never owns content: the worst offender
|
|
400
|
+
// is a `# Table of Contents` H1 that would otherwise wrap the whole document
|
|
401
|
+
// as its "section".
|
|
402
|
+
const TOC_TITLE_PATTERNS = [/^contents$/i, /^table of contents$/i];
|
|
403
|
+
// A heading that delimits content structure. Text-less headings (a bare `#`,
|
|
404
|
+
// an image-only heading) are spacers in real docs; TOC headings are structure
|
|
405
|
+
// mirrors. Neither participates in the section tree.
|
|
406
|
+
function isStructuralHeading(node) {
|
|
407
|
+
const title = getNodeText(node);
|
|
408
|
+
return (!!title && !TOC_TITLE_PATTERNS.some(pattern => pattern.test(title.trim())));
|
|
409
|
+
}
|
|
392
410
|
/** Never duplicates "Awesome": if the title already contains it, append the suffix verbatim; otherwise prefix first. */
|
|
393
411
|
function brandTitle(title) {
|
|
394
412
|
const trimmed = title.trim();
|
|
@@ -442,6 +460,13 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepository) {
|
|
|
442
460
|
let documentTitle = titleHeadingIndex === -1
|
|
443
461
|
? ''
|
|
444
462
|
: getNodeText(tree.children[titleHeadingIndex]);
|
|
463
|
+
// The heading branding owns even when it isn't a *valid* title: a generic
|
|
464
|
+
// first H1 ("# Guides", "# Contents") is still the de-facto title slot —
|
|
465
|
+
// applyBrandingToTree replaces it — so the section tree must not treat it
|
|
466
|
+
// as a section wrapping the whole document.
|
|
467
|
+
const titleSlotIndex = titleHeadingIndex !== -1
|
|
468
|
+
? titleHeadingIndex
|
|
469
|
+
: tree.children.findIndex((node) => node.type === 'heading' && node.depth === 1);
|
|
445
470
|
// Derive a subject from the *source* repository name when no valid H1 is
|
|
446
471
|
// present. Using the source — not the enhanced/mirror repo — keeps the org
|
|
447
472
|
// name out of the title.
|
|
@@ -451,52 +476,146 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepository) {
|
|
|
451
476
|
documentTitle = formatRepoNameAsTitle(repoName);
|
|
452
477
|
}
|
|
453
478
|
}
|
|
479
|
+
const sectionDepth = findSectionDepth(tree, titleSlotIndex);
|
|
454
480
|
const sections = [];
|
|
455
|
-
|
|
456
|
-
for (
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
481
|
+
const stack = [];
|
|
482
|
+
for (let i = 0; i < tree.children.length; i++) {
|
|
483
|
+
const node = tree.children[i];
|
|
484
|
+
if (node.type === 'heading') {
|
|
485
|
+
// The title-slot H1 belongs to branding/metadata; non-structural
|
|
486
|
+
// headings (see isStructuralHeading) delimit nothing. Neither
|
|
487
|
+
// participates in the section tree.
|
|
488
|
+
if (i === titleSlotIndex || !isStructuralHeading(node)) {
|
|
489
|
+
continue;
|
|
460
490
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
items: [],
|
|
464
|
-
title: getNodeText(node),
|
|
465
|
-
};
|
|
491
|
+
closeContainers(stack, node.depth, sections);
|
|
492
|
+
openContainer(stack, node, sectionDepth, repoInfoMap);
|
|
466
493
|
}
|
|
467
|
-
else if (
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
else {
|
|
476
|
-
currentSection.description = paragraphText;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
else if (node.type === 'list') {
|
|
481
|
-
const items = processListRecursively(node, repoInfoMap, sortOptions);
|
|
482
|
-
if (items.length > 0) {
|
|
483
|
-
currentSection.items = items;
|
|
484
|
-
sections.push(currentSection);
|
|
485
|
-
}
|
|
486
|
-
currentSection = null;
|
|
494
|
+
else if (node.type === 'paragraph' || node.type === 'blockquote') {
|
|
495
|
+
const text = getNodeText(node);
|
|
496
|
+
const container = stack[stack.length - 1];
|
|
497
|
+
// Avoid adding boilerplate "back to top" links to descriptions.
|
|
498
|
+
if (container && text && !text.includes('back to top')) {
|
|
499
|
+
container.description = container.description
|
|
500
|
+
? `${container.description}\n${text}`
|
|
501
|
+
: text;
|
|
487
502
|
}
|
|
488
503
|
}
|
|
489
504
|
else if (node.type === 'list') {
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
|
|
505
|
+
// Every list inside the open container contributes items — a section is
|
|
506
|
+
// not closed by its first list. With no open container (preamble), the
|
|
507
|
+
// list is not part of any JSON section, but its AST is still sorted so
|
|
508
|
+
// the rendered markdown matches.
|
|
509
|
+
const items = processListRecursively(node, repoInfoMap, sortOptions);
|
|
510
|
+
const container = stack[stack.length - 1];
|
|
511
|
+
if (container) {
|
|
512
|
+
container.children.push(...items);
|
|
513
|
+
}
|
|
493
514
|
}
|
|
494
515
|
}
|
|
495
|
-
|
|
496
|
-
sections.push(currentSection);
|
|
497
|
-
}
|
|
516
|
+
closeContainers(stack, 0, sections);
|
|
498
517
|
return { sections, title: documentTitle, titleHeadingIndex };
|
|
499
518
|
}
|
|
519
|
+
// The heading depth that opens top-level sections: the shallowest structural
|
|
520
|
+
// heading in the document other than the title slot. H1s count — ~20% of
|
|
521
|
+
// mirror READMEs use `# Section` after the title H1, and hardcoding H2 would
|
|
522
|
+
// drop all their items. Non-structural headings are skipped here too (same
|
|
523
|
+
// rule as the walk). Infinity when there is no such heading (no sections).
|
|
524
|
+
function findSectionDepth(tree, titleSlotIndex) {
|
|
525
|
+
let depth = Infinity;
|
|
526
|
+
tree.children.forEach((node, i) => {
|
|
527
|
+
if (node.type === 'heading' &&
|
|
528
|
+
i !== titleSlotIndex &&
|
|
529
|
+
isStructuralHeading(node) &&
|
|
530
|
+
node.depth < depth) {
|
|
531
|
+
depth = node.depth;
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
return depth;
|
|
535
|
+
}
|
|
536
|
+
// A heading whose only link is a live GitHub link represents a resource, not a
|
|
537
|
+
// container — the link-heading pattern (`#### [Repo](github…)`). Badge images
|
|
538
|
+
// wrapped in links or multiple links disqualify (more than one link means the
|
|
539
|
+
// heading is not "the" resource), as does a dead target.
|
|
540
|
+
function soleLiveHeadingLink(heading, repoInfoMap) {
|
|
541
|
+
const links = heading.children.filter((child) => child.type === 'link');
|
|
542
|
+
if (links.length !== 1) {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
return repoInfoMap.get(links[0].url) ?? null;
|
|
546
|
+
}
|
|
547
|
+
function openContainer(stack, heading, sectionDepth, repoInfoMap) {
|
|
548
|
+
const title = getNodeText(heading);
|
|
549
|
+
// Sections sit at the section level — and any heading met with an empty
|
|
550
|
+
// stack is promoted: a deeper heading before the first section (orphan
|
|
551
|
+
// subheading) still owns its subtree, and a link-heading at section level
|
|
552
|
+
// becomes a section rather than a top-level item, which the contract has no
|
|
553
|
+
// place for.
|
|
554
|
+
if (stack.length === 0 || heading.depth === sectionDepth) {
|
|
555
|
+
stack.push({
|
|
556
|
+
children: [],
|
|
557
|
+
description: '',
|
|
558
|
+
headingDepth: heading.depth,
|
|
559
|
+
kind: 'section',
|
|
560
|
+
title,
|
|
561
|
+
});
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const repoInfo = soleLiveHeadingLink(heading, repoInfoMap);
|
|
565
|
+
stack.push({
|
|
566
|
+
children: [],
|
|
567
|
+
description: '',
|
|
568
|
+
headingDepth: heading.depth,
|
|
569
|
+
kind: repoInfo ? 'item' : 'group',
|
|
570
|
+
repoInfo: repoInfo ?? undefined,
|
|
571
|
+
title,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
// Finalize every container a heading of `depth` closes (same-or-shallower),
|
|
575
|
+
// bottom-up so each finalized node lands in its parent. Pruning falls out of
|
|
576
|
+
// the finalize rule: a section/group whose children array is empty (no items
|
|
577
|
+
// anywhere beneath — lists only return item-bearing nodes, and empty children
|
|
578
|
+
// were never appended) is dropped; an item always survives, it IS the content.
|
|
579
|
+
// The stack bottom is always a section (openContainer's promotion guarantees
|
|
580
|
+
// it), so a finalized group/item always has a parent to land in.
|
|
581
|
+
function closeContainers(stack, depth, sections) {
|
|
582
|
+
while (stack.length > 0 &&
|
|
583
|
+
stack[stack.length - 1].headingDepth >= depth) {
|
|
584
|
+
const container = stack.pop();
|
|
585
|
+
if (container.children.length === 0 && container.kind !== 'item') {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
const description = container.description || null;
|
|
589
|
+
if (container.kind === 'section') {
|
|
590
|
+
sections.push({
|
|
591
|
+
description,
|
|
592
|
+
items: container.children,
|
|
593
|
+
title: container.title,
|
|
594
|
+
});
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const parent = stack[stack.length - 1];
|
|
598
|
+
// Non-section containers always have an open parent (stack invariant), and
|
|
599
|
+
// kind === 'item' exactly when repoInfo is set.
|
|
600
|
+
if (container.repoInfo) {
|
|
601
|
+
parent.children.push({
|
|
602
|
+
children: container.children,
|
|
603
|
+
description,
|
|
604
|
+
node_type: 'item',
|
|
605
|
+
repo_info: toRepoInfo(container.repoInfo),
|
|
606
|
+
title: container.title,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
parent.children.push({
|
|
611
|
+
children: container.children,
|
|
612
|
+
description,
|
|
613
|
+
node_type: 'group',
|
|
614
|
+
title: container.title,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
500
619
|
function serializeAst(tree, originalContent) {
|
|
501
620
|
let finalContent = unified()
|
|
502
621
|
.use(remarkStringify)
|