@gsa-tts/graymatter-ui 0.3.23 → 0.4.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gsa-tts/graymatter-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -47,7 +47,10 @@
|
|
|
47
47
|
"@testing-library/jest-dom": "^6.6.3",
|
|
48
48
|
"@testing-library/svelte": "^5.2.8",
|
|
49
49
|
"@testing-library/user-event": "^14.6.1",
|
|
50
|
+
"@types/mdast": "^4.0.4",
|
|
51
|
+
"@types/mdx": "^2.0.13",
|
|
50
52
|
"@types/node": "^22.13.0",
|
|
53
|
+
"@types/unist": "^3.0.3",
|
|
51
54
|
"@uswds/compile": "1.2.2",
|
|
52
55
|
"@vitest/coverage-istanbul": "^3.2.4",
|
|
53
56
|
"astro": "^5.13.7",
|
|
@@ -58,15 +61,16 @@
|
|
|
58
61
|
"typescript": "5.7.3",
|
|
59
62
|
"vite": "^6.3.5",
|
|
60
63
|
"vitest": "^3.0.7",
|
|
61
|
-
"@gsa-tts/graymatter-eslint": "0.0.2",
|
|
62
64
|
"@gsa-tts/graymatter-typescript-config": "0.0.2",
|
|
63
|
-
"@gsa-tts/graymatter-vitest-config": "0.0.1"
|
|
65
|
+
"@gsa-tts/graymatter-vitest-config": "0.0.1",
|
|
66
|
+
"@gsa-tts/graymatter-eslint": "0.0.2"
|
|
64
67
|
},
|
|
65
68
|
"dependencies": {
|
|
66
69
|
"@fontsource-variable/archivo": "^5.2.6",
|
|
67
70
|
"@fontsource/space-mono": "^5.2.8",
|
|
68
71
|
"@uswds/uswds": "3.12.0",
|
|
69
72
|
"slugify": "^1.6.6",
|
|
73
|
+
"unist-util-visit": "^5.0.0",
|
|
70
74
|
"@gsa-tts/graymatter-style-tokens": "0.3.17"
|
|
71
75
|
},
|
|
72
76
|
"scripts": {
|
|
@@ -6,6 +6,7 @@ import purgecss from 'astro-purgecss';
|
|
|
6
6
|
import { join as pathJoin, dirname, resolve } from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { normalizeTrailingSlash } from '../helpers/string-formatters';
|
|
9
|
+
import { remarkWrapPre } from '../utils/remarkPlugins.js';
|
|
9
10
|
|
|
10
11
|
interface AstroConfigOptions {
|
|
11
12
|
appDir: string;
|
|
@@ -95,11 +96,32 @@ export function createAstroConfig({
|
|
|
95
96
|
}
|
|
96
97
|
: baseViteConfig;
|
|
97
98
|
|
|
99
|
+
// Base markdown config with remarkWrapPre plugin for code block wrapping
|
|
100
|
+
const baseMarkdownConfig = {
|
|
101
|
+
remarkPlugins: [remarkWrapPre],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Merge markdown config if provided in overrides
|
|
105
|
+
const mergedMarkdownConfig = overrides.markdown
|
|
106
|
+
? {
|
|
107
|
+
...baseMarkdownConfig,
|
|
108
|
+
...(overrides.markdown as Record<string, unknown>),
|
|
109
|
+
remarkPlugins: [
|
|
110
|
+
...baseMarkdownConfig.remarkPlugins,
|
|
111
|
+
...((overrides.markdown as { remarkPlugins?: unknown[] })
|
|
112
|
+
.remarkPlugins || []),
|
|
113
|
+
],
|
|
114
|
+
}
|
|
115
|
+
: baseMarkdownConfig;
|
|
116
|
+
|
|
98
117
|
return defineConfig({
|
|
99
118
|
base: normalizeTrailingSlash(process.env.BASEURL || ''),
|
|
100
119
|
integrations: process.env.VITEST
|
|
101
120
|
? []
|
|
102
121
|
: overrides.integrations || baseIntegrations,
|
|
122
|
+
// Markdown config with remark plugins (applies to all markdown/MDX processing)
|
|
123
|
+
markdown: mergedMarkdownConfig as typeof baseMarkdownConfig &
|
|
124
|
+
typeof overrides.markdown,
|
|
103
125
|
experimental: {
|
|
104
126
|
// Astro Experimental Fonts API for managing custom fonts
|
|
105
127
|
fonts: [
|
|
@@ -134,7 +156,7 @@ export function createAstroConfig({
|
|
|
134
156
|
// Spread other overrides (excluding the ones we handle specifically)
|
|
135
157
|
...Object.fromEntries(
|
|
136
158
|
Object.entries(overrides).filter(
|
|
137
|
-
([key]) => !['integrations', 'vite'].includes(key)
|
|
159
|
+
([key]) => !['integrations', 'vite', 'markdown'].includes(key)
|
|
138
160
|
)
|
|
139
161
|
),
|
|
140
162
|
});
|
|
@@ -3,6 +3,9 @@ import GlobalAppLayout from './GlobalAppLayout.astro';
|
|
|
3
3
|
import ApiDocSubNavMenu from '../components/ApiDocSubNavMenu.svelte';
|
|
4
4
|
import NavigationInitializer from '../components/NavigationInitializer.svelte';
|
|
5
5
|
import { getBaseUrl } from '../helpers';
|
|
6
|
+
import { getCollection } from 'astro:content';
|
|
7
|
+
import { render } from 'astro:content';
|
|
8
|
+
import { generateApiDocumentationNavData } from '../utils/generateApiNavData.js';
|
|
6
9
|
|
|
7
10
|
const gtmID = import.meta.env.PUBLIC_GTM_ID;
|
|
8
11
|
const {
|
|
@@ -11,12 +14,43 @@ const {
|
|
|
11
14
|
description,
|
|
12
15
|
openGraphImage,
|
|
13
16
|
showAppIcons = false,
|
|
14
|
-
subNavData = null,
|
|
15
17
|
disableCodeCopyButtons = false,
|
|
16
|
-
postItems = null,
|
|
17
18
|
profileMenuData = null,
|
|
18
19
|
hideDiscover = true,
|
|
20
|
+
contentCustomization = undefined,
|
|
19
21
|
} = Astro.props;
|
|
22
|
+
|
|
23
|
+
// Handle ALL logic internally
|
|
24
|
+
// Get collection
|
|
25
|
+
const rawPosts = await getCollection('apiDocumentation');
|
|
26
|
+
|
|
27
|
+
// Sort by sortOrder
|
|
28
|
+
const sortedPosts = [...rawPosts].sort(
|
|
29
|
+
(a, b) => a.data.sortOrder - b.data.sortOrder
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Render posts
|
|
33
|
+
const postItems = await Promise.all(
|
|
34
|
+
sortedPosts.map(async post => {
|
|
35
|
+
const { Content } = await render(post);
|
|
36
|
+
return {
|
|
37
|
+
...post,
|
|
38
|
+
ContentComponent: Content,
|
|
39
|
+
};
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Generate navigation data
|
|
44
|
+
const apiDocsMetadata = sortedPosts.map(doc => ({
|
|
45
|
+
id: doc.id,
|
|
46
|
+
slug: doc.id,
|
|
47
|
+
title: doc.data.title,
|
|
48
|
+
description: doc.data.description,
|
|
49
|
+
sortOrder: doc.data.sortOrder,
|
|
50
|
+
content: doc.body || '',
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
const subNavData = generateApiDocumentationNavData(apiDocsMetadata);
|
|
20
54
|
---
|
|
21
55
|
|
|
22
56
|
<GlobalAppLayout
|
|
@@ -55,7 +89,7 @@ const {
|
|
|
55
89
|
<article class="article-body">
|
|
56
90
|
<h1 class="page-title">API Documentation</h1>
|
|
57
91
|
|
|
58
|
-
<!-- Render postItems
|
|
92
|
+
<!-- Render postItems -->
|
|
59
93
|
{
|
|
60
94
|
postItems &&
|
|
61
95
|
postItems.map((post: any) => (
|
|
@@ -80,6 +114,116 @@ const {
|
|
|
80
114
|
</script>
|
|
81
115
|
)
|
|
82
116
|
}
|
|
117
|
+
<!-- Client-side content replacement -->
|
|
118
|
+
<script is:inline define:vars={{ contentCustomization }}>
|
|
119
|
+
if (!contentCustomization) {
|
|
120
|
+
// no customization provided
|
|
121
|
+
} else {
|
|
122
|
+
const customization = contentCustomization;
|
|
123
|
+
function escapeHtml(input) {
|
|
124
|
+
return String(input)
|
|
125
|
+
.replace(/&/g, '&')
|
|
126
|
+
.replace(/</g, '<')
|
|
127
|
+
.replace(/>/g, '>')
|
|
128
|
+
.replace(/"/g, '"')
|
|
129
|
+
.replace(/'/g, ''');
|
|
130
|
+
}
|
|
131
|
+
function linkify(input) {
|
|
132
|
+
// Preserve markdown links [label](url) using placeholders
|
|
133
|
+
const placeholders = [];
|
|
134
|
+
const withPlaceholders = String(input).replace(
|
|
135
|
+
/\[([^\]]+)\]\(((https?:\/\/|www\.)[^\s)]+)\)/gi,
|
|
136
|
+
(m, label, href) => {
|
|
137
|
+
const safeLabel = escapeHtml(label);
|
|
138
|
+
const normalizedHref = href.startsWith('http')
|
|
139
|
+
? href
|
|
140
|
+
: `https://${href}`;
|
|
141
|
+
const safeHref = escapeHtml(normalizedHref);
|
|
142
|
+
const html = `<a href="${safeHref}" target="_blank" rel="noopener noreferrer">${safeLabel}</a>`;
|
|
143
|
+
const token = `__LINK_PLACEHOLDER_${placeholders.length}__`;
|
|
144
|
+
placeholders.push(html);
|
|
145
|
+
return token;
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Escape remaining content
|
|
150
|
+
const escaped = escapeHtml(withPlaceholders);
|
|
151
|
+
|
|
152
|
+
// Linkify emails
|
|
153
|
+
const withEmails = escaped.replace(
|
|
154
|
+
/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
|
|
155
|
+
email => {
|
|
156
|
+
return `<a href="mailto:${email}">${email}</a>`;
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
// Linkify bare URLs (http/https and www.)
|
|
160
|
+
const urlRegex = /\b((https?:\/\/)[^\s)]+|www\.[^\s)]+)\b/gi;
|
|
161
|
+
const withUrls = withEmails.replace(urlRegex, raw => {
|
|
162
|
+
const href = raw.startsWith('http') ? raw : `https://${raw}`;
|
|
163
|
+
return `<a href="${href}" target="_blank" rel="noopener noreferrer">${raw}</a>`;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Restore placeholders
|
|
167
|
+
return withUrls.replace(
|
|
168
|
+
/__LINK_PLACEHOLDER_(\d+)__/g,
|
|
169
|
+
(m, idx) => placeholders[Number(idx)]
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function replaceContent() {
|
|
174
|
+
// For endpoints
|
|
175
|
+
if (customization.endpointsMessage) {
|
|
176
|
+
const heading = document.getElementById('endpoints');
|
|
177
|
+
if (heading) {
|
|
178
|
+
const section = heading.closest('div');
|
|
179
|
+
if (section) {
|
|
180
|
+
const paragraphs = section.querySelectorAll('p');
|
|
181
|
+
paragraphs.forEach(p => {
|
|
182
|
+
if (
|
|
183
|
+
p.textContent?.includes(
|
|
184
|
+
'API endpoints will be available when you login'
|
|
185
|
+
)
|
|
186
|
+
) {
|
|
187
|
+
p.innerHTML = linkify(customization.endpointsMessage);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// For getting-started
|
|
195
|
+
if (customization.authMessage) {
|
|
196
|
+
const heading = document.getElementById('getting-started');
|
|
197
|
+
if (heading) {
|
|
198
|
+
const section = heading.closest('div');
|
|
199
|
+
if (section) {
|
|
200
|
+
const paragraphs = section.querySelectorAll('p');
|
|
201
|
+
paragraphs.forEach(p => {
|
|
202
|
+
const text = p.textContent || '';
|
|
203
|
+
if (text.includes('All API requests require an API key')) {
|
|
204
|
+
const authPattern =
|
|
205
|
+
/All API requests require an API key\. Upon authentication,.*?available\./s;
|
|
206
|
+
if (authPattern.test(text)) {
|
|
207
|
+
p.innerHTML = linkify(customization.authMessage);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Run synchronously before paint
|
|
217
|
+
if (document.readyState === 'loading') {
|
|
218
|
+
document.addEventListener('DOMContentLoaded', replaceContent, {
|
|
219
|
+
once: true,
|
|
220
|
+
});
|
|
221
|
+
} else {
|
|
222
|
+
replaceContent();
|
|
223
|
+
}
|
|
224
|
+
setTimeout(replaceContent, 0);
|
|
225
|
+
}
|
|
226
|
+
</script>
|
|
83
227
|
|
|
84
228
|
<style>
|
|
85
229
|
body {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { glob } from 'astro/loaders';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates the API documentation collection for Astro apps.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { defineCollection, z } from 'astro:content';
|
|
9
|
+
* import { createApiDocumentationCollection } from '@gsa-tts/graymatter-ui/utils/apiDocumentationConfig.js';
|
|
10
|
+
*
|
|
11
|
+
* export const collections = {
|
|
12
|
+
* apiDocumentation: createApiDocumentationCollection({ defineCollection, z }),
|
|
13
|
+
* };
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function createApiDocumentationCollection({
|
|
17
|
+
defineCollection,
|
|
18
|
+
z,
|
|
19
|
+
}: {
|
|
20
|
+
defineCollection: typeof import('astro:content').defineCollection;
|
|
21
|
+
z: typeof import('astro:content').z;
|
|
22
|
+
}) {
|
|
23
|
+
return defineCollection({
|
|
24
|
+
loader: glob({
|
|
25
|
+
base: './node_modules/@gsa-tts/graymatter-ui/src/content/api',
|
|
26
|
+
pattern: '**/*.{md,mdx}',
|
|
27
|
+
}),
|
|
28
|
+
schema: () =>
|
|
29
|
+
z.object({
|
|
30
|
+
title: z.string(),
|
|
31
|
+
description: z.string(),
|
|
32
|
+
sortOrder: z.number(),
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Note: Do not export remarkPlugins.js or apiDocumentationConfig.js from here.
|
|
2
|
+
// These are Astro-specific utilities that should only be imported directly by their specific paths.
|
|
3
|
+
// Exporting them would cause bundling issues for Svelte/SvelteKit apps that import from this barrel export.
|
|
4
|
+
|
|
1
5
|
export * from './getAppUrls.js';
|
|
2
6
|
export * from './navigationControl.js';
|
|
3
7
|
export * from './copyButtonScript.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
import type { Root } from 'mdast';
|
|
3
|
+
import type { Code } from 'mdast';
|
|
4
|
+
import type { Parent } from 'unist';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MDX-specific node type for JSX flow elements
|
|
8
|
+
*/
|
|
9
|
+
interface MdxJsxFlowElement extends Parent {
|
|
10
|
+
type: 'mdxJsxFlowElement';
|
|
11
|
+
name: string;
|
|
12
|
+
attributes?: Array<{
|
|
13
|
+
type: 'mdxJsxAttribute';
|
|
14
|
+
name: string;
|
|
15
|
+
value: string;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Remark plugin to wrap code blocks in a div with className 'ai-code-block-wrapper'
|
|
21
|
+
*
|
|
22
|
+
* This enables the copy button functionality for code blocks in documentation.
|
|
23
|
+
*
|
|
24
|
+
* @returns A remark plugin function
|
|
25
|
+
*/
|
|
26
|
+
export function remarkWrapPre() {
|
|
27
|
+
return (tree: Root) => {
|
|
28
|
+
visit(
|
|
29
|
+
tree,
|
|
30
|
+
'code',
|
|
31
|
+
(node: Code, index: number | undefined, parent: Parent | undefined) => {
|
|
32
|
+
if (
|
|
33
|
+
node.type === 'code' &&
|
|
34
|
+
typeof index === 'number' &&
|
|
35
|
+
parent &&
|
|
36
|
+
'children' in parent
|
|
37
|
+
) {
|
|
38
|
+
const wrapper: MdxJsxFlowElement = {
|
|
39
|
+
type: 'mdxJsxFlowElement',
|
|
40
|
+
name: 'div',
|
|
41
|
+
attributes: [
|
|
42
|
+
{
|
|
43
|
+
type: 'mdxJsxAttribute',
|
|
44
|
+
name: 'className',
|
|
45
|
+
value: 'ai-code-block-wrapper',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
children: [node],
|
|
49
|
+
};
|
|
50
|
+
parent.children.splice(index, 1, wrapper);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
}
|