@defra/docusaurus-theme-govuk 0.0.17-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 +59 -3
- package/src/css/prose-scope.scss +8 -0
- package/src/css/theme.scss +3 -1
- package/src/theme/Admonition/index.js +14 -19
- package/src/theme/MDXComponents/index.js +59 -47
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,10 +104,18 @@
|
|
|
56
104
|
font-size: 0.875rem;
|
|
57
105
|
line-height: 1.6;
|
|
58
106
|
font-family: Menlo, Consolas, 'Courier New', monospace;
|
|
59
|
-
position: relative;
|
|
60
|
-
border: 1px solid #cecece;
|
|
61
107
|
background-color: #f4f8fb;
|
|
62
|
-
|
|
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
|
+
}
|
|
63
119
|
}
|
|
64
120
|
|
|
65
121
|
.app-code-block__copy {
|
package/src/css/prose-scope.scss
CHANGED
|
@@ -22,6 +22,14 @@
|
|
|
22
22
|
|
|
23
23
|
// Extra top padding on headings that follow a code block,
|
|
24
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
|
+
|
|
25
33
|
@media (min-width: 40.0625em) {
|
|
26
34
|
.app-code-block + h2:not(.app-no-prose *),
|
|
27
35
|
.app-code-block + h3:not(.app-no-prose *),
|
package/src/css/theme.scss
CHANGED
|
@@ -17,7 +17,9 @@
|
|
|
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
|
|
|
@@ -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
|
);
|
|
@@ -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} />;
|
|
@@ -84,55 +130,21 @@ const MDXComponents = {
|
|
|
84
130
|
td: (props) => <td className="govuk-table__cell" {...props} />,
|
|
85
131
|
// Blockquotes rendered as GOV.UK InsetText, with support for GitHub-style alerts
|
|
86
132
|
// e.g. > [!NOTE], > [!WARNING], > [!TIP], > [!IMPORTANT], > [!CAUTION]
|
|
87
|
-
blockquote: ({children
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const childArray = React.Children.toArray(children);
|
|
91
|
-
// Skip leading whitespace text nodes — MDX can inject them
|
|
92
|
-
const firstChildIndex = childArray.findIndex((c) => React.isValidElement(c));
|
|
93
|
-
const firstChild = childArray[firstChildIndex];
|
|
94
|
-
|
|
95
|
-
// Don't check type === 'p': when a p override is registered in MDXComponents,
|
|
96
|
-
// MDX sets the element type to the override function, not the string 'p'.
|
|
97
|
-
if (firstChild?.props) {
|
|
98
|
-
const pChildren = React.Children.toArray(firstChild.props.children);
|
|
99
|
-
|
|
100
|
-
// Remark can split "[!NOTE]" across adjacent text nodes (e.g. "[" + "!NOTE]\n...")
|
|
101
|
-
// when the paragraph also contains inline links. Merge leading text nodes before
|
|
102
|
-
// testing so the marker is always visible as a single string.
|
|
103
|
-
let mergedLeadingText = '';
|
|
104
|
-
let mergeCount = 0;
|
|
105
|
-
for (const child of pChildren) {
|
|
106
|
-
if (typeof child !== 'string') break;
|
|
107
|
-
mergedLeadingText += child;
|
|
108
|
-
mergeCount += 1;
|
|
109
|
-
}
|
|
133
|
+
blockquote: ({children}) => {
|
|
134
|
+
const alert = parseGithubAlert(children);
|
|
135
|
+
if (!alert) return <InsetText>{children}</InsetText>;
|
|
110
136
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const remainingText = mergedLeadingText.slice(match[0].length).trimStart();
|
|
115
|
-
const newPChildren = [
|
|
116
|
-
...(remainingText ? [remainingText] : []),
|
|
117
|
-
...pChildren.slice(mergeCount),
|
|
118
|
-
].filter((c) => !(typeof c === 'string' && c.trim() === ''));
|
|
119
|
-
|
|
120
|
-
const contentParagraph = newPChildren.length > 0
|
|
121
|
-
? React.cloneElement(firstChild, {key: 'content'}, ...newPChildren)
|
|
122
|
-
: null;
|
|
123
|
-
// Use firstChildIndex + 1 so we don't re-include the original <p>
|
|
124
|
-
const allContent = [contentParagraph, ...childArray.slice(firstChildIndex + 1)].filter(Boolean);
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<InsetText {...rest}>
|
|
128
|
-
<p key="title"><strong>{alertType}</strong></p>
|
|
129
|
-
{allContent}
|
|
130
|
-
</InsetText>
|
|
131
|
-
);
|
|
132
|
-
}
|
|
137
|
+
const {config, allContent} = alert;
|
|
138
|
+
if (!config.modifier) {
|
|
139
|
+
return <WarningText>{allContent}</WarningText>;
|
|
133
140
|
}
|
|
134
141
|
|
|
135
|
-
return
|
|
142
|
+
return (
|
|
143
|
+
<InsetText className={config.modifier}>
|
|
144
|
+
<p key="title"><strong>{config.label.toUpperCase()}</strong></p>
|
|
145
|
+
{allContent}
|
|
146
|
+
</InsetText>
|
|
147
|
+
);
|
|
136
148
|
},
|
|
137
149
|
h1: (props) => <Heading as="h1" {...props} />,
|
|
138
150
|
h2: (props) => <Heading as="h2" {...props} />,
|