@mintlify/scraping 4.0.1160 → 4.0.1162

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.
@@ -3,6 +3,7 @@ import fse from 'fs-extra';
3
3
  import path from 'node:path';
4
4
 
5
5
  import { escapeMdxText, markdownToMdx } from '../convert.js';
6
+ import { inlineCode, linkedCode, responseField, truncate } from '../mdx.js';
6
7
  import type { SdkNavGroup, SdkPage, SdkPageTarget, SdkReference } from '../types.js';
7
8
 
8
9
  const KIND = {
@@ -489,7 +490,7 @@ class TypedocRenderer {
489
490
  const lines: string[] = ['## Members'];
490
491
  for (const member of node.children ?? []) {
491
492
  lines.push(
492
- this.responseField(
493
+ responseField(
493
494
  member.name,
494
495
  member.type?.type === 'literal' ? JSON.stringify(member.type.value) : '',
495
496
  false,
@@ -547,24 +548,7 @@ class TypedocRenderer {
547
548
  const typeHref = tokens.length === 1 && only?.href ? `/${only.href}` : undefined;
548
549
  const linked = typeHref ? undefined : this.linkedType(type);
549
550
  const content = [linked && `**Type:** ${linked}`, body].filter(Boolean).join('\n\n');
550
- return this.responseField(name, this.type(type), optional, content, typeHref);
551
- }
552
-
553
- private responseField(
554
- name: string,
555
- type: string,
556
- optional: boolean,
557
- body?: string,
558
- typeHref?: string
559
- ): string {
560
- const typeAttr = type ? ` type=${this.jsxString(truncate(type, 80))}` : '';
561
- const typeHrefAttr = typeHref ? ` typeHref=${this.jsxString(typeHref)}` : '';
562
- const requiredAttr = optional ? '' : ' required';
563
- return `<ResponseField name=${this.jsxString(name)}${typeAttr}${typeHrefAttr}${requiredAttr}>\n${indent(body || '')}\n</ResponseField>`;
564
- }
565
-
566
- private jsxString(value: string): string {
567
- return `{${JSON.stringify(value)}}`;
551
+ return responseField(name, this.type(type), optional, content, typeHref);
568
552
  }
569
553
 
570
554
  private codeBlock(code: string): string {
@@ -623,18 +607,12 @@ class TypedocRenderer {
623
607
  }
624
608
 
625
609
  private linkedType(type?: TypedocType): string | undefined {
626
- const tokens = this.typeTokens(type);
627
- if (!tokens.some((token) => token.href)) return undefined;
628
- const merged: TypeToken[] = [];
629
- for (const token of tokens) {
630
- const last = merged.at(-1);
631
- if (last && !last.href && !token.href) last.text += token.text;
632
- else merged.push({ ...token });
633
- }
634
- const children = merged
635
- .map((token) => (token.href ? `[${token.text}](/${token.href})` : this.jsxString(token.text)))
636
- .join('');
637
- return `<code>${children}</code>`;
610
+ return linkedCode(
611
+ this.typeTokens(type).map((token) => ({
612
+ text: token.text,
613
+ ...(token.href ? { href: `/${token.href}` } : {}),
614
+ }))
615
+ );
638
616
  }
639
617
 
640
618
  private typeTokens(type?: TypedocType): TypeToken[] {
@@ -725,24 +703,3 @@ function headingText(title: unknown): string {
725
703
  function detailHeading(title: string, depth: number): string {
726
704
  return depth <= MAX_HEADING_DEPTH ? `${'#'.repeat(depth)} ${title}` : `**${title}**`;
727
705
  }
728
-
729
- function indent(text: string): string {
730
- return text
731
- .split('\n')
732
- .map((line) => (line ? ` ${line}` : line))
733
- .join('\n');
734
- }
735
-
736
- function truncate(text: string, max: number): string {
737
- return text.length > max ? `${text.slice(0, max - 1)}…` : text;
738
- }
739
-
740
- function inlineCode(text: string): string {
741
- const code = text.trim();
742
- if (!code) return text;
743
- const start = text.indexOf(code);
744
- const longestRun = Math.max(0, ...(code.match(/`+/g) ?? []).map((run) => run.length));
745
- const fence = '`'.repeat(longestRun + 1);
746
- const pad = code.startsWith('`') || code.endsWith('`') ? ' ' : '';
747
- return `${text.slice(0, start)}${fence}${pad}${code}${pad}${fence}${text.slice(start + code.length)}`;
748
- }
@@ -3,6 +3,7 @@ import type { ResolvedSdkTarget, SdkSymbolKind } from '@mintlify/models';
3
3
  import type { SdkPage, SdkPageTarget, SdkReference } from './types.js';
4
4
 
5
5
  const KIND_DIRS: Record<SdkSymbolKind, string> = {
6
+ package: 'packages',
6
7
  class: 'classes',
7
8
  interface: 'interfaces',
8
9
  enum: 'enums',
@@ -13,20 +14,41 @@ const KIND_DIRS: Record<SdkSymbolKind, string> = {
13
14
  property: 'properties',
14
15
  };
15
16
 
17
+ type SdkTargetRequest = Pick<ResolvedSdkTarget, 'kind' | 'name' | 'parent' | 'namespace'>;
18
+
19
+ // An explicit namespace (object-form frontmatter) must match the page's exactly;
20
+ // a dotted shorthand such as `audio.client.Foo` matches any trailing part of
21
+ // the page's namespace.
16
22
  export function matchesSdkPageTarget(
17
23
  pageTarget: SdkPageTarget | undefined,
18
- target: Pick<ResolvedSdkTarget, 'kind' | 'name' | 'parent'>
24
+ target: SdkTargetRequest
19
25
  ): boolean {
20
26
  if (!pageTarget) return false;
21
- if (pageTarget.kind !== target.kind || pageTarget.name !== target.name) return false;
22
- if (target.parent) return pageTarget.parent === target.parent;
27
+ if (pageTarget.kind !== target.kind) return false;
28
+ if (target.namespace !== undefined && pageTarget.namespace !== target.namespace) return false;
29
+ const exact = target.namespace !== undefined;
30
+ if (!matchesSdkName(pageTarget.name, pageTarget.namespace, target.name, exact)) return false;
31
+ if (target.parent) {
32
+ return (
33
+ pageTarget.parent !== undefined &&
34
+ matchesSdkName(pageTarget.parent, pageTarget.namespace, target.parent, exact)
35
+ );
36
+ }
23
37
  return pageTarget.parent === undefined;
24
38
  }
25
39
 
26
- function matchesSdkPageHeuristic(
27
- page: SdkPage,
28
- target: Pick<ResolvedSdkTarget, 'kind' | 'name' | 'parent'>
40
+ function matchesSdkName(
41
+ name: string,
42
+ namespace: string | undefined,
43
+ requested: string,
44
+ exact: boolean
29
45
  ): boolean {
46
+ if (requested === name) return true;
47
+ if (exact || namespace === undefined) return false;
48
+ return `.${namespace}.${name}`.endsWith(`.${requested}`);
49
+ }
50
+
51
+ function matchesSdkPageHeuristic(page: SdkPage, target: SdkTargetRequest): boolean {
30
52
  if (target.parent) {
31
53
  return (
32
54
  page.title === `${target.parent}.${target.name}` ||
@@ -46,20 +68,54 @@ function slugify(name: string): string {
46
68
  return name.replace(/[^a-zA-Z0-9-_.]+/g, '-').replace(/^-+|-+$/g, '') || 'item';
47
69
  }
48
70
 
71
+ function qualifiedName(target: SdkPageTarget): string {
72
+ const name = target.parent ? `${target.parent}.${target.name}` : target.name;
73
+ return target.namespace ? `${target.namespace}.${name}` : name;
74
+ }
75
+
76
+ // Several pages can match a bare name. When their targets carry namespaces the
77
+ // request can be qualified, so that is an error listing the candidates; when
78
+ // they do not (TypeDoc has no namespaces), the first match stands as before.
49
79
  export function findSdkPage(
50
80
  reference: SdkReference,
51
- target: Pick<ResolvedSdkTarget, 'kind' | 'name' | 'parent'>
81
+ target: SdkTargetRequest
52
82
  ): SdkPage | undefined {
53
- return (
54
- reference.pages.find((page) => matchesSdkPageTarget(page.target, target)) ??
55
- reference.pages.find((page) => matchesSdkPageHeuristic(page, target))
83
+ const matches = reference.pages.filter((page) => matchesSdkPageTarget(page.target, target));
84
+ if (matches.length > 1) {
85
+ // `package client` names the root package `client` outright even when a
86
+ // `client/` directory also ends in that name; only the shorthand that fits
87
+ // several pages equally is ambiguous.
88
+ const requested = target.parent ? `${target.parent}.${target.name}` : target.name;
89
+ const exact = matches.filter(
90
+ (page) => qualifiedName(page.target as SdkPageTarget) === requested
91
+ );
92
+ if (exact.length === 1) return exact[0];
93
+ }
94
+ if (matches.length > 1 && matches.some((page) => page.target?.namespace)) {
95
+ const candidates = matches.map((page) => qualifiedName(page.target as SdkPageTarget));
96
+ const requested = target.parent ? `${target.parent}.${target.name}` : target.name;
97
+ throw new Error(
98
+ `SDK symbol is ambiguous: ${target.kind} ${requested} matches ${candidates.join(', ')}; qualify it with its package`
99
+ );
100
+ }
101
+ if (matches[0] || target.namespace !== undefined) return matches[0];
102
+ // The slug/title heuristic covers converters that emit no target metadata
103
+ // and TypeDoc members addressed through their namespace (`Auth.Client.login`
104
+ // is the title, while the target only knows `parent: Client`). A page whose
105
+ // target names a different symbol is never picked up by title.
106
+ return reference.pages.find(
107
+ (page) => agreesWithTarget(page.target, target) && matchesSdkPageHeuristic(page, target)
56
108
  );
57
109
  }
58
110
 
59
- export function renderSdkTarget(
60
- reference: SdkReference,
61
- target: Pick<ResolvedSdkTarget, 'kind' | 'name' | 'parent'>
62
- ): SdkPage {
111
+ function agreesWithTarget(pageTarget: SdkPageTarget | undefined, target: SdkTargetRequest) {
112
+ if (!pageTarget) return true;
113
+ if (pageTarget.kind !== target.kind || pageTarget.name !== target.name) return false;
114
+ if (!target.parent) return pageTarget.parent === undefined;
115
+ return pageTarget.parent !== undefined && `.${target.parent}`.endsWith(`.${pageTarget.parent}`);
116
+ }
117
+
118
+ export function renderSdkTarget(reference: SdkReference, target: SdkTargetRequest): SdkPage {
63
119
  const page = findSdkPage(reference, target);
64
120
  if (!page) {
65
121
  const qualified = target.parent ? `${target.parent}.${target.name}` : target.name;
@@ -1,4 +1,5 @@
1
1
  import { convertDocfx } from './converters/docfx.js';
2
+ import { convertGo } from './converters/go.js';
2
3
  import { convertJavadoc } from './converters/javadoc.js';
3
4
  import { convertPhpdoc } from './converters/phpdoc.js';
4
5
  import { convertSphinx } from './converters/sphinx.js';
@@ -11,6 +12,7 @@ const converters: Record<SdkFormat, SdkConverter> = {
11
12
  javadoc: convertJavadoc,
12
13
  sphinx: convertSphinx,
13
14
  phpdoc: convertPhpdoc,
15
+ go: convertGo,
14
16
  };
15
17
 
16
18
  export async function generateSdkReference(
package/src/sdk/mdx.ts ADDED
@@ -0,0 +1,123 @@
1
+ export type LinkedToken = { text: string; href?: string };
2
+
3
+ export function jsxString(value: string): string {
4
+ return `{${JSON.stringify(value)}}`;
5
+ }
6
+
7
+ export function responseField(
8
+ name: string,
9
+ type: string,
10
+ optional: boolean,
11
+ body?: string,
12
+ typeHref?: string,
13
+ id?: string
14
+ ): string {
15
+ const idAttr = id ? ` id=${jsxString(id)}` : '';
16
+ const typeAttr = type ? ` type=${jsxString(truncate(type, 80))}` : '';
17
+ const typeHrefAttr = typeHref ? ` typeHref=${jsxString(typeHref)}` : '';
18
+ const requiredAttr = optional ? '' : ' required';
19
+ return `<ResponseField name=${jsxString(name)}${idAttr}${typeAttr}${typeHrefAttr}${requiredAttr}>\n${indent(body || '')}\n</ResponseField>`;
20
+ }
21
+
22
+ export function escapeInline(text: string): string {
23
+ return collapseWhitespace(text).replace(/[\\`*_{}[\]<>]/g, '\\$&');
24
+ }
25
+
26
+ // Code spans and link text are inline; a blank line inside them would end the
27
+ // paragraph and let the remainder parse as MDX. Line endings (\n, \r, and
28
+ // \r\n, which CommonMark treats alike) become one space along with the
29
+ // indentation that follows them; other whitespace is kept, so a string literal
30
+ // such as "a b" keeps its spacing. The pattern cannot backtrack, so a long
31
+ // run of spaces costs linear time.
32
+ export function collapseWhitespace(text: string): string {
33
+ return text.replace(/[\r\n]+[ \t]*/g, ' ');
34
+ }
35
+
36
+ const SAFE_SCHEMES = /^(?:https?|mailto):/i;
37
+
38
+ // Keeps http(s), mailto, fragments and relative paths; drops javascript:,
39
+ // data:, and every other scheme so artifact text cannot produce an active link.
40
+ // Browsers drop C0 controls, spaces, and DEL while parsing a URL and read a
41
+ // backslash as a slash, so the check runs on the URL as a browser would see it.
42
+ // The returned URL keeps its interior spaces (a file can be named "Getting
43
+ // Started.md") and loses only what a browser drops: surrounding controls and
44
+ // spaces, and tabs or newlines anywhere.
45
+ //
46
+ // A character reference before the first `/`, `?`, or `#` (after any leading
47
+ // slashes, so `/&sol;evil.com` cannot become `//evil.com`) is refused: Markdown
48
+ // leaves `&colon;` and `&#58;` in reference definitions for MDX to decode, so
49
+ // `javascript&colon;x` would become `javascript:x` after this check ran.
50
+ export function safeHref(url: string): string | undefined {
51
+ const cleaned = url.replace(/[\u0000-\u0020\u007f]/g, '');
52
+ const kept = trimControls(url).replace(/[\t\n\r]/g, '');
53
+ if (cleaned === '' || cleaned.startsWith('#')) return kept;
54
+ if (/&[#a-z]/i.test(cleaned.replace(/^[\\/]+/, '').split(/[/?#]/, 1)[0] ?? '')) return undefined;
55
+ if (
56
+ /^[a-z][a-z0-9+.-]*:/i.test(cleaned) ||
57
+ /^[\\/][\\/]/.test(cleaned) ||
58
+ cleaned.startsWith('\\')
59
+ ) {
60
+ return SAFE_SCHEMES.test(cleaned) ? kept : undefined;
61
+ }
62
+ return kept;
63
+ }
64
+
65
+ // Trims C0 controls and spaces from both ends with two index scans; a regex
66
+ // with a `$` alternative retries from every position of a long interior run.
67
+ function trimControls(url: string): string {
68
+ let start = 0;
69
+ let end = url.length;
70
+ while (start < end && url.charCodeAt(start) <= 0x20) start += 1;
71
+ while (end > start && url.charCodeAt(end - 1) <= 0x20) end -= 1;
72
+ return url.slice(start, end);
73
+ }
74
+
75
+ export function encodeHref(text: string): string {
76
+ return text
77
+ .split('/')
78
+ .map((segment) =>
79
+ encodeURIComponent(segment).replace(
80
+ /[()!'*]/g,
81
+ (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`
82
+ )
83
+ )
84
+ .join('/');
85
+ }
86
+
87
+ export function linkedCode(tokens: LinkedToken[]): string | undefined {
88
+ if (!tokens.some((token) => token.href)) return undefined;
89
+ const merged: LinkedToken[] = [];
90
+ for (const token of tokens) {
91
+ const last = merged.at(-1);
92
+ if (last && !last.href && !token.href) last.text += token.text;
93
+ else merged.push({ ...token });
94
+ }
95
+ const children = merged
96
+ .map((token) =>
97
+ token.href
98
+ ? `[${escapeInline(token.text)}](${token.href})`
99
+ : jsxString(collapseWhitespace(token.text))
100
+ )
101
+ .join('');
102
+ return `<code>${children}</code>`;
103
+ }
104
+
105
+ export function indent(text: string): string {
106
+ return text
107
+ .split('\n')
108
+ .map((line) => (line ? ` ${line}` : line))
109
+ .join('\n');
110
+ }
111
+
112
+ export function truncate(text: string, max: number): string {
113
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
114
+ }
115
+
116
+ export function inlineCode(text: string): string {
117
+ const code = collapseWhitespace(text).trim();
118
+ if (!code) return text;
119
+ const longestRun = Math.max(0, ...(code.match(/`+/g) ?? []).map((run) => run.length));
120
+ const fence = '`'.repeat(longestRun + 1);
121
+ const pad = code.startsWith('`') || code.endsWith('`') ? ' ' : '';
122
+ return `${fence}${pad}${code}${pad}${fence}`;
123
+ }
package/src/sdk/types.ts CHANGED
@@ -7,6 +7,7 @@ export type SdkPageTarget = {
7
7
  kind: SdkSymbolKind;
8
8
  name: string;
9
9
  parent?: string;
10
+ namespace?: string;
10
11
  };
11
12
 
12
13
  export type SdkPage = {
@@ -15,6 +16,7 @@ export type SdkPage = {
15
16
  description?: string;
16
17
  tag?: string;
17
18
  icon?: string;
19
+ intro?: string;
18
20
  content: string;
19
21
  target?: SdkPageTarget;
20
22
  };