@enhansome/core 1.10.0 → 1.10.2
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 +5 -4
- package/dist/github.js +7 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/markdown.d.ts +3 -3
- package/dist/markdown.js +34 -83
- package/dist/orchestrator.d.ts +3 -4
- 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
|
@@ -77,11 +77,12 @@ export interface MakeOctokitOptions {
|
|
|
77
77
|
export declare function makeOctokit(token: string, { log, maxRetries, maxWaitSeconds, throttle, }?: MakeOctokitOptions): GithubClient;
|
|
78
78
|
export declare function getRepoInfo(octokit: GithubClient, owner: string, repo: string): Promise<RepoInfoDetails>;
|
|
79
79
|
/**
|
|
80
|
-
* The repo's
|
|
81
|
-
* node ids on
|
|
82
|
-
* metadata
|
|
80
|
+
* The repo's full info, for the JSON metadata — the numeric id consumers key
|
|
81
|
+
* stable node ids on, plus the stars/language/last_commit the root's own
|
|
82
|
+
* metadata row is built from. Returns null (logged) instead of failing the
|
|
83
|
+
* run: metadata is not a gate; consumers fall back when it is missing.
|
|
83
84
|
*/
|
|
84
|
-
export declare function
|
|
85
|
+
export declare function getRepoInfoOrNull(octokit: GithubClient, owner: string, repo: string): Promise<null | RepoInfoDetails>;
|
|
85
86
|
export declare function getReadme(octokit: GithubClient, owner: string, repo: string, format?: 'html' | 'raw'): Promise<string>;
|
|
86
87
|
/** Root file/directory names — the compile-manifest gate reads them to tell a
|
|
87
88
|
* directory of resources from a repo that IS the deliverable. A `path: ''`
|
package/dist/github.js
CHANGED
|
@@ -83,16 +83,17 @@ export async function getRepoInfo(octokit, owner, repo) {
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
|
-
* The repo's
|
|
87
|
-
* node ids on
|
|
88
|
-
* metadata
|
|
86
|
+
* The repo's full info, for the JSON metadata — the numeric id consumers key
|
|
87
|
+
* stable node ids on, plus the stars/language/last_commit the root's own
|
|
88
|
+
* metadata row is built from. Returns null (logged) instead of failing the
|
|
89
|
+
* run: metadata is not a gate; consumers fall back when it is missing.
|
|
89
90
|
*/
|
|
90
|
-
export async function
|
|
91
|
+
export async function getRepoInfoOrNull(octokit, owner, repo) {
|
|
91
92
|
try {
|
|
92
|
-
return
|
|
93
|
+
return await getRepoInfo(octokit, owner, repo);
|
|
93
94
|
}
|
|
94
95
|
catch (error) {
|
|
95
|
-
octokit.log.error(`Failed to fetch repo
|
|
96
|
+
octokit.log.error(`Failed to fetch repo info for ${owner}/${repo}: ${formatRequestError(error)}`);
|
|
96
97
|
return null;
|
|
97
98
|
}
|
|
98
99
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { formatRequestError, getLatestCommitSha, getReadme,
|
|
1
|
+
export { formatRequestError, getLatestCommitSha, getReadme, getRepoInfo, getRepoInfoOrNull, getRootEntryNames, makeOctokit, parseGitHubUrl, parseOwnerRepo, } from './github.js';
|
|
2
2
|
export type { GithubClient, MakeOctokitOptions, RepoIdentifier, RepoInfoDetails, ThrottleOptions, } from './github.js';
|
|
3
3
|
export type { Logger } from './logger.js';
|
|
4
4
|
export { consoleLog, silentLog } from './logger.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { formatRequestError, getLatestCommitSha, getReadme,
|
|
1
|
+
export { formatRequestError, getLatestCommitSha, getReadme, getRepoInfo, getRepoInfoOrNull, getRootEntryNames, makeOctokit, parseGitHubUrl, parseOwnerRepo, } from './github.js';
|
|
2
2
|
export { consoleLog, silentLog } from './logger.js';
|
|
3
3
|
export { toRepoInfo } from './markdown.js';
|
|
4
4
|
export { enhance } from './orchestrator.js';
|
package/dist/markdown.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export declare function toRepoInfo(details: RepoInfoDetails): RepoInfo;
|
|
|
30
30
|
export interface JsonItem {
|
|
31
31
|
children: JsonNode[];
|
|
32
32
|
description: null | string;
|
|
33
|
+
first_seen?: string;
|
|
33
34
|
node_type: 'item';
|
|
34
35
|
repo_info: RepoInfo;
|
|
35
36
|
title: string;
|
|
@@ -45,8 +46,7 @@ export interface JsonMetadata {
|
|
|
45
46
|
enhanced_repository: null | string;
|
|
46
47
|
enhanced_repository_description: null | string;
|
|
47
48
|
last_updated: string;
|
|
48
|
-
|
|
49
|
-
original_repository_id: null | number;
|
|
49
|
+
original_repository_info: null | RepoInfo;
|
|
50
50
|
original_repository_sha: null | string;
|
|
51
51
|
title: string;
|
|
52
52
|
}
|
|
@@ -55,7 +55,7 @@ export interface JsonSection {
|
|
|
55
55
|
items: JsonNode[];
|
|
56
56
|
title: string;
|
|
57
57
|
}
|
|
58
|
-
export declare function processMarkdownContent(originalContent: string, token: string, replacements
|
|
58
|
+
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<{
|
|
59
59
|
finalContent: string;
|
|
60
60
|
jsonData: JsonOutput;
|
|
61
61
|
}>;
|
package/dist/markdown.js
CHANGED
|
@@ -4,6 +4,7 @@ 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. */
|
|
@@ -79,7 +80,7 @@ async function fetchTargetData(urls, repos) {
|
|
|
79
80
|
log.info(`Target fetch: ${repoInfoMap.size}/${urls.size} repo-info ok in ${Date.now() - fetchStart}ms (concurrency ${FETCH_CONCURRENCY}).`);
|
|
80
81
|
return repoInfoMap;
|
|
81
82
|
}
|
|
82
|
-
export async function processMarkdownContent(originalContent, token, replacements = [], sortOptions = { by: '', minLinks: 2 },
|
|
83
|
+
export async function processMarkdownContent(originalContent, token, replacements = [], sortOptions = { by: '', minLinks: 2 }, relativeLinkPrefix = '', enhancedRepository, enhancedRepositoryDescription, originalRepositorySha, originalRepositoryInfo, previousJson, now = new Date(), log = consoleLog) {
|
|
83
84
|
const repos = createRepoInfoLookup(token, log);
|
|
84
85
|
const brandingEnabled = replacements.some(rule => rule.type === 'branding');
|
|
85
86
|
const contentAfterReplacements = applyTextReplacements(originalContent, replacements.filter(rule => rule.type !== 'branding'), log);
|
|
@@ -88,9 +89,8 @@ export async function processMarkdownContent(originalContent, token, replacement
|
|
|
88
89
|
normalizeGitHubUrls(tree);
|
|
89
90
|
const githubUrls = collectGitHubLinks(tree);
|
|
90
91
|
const repoInfoMap = await fetchTargetData(githubUrls, repos);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const { sections, title: rawTitle, titleHeadingIndex, } = processTree(tree, repoInfoMap, sortOptions, originalRepository);
|
|
92
|
+
const firstSeen = firstSeenFor(previousJson, now);
|
|
93
|
+
const { sections, title: rawTitle, titleHeadingIndex, } = processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo, firstSeen);
|
|
94
94
|
// Single source of truth for the document title: brand it once and use the
|
|
95
95
|
// same value for the markdown H1 and metadata.title (parity).
|
|
96
96
|
const title = brandingEnabled ? brandTitle(rawTitle) : rawTitle;
|
|
@@ -99,8 +99,9 @@ export async function processMarkdownContent(originalContent, token, replacement
|
|
|
99
99
|
}
|
|
100
100
|
const metadata = {
|
|
101
101
|
last_updated: now.toISOString(),
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
original_repository_info: originalRepositoryInfo
|
|
103
|
+
? toRepoInfo(originalRepositoryInfo)
|
|
104
|
+
: null,
|
|
104
105
|
original_repository_sha: (originalRepositorySha?.trim() ?? '') || null,
|
|
105
106
|
enhanced_repository: (enhancedRepository?.trim() ?? '') || null,
|
|
106
107
|
enhanced_repository_description: (enhancedRepositoryDescription?.trim() ?? '') || null,
|
|
@@ -440,7 +441,7 @@ function splitEntryText(inlines) {
|
|
|
440
441
|
// group — never a `repo_info`, that's the identity-borrowing bug; no own link
|
|
441
442
|
// and no children is a non-GitHub leaf, kept in markdown, dropped from JSON.
|
|
442
443
|
// TODO(future): preserve non-GitHub leaves in a separate shape.
|
|
443
|
-
function emitEntryNodes(githubUrl, repoInfo, text, childrenJson) {
|
|
444
|
+
function emitEntryNodes(githubUrl, repoInfo, text, childrenJson, firstSeen) {
|
|
444
445
|
if (githubUrl && repoInfo) {
|
|
445
446
|
return [
|
|
446
447
|
{
|
|
@@ -449,6 +450,7 @@ function emitEntryNodes(githubUrl, repoInfo, text, childrenJson) {
|
|
|
449
450
|
description: text.description || null,
|
|
450
451
|
children: childrenJson,
|
|
451
452
|
repo_info: toRepoInfo(repoInfo),
|
|
453
|
+
first_seen: firstSeen(repoInfo.id),
|
|
452
454
|
},
|
|
453
455
|
];
|
|
454
456
|
}
|
|
@@ -467,7 +469,7 @@ function emitEntryNodes(githubUrl, repoInfo, text, childrenJson) {
|
|
|
467
469
|
}
|
|
468
470
|
return [];
|
|
469
471
|
}
|
|
470
|
-
function processListRecursively(listNode, repoInfoMap, sortOptions, isNested = false,
|
|
472
|
+
function processListRecursively(listNode, repoInfoMap, sortOptions, firstSeen, isNested = false,
|
|
471
473
|
// The caller's section-scope gate decision (sectionGatePasses). Absent for
|
|
472
474
|
// nested lists (emitted under their parent regardless) and for top-level
|
|
473
475
|
// lists with no open container (preamble), which gate per list.
|
|
@@ -494,8 +496,8 @@ sectionGateOpen) {
|
|
|
494
496
|
// regardless of the gate: the top-level call already gated the section.
|
|
495
497
|
const nestedContent = itemNode.children.filter((child) => child.type === 'list' || child.type === 'table');
|
|
496
498
|
const childrenJson = nestedContent.flatMap(child => child.type === 'list'
|
|
497
|
-
? processListRecursively(child, repoInfoMap, sortOptions, true)
|
|
498
|
-
: processTableRows(child, repoInfoMap, true));
|
|
499
|
+
? processListRecursively(child, repoInfoMap, sortOptions, firstSeen, true)
|
|
500
|
+
: processTableRows(child, repoInfoMap, true, firstSeen));
|
|
499
501
|
// Title/description split on the FIRST paragraph only — a paper-list
|
|
500
502
|
// entry's identity link may live in a later paragraph (findOwnGitHubLink)
|
|
501
503
|
// while its title text stays the leading one.
|
|
@@ -509,7 +511,7 @@ sectionGateOpen) {
|
|
|
509
511
|
// The shared title fallbacks (an inline-code link label carries no text
|
|
510
512
|
// nodes, so the split alone can leave an empty title).
|
|
511
513
|
entryText.title = entryTitle(entryText.title, ownLink, repoInfo);
|
|
512
|
-
const emitted = emitEntryNodes(githubUrl, repoInfo, entryText, childrenJson);
|
|
514
|
+
const emitted = emitEntryNodes(githubUrl, repoInfo, entryText, childrenJson, firstSeen);
|
|
513
515
|
entries.push({ emitted, node: itemNode, repoInfo });
|
|
514
516
|
}
|
|
515
517
|
if (sortOptions.by) {
|
|
@@ -537,18 +539,7 @@ function entryTitle(base, ownLink, repoInfo) {
|
|
|
537
539
|
}
|
|
538
540
|
return repoInfo ? `${repoInfo.owner}/${repoInfo.repo}` : base;
|
|
539
541
|
}
|
|
540
|
-
|
|
541
|
-
// scala's `[name](repo) | description`, spec tables with the link in a later
|
|
542
|
-
// column — holds ONE repo per row: the title is the first cell's text (the
|
|
543
|
-
// own link's text, then the repo name, when the first cell is empty), the
|
|
544
|
-
// description the remaining cells' text (badge images carry no text nodes, so
|
|
545
|
-
// they never pollute it). Card grids pin several repos per row, each cell its
|
|
546
|
-
// own card, so those emit one entry per link-bearing cell. A linked row above
|
|
547
|
-
// the delimiter is content like any other (grid tables have no label header;
|
|
548
|
-
// pure-label header rows carry no links and emit nothing). Rows stay in source
|
|
549
|
-
// order — unlike the unranked lists the product sorts, a table's row order is
|
|
550
|
-
// part of its meaning.
|
|
551
|
-
function processTableRows(tableNode, repoInfoMap, gateOpen) {
|
|
542
|
+
function processTableRows(tableNode, repoInfoMap, gateOpen, firstSeen) {
|
|
552
543
|
if (!gateOpen) {
|
|
553
544
|
return [];
|
|
554
545
|
}
|
|
@@ -590,7 +581,7 @@ function processTableRows(tableNode, repoInfoMap, gateOpen) {
|
|
|
590
581
|
items.push(...emitEntryNodes(ownLink.url, repoInfo, {
|
|
591
582
|
title: entryTitle(titleText.title, ownLink, repoInfo),
|
|
592
583
|
description,
|
|
593
|
-
}, []));
|
|
584
|
+
}, [], firstSeen));
|
|
594
585
|
continue;
|
|
595
586
|
}
|
|
596
587
|
const emittedUrls = new Set();
|
|
@@ -604,34 +595,15 @@ function processTableRows(tableNode, repoInfoMap, gateOpen) {
|
|
|
604
595
|
items.push(...emitEntryNodes(ownLink.url, repoInfo, {
|
|
605
596
|
title: entryTitle(cellText.title, ownLink, repoInfo),
|
|
606
597
|
description: cellText.description,
|
|
607
|
-
}, []));
|
|
598
|
+
}, [], firstSeen));
|
|
608
599
|
}
|
|
609
600
|
}
|
|
610
601
|
return items;
|
|
611
602
|
}
|
|
612
|
-
// A top-level paragraph can BE an entry, not only feed container prose. Two
|
|
613
|
-
// corpus families qualify (progress/empty-tree-parses.md, step 4): a GitHub
|
|
614
|
-
// link LEADING the paragraph behind a short entry label, or the paragraph
|
|
615
|
-
// ending in a tag cluster whose GitHub link carries the identity — the
|
|
616
|
-
// paper-list shape whose first link points at the paper and a [Code]/[Github]
|
|
617
|
-
// tag at the end, name/author lines ending in that tag, and dated lines
|
|
618
|
-
// ("… [Github] 4 Feb 2023"). Prose that mentions a repo ("Please see
|
|
619
|
-
// CONTRIBUTING", "See also [repo]", intro text ending in a bare repo URL) is
|
|
620
|
-
// neither and stays description. Calibrated on the 2,293-README corpus plus
|
|
621
|
-
// the fixture fleet, not intuition: an entry label is a TAG (empty, CJK/emoji,
|
|
622
|
-
// bracketed, or colon-terminated) — never bare English prose — and the
|
|
623
|
-
// identity link must carry text, so the ubiquitous image-only awesome badge
|
|
624
|
-
// is not an entry.
|
|
625
603
|
const ENTRY_LABEL_MAX = 15;
|
|
626
604
|
const ENTRY_TRAILING_MAX = 3;
|
|
627
605
|
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;
|
|
628
606
|
const URL_LINK_TEXT = /^https?:\/\/\S+$/i;
|
|
629
|
-
// A title that names nothing — the degenerate families every entry source can
|
|
630
|
-
// emit: a pure number/punctuation run (a table's rank or year column, "15."
|
|
631
|
-
// / "2023" / "2025-05" / "-"), a URL (a link whose label is the URL itself,
|
|
632
|
-
// scheme or scheme-less `github.com/…`), or a bare tag word ("GitHub",
|
|
633
|
-
// "Source code" — best-of's generated lines). CJK labels are letters
|
|
634
|
-
// (`\p{L}`) and stay titles.
|
|
635
607
|
const URL_TITLE = /^(?:[a-z][a-z0-9+.-]*:\/\/|www\.)\S+$|^github\.com\/\S+$/i;
|
|
636
608
|
function isDegenerateTitle(title) {
|
|
637
609
|
const trimmed = title.trim();
|
|
@@ -640,9 +612,6 @@ function isDegenerateTitle(title) {
|
|
|
640
612
|
TAG_LINK_TEXT.test(trimmed) ||
|
|
641
613
|
!/[\p{L}]/u.test(trimmed)));
|
|
642
614
|
}
|
|
643
|
-
// A link label that can serve as a title. The degenerate shapes are excluded
|
|
644
|
-
// twice over — a URL label stays a URL, and a tag label names the link's role
|
|
645
|
-
// ("[Github](repo)"), not the repo.
|
|
646
615
|
function isMeaningfulLinkText(text) {
|
|
647
616
|
return text !== '' && !isDegenerateTitle(text);
|
|
648
617
|
}
|
|
@@ -758,13 +727,13 @@ function blockquoteEntries(blockquote) {
|
|
|
758
727
|
// The shared entry emission for the non-list sources (paragraph entries,
|
|
759
728
|
// blockquote cards): resolve, split, title-fallback — the same decisions the
|
|
760
729
|
// list and table paths make through the same helpers.
|
|
761
|
-
function entryNodesFor(ownLink, inlines, repoInfoMap) {
|
|
730
|
+
function entryNodesFor(ownLink, inlines, repoInfoMap, firstSeen) {
|
|
762
731
|
const repoInfo = repoInfoMap.get(ownLink.url) ?? null;
|
|
763
732
|
const entryText = splitEntryText(inlines);
|
|
764
733
|
return emitEntryNodes(ownLink.url, repoInfo, {
|
|
765
734
|
title: entryTitle(entryText.title, ownLink, repoInfo),
|
|
766
735
|
description: entryText.description,
|
|
767
|
-
}, []);
|
|
736
|
+
}, [], firstSeen);
|
|
768
737
|
}
|
|
769
738
|
// The <details><summary>…</summary> collapsible-section idiom: the summary
|
|
770
739
|
// text delimits structure like a heading would (java's generated README,
|
|
@@ -812,22 +781,6 @@ const INVALID_TITLE_PATTERNS = [
|
|
|
812
781
|
/^video$/i,
|
|
813
782
|
/^science/i,
|
|
814
783
|
];
|
|
815
|
-
function repoNameFromIdentifier(identifier) {
|
|
816
|
-
const trimmed = identifier.trim();
|
|
817
|
-
if (!trimmed) {
|
|
818
|
-
return '';
|
|
819
|
-
}
|
|
820
|
-
if (/^https?:\/\//i.test(trimmed)) {
|
|
821
|
-
const parts = trimmed
|
|
822
|
-
.replace(/\.git$/i, '')
|
|
823
|
-
.replace(/[?#].*$/, '')
|
|
824
|
-
.split('/')
|
|
825
|
-
.filter(Boolean);
|
|
826
|
-
return parts[parts.length - 1] ?? '';
|
|
827
|
-
}
|
|
828
|
-
const slashIndex = trimmed.lastIndexOf('/');
|
|
829
|
-
return slashIndex === -1 ? trimmed : trimmed.slice(slashIndex + 1);
|
|
830
|
-
}
|
|
831
784
|
function formatRepoNameAsTitle(repoName) {
|
|
832
785
|
const cleaned = repoName.replace(/[-_]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
833
786
|
if (/^awesome\s+/i.test(cleaned)) {
|
|
@@ -911,7 +864,7 @@ function applyBrandingToTree(tree, title, titleHeadingIndex) {
|
|
|
911
864
|
tree.children.unshift(heading);
|
|
912
865
|
}
|
|
913
866
|
}
|
|
914
|
-
function processTree(tree, repoInfoMap, sortOptions,
|
|
867
|
+
function processTree(tree, repoInfoMap, sortOptions, originalRepositoryInfo, firstSeen) {
|
|
915
868
|
// Remember the title H1's index so branding replaces the exact same node;
|
|
916
869
|
// scope matches branding/section-building so they can't drift apart.
|
|
917
870
|
const titleHeadingIndex = findTitleHeadingIndex(tree);
|
|
@@ -922,11 +875,8 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepository) {
|
|
|
922
875
|
// Derive a subject from the *source* repository name when no valid H1 is
|
|
923
876
|
// present. Using the source — not the enhanced/mirror repo — keeps the org
|
|
924
877
|
// name out of the title.
|
|
925
|
-
if (documentTitle === '' &&
|
|
926
|
-
|
|
927
|
-
if (repoName) {
|
|
928
|
-
documentTitle = formatRepoNameAsTitle(repoName);
|
|
929
|
-
}
|
|
878
|
+
if (documentTitle === '' && originalRepositoryInfo) {
|
|
879
|
+
documentTitle = formatRepoNameAsTitle(originalRepositoryInfo.repo);
|
|
930
880
|
}
|
|
931
881
|
const sectionDepth = findSectionDepth(tree, titleSlotIndex);
|
|
932
882
|
const gateForSection = sectionGatePasses(tree, titleSlotIndex, sectionDepth, repoInfoMap, sortOptions);
|
|
@@ -945,7 +895,7 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepository) {
|
|
|
945
895
|
openImplicitSection(stack, atIndex);
|
|
946
896
|
}
|
|
947
897
|
if (gateForSection(stack[0])) {
|
|
948
|
-
stack[stack.length - 1].children.push(...entryNodesFor(ownLink, inlines, repoInfoMap));
|
|
898
|
+
stack[stack.length - 1].children.push(...entryNodesFor(ownLink, inlines, repoInfoMap, firstSeen));
|
|
949
899
|
}
|
|
950
900
|
};
|
|
951
901
|
// A standalone entry restating the repo of an open item container (crypto's
|
|
@@ -972,7 +922,7 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepository) {
|
|
|
972
922
|
const promotedEntry = headingEntry && getInlineText(headingEntry.link.children)
|
|
973
923
|
? headingEntry
|
|
974
924
|
: null;
|
|
975
|
-
closeContainers(stack, node.depth, sectionRecords, !!promotedEntry);
|
|
925
|
+
closeContainers(stack, node.depth, sectionRecords, firstSeen, !!promotedEntry);
|
|
976
926
|
openContainer(stack, node, i, sectionDepth, promotedEntry, headingEntry);
|
|
977
927
|
}
|
|
978
928
|
else if (node.type === 'paragraph') {
|
|
@@ -1024,7 +974,7 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepository) {
|
|
|
1024
974
|
// section, which keeps collecting after the collapsible block.
|
|
1025
975
|
const summaryTitle = detailsSummaryTitle(node.value);
|
|
1026
976
|
if (summaryTitle) {
|
|
1027
|
-
closeInnermostDetails(stack, sectionRecords);
|
|
977
|
+
closeInnermostDetails(stack, sectionRecords, firstSeen);
|
|
1028
978
|
// Container depths never decrease going up the stack. When the open
|
|
1029
979
|
// containers sit deeper than sectionDepth (a mid-document H1 defines
|
|
1030
980
|
// sectionDepth while the content sections run deeper), the
|
|
@@ -1045,7 +995,7 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepository) {
|
|
|
1045
995
|
});
|
|
1046
996
|
}
|
|
1047
997
|
else if (DETAILS_CLOSE.test(node.value)) {
|
|
1048
|
-
closeInnermostDetails(stack, sectionRecords);
|
|
998
|
+
closeInnermostDetails(stack, sectionRecords, firstSeen);
|
|
1049
999
|
}
|
|
1050
1000
|
}
|
|
1051
1001
|
else if (node.type === 'list') {
|
|
@@ -1059,7 +1009,7 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepository) {
|
|
|
1059
1009
|
// Every list inside the open container contributes items — a section is
|
|
1060
1010
|
// not closed by its first list — and the minLinks gate is decided per
|
|
1061
1011
|
// section, against the whole section subtree.
|
|
1062
|
-
const items = processListRecursively(node, repoInfoMap, sortOptions, false, gateForSection(stack[0]));
|
|
1012
|
+
const items = processListRecursively(node, repoInfoMap, sortOptions, firstSeen, false, gateForSection(stack[0]));
|
|
1063
1013
|
stack[stack.length - 1].children.push(...items);
|
|
1064
1014
|
}
|
|
1065
1015
|
else if (node.type === 'table') {
|
|
@@ -1069,11 +1019,11 @@ function processTree(tree, repoInfoMap, sortOptions, originalRepository) {
|
|
|
1069
1019
|
if (stack.length === 0) {
|
|
1070
1020
|
openImplicitSection(stack, i);
|
|
1071
1021
|
}
|
|
1072
|
-
const items = processTableRows(node, repoInfoMap, gateForSection(stack[0]));
|
|
1022
|
+
const items = processTableRows(node, repoInfoMap, gateForSection(stack[0]), firstSeen);
|
|
1073
1023
|
stack[stack.length - 1].children.push(...items);
|
|
1074
1024
|
}
|
|
1075
1025
|
}
|
|
1076
|
-
closeContainers(stack, 0, sectionRecords);
|
|
1026
|
+
closeContainers(stack, 0, sectionRecords, firstSeen);
|
|
1077
1027
|
const sections = sectionRecords
|
|
1078
1028
|
.sort((a, b) => a.headingIndex - b.headingIndex)
|
|
1079
1029
|
.map(record => record.section);
|
|
@@ -1334,7 +1284,7 @@ function sectionGatePasses(tree, titleSlotIndex, sectionDepth, repoInfoMap, sort
|
|
|
1334
1284
|
// item-bearing nodes) is dropped; an item always survives, it IS the content.
|
|
1335
1285
|
// Non-section containers always have an open parent (stack invariant), and
|
|
1336
1286
|
// kind === 'item' exactly when repoInfo is set.
|
|
1337
|
-
function finalizeContainer(container, stack, sectionRecords) {
|
|
1287
|
+
function finalizeContainer(container, stack, sectionRecords, firstSeen) {
|
|
1338
1288
|
if (container.children.length === 0 && container.kind !== 'item') {
|
|
1339
1289
|
return;
|
|
1340
1290
|
}
|
|
@@ -1354,6 +1304,7 @@ function finalizeContainer(container, stack, sectionRecords) {
|
|
|
1354
1304
|
node_type: 'item',
|
|
1355
1305
|
repo_info: toRepoInfo(container.repoInfo),
|
|
1356
1306
|
title: container.title,
|
|
1307
|
+
first_seen: firstSeen(container.repoInfo.id),
|
|
1357
1308
|
});
|
|
1358
1309
|
}
|
|
1359
1310
|
else {
|
|
@@ -1371,13 +1322,13 @@ function finalizeContainer(container, stack, sectionRecords) {
|
|
|
1371
1322
|
// group/item always has a parent to land in. stopAtSynthesized: the pop an
|
|
1372
1323
|
// entry heading triggers must stop at the synthesized section wrapping its
|
|
1373
1324
|
// run — the next entry heading of the run lands back inside it.
|
|
1374
|
-
function closeContainers(stack, depth, sectionRecords, stopAtSynthesized = false) {
|
|
1325
|
+
function closeContainers(stack, depth, sectionRecords, firstSeen, stopAtSynthesized = false) {
|
|
1375
1326
|
while (stack.length > 0 &&
|
|
1376
1327
|
stack[stack.length - 1].headingDepth >= depth) {
|
|
1377
1328
|
if (stopAtSynthesized && stack[stack.length - 1].openedBySynthesis) {
|
|
1378
1329
|
break;
|
|
1379
1330
|
}
|
|
1380
|
-
finalizeContainer(stack.pop(), stack, sectionRecords);
|
|
1331
|
+
finalizeContainer(stack.pop(), stack, sectionRecords, firstSeen);
|
|
1381
1332
|
}
|
|
1382
1333
|
}
|
|
1383
1334
|
// Ends the innermost open details-section and everything opened inside it,
|
|
@@ -1385,7 +1336,7 @@ function closeContainers(stack, depth, sectionRecords, stopAtSynthesized = false
|
|
|
1385
1336
|
// details boundary never ends its parent section, so content after the
|
|
1386
1337
|
// collapsible block keeps collecting under it. A stray </details> (no
|
|
1387
1338
|
// details-section open) is a no-op.
|
|
1388
|
-
function closeInnermostDetails(stack, sectionRecords) {
|
|
1339
|
+
function closeInnermostDetails(stack, sectionRecords, firstSeen) {
|
|
1389
1340
|
let detailsIndex = -1;
|
|
1390
1341
|
for (let s = stack.length - 1; s >= 0; s--) {
|
|
1391
1342
|
if (stack[s].openedByDetails) {
|
|
@@ -1397,7 +1348,7 @@ function closeInnermostDetails(stack, sectionRecords) {
|
|
|
1397
1348
|
return;
|
|
1398
1349
|
}
|
|
1399
1350
|
while (stack.length > detailsIndex) {
|
|
1400
|
-
finalizeContainer(stack.pop(), stack, sectionRecords);
|
|
1351
|
+
finalizeContainer(stack.pop(), stack, sectionRecords, firstSeen);
|
|
1401
1352
|
}
|
|
1402
1353
|
}
|
|
1403
1354
|
function serializeAst(tree, originalContent) {
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { Logger } from './logger.js';
|
|
2
|
+
import type { RepoInfoDetails } from './github.js';
|
|
2
3
|
import { JsonOutput, ReplacementRule } from './markdown.js';
|
|
3
4
|
export interface EnhanceOptions {
|
|
4
5
|
content: string;
|
|
5
6
|
disableBranding?: boolean;
|
|
6
7
|
enhancedRepository?: string;
|
|
7
8
|
enhancedRepositoryDescription?: string;
|
|
8
|
-
/** Defaults to the console sink; pass your own (e.g. an Actions workflow-command sink) to route diagnostics. */
|
|
9
9
|
log?: Logger;
|
|
10
10
|
now?: Date;
|
|
11
|
-
|
|
12
|
-
originalRepositoryId?: number;
|
|
11
|
+
originalRepositoryInfo?: null | RepoInfoDetails;
|
|
13
12
|
originalRepositorySha?: string;
|
|
13
|
+
previousJson?: JsonOutput;
|
|
14
14
|
relativeLinkPrefix?: string;
|
|
15
|
-
/** Text substitutions applied to the source before it is parsed. */
|
|
16
15
|
replacements?: ReplacementRule[];
|
|
17
16
|
sortBy?: '' | 'last_commit' | 'stars';
|
|
18
17
|
token: string;
|
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(),
|
|
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,
|
|
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,
|