@stainless-api/docs 0.1.0-beta.1 → 0.1.0-beta.100
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/CHANGELOG.md +917 -0
- package/eslint-suppressions.json +27 -0
- package/locals.d.ts +17 -0
- package/package.json +50 -40
- package/playground-virtual-modules.d.ts +96 -0
- package/plugin/assets/languages/cli.svg +14 -0
- package/plugin/assets/languages/csharp.svg +1 -0
- package/plugin/assets/languages/php.svg +4 -0
- package/plugin/buildAlgoliaIndex.ts +40 -39
- package/plugin/components/MethodDescription.tsx +54 -0
- package/plugin/components/RequestBuilder/ParamEditor.tsx +55 -0
- package/plugin/components/RequestBuilder/SnippetStainlessIsland.tsx +107 -0
- package/plugin/components/RequestBuilder/index.tsx +37 -0
- package/plugin/components/RequestBuilder/props.ts +9 -0
- package/plugin/components/RequestBuilder/spec-helpers.ts +47 -0
- package/plugin/components/RequestBuilder/styles.css +67 -0
- package/plugin/components/SDKSelect.astro +18 -105
- package/plugin/components/SnippetCode.tsx +111 -66
- package/plugin/components/StainlessIslands.tsx +126 -0
- package/plugin/components/search/SearchAlgolia.astro +45 -35
- package/plugin/components/search/SearchIsland.tsx +47 -29
- package/plugin/generateAPIReferenceLink.ts +2 -2
- package/plugin/globalJs/ai-dropdown-options.ts +243 -0
- package/plugin/globalJs/code-snippets.ts +40 -11
- package/plugin/globalJs/copy.ts +95 -17
- package/plugin/globalJs/create-playground.shim.ts +3 -0
- package/plugin/globalJs/method-descriptions.ts +33 -0
- package/plugin/globalJs/navigation.ts +12 -33
- package/plugin/globalJs/playground-data.shim.ts +1 -0
- package/plugin/globalJs/playground-data.ts +14 -0
- package/plugin/helpers/generateDocsRoutes.ts +59 -0
- package/plugin/helpers/getPageLoadEvent.ts +1 -1
- package/plugin/helpers/multiSpec.ts +8 -0
- package/plugin/index.ts +299 -117
- package/plugin/languages.ts +8 -2
- package/plugin/loadPluginConfig.ts +254 -102
- package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +1 -1
- package/plugin/react/Routing.tsx +210 -140
- package/plugin/referencePlaceholderUtils.ts +18 -15
- package/plugin/replaceSidebarPlaceholderMiddleware.ts +40 -32
- package/plugin/routes/Docs.astro +70 -119
- package/plugin/routes/DocsStatic.astro +6 -5
- package/plugin/routes/Overview.astro +37 -27
- package/plugin/routes/markdown.ts +13 -12
- package/plugin/{cms → sidebar-utils}/sidebar-builder.ts +49 -60
- package/plugin/specs/FileCache.ts +99 -0
- package/plugin/specs/fetchSpecSSR.ts +27 -0
- package/plugin/specs/generateSpec.ts +112 -0
- package/plugin/specs/index.ts +132 -0
- package/plugin/specs/inputResolver.ts +146 -0
- package/plugin/{cms → specs}/worker.ts +82 -5
- package/plugin/vendor/preview.worker.docs.js +22406 -17955
- package/plugin/vendor/templates/cli.md +1 -0
- package/plugin/vendor/templates/go.md +4 -2
- package/plugin/vendor/templates/java.md +3 -1
- package/plugin/vendor/templates/kotlin.md +3 -1
- package/plugin/vendor/templates/node.md +4 -2
- package/plugin/vendor/templates/python.md +4 -2
- package/plugin/vendor/templates/ruby.md +4 -2
- package/plugin/vendor/templates/terraform.md +1 -1
- package/plugin/vendor/templates/typescript.md +3 -1
- package/resolveSrcFile.ts +10 -0
- package/scripts/vendor_deps.ts +5 -5
- package/shared/getProsePages.ts +42 -0
- package/shared/getSharedLogger.ts +15 -0
- package/shared/terminalUtils.ts +3 -0
- package/shared/virtualModule.ts +54 -1
- package/src/content.config.ts +9 -0
- package/stl-docs/components/AIDropdown.tsx +63 -0
- package/stl-docs/components/AiChatIsland.tsx +14 -0
- package/stl-docs/components/{content-panel/ContentBreadcrumbs.tsx → ContentBreadcrumbs.tsx} +2 -2
- package/stl-docs/components/Head.astro +20 -0
- package/stl-docs/components/Header.astro +7 -9
- package/stl-docs/components/PageFrame.astro +18 -0
- package/stl-docs/components/PageTitle.astro +82 -0
- package/stl-docs/components/TableOfContents.astro +34 -0
- package/stl-docs/components/ThemeProvider.astro +36 -0
- package/stl-docs/components/ThemeSelect.astro +84 -144
- package/stl-docs/components/content-panel/ContentPanel.astro +16 -46
- package/stl-docs/components/headers/DefaultHeader.astro +1 -1
- package/stl-docs/components/headers/HeaderLinks.astro +1 -1
- package/stl-docs/components/headers/SplashMobileMenuToggle.astro +17 -1
- package/stl-docs/components/headers/StackedHeader.astro +29 -24
- package/stl-docs/components/icons/chat-gpt.tsx +17 -0
- package/stl-docs/components/icons/claude.tsx +10 -0
- package/stl-docs/components/icons/cursor.tsx +10 -0
- package/stl-docs/components/icons/gemini.tsx +19 -0
- package/stl-docs/components/icons/markdown.tsx +10 -0
- package/stl-docs/components/index.ts +1 -0
- package/stl-docs/components/mintlify-compat/Accordion.astro +7 -38
- package/stl-docs/components/mintlify-compat/AccordionGroup.astro +9 -23
- package/stl-docs/components/mintlify-compat/Columns.astro +40 -42
- package/stl-docs/components/mintlify-compat/Frame.astro +16 -18
- package/stl-docs/components/mintlify-compat/callouts/Callout.astro +10 -3
- package/stl-docs/components/mintlify-compat/callouts/Check.astro +7 -3
- package/stl-docs/components/mintlify-compat/callouts/Danger.astro +7 -3
- package/stl-docs/components/mintlify-compat/callouts/Info.astro +7 -3
- package/stl-docs/components/mintlify-compat/callouts/Note.astro +7 -3
- package/stl-docs/components/mintlify-compat/callouts/Tip.astro +7 -3
- package/stl-docs/components/mintlify-compat/callouts/Warning.astro +7 -3
- package/stl-docs/components/mintlify-compat/card.css +33 -35
- package/stl-docs/components/mintlify-compat/index.ts +2 -4
- package/stl-docs/components/nav-tabs/NavDropdown.astro +31 -75
- package/stl-docs/components/nav-tabs/NavTabs.astro +78 -80
- package/stl-docs/components/nav-tabs/SecondaryNavTabs.astro +15 -8
- package/stl-docs/components/nav-tabs/buildNavLinks.ts +4 -3
- package/stl-docs/components/pagination/HomeLink.astro +10 -0
- package/stl-docs/components/pagination/Pagination.astro +175 -0
- package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +22 -0
- package/stl-docs/components/pagination/PaginationLinkQuiet.astro +13 -0
- package/stl-docs/components/pagination/util.ts +71 -0
- package/stl-docs/components/scripts.ts +1 -0
- package/stl-docs/components/sidebars/BaseSidebar.astro +87 -0
- package/stl-docs/components/sidebars/SDKSelectSidebar.astro +8 -0
- package/stl-docs/components/sidebars/SidebarWithComponents.tsx +10 -0
- package/stl-docs/components/sidebars/convertAstroSidebarToStl.tsx +62 -0
- package/stl-docs/disableCalloutSyntax.ts +36 -0
- package/stl-docs/fonts.ts +186 -0
- package/stl-docs/index.ts +159 -43
- package/stl-docs/loadStlDocsConfig.ts +60 -9
- package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +61 -0
- package/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts +41 -0
- package/stl-docs/proseMarkdown/toMarkdown.ts +158 -0
- package/stl-docs/proseSearchIndexing.ts +606 -0
- package/stl-docs/tabsMiddleware.ts +14 -5
- package/styles/code.css +133 -136
- package/styles/links.css +11 -48
- package/styles/method-descriptions.css +36 -0
- package/styles/overrides.css +49 -57
- package/styles/page.css +100 -59
- package/styles/sdk_select.css +9 -7
- package/styles/search.css +57 -69
- package/styles/sidebar.css +26 -156
- package/styles/{variables.css → sl-variables.css} +3 -2
- package/styles/stldocs-variables.css +6 -0
- package/styles/toc.css +41 -34
- package/theme.css +13 -3
- package/tsconfig.json +2 -5
- package/virtual-module.d.ts +65 -7
- package/components/variables.css +0 -139
- package/plugin/cms/client.ts +0 -62
- package/plugin/cms/server.ts +0 -268
- package/plugin/globalJs/ai-dropdown.ts +0 -57
- package/stl-docs/components/APIReferenceAIDropdown.tsx +0 -86
- package/stl-docs/components/Sidebar.astro +0 -11
- package/stl-docs/components/content-panel/ProseAIDropdown.tsx +0 -64
- package/stl-docs/components/mintlify-compat/Step.astro +0 -58
- package/stl-docs/components/mintlify-compat/Steps.astro +0 -17
- package/styles/fonts.css +0 -68
- /package/{plugin/assets → assets}/fonts/geist/OFL.txt +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-latin.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin.woff2 +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { StainlessStarlightUserConfig } from '../plugin/loadPluginConfig';
|
|
2
|
-
import type { StarlightUserConfig } from '@astrojs/starlight/types';
|
|
2
|
+
import type { StarlightPlugin, StarlightUserConfig } from '@astrojs/starlight/types';
|
|
3
3
|
import type { ButtonVariant } from '@stainless-api/ui-primitives';
|
|
4
4
|
import type { AnchorHTMLAttributes } from 'react';
|
|
5
5
|
import type starlight from '@astrojs/starlight';
|
|
6
|
+
import { normalizeFonts, type StlDocsFontConfig } from './fonts';
|
|
6
7
|
|
|
7
8
|
type StarlightConfig = Parameters<typeof starlight>[0];
|
|
8
9
|
|
|
@@ -32,20 +33,22 @@ type PassThroughStarlightConfigOptions = Pick<
|
|
|
32
33
|
| 'lastUpdated'
|
|
33
34
|
| 'pagination'
|
|
34
35
|
| 'sidebar'
|
|
36
|
+
| 'expressiveCode'
|
|
35
37
|
>;
|
|
36
38
|
|
|
37
39
|
type ExperimentalStarlightCompatibilityConfig = Pick<
|
|
38
40
|
StarlightConfigDefined,
|
|
39
|
-
'components' | 'routeMiddleware' | 'plugins'
|
|
41
|
+
'components' | 'routeMiddleware' | 'plugins' | 'prerender'
|
|
40
42
|
>;
|
|
41
43
|
|
|
42
|
-
type Tabs = {
|
|
44
|
+
export type Tabs = {
|
|
43
45
|
label: string;
|
|
44
46
|
link: string;
|
|
45
47
|
sidebar?: SidebarEntry[];
|
|
46
48
|
/**
|
|
47
49
|
* Whether to hide the tab in the tab bar.
|
|
48
|
-
*
|
|
50
|
+
*
|
|
51
|
+
* @default false
|
|
49
52
|
*/
|
|
50
53
|
hidden?: boolean;
|
|
51
54
|
}[];
|
|
@@ -58,16 +61,48 @@ export type HeaderLink = {
|
|
|
58
61
|
};
|
|
59
62
|
|
|
60
63
|
export type StainlessDocsUserConfig = {
|
|
61
|
-
apiReference
|
|
64
|
+
apiReference?: StainlessStarlightUserConfig;
|
|
62
65
|
tabs?: Tabs;
|
|
63
66
|
header?: {
|
|
64
67
|
layout?: HeaderLayout;
|
|
65
68
|
links?: HeaderLink[];
|
|
66
69
|
};
|
|
70
|
+
fonts?: StlDocsFontConfig;
|
|
67
71
|
experimental?: {
|
|
68
72
|
starlightCompat?: ExperimentalStarlightCompatibilityConfig;
|
|
69
73
|
enableClientRouter?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Disable markdown rendering for prose content. Only disable this if it is causing issues.
|
|
76
|
+
*
|
|
77
|
+
* @default false
|
|
78
|
+
*/
|
|
79
|
+
disableProseMarkdownRendering?: boolean;
|
|
80
|
+
aiChat?: { chatComponentPath: string };
|
|
81
|
+
/**
|
|
82
|
+
* Whether to link group titles to overview pages. Note: overview pages must already be present in the sidebar for this to work.
|
|
83
|
+
*
|
|
84
|
+
* @default false
|
|
85
|
+
*/
|
|
86
|
+
linkGroupTitlesToOverviewPages?: boolean;
|
|
70
87
|
};
|
|
88
|
+
/**
|
|
89
|
+
* Whether to show the context menu with options like "Copy as Markdown" and "Open in ChatGPT".
|
|
90
|
+
*
|
|
91
|
+
* @default true
|
|
92
|
+
*/
|
|
93
|
+
contextMenu?: boolean;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether to render page descriptions in prose page headers
|
|
97
|
+
*
|
|
98
|
+
* @default true
|
|
99
|
+
*/
|
|
100
|
+
renderPageDescriptions?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Stainless Docs plugins.
|
|
103
|
+
* Each plugin is a function that receives the normalized config and returns a Starlight plugin.
|
|
104
|
+
*/
|
|
105
|
+
plugins?: ((config: Exclude<NormalizedStainlessDocsConfig, 'plugins'>) => StarlightPlugin)[];
|
|
71
106
|
} & PassThroughStarlightConfigOptions;
|
|
72
107
|
|
|
73
108
|
type HeaderLayout = 'default' | 'stacked';
|
|
@@ -112,6 +147,7 @@ function normalizeConfig(userConfig: StainlessDocsUserConfig) {
|
|
|
112
147
|
layout: userConfig.header?.layout ?? 'default',
|
|
113
148
|
links: userConfig.header?.links ?? [],
|
|
114
149
|
},
|
|
150
|
+
fonts: normalizeFonts(userConfig.fonts),
|
|
115
151
|
starlightPassThrough: {
|
|
116
152
|
tableOfContents: userConfig.tableOfContents,
|
|
117
153
|
titleDelimiter: userConfig.titleDelimiter,
|
|
@@ -119,6 +155,13 @@ function normalizeConfig(userConfig: StainlessDocsUserConfig) {
|
|
|
119
155
|
description: userConfig.description,
|
|
120
156
|
tagline: userConfig.tagline,
|
|
121
157
|
logo: userConfig.logo,
|
|
158
|
+
favicon: userConfig.favicon,
|
|
159
|
+
disable404Route: userConfig.disable404Route,
|
|
160
|
+
editLink: userConfig.editLink,
|
|
161
|
+
locales: userConfig.locales,
|
|
162
|
+
lastUpdated: userConfig.lastUpdated,
|
|
163
|
+
pagination: userConfig.pagination,
|
|
164
|
+
prerender: userConfig.experimental?.starlightCompat?.prerender ?? true,
|
|
122
165
|
},
|
|
123
166
|
starlightCompat: {
|
|
124
167
|
components: userConfig.experimental?.starlightCompat?.components ?? {},
|
|
@@ -126,8 +169,16 @@ function normalizeConfig(userConfig: StainlessDocsUserConfig) {
|
|
|
126
169
|
routeMiddleware: normalizeRouteMiddleware(userConfig),
|
|
127
170
|
},
|
|
128
171
|
enableClientRouter: userConfig.experimental?.enableClientRouter ?? false,
|
|
129
|
-
apiReference: userConfig.apiReference,
|
|
172
|
+
apiReference: userConfig.apiReference ?? null,
|
|
130
173
|
sidebar: userConfig.sidebar,
|
|
174
|
+
enableProseMarkdownRendering:
|
|
175
|
+
userConfig.experimental?.disableProseMarkdownRendering === true ? false : true,
|
|
176
|
+
contextMenu: userConfig.contextMenu ?? true,
|
|
177
|
+
expressiveCode: userConfig.expressiveCode,
|
|
178
|
+
renderPageDescriptions: userConfig.renderPageDescriptions ?? true,
|
|
179
|
+
plugins: userConfig.plugins ?? [],
|
|
180
|
+
aiChat: userConfig.experimental?.aiChat,
|
|
181
|
+
linkGroupTitlesToOverviewPages: userConfig.experimental?.linkGroupTitlesToOverviewPages ?? false,
|
|
131
182
|
};
|
|
132
183
|
|
|
133
184
|
return configWithDefaults;
|
|
@@ -136,12 +187,12 @@ function normalizeConfig(userConfig: StainlessDocsUserConfig) {
|
|
|
136
187
|
export type NormalizedStainlessDocsConfig = ReturnType<typeof normalizeConfig>;
|
|
137
188
|
|
|
138
189
|
/*
|
|
139
|
-
The goal of the code in this file is to take a user's config and normalize it.
|
|
190
|
+
The goal of the code in this file is to take a user's config and normalize it.
|
|
140
191
|
Specifically: we want a single complete config format used throughout the internals of the plugin.
|
|
141
192
|
|
|
142
193
|
We've tried to avoid any config values being optional/undefined. To accomplish this:
|
|
143
|
-
- Any optional config values should have their defaults set here: eg. basePath defaults to /api
|
|
144
|
-
- If a field is only used in certain contexts, we make each context a discriminated union (see
|
|
194
|
+
- Any optional config values should have their defaults set here: eg. basePath defaults to /api
|
|
195
|
+
- If a field is only used in certain contexts, we make each context a discriminated union (see SDKJSONInputs)
|
|
145
196
|
- We prefer empty arrays over undefined/null
|
|
146
197
|
*/
|
|
147
198
|
export function parseStlDocsConfig(userConfig: StainlessDocsUserConfig) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro';
|
|
2
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
3
|
+
import { toMarkdown } from './toMarkdown';
|
|
4
|
+
import { resolveSrcFile } from '../../resolveSrcFile';
|
|
5
|
+
import { getSharedLogger } from '../../shared/getSharedLogger';
|
|
6
|
+
import { bold } from '../../shared/terminalUtils';
|
|
7
|
+
import { getProsePages } from '../../shared/getProsePages';
|
|
8
|
+
|
|
9
|
+
export function stainlessDocsMarkdownRenderer({
|
|
10
|
+
enabled,
|
|
11
|
+
apiReferenceBasePath,
|
|
12
|
+
}: {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
apiReferenceBasePath: string | null;
|
|
15
|
+
}): AstroIntegration {
|
|
16
|
+
return {
|
|
17
|
+
name: 'stl-docs-md',
|
|
18
|
+
hooks: {
|
|
19
|
+
'astro:config:setup': ({ addMiddleware }) => {
|
|
20
|
+
if (enabled) {
|
|
21
|
+
addMiddleware({
|
|
22
|
+
entrypoint: resolveSrcFile('/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts'),
|
|
23
|
+
order: 'post',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
'astro:build:done': async ({ logger: localLogger, dir }) => {
|
|
28
|
+
const logger = getSharedLogger({ fallback: localLogger });
|
|
29
|
+
if (!enabled) {
|
|
30
|
+
logger.info('Stainless Docs prose Markdown rendering is disabled, skipping...');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const outputBasePath = dir.pathname;
|
|
34
|
+
const pagesToRender = await getProsePages({ apiReferenceBasePath, outputBasePath });
|
|
35
|
+
|
|
36
|
+
logger.info(bold(`Building ${pagesToRender.length} Markdown pages for prose content`));
|
|
37
|
+
|
|
38
|
+
let completedCount = 0;
|
|
39
|
+
|
|
40
|
+
for (const absHtmlPath of pagesToRender) {
|
|
41
|
+
const txt = await readFile(absHtmlPath, 'utf-8');
|
|
42
|
+
const md = await toMarkdown(txt);
|
|
43
|
+
if (md) {
|
|
44
|
+
const absMdPath = absHtmlPath.replace('.html', '.md');
|
|
45
|
+
await writeFile(absMdPath, md, 'utf-8');
|
|
46
|
+
|
|
47
|
+
completedCount++;
|
|
48
|
+
|
|
49
|
+
const relHtmlPath = absHtmlPath.replace(outputBasePath, '');
|
|
50
|
+
const relMdPath = absMdPath.replace(outputBasePath, '');
|
|
51
|
+
|
|
52
|
+
logger.info(`(${completedCount}/${pagesToRender.length}) ${relHtmlPath} -> ${relMdPath}`);
|
|
53
|
+
} else {
|
|
54
|
+
logger.error(`Failed to render ${absHtmlPath} as Markdown`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineMiddleware } from 'astro:middleware';
|
|
2
|
+
import { toMarkdown } from './toMarkdown';
|
|
3
|
+
import { API_REFERENCE_BASE_PATH } from 'virtual:stl-docs-virtual-module';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
// this is only run in `astro dev` for rendering prose content as Markdown on the fly.
|
|
7
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
8
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
9
|
+
if (!import.meta.env.DEV) {
|
|
10
|
+
return next();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const resolvedBasePath = path.posix.join(import.meta.env.BASE_URL ?? '', API_REFERENCE_BASE_PATH);
|
|
14
|
+
if (resolvedBasePath && context.url.pathname.startsWith(resolvedBasePath)) {
|
|
15
|
+
// handled by the API reference API route in stl-starlight plugin
|
|
16
|
+
return next();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!context.url.pathname.endsWith('/index.md')) {
|
|
20
|
+
return next();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const pathname = context.url.pathname.replace('index.md', '');
|
|
24
|
+
|
|
25
|
+
// We must trim the trailing slash to support astro configs with `trailingSlash: 'never'`
|
|
26
|
+
const cleanPathname = pathname !== '/' ? pathname.replace(/\/$/, '') : pathname;
|
|
27
|
+
const htmlUrl = new URL(cleanPathname, context.url);
|
|
28
|
+
|
|
29
|
+
const resp = await fetch(htmlUrl);
|
|
30
|
+
if (!resp.ok) {
|
|
31
|
+
return new Response('Failed to fetch HTML', { status: 400 });
|
|
32
|
+
}
|
|
33
|
+
const html = await resp.text();
|
|
34
|
+
const md = await toMarkdown(html);
|
|
35
|
+
|
|
36
|
+
if (!md) {
|
|
37
|
+
return new Response('Failed to render Markdown', { status: 400 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new Response(md, { status: 200 });
|
|
41
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import rehypeParse from 'rehype-parse';
|
|
3
|
+
import rehypeRemark from 'rehype-remark';
|
|
4
|
+
import remarkGfm from 'remark-gfm';
|
|
5
|
+
import remarkStringify from 'remark-stringify';
|
|
6
|
+
import { HTMLElement, parse } from 'node-html-parser';
|
|
7
|
+
|
|
8
|
+
type PaginationLink = {
|
|
9
|
+
href: string;
|
|
10
|
+
label: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type PaginationItems = {
|
|
14
|
+
prev: PaginationLink | null;
|
|
15
|
+
next: PaginationLink | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function parsePaginationLink(footer: HTMLElement, rel: 'next' | 'prev'): PaginationLink | null {
|
|
19
|
+
const link = footer.querySelector(`.pagination-links a[rel="${rel}"]`);
|
|
20
|
+
if (!link) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const title = link.querySelector('.link-title');
|
|
25
|
+
if (!title) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const href = link.getAttribute('href');
|
|
30
|
+
if (!href) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
href,
|
|
36
|
+
label: title.text,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isRelativeLink(href: string) {
|
|
41
|
+
return href.startsWith('/');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasExtension(href: string) {
|
|
45
|
+
return href.includes('.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function removeTrailingSlash(href: string) {
|
|
49
|
+
return href.replace(/\/$/, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeMarkdownLinks(el: HTMLElement) {
|
|
53
|
+
el.querySelectorAll('a').forEach((a) => {
|
|
54
|
+
const href = a.getAttribute('href');
|
|
55
|
+
if (!href) {
|
|
56
|
+
return a;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (isRelativeLink(href) && !hasExtension(href)) {
|
|
60
|
+
if (href === '/') {
|
|
61
|
+
a.setAttribute('href', '/index.md');
|
|
62
|
+
} else {
|
|
63
|
+
a.setAttribute('href', `${removeTrailingSlash(href)}/index.md`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return a;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function removeHiddenElements(el: HTMLElement) {
|
|
71
|
+
const hiddenSelectors = ['.sl-anchor-link'];
|
|
72
|
+
for (const selector of hiddenSelectors) {
|
|
73
|
+
const hiddenElements = el.querySelectorAll(selector);
|
|
74
|
+
for (const hiddenElement of hiddenElements) {
|
|
75
|
+
hiddenElement.remove();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function toMarkdown(html: string) {
|
|
81
|
+
const root = parse(html);
|
|
82
|
+
|
|
83
|
+
const mainEl = root.querySelector('main');
|
|
84
|
+
|
|
85
|
+
if (!mainEl) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
makeMarkdownLinks(mainEl);
|
|
90
|
+
|
|
91
|
+
const footer = mainEl.querySelector('footer');
|
|
92
|
+
|
|
93
|
+
const markdownContentEl = mainEl.querySelector('.sl-markdown-content');
|
|
94
|
+
if (!markdownContentEl) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
removeHiddenElements(markdownContentEl);
|
|
99
|
+
|
|
100
|
+
const articleContent = markdownContentEl.innerHTML;
|
|
101
|
+
|
|
102
|
+
const paginationLinks: PaginationItems = {
|
|
103
|
+
prev: null,
|
|
104
|
+
next: null,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (footer) {
|
|
108
|
+
paginationLinks.prev = parsePaginationLink(footer, 'prev');
|
|
109
|
+
paginationLinks.next = parsePaginationLink(footer, 'next');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let md = (
|
|
113
|
+
await unified()
|
|
114
|
+
.use(rehypeParse, { fragment: true }) // parse HTML
|
|
115
|
+
.use(rehypeRemark) // rehype (HTML) -> remark (MD AST)
|
|
116
|
+
.use(remarkGfm) // tables, strikethrough, autolinks, etc.
|
|
117
|
+
.use(remarkStringify, {
|
|
118
|
+
fences: true,
|
|
119
|
+
bullet: '-',
|
|
120
|
+
listItemIndent: 'one',
|
|
121
|
+
rule: '-',
|
|
122
|
+
})
|
|
123
|
+
.process(articleContent)
|
|
124
|
+
).toString();
|
|
125
|
+
|
|
126
|
+
const title = root.querySelector('title')?.textContent;
|
|
127
|
+
const description = root.querySelector('meta[name="description"]')?.attributes.content;
|
|
128
|
+
const lastUpdated = root.querySelector('.meta time')?.attributes.datetime;
|
|
129
|
+
|
|
130
|
+
// let htmlUrl = url.toString().replace('.md', '');
|
|
131
|
+
// if (htmlUrl.endsWith('/index')) {
|
|
132
|
+
// htmlUrl = htmlUrl.replace('/index', '');
|
|
133
|
+
// }
|
|
134
|
+
|
|
135
|
+
md = [
|
|
136
|
+
'---',
|
|
137
|
+
`title: ${title}`,
|
|
138
|
+
description ? `description: ${description}` : [],
|
|
139
|
+
lastUpdated ? `lastUpdated: ${lastUpdated}` : [],
|
|
140
|
+
// `source_url:`,
|
|
141
|
+
// ` html: ${htmlUrl}`,
|
|
142
|
+
// ` md: ${url.toString()}`,
|
|
143
|
+
'---\n',
|
|
144
|
+
md,
|
|
145
|
+
]
|
|
146
|
+
.flat()
|
|
147
|
+
.join('\n');
|
|
148
|
+
|
|
149
|
+
if (paginationLinks.prev) {
|
|
150
|
+
md += `\n\n[Previous](${paginationLinks.prev.href})`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (paginationLinks.next) {
|
|
154
|
+
md += `\n\n[Next](${paginationLinks.next.href})`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return md;
|
|
158
|
+
}
|