@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 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.17-alpha",
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",
@@ -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
- margin: 0;
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 {
@@ -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 *),
@@ -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 admonitionTitles = {
5
- note: 'Note',
6
- tip: 'Tip',
7
- info: 'Info',
8
- warning: 'Warning',
9
- danger: 'Danger',
10
- caution: 'Caution',
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 displayTitle = title || admonitionTitles[type] || 'Note';
15
+ const config = ADMONITION_CONFIGS[type] ?? ADMONITION_CONFIGS.note;
16
+ const displayTitle = (title || config.label).toUpperCase();
15
17
 
16
- // Warning and danger types use GOV.UK WarningText
17
- if (type === 'warning' || type === 'danger' || type === 'caution') {
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
- {title && <strong>{displayTitle}: </strong>}
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, ...rest}) => {
88
- // Use \s* to tolerate any whitespace (newlines, \r\n, spaces) around the marker
89
- const alertPattern = /^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/i;
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
- const match = alertPattern.exec(mergedLeadingText);
112
- if (match) {
113
- const alertType = match[1].toUpperCase();
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 <InsetText {...rest}>{children}</InsetText>;
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} />,