@defra/docusaurus-theme-govuk 0.0.16-alpha → 0.0.18-alpha
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/index.js +68 -1
- package/package.json +1 -1
- package/src/css/components.scss +93 -5
- package/src/css/prose-scope.scss +84 -1
- package/src/css/theme.scss +29 -1
- package/src/theme/Admonition/index.js +14 -19
- package/src/theme/CodeBlock/index.js +1 -1
- package/src/theme/MDXComponents/index.js +120 -3
package/index.js
CHANGED
|
@@ -8,6 +8,61 @@ const removeMarkdown = require('remove-markdown');
|
|
|
8
8
|
// handles deduplication automatically (second "Options" → "options-1", etc.).
|
|
9
9
|
const GithubSlugger = require('github-slugger');
|
|
10
10
|
|
|
11
|
+
// Remark plugin: transforms GitHub-style alert blockquotes into Docusaurus
|
|
12
|
+
// containerDirective nodes so they render via the Admonition component.
|
|
13
|
+
//
|
|
14
|
+
// > [!NOTE] → :::note
|
|
15
|
+
// > [!TIP] → :::tip
|
|
16
|
+
// > [!IMPORTANT] → :::important
|
|
17
|
+
// > [!WARNING] → :::warning
|
|
18
|
+
// > [!CAUTION] → :::caution
|
|
19
|
+
//
|
|
20
|
+
// Runs in beforeDefaultRemarkPlugins so containerDirective nodes are in place
|
|
21
|
+
// before Docusaurus's admonition remark plugin processes them.
|
|
22
|
+
function githubAlertToDirective(blockquote) {
|
|
23
|
+
const first = blockquote.children?.[0];
|
|
24
|
+
if (first?.type !== 'paragraph') return null;
|
|
25
|
+
|
|
26
|
+
const firstText = first.children?.[0];
|
|
27
|
+
if (firstText?.type !== 'text') return null;
|
|
28
|
+
|
|
29
|
+
const match = firstText.value.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][ \t]*\n?/i);
|
|
30
|
+
if (!match) return null;
|
|
31
|
+
|
|
32
|
+
const type = match[1].toLowerCase();
|
|
33
|
+
const rest = firstText.value.slice(match[0].length);
|
|
34
|
+
|
|
35
|
+
let children;
|
|
36
|
+
if (rest.trim()) {
|
|
37
|
+
children = [
|
|
38
|
+
{ ...first, children: [{ ...firstText, value: rest }, ...first.children.slice(1)] },
|
|
39
|
+
...blockquote.children.slice(1),
|
|
40
|
+
];
|
|
41
|
+
} else if (first.children.length > 1) {
|
|
42
|
+
children = [{ ...first, children: first.children.slice(1) }, ...blockquote.children.slice(1)];
|
|
43
|
+
} else {
|
|
44
|
+
children = blockquote.children.slice(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { type: 'containerDirective', name: type, attributes: {}, children };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function walkGithubAlerts(node) {
|
|
51
|
+
if (!Array.isArray(node.children)) return;
|
|
52
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
53
|
+
const child = node.children[i];
|
|
54
|
+
if (child.type === 'blockquote') {
|
|
55
|
+
const directive = githubAlertToDirective(child);
|
|
56
|
+
if (directive) { node.children[i] = directive; continue; }
|
|
57
|
+
}
|
|
58
|
+
walkGithubAlerts(child);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function remarkGithubAlerts() {
|
|
63
|
+
return walkGithubAlerts;
|
|
64
|
+
}
|
|
65
|
+
|
|
11
66
|
// Remark plugin: converts `<!-- no-sidebar -->` inline HTML comments inside
|
|
12
67
|
// headings into a `data-no-sidebar` HTML attribute (via mdast hProperties) so
|
|
13
68
|
// the runtime DOM scanner can detect and skip them. MDX/remark-rehype drops
|
|
@@ -102,7 +157,19 @@ function injectIntoUses(uses) {
|
|
|
102
157
|
if (typeof use?.loader === 'string' && use.loader.includes('mdx-loader')) {
|
|
103
158
|
use.options = use.options || {};
|
|
104
159
|
use.options.beforeDefaultRemarkPlugins = use.options.beforeDefaultRemarkPlugins || [];
|
|
105
|
-
use.options.beforeDefaultRemarkPlugins.push(remarkNoSidebar);
|
|
160
|
+
use.options.beforeDefaultRemarkPlugins.push(remarkNoSidebar, remarkGithubAlerts);
|
|
161
|
+
|
|
162
|
+
// Add GitHub alert types not in Docusaurus's default keyword list.
|
|
163
|
+
if (use.options.admonitions !== false) {
|
|
164
|
+
const existing = use.options.admonitions || {};
|
|
165
|
+
const keywords = existing.keywords || ['note', 'tip', 'info', 'warning', 'danger'];
|
|
166
|
+
const extra = ['caution', 'important'].filter(k => !keywords.includes(k));
|
|
167
|
+
if (extra.length) {
|
|
168
|
+
use.options.admonitions = { ...existing, keywords: [...keywords, ...extra] };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
106
173
|
}
|
|
107
174
|
}
|
|
108
175
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/docusaurus-theme-govuk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18-alpha",
|
|
4
4
|
"description": "A Docusaurus theme implementing the GOV.UK Design System for consistent, accessible documentation sites",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"license": "MIT",
|
package/src/css/components.scss
CHANGED
|
@@ -34,6 +34,54 @@
|
|
|
34
34
|
min-width: 0;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Admonition colour modifiers.
|
|
38
|
+
// The thick left bar is drawn via ::before so it overlaps the thin border corners
|
|
39
|
+
// cleanly — CSS border mitering would otherwise show a pale sliver at the joins.
|
|
40
|
+
// border-left-width is kept at 5px (matching GOV.UK's original) but made transparent
|
|
41
|
+
// so the element's layout is unchanged; ::before paints over that space.
|
|
42
|
+
.govuk-inset-text.govuk-inset-text--note,
|
|
43
|
+
.govuk-inset-text.govuk-inset-text--tip,
|
|
44
|
+
.govuk-inset-text.govuk-inset-text--info,
|
|
45
|
+
.govuk-inset-text.govuk-inset-text--important {
|
|
46
|
+
border: 1px solid #bad3ea;
|
|
47
|
+
border-left-width: 10px;
|
|
48
|
+
border-left-color: transparent;
|
|
49
|
+
background: none;
|
|
50
|
+
position: relative;
|
|
51
|
+
|
|
52
|
+
&::before {
|
|
53
|
+
content: '';
|
|
54
|
+
position: absolute;
|
|
55
|
+
top: -1px;
|
|
56
|
+
bottom: -1px;
|
|
57
|
+
left: -10px;
|
|
58
|
+
width: 10px;
|
|
59
|
+
background: #1d70b8;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
> p:first-child strong { color: #1d70b8; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.govuk-inset-text.govuk-inset-text--caution {
|
|
66
|
+
border: 1px solid #fbd6c3;
|
|
67
|
+
border-left-width: 10px;
|
|
68
|
+
border-left-color: transparent;
|
|
69
|
+
background: none;
|
|
70
|
+
position: relative;
|
|
71
|
+
|
|
72
|
+
&::before {
|
|
73
|
+
content: '';
|
|
74
|
+
position: absolute;
|
|
75
|
+
top: -1px;
|
|
76
|
+
bottom: -1px;
|
|
77
|
+
left: -10px;
|
|
78
|
+
width: 10px;
|
|
79
|
+
background: #f47738;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
> p:first-child strong { color: #f47738; }
|
|
83
|
+
}
|
|
84
|
+
|
|
37
85
|
// Code block
|
|
38
86
|
.app-code-block {
|
|
39
87
|
position: relative;
|
|
@@ -56,9 +104,18 @@
|
|
|
56
104
|
font-size: 0.875rem;
|
|
57
105
|
line-height: 1.6;
|
|
58
106
|
font-family: Menlo, Consolas, 'Courier New', monospace;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
107
|
+
background-color: #f4f8fb;
|
|
108
|
+
|
|
109
|
+
code {
|
|
110
|
+
display: block;
|
|
111
|
+
|
|
112
|
+
> div::after {
|
|
113
|
+
content: '';
|
|
114
|
+
display: inline-block;
|
|
115
|
+
width: calc(1rem + 70px);
|
|
116
|
+
line-height: 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
62
119
|
}
|
|
63
120
|
|
|
64
121
|
.app-code-block__copy {
|
|
@@ -134,7 +191,7 @@
|
|
|
134
191
|
.app-layout-sidebar__nav .not-govuk-navigation-menu__list__subitems {
|
|
135
192
|
|
|
136
193
|
.not-govuk-navigation-menu__list__item {
|
|
137
|
-
padding: 5px 0
|
|
194
|
+
padding: 5px 0;
|
|
138
195
|
margin-bottom: 5px;
|
|
139
196
|
border-left-width: 0px;
|
|
140
197
|
border-left-style: solid;
|
|
@@ -145,7 +202,8 @@
|
|
|
145
202
|
|
|
146
203
|
@media (min-width: 48.125em) {
|
|
147
204
|
border-left: 4px solid #1d70b8;
|
|
148
|
-
padding-left:
|
|
205
|
+
padding-left: 11px;
|
|
206
|
+
margin-left: -15px;
|
|
149
207
|
font-weight: bold;
|
|
150
208
|
|
|
151
209
|
.not-govuk-navigation-menu__list__link {
|
|
@@ -162,6 +220,36 @@
|
|
|
162
220
|
padding-left: 0;
|
|
163
221
|
}
|
|
164
222
|
|
|
223
|
+
// API definition list (Type, Default, etc.)
|
|
224
|
+
.app-definition-list {
|
|
225
|
+
margin: 0 0 govuk-spacing(4);
|
|
226
|
+
padding: 0;
|
|
227
|
+
|
|
228
|
+
&__item {
|
|
229
|
+
display: flex;
|
|
230
|
+
gap: govuk-spacing(2);
|
|
231
|
+
align-items: baseline;
|
|
232
|
+
margin-bottom: govuk-spacing(1);
|
|
233
|
+
|
|
234
|
+
dt {
|
|
235
|
+
font-weight: 700;
|
|
236
|
+
flex-shrink: 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
dd {
|
|
240
|
+
margin: 0;
|
|
241
|
+
|
|
242
|
+
code {
|
|
243
|
+
color: #484949;
|
|
244
|
+
font-size: 0.85em;
|
|
245
|
+
background-color: #f4f8fb;
|
|
246
|
+
padding: 1px 4px;
|
|
247
|
+
border-radius: 2px;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
165
253
|
// Sidebar nav section headings and mobile toggle
|
|
166
254
|
// Desktop: headings are non-link spans, visually distinct from link items
|
|
167
255
|
.not-govuk-navigation-menu__list__heading {
|
package/src/css/prose-scope.scss
CHANGED
|
@@ -20,8 +20,29 @@
|
|
|
20
20
|
@extend %govuk-heading-s;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// Extra top padding on headings that follow a code block,
|
|
24
|
+
// matching the spacing govuk-frontend adds after paragraphs and lists
|
|
25
|
+
.app-code-block + h2:not(.app-no-prose *),
|
|
26
|
+
.app-code-block + h3:not(.app-no-prose *),
|
|
27
|
+
.app-code-block + h4:not(.app-no-prose *),
|
|
28
|
+
.app-code-block + h5:not(.app-no-prose *),
|
|
29
|
+
.app-code-block + h6:not(.app-no-prose *) {
|
|
30
|
+
padding-top: 5px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@media (min-width: 40.0625em) {
|
|
34
|
+
.app-code-block + h2:not(.app-no-prose *),
|
|
35
|
+
.app-code-block + h3:not(.app-no-prose *),
|
|
36
|
+
.app-code-block + h4:not(.app-no-prose *),
|
|
37
|
+
.app-code-block + h5:not(.app-no-prose *),
|
|
38
|
+
.app-code-block + h6:not(.app-no-prose *) {
|
|
39
|
+
padding-top: 20px;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
23
43
|
// Body text
|
|
24
|
-
p:not(.app-no-prose *)
|
|
44
|
+
p:not(.app-no-prose *),
|
|
45
|
+
.app-definition-list:not(.app-no-prose *) {
|
|
25
46
|
@extend %govuk-body-m;
|
|
26
47
|
}
|
|
27
48
|
|
|
@@ -55,5 +76,67 @@
|
|
|
55
76
|
@extend %govuk-section-break;
|
|
56
77
|
@extend %govuk-section-break--visible;
|
|
57
78
|
@extend %govuk-section-break--xl;
|
|
79
|
+
border-color: #cecece;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Inline code styling (paragraphs, list items, headings etc — but not inside pre blocks)
|
|
83
|
+
code:not(pre *):not(h1 *):not(h2 *):not(h3 *):not(h4 *):not(h5 *):not(h6 *):not(.app-no-prose *) {
|
|
84
|
+
color: #484949;
|
|
85
|
+
font-size: 0.85em;
|
|
86
|
+
background-color: #f4f8fb;
|
|
87
|
+
padding: 1px 4px;
|
|
88
|
+
border-radius: 2px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Lighter table borders
|
|
92
|
+
.govuk-table__header,
|
|
93
|
+
.govuk-table__cell {
|
|
94
|
+
border-bottom-color: #cecece;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Remove the table's last row border when a section break immediately follows,
|
|
98
|
+
// so the two rules don't double up visually.
|
|
99
|
+
// Border is on td/th cells, not tr, so target those directly.
|
|
100
|
+
table:has(+ hr) tr:last-child th,
|
|
101
|
+
table:has(+ hr) tr:last-child td {
|
|
102
|
+
border-bottom: none;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Remove bottom margin from the last paragraph inside inset and warning text
|
|
106
|
+
.govuk-inset-text,
|
|
107
|
+
.govuk-warning-text {
|
|
108
|
+
p:last-child:not(.app-no-prose *) {
|
|
109
|
+
margin-bottom: 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Code block copy button
|
|
114
|
+
.app-code-block__copy {
|
|
115
|
+
font-size: 0.9rem;
|
|
116
|
+
min-width: 70px;
|
|
117
|
+
padding: 3px 10px;
|
|
118
|
+
border: 1px solid #1a65a6;
|
|
119
|
+
color: #1a65a6;
|
|
120
|
+
box-shadow: 0 2px 0 0 #1a65a6;
|
|
121
|
+
background-color: #fff;
|
|
122
|
+
text-align: center;
|
|
123
|
+
text-decoration: none;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.app-code-block__copy:active {
|
|
128
|
+
border: 2px solid var(--govuk-focus-colour, #ffdd00);
|
|
129
|
+
padding: 2px 10px;
|
|
130
|
+
outline: 2px solid rgba(0, 0, 0, 0);
|
|
131
|
+
box-shadow: none;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.app-code-block__copy:focus:not(:hover) {
|
|
135
|
+
border: 2px solid var(--govuk-focus-colour, #ffdd00);
|
|
136
|
+
padding: 2px 10px;
|
|
137
|
+
outline: 2px solid rgba(0, 0, 0, 0);
|
|
138
|
+
color: var(--govuk-focus-text-colour, #0b0c0c);
|
|
139
|
+
background-color: var(--govuk-focus-colour, #ffdd00);
|
|
140
|
+
box-shadow: 0 2px 0 0 var(--govuk-focus-text-colour, #0b0c0c);
|
|
58
141
|
}
|
|
59
142
|
}
|
package/src/css/theme.scss
CHANGED
|
@@ -17,10 +17,38 @@
|
|
|
17
17
|
* without this, outer chrome inherits the browser default serif.
|
|
18
18
|
* GDS Transport is not bundled — Helvetica/Arial is used instead.
|
|
19
19
|
*/
|
|
20
|
-
body
|
|
20
|
+
body,
|
|
21
|
+
[class^="govuk-"],
|
|
22
|
+
[class*=" govuk-"] {
|
|
21
23
|
font-family: Helvetica, Arial, sans-serif;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
/* Wider page width — overrides the 960px default from govuk-frontend.
|
|
27
|
+
* !important is needed because @not-govuk/width-container injects its CSS
|
|
28
|
+
* via the JS bundle, which loads after static stylesheets and wins the cascade.
|
|
29
|
+
*
|
|
30
|
+
* The package's margin: auto rule fires at 1020px (960 + 30 + 30), which is too
|
|
31
|
+
* early for our wider container. We restore the 30px gutters from 1020px and only
|
|
32
|
+
* allow auto-centering once the viewport is wide enough to actually cap at 1100px
|
|
33
|
+
* (1100 + 30 + 30 = 1160px). */
|
|
34
|
+
.govuk-width-container {
|
|
35
|
+
max-width: 1100px !important;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@media (min-width: 1020px) {
|
|
39
|
+
.govuk-width-container {
|
|
40
|
+
margin-right: 30px !important;
|
|
41
|
+
margin-left: 30px !important;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@media (min-width: 1160px) {
|
|
46
|
+
.govuk-width-container {
|
|
47
|
+
margin-right: auto !important;
|
|
48
|
+
margin-left: auto !important;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
24
52
|
/*
|
|
25
53
|
* Sticky footer: Docusaurus inserts a #__docusaurus wrapper between body
|
|
26
54
|
* and our layout, breaking GOV.UK Frontend's flex sticky-footer chain.
|
|
@@ -1,32 +1,27 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import {InsetText, WarningText} from '@not-govuk/simple-components';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
note: 'Note',
|
|
6
|
-
tip: 'Tip',
|
|
7
|
-
info: 'Info',
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
const ADMONITION_CONFIGS = {
|
|
5
|
+
note: { label: 'Note', modifier: 'govuk-inset-text--note' },
|
|
6
|
+
tip: { label: 'Tip', modifier: 'govuk-inset-text--tip' },
|
|
7
|
+
info: { label: 'Info', modifier: 'govuk-inset-text--info' },
|
|
8
|
+
important: { label: 'Important', modifier: 'govuk-inset-text--important' },
|
|
9
|
+
caution: { label: 'Caution', modifier: 'govuk-inset-text--caution' },
|
|
10
|
+
warning: { label: 'Warning', modifier: null },
|
|
11
|
+
danger: { label: 'Danger', modifier: null },
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export default function Admonition({type = 'note', title, children}) {
|
|
14
|
-
const
|
|
15
|
+
const config = ADMONITION_CONFIGS[type] ?? ADMONITION_CONFIGS.note;
|
|
16
|
+
const displayTitle = (title || config.label).toUpperCase();
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<WarningText>
|
|
20
|
-
<strong>{displayTitle}: </strong>
|
|
21
|
-
{children}
|
|
22
|
-
</WarningText>
|
|
23
|
-
);
|
|
18
|
+
if (!config.modifier) {
|
|
19
|
+
return <WarningText>{children}</WarningText>;
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
// All other types use GOV.UK InsetText
|
|
27
22
|
return (
|
|
28
|
-
<InsetText>
|
|
29
|
-
|
|
23
|
+
<InsetText className={config.modifier}>
|
|
24
|
+
<p><strong>{displayTitle}</strong></p>
|
|
30
25
|
{children}
|
|
31
26
|
</InsetText>
|
|
32
27
|
);
|
|
@@ -28,7 +28,7 @@ export default function CodeBlock({children, className: classNameProp, title}) {
|
|
|
28
28
|
</div>
|
|
29
29
|
)}
|
|
30
30
|
<Highlight theme={themes.github} code={codeString} language={language}>
|
|
31
|
-
{({style, tokens, getLineProps, getTokenProps}) => (
|
|
31
|
+
{({style: {backgroundColor: _bg, ...style}, tokens, getLineProps, getTokenProps}) => (
|
|
32
32
|
<pre className="app-code-block__pre" style={style}>
|
|
33
33
|
<button
|
|
34
34
|
type="button"
|
|
@@ -2,7 +2,53 @@ import React from 'react';
|
|
|
2
2
|
import CodeBlock from '@theme/CodeBlock';
|
|
3
3
|
import Heading from '@theme/Heading';
|
|
4
4
|
import Admonition from '@theme/Admonition';
|
|
5
|
-
import {InsetText} from '@not-govuk/simple-components';
|
|
5
|
+
import {InsetText, WarningText} from '@not-govuk/simple-components';
|
|
6
|
+
|
|
7
|
+
const ALERT_CONFIGS = {
|
|
8
|
+
NOTE: { label: 'Note', modifier: 'govuk-inset-text--note' },
|
|
9
|
+
TIP: { label: 'Tip', modifier: 'govuk-inset-text--tip' },
|
|
10
|
+
INFO: { label: 'Info', modifier: 'govuk-inset-text--info' },
|
|
11
|
+
IMPORTANT: { label: 'Important', modifier: 'govuk-inset-text--important' },
|
|
12
|
+
CAUTION: { label: 'Caution', modifier: 'govuk-inset-text--caution' },
|
|
13
|
+
WARNING: { label: 'Warning', modifier: null }, // uses WarningText
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ALERT_PATTERN = /^\s*\[!(NOTE|TIP|INFO|IMPORTANT|WARNING|CAUTION)\]\s*/i;
|
|
17
|
+
|
|
18
|
+
// Parses blockquote children for a GitHub-style alert marker.
|
|
19
|
+
// Returns {config, allContent} if found, null otherwise.
|
|
20
|
+
function parseGithubAlert(children) {
|
|
21
|
+
const childArray = React.Children.toArray(children);
|
|
22
|
+
const firstChildIndex = childArray.findIndex((c) => React.isValidElement(c));
|
|
23
|
+
const firstChild = childArray[firstChildIndex];
|
|
24
|
+
if (!firstChild?.props) return null;
|
|
25
|
+
|
|
26
|
+
const pChildren = React.Children.toArray(firstChild.props.children);
|
|
27
|
+
let mergedLeadingText = '';
|
|
28
|
+
let mergeCount = 0;
|
|
29
|
+
for (const child of pChildren) {
|
|
30
|
+
if (typeof child !== 'string') break;
|
|
31
|
+
mergedLeadingText += child;
|
|
32
|
+
mergeCount += 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const match = ALERT_PATTERN.exec(mergedLeadingText);
|
|
36
|
+
if (!match) return null;
|
|
37
|
+
|
|
38
|
+
const config = ALERT_CONFIGS[match[1].toUpperCase()];
|
|
39
|
+
const remainingText = mergedLeadingText.slice(match[0].length).trimStart();
|
|
40
|
+
const newPChildren = [
|
|
41
|
+
...(remainingText ? [remainingText] : []),
|
|
42
|
+
...pChildren.slice(mergeCount),
|
|
43
|
+
].filter((c) => !(typeof c === 'string' && c.trim() === ''));
|
|
44
|
+
|
|
45
|
+
const contentParagraph = newPChildren.length > 0
|
|
46
|
+
? React.cloneElement(firstChild, {key: 'content'}, ...newPChildren)
|
|
47
|
+
: null;
|
|
48
|
+
const allContent = [contentParagraph, ...childArray.slice(firstChildIndex + 1)].filter(Boolean);
|
|
49
|
+
|
|
50
|
+
return {config, allContent};
|
|
51
|
+
}
|
|
6
52
|
|
|
7
53
|
function GovukLink(props) {
|
|
8
54
|
return <a className="govuk-link" {...props} />;
|
|
@@ -20,6 +66,61 @@ const MDXComponents = {
|
|
|
20
66
|
}
|
|
21
67
|
return <pre {...props} />;
|
|
22
68
|
},
|
|
69
|
+
// Paragraphs: detect API definition patterns like **Type:** `string` and render as <dl>
|
|
70
|
+
p: ({children, ...rest}) => {
|
|
71
|
+
const childArray = React.Children.toArray(children);
|
|
72
|
+
const firstChild = childArray[0];
|
|
73
|
+
|
|
74
|
+
// A definition-list paragraph starts with a <strong> whose text ends with ':'
|
|
75
|
+
const isDefinitionTerm = (node) =>
|
|
76
|
+
React.isValidElement(node) &&
|
|
77
|
+
node.type === 'strong' &&
|
|
78
|
+
typeof node.props.children === 'string' &&
|
|
79
|
+
node.props.children.trim().endsWith(':');
|
|
80
|
+
|
|
81
|
+
if (!isDefinitionTerm(firstChild)) {
|
|
82
|
+
return <p {...rest}>{children}</p>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Group children into [{term, defs}] pairs split on each <strong> label
|
|
86
|
+
const items = [];
|
|
87
|
+
let current = null;
|
|
88
|
+
for (const child of childArray) {
|
|
89
|
+
if (isDefinitionTerm(child)) {
|
|
90
|
+
if (current) items.push(current);
|
|
91
|
+
current = {term: child.props.children, defs: []};
|
|
92
|
+
} else if (current) {
|
|
93
|
+
// Skip bare whitespace/newline separators between term and value
|
|
94
|
+
if (typeof child === 'string' && child.trim() === '') continue;
|
|
95
|
+
current.defs.push(child);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (current) items.push(current);
|
|
99
|
+
|
|
100
|
+
// Render <dd> content, converting any **Required** (or similar flags) to (required)
|
|
101
|
+
const renderDefs = (defs) => defs.map((node) => {
|
|
102
|
+
if (
|
|
103
|
+
React.isValidElement(node) &&
|
|
104
|
+
node.type === 'strong' &&
|
|
105
|
+
typeof node.props.children === 'string'
|
|
106
|
+
) {
|
|
107
|
+
const label = node.props.children.trim().toLowerCase();
|
|
108
|
+
return <strong key={label}> ({label})</strong>;
|
|
109
|
+
}
|
|
110
|
+
return node;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<dl className="app-definition-list">
|
|
115
|
+
{items.map((item) => (
|
|
116
|
+
<div key={item.term} className="app-definition-list__item">
|
|
117
|
+
<dt>{item.term}</dt>
|
|
118
|
+
<dd>{renderDefs(item.defs)}</dd>
|
|
119
|
+
</div>
|
|
120
|
+
))}
|
|
121
|
+
</dl>
|
|
122
|
+
);
|
|
123
|
+
},
|
|
23
124
|
// GOV.UK table styling
|
|
24
125
|
table: (props) => <table className="govuk-table" {...props} />,
|
|
25
126
|
thead: (props) => <thead className="govuk-table__head" {...props} />,
|
|
@@ -27,8 +128,24 @@ const MDXComponents = {
|
|
|
27
128
|
tr: (props) => <tr className="govuk-table__row" {...props} />,
|
|
28
129
|
th: (props) => <th className="govuk-table__header" {...props} />,
|
|
29
130
|
td: (props) => <td className="govuk-table__cell" {...props} />,
|
|
30
|
-
// Blockquotes rendered as GOV.UK InsetText
|
|
31
|
-
|
|
131
|
+
// Blockquotes rendered as GOV.UK InsetText, with support for GitHub-style alerts
|
|
132
|
+
// e.g. > [!NOTE], > [!WARNING], > [!TIP], > [!IMPORTANT], > [!CAUTION]
|
|
133
|
+
blockquote: ({children}) => {
|
|
134
|
+
const alert = parseGithubAlert(children);
|
|
135
|
+
if (!alert) return <InsetText>{children}</InsetText>;
|
|
136
|
+
|
|
137
|
+
const {config, allContent} = alert;
|
|
138
|
+
if (!config.modifier) {
|
|
139
|
+
return <WarningText>{allContent}</WarningText>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<InsetText className={config.modifier}>
|
|
144
|
+
<p key="title"><strong>{config.label.toUpperCase()}</strong></p>
|
|
145
|
+
{allContent}
|
|
146
|
+
</InsetText>
|
|
147
|
+
);
|
|
148
|
+
},
|
|
32
149
|
h1: (props) => <Heading as="h1" {...props} />,
|
|
33
150
|
h2: (props) => <Heading as="h2" {...props} />,
|
|
34
151
|
h3: (props) => <Heading as="h3" {...props} />,
|