@enhansome/core 1.10.1 → 1.11.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/first-seen.d.ts +3 -0
- package/dist/first-seen.js +20 -0
- package/dist/github.d.ts +2 -0
- package/dist/github.js +2 -0
- package/dist/markdown.d.ts +7 -1
- package/dist/markdown.js +34 -58
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function firstSeenFor(previous, now) {
|
|
2
|
+
const index = new Map();
|
|
3
|
+
if (previous) {
|
|
4
|
+
for (const section of previous.items) {
|
|
5
|
+
collect(section.items, index);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
return repoId => index.get(repoId) ?? now.toISOString();
|
|
9
|
+
}
|
|
10
|
+
function collect(nodes, index) {
|
|
11
|
+
for (const node of nodes) {
|
|
12
|
+
if (node.node_type === 'item' && node.first_seen) {
|
|
13
|
+
const existing = index.get(node.repo_info.id);
|
|
14
|
+
if (existing === undefined || node.first_seen < existing) {
|
|
15
|
+
index.set(node.repo_info.id, node.first_seen);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
collect(node.children, index);
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/github.d.ts
CHANGED
|
@@ -8,8 +8,10 @@ export type GithubClient = InstanceType<typeof HardenedOctokit>;
|
|
|
8
8
|
export interface RepoInfoDetails {
|
|
9
9
|
archived: boolean;
|
|
10
10
|
description: null | string;
|
|
11
|
+
homepage: null | string;
|
|
11
12
|
id: number;
|
|
12
13
|
language: null | string;
|
|
14
|
+
license: null | string;
|
|
13
15
|
open_issues_count: number;
|
|
14
16
|
owner: string;
|
|
15
17
|
pushed_at: null | string;
|
package/dist/github.js
CHANGED
|
@@ -72,8 +72,10 @@ export async function getRepoInfo(octokit, owner, repo) {
|
|
|
72
72
|
return {
|
|
73
73
|
archived: data.archived,
|
|
74
74
|
description: data.description ?? null,
|
|
75
|
+
homepage: data.homepage || null,
|
|
75
76
|
id: data.id,
|
|
76
77
|
language: data.language,
|
|
78
|
+
license: data.license?.spdx_id ?? null,
|
|
77
79
|
open_issues_count: data.open_issues_count,
|
|
78
80
|
owner: data.owner.login,
|
|
79
81
|
pushed_at: data.pushed_at,
|
package/dist/markdown.d.ts
CHANGED
|
@@ -18,18 +18,24 @@ export interface SortOptions {
|
|
|
18
18
|
}
|
|
19
19
|
export interface RepoInfo {
|
|
20
20
|
archived: boolean;
|
|
21
|
+
description: null | string;
|
|
22
|
+
homepage: null | string;
|
|
21
23
|
id: number;
|
|
22
24
|
language: null | string;
|
|
25
|
+
license: null | string;
|
|
23
26
|
last_commit: null | string;
|
|
27
|
+
open_issues: number;
|
|
24
28
|
owner: string;
|
|
25
29
|
repo: string;
|
|
26
30
|
stars: number;
|
|
31
|
+
topics: string[];
|
|
27
32
|
}
|
|
28
33
|
/** The sole crossing from the API shape to the emitted one, so both agree on the renames. */
|
|
29
34
|
export declare function toRepoInfo(details: RepoInfoDetails): RepoInfo;
|
|
30
35
|
export interface JsonItem {
|
|
31
36
|
children: JsonNode[];
|
|
32
37
|
description: null | string;
|
|
38
|
+
first_seen?: string;
|
|
33
39
|
node_type: 'item';
|
|
34
40
|
repo_info: RepoInfo;
|
|
35
41
|
title: string;
|
|
@@ -54,7 +60,7 @@ export interface JsonSection {
|
|
|
54
60
|
items: JsonNode[];
|
|
55
61
|
title: string;
|
|
56
62
|
}
|
|
57
|
-
export declare function processMarkdownContent(originalContent: string, token: string, replacements?: ReplacementRule[], sortOptions?: SortOptions, relativeLinkPrefix?: string, enhancedRepository?: string, enhancedRepositoryDescription?: string, originalRepositorySha?: string, originalRepositoryInfo?: null | RepoInfoDetails, now?: Date, log?: Logger): Promise<{
|
|
63
|
+
export declare function processMarkdownContent(originalContent: string, token: string, replacements?: ReplacementRule[], sortOptions?: SortOptions, relativeLinkPrefix?: string, enhancedRepository?: string, enhancedRepositoryDescription?: string, originalRepositorySha?: string, originalRepositoryInfo?: null | RepoInfoDetails, previousJson?: JsonOutput, now?: Date, log?: Logger): Promise<{
|
|
58
64
|
finalContent: string;
|
|
59
65
|
jsonData: JsonOutput;
|
|
60
66
|
}>;
|
package/dist/markdown.js
CHANGED
|
@@ -4,18 +4,24 @@ import remarkParse from 'remark-parse';
|
|
|
4
4
|
import remarkStringify from 'remark-stringify';
|
|
5
5
|
import { unified } from 'unified';
|
|
6
6
|
import { visit } from 'unist-util-visit';
|
|
7
|
+
import { firstSeenFor } from './first-seen.js';
|
|
7
8
|
import { formatRequestError, getRepoInfo as fetchRepoInfo, makeOctokit, parseGitHubUrl, } from './github.js';
|
|
8
9
|
import { consoleLog } from './logger.js';
|
|
9
10
|
/** The sole crossing from the API shape to the emitted one, so both agree on the renames. */
|
|
10
11
|
export function toRepoInfo(details) {
|
|
11
12
|
return {
|
|
12
13
|
archived: details.archived,
|
|
14
|
+
description: details.description,
|
|
15
|
+
homepage: details.homepage,
|
|
13
16
|
id: details.id,
|
|
14
17
|
language: details.language,
|
|
18
|
+
license: details.license,
|
|
15
19
|
last_commit: details.pushed_at,
|
|
20
|
+
open_issues: details.open_issues_count,
|
|
16
21
|
owner: details.owner,
|
|
17
22
|
repo: details.repo,
|
|
18
23
|
stars: details.stargazers_count,
|
|
24
|
+
topics: details.topics,
|
|
19
25
|
};
|
|
20
26
|
}
|
|
21
27
|
// The lookup's single throttled client coordinates rate limits across the whole
|
|
@@ -79,7 +85,7 @@ async function fetchTargetData(urls, repos) {
|
|
|
79
85
|
log.info(`Target fetch: ${repoInfoMap.size}/${urls.size} repo-info ok in ${Date.now() - fetchStart}ms (concurrency ${FETCH_CONCURRENCY}).`);
|
|
80
86
|
return repoInfoMap;
|
|
81
87
|
}
|
|
82
|
-
export async function processMarkdownContent(originalContent, token, replacements = [], sortOptions = { by: '', minLinks: 2 }, relativeLinkPrefix = '', enhancedRepository, enhancedRepositoryDescription, originalRepositorySha, originalRepositoryInfo, now = new Date(), log = consoleLog) {
|
|
88
|
+
export async function processMarkdownContent(originalContent, token, replacements = [], sortOptions = { by: '', minLinks: 2 }, relativeLinkPrefix = '', enhancedRepository, enhancedRepositoryDescription, originalRepositorySha, originalRepositoryInfo, previousJson, now = new Date(), log = consoleLog) {
|
|
83
89
|
const repos = createRepoInfoLookup(token, log);
|
|
84
90
|
const brandingEnabled = replacements.some(rule => rule.type === 'branding');
|
|
85
91
|
const contentAfterReplacements = applyTextReplacements(originalContent, replacements.filter(rule => rule.type !== 'branding'), log);
|
|
@@ -88,7 +94,8 @@ export async function processMarkdownContent(originalContent, token, replacement
|
|
|
88
94
|
normalizeGitHubUrls(tree);
|
|
89
95
|
const githubUrls = collectGitHubLinks(tree);
|
|
90
96
|
const repoInfoMap = await fetchTargetData(githubUrls, repos);
|
|
91
|
-
const
|
|
97
|
+
const firstSeen = firstSeenFor(previousJson, now);
|
|
98
|
+
const { sections, title: rawTitle, titleHeadingIndex, } = processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo, firstSeen);
|
|
92
99
|
// Single source of truth for the document title: brand it once and use the
|
|
93
100
|
// same value for the markdown H1 and metadata.title (parity).
|
|
94
101
|
const title = brandingEnabled ? brandTitle(rawTitle) : rawTitle;
|
|
@@ -439,7 +446,7 @@ function splitEntryText(inlines) {
|
|
|
439
446
|
// group — never a `repo_info`, that's the identity-borrowing bug; no own link
|
|
440
447
|
// and no children is a non-GitHub leaf, kept in markdown, dropped from JSON.
|
|
441
448
|
// TODO(future): preserve non-GitHub leaves in a separate shape.
|
|
442
|
-
function emitEntryNodes(githubUrl, repoInfo, text, childrenJson) {
|
|
449
|
+
function emitEntryNodes(githubUrl, repoInfo, text, childrenJson, firstSeen) {
|
|
443
450
|
if (githubUrl && repoInfo) {
|
|
444
451
|
return [
|
|
445
452
|
{
|
|
@@ -448,6 +455,7 @@ function emitEntryNodes(githubUrl, repoInfo, text, childrenJson) {
|
|
|
448
455
|
description: text.description || null,
|
|
449
456
|
children: childrenJson,
|
|
450
457
|
repo_info: toRepoInfo(repoInfo),
|
|
458
|
+
first_seen: firstSeen(repoInfo.id),
|
|
451
459
|
},
|
|
452
460
|
];
|
|
453
461
|
}
|
|
@@ -466,7 +474,7 @@ function emitEntryNodes(githubUrl, repoInfo, text, childrenJson) {
|
|
|
466
474
|
}
|
|
467
475
|
return [];
|
|
468
476
|
}
|
|
469
|
-
function processListRecursively(listNode, repoInfoMap, sortOptions, isNested = false,
|
|
477
|
+
function processListRecursively(listNode, repoInfoMap, sortOptions, firstSeen, isNested = false,
|
|
470
478
|
// The caller's section-scope gate decision (sectionGatePasses). Absent for
|
|
471
479
|
// nested lists (emitted under their parent regardless) and for top-level
|
|
472
480
|
// lists with no open container (preamble), which gate per list.
|
|
@@ -493,8 +501,8 @@ sectionGateOpen) {
|
|
|
493
501
|
// regardless of the gate: the top-level call already gated the section.
|
|
494
502
|
const nestedContent = itemNode.children.filter((child) => child.type === 'list' || child.type === 'table');
|
|
495
503
|
const childrenJson = nestedContent.flatMap(child => child.type === 'list'
|
|
496
|
-
? processListRecursively(child, repoInfoMap, sortOptions, true)
|
|
497
|
-
: processTableRows(child, repoInfoMap, true));
|
|
504
|
+
? processListRecursively(child, repoInfoMap, sortOptions, firstSeen, true)
|
|
505
|
+
: processTableRows(child, repoInfoMap, true, firstSeen));
|
|
498
506
|
// Title/description split on the FIRST paragraph only — a paper-list
|
|
499
507
|
// entry's identity link may live in a later paragraph (findOwnGitHubLink)
|
|
500
508
|
// while its title text stays the leading one.
|
|
@@ -508,7 +516,7 @@ sectionGateOpen) {
|
|
|
508
516
|
// The shared title fallbacks (an inline-code link label carries no text
|
|
509
517
|
// nodes, so the split alone can leave an empty title).
|
|
510
518
|
entryText.title = entryTitle(entryText.title, ownLink, repoInfo);
|
|
511
|
-
const emitted = emitEntryNodes(githubUrl, repoInfo, entryText, childrenJson);
|
|
519
|
+
const emitted = emitEntryNodes(githubUrl, repoInfo, entryText, childrenJson, firstSeen);
|
|
512
520
|
entries.push({ emitted, node: itemNode, repoInfo });
|
|
513
521
|
}
|
|
514
522
|
if (sortOptions.by) {
|
|
@@ -536,18 +544,7 @@ function entryTitle(base, ownLink, repoInfo) {
|
|
|
536
544
|
}
|
|
537
545
|
return repoInfo ? `${repoInfo.owner}/${repoInfo.repo}` : base;
|
|
538
546
|
}
|
|
539
|
-
|
|
540
|
-
// scala's `[name](repo) | description`, spec tables with the link in a later
|
|
541
|
-
// column — holds ONE repo per row: the title is the first cell's text (the
|
|
542
|
-
// own link's text, then the repo name, when the first cell is empty), the
|
|
543
|
-
// description the remaining cells' text (badge images carry no text nodes, so
|
|
544
|
-
// they never pollute it). Card grids pin several repos per row, each cell its
|
|
545
|
-
// own card, so those emit one entry per link-bearing cell. A linked row above
|
|
546
|
-
// the delimiter is content like any other (grid tables have no label header;
|
|
547
|
-
// pure-label header rows carry no links and emit nothing). Rows stay in source
|
|
548
|
-
// order — unlike the unranked lists the product sorts, a table's row order is
|
|
549
|
-
// part of its meaning.
|
|
550
|
-
function processTableRows(tableNode, repoInfoMap, gateOpen) {
|
|
547
|
+
function processTableRows(tableNode, repoInfoMap, gateOpen, firstSeen) {
|
|
551
548
|
if (!gateOpen) {
|
|
552
549
|
return [];
|
|
553
550
|
}
|
|
@@ -589,7 +586,7 @@ function processTableRows(tableNode, repoInfoMap, gateOpen) {
|
|
|
589
586
|
items.push(...emitEntryNodes(ownLink.url, repoInfo, {
|
|
590
587
|
title: entryTitle(titleText.title, ownLink, repoInfo),
|
|
591
588
|
description,
|
|
592
|
-
}, []));
|
|
589
|
+
}, [], firstSeen));
|
|
593
590
|
continue;
|
|
594
591
|
}
|
|
595
592
|
const emittedUrls = new Set();
|
|
@@ -603,34 +600,15 @@ function processTableRows(tableNode, repoInfoMap, gateOpen) {
|
|
|
603
600
|
items.push(...emitEntryNodes(ownLink.url, repoInfo, {
|
|
604
601
|
title: entryTitle(cellText.title, ownLink, repoInfo),
|
|
605
602
|
description: cellText.description,
|
|
606
|
-
}, []));
|
|
603
|
+
}, [], firstSeen));
|
|
607
604
|
}
|
|
608
605
|
}
|
|
609
606
|
return items;
|
|
610
607
|
}
|
|
611
|
-
// A top-level paragraph can BE an entry, not only feed container prose. Two
|
|
612
|
-
// corpus families qualify (progress/empty-tree-parses.md, step 4): a GitHub
|
|
613
|
-
// link LEADING the paragraph behind a short entry label, or the paragraph
|
|
614
|
-
// ending in a tag cluster whose GitHub link carries the identity — the
|
|
615
|
-
// paper-list shape whose first link points at the paper and a [Code]/[Github]
|
|
616
|
-
// tag at the end, name/author lines ending in that tag, and dated lines
|
|
617
|
-
// ("… [Github] 4 Feb 2023"). Prose that mentions a repo ("Please see
|
|
618
|
-
// CONTRIBUTING", "See also [repo]", intro text ending in a bare repo URL) is
|
|
619
|
-
// neither and stays description. Calibrated on the 2,293-README corpus plus
|
|
620
|
-
// the fixture fleet, not intuition: an entry label is a TAG (empty, CJK/emoji,
|
|
621
|
-
// bracketed, or colon-terminated) — never bare English prose — and the
|
|
622
|
-
// identity link must carry text, so the ubiquitous image-only awesome badge
|
|
623
|
-
// is not an entry.
|
|
624
608
|
const ENTRY_LABEL_MAX = 15;
|
|
625
609
|
const ENTRY_TRAILING_MAX = 3;
|
|
626
610
|
const TAG_LINK_TEXT = /^[\[\]()*\s:_-]*(?:source\s+code|code|github|repo|source|src|project|paper|page|web|site|home|official|notebook|demo|data|docs|implementation|arxiv)\b[\[\]()*\s:_-]*$/i;
|
|
627
611
|
const URL_LINK_TEXT = /^https?:\/\/\S+$/i;
|
|
628
|
-
// A title that names nothing — the degenerate families every entry source can
|
|
629
|
-
// emit: a pure number/punctuation run (a table's rank or year column, "15."
|
|
630
|
-
// / "2023" / "2025-05" / "-"), a URL (a link whose label is the URL itself,
|
|
631
|
-
// scheme or scheme-less `github.com/…`), or a bare tag word ("GitHub",
|
|
632
|
-
// "Source code" — best-of's generated lines). CJK labels are letters
|
|
633
|
-
// (`\p{L}`) and stay titles.
|
|
634
612
|
const URL_TITLE = /^(?:[a-z][a-z0-9+.-]*:\/\/|www\.)\S+$|^github\.com\/\S+$/i;
|
|
635
613
|
function isDegenerateTitle(title) {
|
|
636
614
|
const trimmed = title.trim();
|
|
@@ -639,9 +617,6 @@ function isDegenerateTitle(title) {
|
|
|
639
617
|
TAG_LINK_TEXT.test(trimmed) ||
|
|
640
618
|
!/[\p{L}]/u.test(trimmed)));
|
|
641
619
|
}
|
|
642
|
-
// A link label that can serve as a title. The degenerate shapes are excluded
|
|
643
|
-
// twice over — a URL label stays a URL, and a tag label names the link's role
|
|
644
|
-
// ("[Github](repo)"), not the repo.
|
|
645
620
|
function isMeaningfulLinkText(text) {
|
|
646
621
|
return text !== '' && !isDegenerateTitle(text);
|
|
647
622
|
}
|
|
@@ -757,13 +732,13 @@ function blockquoteEntries(blockquote) {
|
|
|
757
732
|
// The shared entry emission for the non-list sources (paragraph entries,
|
|
758
733
|
// blockquote cards): resolve, split, title-fallback — the same decisions the
|
|
759
734
|
// list and table paths make through the same helpers.
|
|
760
|
-
function entryNodesFor(ownLink, inlines, repoInfoMap) {
|
|
735
|
+
function entryNodesFor(ownLink, inlines, repoInfoMap, firstSeen) {
|
|
761
736
|
const repoInfo = repoInfoMap.get(ownLink.url) ?? null;
|
|
762
737
|
const entryText = splitEntryText(inlines);
|
|
763
738
|
return emitEntryNodes(ownLink.url, repoInfo, {
|
|
764
739
|
title: entryTitle(entryText.title, ownLink, repoInfo),
|
|
765
740
|
description: entryText.description,
|
|
766
|
-
}, []);
|
|
741
|
+
}, [], firstSeen);
|
|
767
742
|
}
|
|
768
743
|
// The <details><summary>…</summary> collapsible-section idiom: the summary
|
|
769
744
|
// text delimits structure like a heading would (java's generated README,
|
|
@@ -894,7 +869,7 @@ function applyBrandingToTree(tree, title, titleHeadingIndex) {
|
|
|
894
869
|
tree.children.unshift(heading);
|
|
895
870
|
}
|
|
896
871
|
}
|
|
897
|
-
function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo) {
|
|
872
|
+
function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo, firstSeen) {
|
|
898
873
|
// Remember the title H1's index so branding replaces the exact same node;
|
|
899
874
|
// scope matches branding/section-building so they can't drift apart.
|
|
900
875
|
const titleHeadingIndex = findTitleHeadingIndex(tree);
|
|
@@ -925,7 +900,7 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo) {
|
|
|
925
900
|
openImplicitSection(stack, atIndex);
|
|
926
901
|
}
|
|
927
902
|
if (gateForSection(stack[0])) {
|
|
928
|
-
stack[stack.length - 1].children.push(...entryNodesFor(ownLink, inlines, repoInfoMap));
|
|
903
|
+
stack[stack.length - 1].children.push(...entryNodesFor(ownLink, inlines, repoInfoMap, firstSeen));
|
|
929
904
|
}
|
|
930
905
|
};
|
|
931
906
|
// A standalone entry restating the repo of an open item container (crypto's
|
|
@@ -952,7 +927,7 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo) {
|
|
|
952
927
|
const promotedEntry = headingEntry && getInlineText(headingEntry.link.children)
|
|
953
928
|
? headingEntry
|
|
954
929
|
: null;
|
|
955
|
-
closeContainers(stack, node.depth, sectionRecords, !!promotedEntry);
|
|
930
|
+
closeContainers(stack, node.depth, sectionRecords, firstSeen, !!promotedEntry);
|
|
956
931
|
openContainer(stack, node, i, sectionDepth, promotedEntry, headingEntry);
|
|
957
932
|
}
|
|
958
933
|
else if (node.type === 'paragraph') {
|
|
@@ -1004,7 +979,7 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo) {
|
|
|
1004
979
|
// section, which keeps collecting after the collapsible block.
|
|
1005
980
|
const summaryTitle = detailsSummaryTitle(node.value);
|
|
1006
981
|
if (summaryTitle) {
|
|
1007
|
-
closeInnermostDetails(stack, sectionRecords);
|
|
982
|
+
closeInnermostDetails(stack, sectionRecords, firstSeen);
|
|
1008
983
|
// Container depths never decrease going up the stack. When the open
|
|
1009
984
|
// containers sit deeper than sectionDepth (a mid-document H1 defines
|
|
1010
985
|
// sectionDepth while the content sections run deeper), the
|
|
@@ -1025,7 +1000,7 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo) {
|
|
|
1025
1000
|
});
|
|
1026
1001
|
}
|
|
1027
1002
|
else if (DETAILS_CLOSE.test(node.value)) {
|
|
1028
|
-
closeInnermostDetails(stack, sectionRecords);
|
|
1003
|
+
closeInnermostDetails(stack, sectionRecords, firstSeen);
|
|
1029
1004
|
}
|
|
1030
1005
|
}
|
|
1031
1006
|
else if (node.type === 'list') {
|
|
@@ -1039,7 +1014,7 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo) {
|
|
|
1039
1014
|
// Every list inside the open container contributes items — a section is
|
|
1040
1015
|
// not closed by its first list — and the minLinks gate is decided per
|
|
1041
1016
|
// section, against the whole section subtree.
|
|
1042
|
-
const items = processListRecursively(node, repoInfoMap, sortOptions, false, gateForSection(stack[0]));
|
|
1017
|
+
const items = processListRecursively(node, repoInfoMap, sortOptions, firstSeen, false, gateForSection(stack[0]));
|
|
1043
1018
|
stack[stack.length - 1].children.push(...items);
|
|
1044
1019
|
}
|
|
1045
1020
|
else if (node.type === 'table') {
|
|
@@ -1049,11 +1024,11 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo) {
|
|
|
1049
1024
|
if (stack.length === 0) {
|
|
1050
1025
|
openImplicitSection(stack, i);
|
|
1051
1026
|
}
|
|
1052
|
-
const items = processTableRows(node, repoInfoMap, gateForSection(stack[0]));
|
|
1027
|
+
const items = processTableRows(node, repoInfoMap, gateForSection(stack[0]), firstSeen);
|
|
1053
1028
|
stack[stack.length - 1].children.push(...items);
|
|
1054
1029
|
}
|
|
1055
1030
|
}
|
|
1056
|
-
closeContainers(stack, 0, sectionRecords);
|
|
1031
|
+
closeContainers(stack, 0, sectionRecords, firstSeen);
|
|
1057
1032
|
const sections = sectionRecords
|
|
1058
1033
|
.sort((a, b) => a.headingIndex - b.headingIndex)
|
|
1059
1034
|
.map(record => record.section);
|
|
@@ -1314,7 +1289,7 @@ function sectionGatePasses(tree, titleSlotIndex, sectionDepth, repoInfoMap, sort
|
|
|
1314
1289
|
// item-bearing nodes) is dropped; an item always survives, it IS the content.
|
|
1315
1290
|
// Non-section containers always have an open parent (stack invariant), and
|
|
1316
1291
|
// kind === 'item' exactly when repoInfo is set.
|
|
1317
|
-
function finalizeContainer(container, stack, sectionRecords) {
|
|
1292
|
+
function finalizeContainer(container, stack, sectionRecords, firstSeen) {
|
|
1318
1293
|
if (container.children.length === 0 && container.kind !== 'item') {
|
|
1319
1294
|
return;
|
|
1320
1295
|
}
|
|
@@ -1334,6 +1309,7 @@ function finalizeContainer(container, stack, sectionRecords) {
|
|
|
1334
1309
|
node_type: 'item',
|
|
1335
1310
|
repo_info: toRepoInfo(container.repoInfo),
|
|
1336
1311
|
title: container.title,
|
|
1312
|
+
first_seen: firstSeen(container.repoInfo.id),
|
|
1337
1313
|
});
|
|
1338
1314
|
}
|
|
1339
1315
|
else {
|
|
@@ -1351,13 +1327,13 @@ function finalizeContainer(container, stack, sectionRecords) {
|
|
|
1351
1327
|
// group/item always has a parent to land in. stopAtSynthesized: the pop an
|
|
1352
1328
|
// entry heading triggers must stop at the synthesized section wrapping its
|
|
1353
1329
|
// run — the next entry heading of the run lands back inside it.
|
|
1354
|
-
function closeContainers(stack, depth, sectionRecords, stopAtSynthesized = false) {
|
|
1330
|
+
function closeContainers(stack, depth, sectionRecords, firstSeen, stopAtSynthesized = false) {
|
|
1355
1331
|
while (stack.length > 0 &&
|
|
1356
1332
|
stack[stack.length - 1].headingDepth >= depth) {
|
|
1357
1333
|
if (stopAtSynthesized && stack[stack.length - 1].openedBySynthesis) {
|
|
1358
1334
|
break;
|
|
1359
1335
|
}
|
|
1360
|
-
finalizeContainer(stack.pop(), stack, sectionRecords);
|
|
1336
|
+
finalizeContainer(stack.pop(), stack, sectionRecords, firstSeen);
|
|
1361
1337
|
}
|
|
1362
1338
|
}
|
|
1363
1339
|
// Ends the innermost open details-section and everything opened inside it,
|
|
@@ -1365,7 +1341,7 @@ function closeContainers(stack, depth, sectionRecords, stopAtSynthesized = false
|
|
|
1365
1341
|
// details boundary never ends its parent section, so content after the
|
|
1366
1342
|
// collapsible block keeps collecting under it. A stray </details> (no
|
|
1367
1343
|
// details-section open) is a no-op.
|
|
1368
|
-
function closeInnermostDetails(stack, sectionRecords) {
|
|
1344
|
+
function closeInnermostDetails(stack, sectionRecords, firstSeen) {
|
|
1369
1345
|
let detailsIndex = -1;
|
|
1370
1346
|
for (let s = stack.length - 1; s >= 0; s--) {
|
|
1371
1347
|
if (stack[s].openedByDetails) {
|
|
@@ -1377,7 +1353,7 @@ function closeInnermostDetails(stack, sectionRecords) {
|
|
|
1377
1353
|
return;
|
|
1378
1354
|
}
|
|
1379
1355
|
while (stack.length > detailsIndex) {
|
|
1380
|
-
finalizeContainer(stack.pop(), stack, sectionRecords);
|
|
1356
|
+
finalizeContainer(stack.pop(), stack, sectionRecords, firstSeen);
|
|
1381
1357
|
}
|
|
1382
1358
|
}
|
|
1383
1359
|
function serializeAst(tree, originalContent) {
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -6,11 +6,11 @@ export interface EnhanceOptions {
|
|
|
6
6
|
disableBranding?: boolean;
|
|
7
7
|
enhancedRepository?: string;
|
|
8
8
|
enhancedRepositoryDescription?: string;
|
|
9
|
-
/** Defaults to the console sink; pass your own (e.g. an Actions workflow-command sink) to route diagnostics. */
|
|
10
9
|
log?: Logger;
|
|
11
10
|
now?: Date;
|
|
12
11
|
originalRepositoryInfo?: null | RepoInfoDetails;
|
|
13
12
|
originalRepositorySha?: string;
|
|
13
|
+
previousJson?: JsonOutput;
|
|
14
14
|
relativeLinkPrefix?: string;
|
|
15
15
|
replacements?: ReplacementRule[];
|
|
16
16
|
sortBy?: '' | 'last_commit' | 'stars';
|
package/dist/orchestrator.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { processMarkdownContent, } from './markdown.js';
|
|
2
2
|
export async function enhance(options) {
|
|
3
|
-
const { content, disableBranding = false, log, now = new Date(), originalRepositoryInfo, originalRepositorySha, relativeLinkPrefix = '', replacements = [], sortBy = '', enhancedRepository, enhancedRepositoryDescription, token, } = options;
|
|
3
|
+
const { content, disableBranding = false, log, now = new Date(), originalRepositoryInfo, originalRepositorySha, previousJson, relativeLinkPrefix = '', replacements = [], sortBy = '', enhancedRepository, enhancedRepositoryDescription, token, } = options;
|
|
4
4
|
// Branding is an internal rule prepended to the caller's own; build a fresh
|
|
5
5
|
// array so the caller's `replacements` is never mutated.
|
|
6
6
|
const branding = { type: 'branding' };
|
|
@@ -9,7 +9,7 @@ export async function enhance(options) {
|
|
|
9
9
|
by: sortBy,
|
|
10
10
|
minLinks: 2,
|
|
11
11
|
};
|
|
12
|
-
const { finalContent, jsonData } = await processMarkdownContent(content, token, rules, sortOptions, relativeLinkPrefix, enhancedRepository, enhancedRepositoryDescription, originalRepositorySha, originalRepositoryInfo, now, log);
|
|
12
|
+
const { finalContent, jsonData } = await processMarkdownContent(content, token, rules, sortOptions, relativeLinkPrefix, enhancedRepository, enhancedRepositoryDescription, originalRepositorySha, originalRepositoryInfo, previousJson, now, log);
|
|
13
13
|
return {
|
|
14
14
|
finalContent,
|
|
15
15
|
jsonData,
|