@eeacms/volto-cca-policy 1.0.25 → 1.0.26
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 +20 -13
- package/package.json +1 -1
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.jsx +16 -24
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.test.jsx +21 -2
- package/src/components/Search/NavigatorGuide/NavigatorGuideContentView.jsx +153 -135
- package/src/components/Search/NavigatorGuide/NavigatorGuideContentView.test.jsx +108 -2
- package/src/components/Search/NavigatorGuide/utils.js +15 -0
- package/src/components/Search/NavigatorGuide/utils.test.js +18 -1
- package/src/components/index.js +2 -0
- package/src/components/manage/Blocks/ChatbotCatalogue/astUtils.js +129 -3
- package/src/components/manage/Blocks/ChatbotCatalogue/astUtils.test.js +207 -0
- package/src/components/manage/Blocks/ChatbotCatalogue/docCards.js +8 -0
- package/src/components/manage/Blocks/ChatbotCatalogue/docCards.test.js +72 -0
- package/src/components/manage/Blocks/ChatbotCatalogue/nextSteps.js +4 -0
- package/src/components/manage/Blocks/ChatbotCatalogue/nextSteps.test.js +48 -0
- package/src/components/theme/CompareTools/CompareToolsPanel.jsx +1 -1
- package/src/components/theme/CompareTools/CompareToolsView.jsx +1 -1
- package/src/components/theme/Content/Sections/MetadataItemList.jsx +7 -24
- package/src/components/theme/TagOverflowPopup.jsx +28 -0
- package/src/components/theme/Views/RelatedTools.jsx +7 -25
- package/src/search/navigator_guide/facets.js +1 -0
- package/src/search/navigator_guide/guideSteps.js +1 -1
- package/theme/globals/navigator.less +11 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import '@testing-library/jest-dom';
|
|
3
|
-
import { fireEvent, render } from '@testing-library/react';
|
|
3
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
4
4
|
import { IntlProvider } from 'react-intl';
|
|
5
5
|
import { useAtom } from 'jotai';
|
|
6
6
|
import { useSelector } from 'react-redux';
|
|
@@ -21,13 +21,14 @@ jest.mock('./useGuideFacetOptions', () => ({
|
|
|
21
21
|
default: jest.fn(),
|
|
22
22
|
}));
|
|
23
23
|
|
|
24
|
-
const renderGuide = (result) => {
|
|
24
|
+
const renderGuide = (result, searchContext = {}) => {
|
|
25
25
|
useSearchContext.mockReturnValue({
|
|
26
26
|
filters: [{ field: 'cca_adaptation_sectors.keyword', values: ['Water'] }],
|
|
27
27
|
facets: {},
|
|
28
28
|
results: [result],
|
|
29
29
|
totalResults: 1,
|
|
30
30
|
isLoading: false,
|
|
31
|
+
...searchContext,
|
|
31
32
|
});
|
|
32
33
|
return render(
|
|
33
34
|
<IntlProvider locale="en">
|
|
@@ -92,3 +93,108 @@ describe('NavigatorGuideContentView thumbnails', () => {
|
|
|
92
93
|
expect(thumbnail.querySelector('.ri-stack-line')).toBeInTheDocument();
|
|
93
94
|
});
|
|
94
95
|
});
|
|
96
|
+
|
|
97
|
+
describe('NavigatorGuideContentView adaptation steps', () => {
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
jest.clearAllMocks();
|
|
100
|
+
useAtom.mockReturnValue([2, jest.fn()]);
|
|
101
|
+
useSelector.mockReturnValue('en');
|
|
102
|
+
useHistory.mockReturnValue({ push: jest.fn() });
|
|
103
|
+
useGuideFacetOptions.mockReturnValue({
|
|
104
|
+
'cca_adaptation_support_cycle_step.keyword': [
|
|
105
|
+
'Step 2: Assess',
|
|
106
|
+
'Step 1: Prepare',
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('orders steps and requires every selected step', () => {
|
|
112
|
+
const addFilter = jest.fn();
|
|
113
|
+
renderGuide(
|
|
114
|
+
{ title: 'Guide tool', href: '/tools/guide-tool' },
|
|
115
|
+
{
|
|
116
|
+
filters: [],
|
|
117
|
+
addFilter,
|
|
118
|
+
facets: {
|
|
119
|
+
'cca_adaptation_support_cycle_step.keyword': [
|
|
120
|
+
{
|
|
121
|
+
data: [
|
|
122
|
+
{ value: 'Step 2: Assess', count: 9 },
|
|
123
|
+
{ value: 'Step 1: Prepare', count: 1 },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const labels = screen.getAllByText(/^Step [12]:/);
|
|
132
|
+
expect(labels.map((label) => label.textContent)).toEqual([
|
|
133
|
+
'Step 1: Prepare',
|
|
134
|
+
'Step 2: Assess',
|
|
135
|
+
]);
|
|
136
|
+
fireEvent.click(screen.getByText('Step 2: Assess'));
|
|
137
|
+
expect(addFilter).toHaveBeenCalledWith(
|
|
138
|
+
'cca_adaptation_support_cycle_step.keyword',
|
|
139
|
+
'Step 2: Assess',
|
|
140
|
+
'all',
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('does not show an unrelated selected step on a preview tool', () => {
|
|
145
|
+
const { container } = renderGuide(
|
|
146
|
+
{
|
|
147
|
+
title: 'Guide tool',
|
|
148
|
+
href: '/tools/guide-tool',
|
|
149
|
+
cca_adaptation_support_cycle_step: { raw: ['Step 1: Prepare'] },
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
filters: [
|
|
153
|
+
{
|
|
154
|
+
field: 'cca_adaptation_support_cycle_step.keyword',
|
|
155
|
+
values: ['Step 1: Prepare', 'Step 2: Assess'],
|
|
156
|
+
type: 'all',
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(
|
|
163
|
+
container.querySelectorAll(
|
|
164
|
+
'.navigator-guide-preview-tags .navigator-tag',
|
|
165
|
+
),
|
|
166
|
+
).toHaveLength(1);
|
|
167
|
+
expect(
|
|
168
|
+
container.querySelector('.navigator-guide-preview-tags'),
|
|
169
|
+
).toHaveTextContent('Step 1');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('NavigatorGuideContentView climate hazards', () => {
|
|
174
|
+
beforeEach(() => {
|
|
175
|
+
jest.clearAllMocks();
|
|
176
|
+
useAtom.mockReturnValue([1, jest.fn()]);
|
|
177
|
+
useSelector.mockReturnValue('en');
|
|
178
|
+
useHistory.mockReturnValue({ push: jest.fn() });
|
|
179
|
+
useGuideFacetOptions.mockReturnValue({});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('uses the hazard tag style for a selected climate hazard', () => {
|
|
183
|
+
const { container } = renderGuide(
|
|
184
|
+
{
|
|
185
|
+
title: 'Guide tool',
|
|
186
|
+
href: '/tools/guide-tool',
|
|
187
|
+
cca_climate_impacts: { raw: ['Flooding'] },
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
filters: [
|
|
191
|
+
{ field: 'cca_climate_impacts.keyword', values: ['Flooding'] },
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
expect(
|
|
197
|
+
container.querySelector('.navigator-guide-preview-tags .hazard'),
|
|
198
|
+
).toHaveTextContent('Flooding');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -28,3 +28,18 @@ export const mergeGuideOptions = (
|
|
|
28
28
|
!selectedValues.includes(option.value),
|
|
29
29
|
}));
|
|
30
30
|
};
|
|
31
|
+
|
|
32
|
+
export const sortAdaptationSteps = (options) =>
|
|
33
|
+
[...options].sort((a, b) => {
|
|
34
|
+
const aStep = /^Step\s+(\d+)\b/i.exec(a.value);
|
|
35
|
+
const bStep = /^Step\s+(\d+)\b/i.exec(b.value);
|
|
36
|
+
|
|
37
|
+
if (aStep && bStep) {
|
|
38
|
+
return (
|
|
39
|
+
Number(aStep[1]) - Number(bStep[1]) || a.value.localeCompare(b.value)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (aStep) return -1;
|
|
43
|
+
if (bStep) return 1;
|
|
44
|
+
return a.value.localeCompare(b.value);
|
|
45
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mergeGuideOptions } from './utils';
|
|
1
|
+
import { mergeGuideOptions, sortAdaptationSteps } from './utils';
|
|
2
2
|
|
|
3
3
|
describe('Navigator Guide utilities', () => {
|
|
4
4
|
it('keeps unavailable values and marks them as disabled', () => {
|
|
@@ -37,4 +37,21 @@ describe('Navigator Guide utilities', () => {
|
|
|
37
37
|
{ value: 'Energy', count: 3, disabled: false },
|
|
38
38
|
]);
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
it('keeps adaptation steps in numeric order regardless of match counts', () => {
|
|
42
|
+
const options = [
|
|
43
|
+
{ value: 'Step 10: Review', count: 9 },
|
|
44
|
+
{ value: 'Step 3: Plan', count: 20 },
|
|
45
|
+
{ value: 'Step 1: Prepare', count: 1 },
|
|
46
|
+
{ value: 'Step 2: Assess', count: 5 },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
expect(sortAdaptationSteps(options).map(({ value }) => value)).toEqual([
|
|
50
|
+
'Step 1: Prepare',
|
|
51
|
+
'Step 2: Assess',
|
|
52
|
+
'Step 3: Plan',
|
|
53
|
+
'Step 10: Review',
|
|
54
|
+
]);
|
|
55
|
+
expect(options[0].value).toBe('Step 10: Review');
|
|
56
|
+
});
|
|
40
57
|
});
|
package/src/components/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { default as BannerTitle } from './theme/BannerTitle/BannerTitle';
|
|
2
2
|
export { default as PortalMessage } from './theme/PortalMessage/PortalMessage';
|
|
3
|
+
export { default as TagOverflowPopup } from './theme/TagOverflowPopup';
|
|
4
|
+
export { default as ToolThumbnail } from './theme/ToolThumbnail/ToolThumbnail';
|
|
3
5
|
export { default as TranslationDisclaimer } from './theme/TranslationDisclaimer/TranslationDisclaimer';
|
|
4
6
|
export { default as ASTNavigation } from './theme/ASTNavigation/ASTNavigation';
|
|
5
7
|
export { default as RedirectToLogin } from './theme/RedirectToLogin/RedirectToLogin';
|
|
@@ -136,12 +136,20 @@ export function parseMarkdownLines(content) {
|
|
|
136
136
|
continue;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// List item: 1. Item or * Item or - Item
|
|
140
|
-
const listMatch = trimmed.match(/^(\d+\.|[*+-])\s+(.*)$/);
|
|
139
|
+
// List item: 1. Item or * Item or - Item or [2] Item
|
|
140
|
+
const listMatch = trimmed.match(/^(\d+\.|[*+-]|\[\d+\])\s+(.*)$/);
|
|
141
141
|
if (listMatch) {
|
|
142
142
|
flushParagraph();
|
|
143
143
|
const isOrdered = /^\d+\./.test(listMatch[1]);
|
|
144
|
-
|
|
144
|
+
let itemText = listMatch[2].trim();
|
|
145
|
+
|
|
146
|
+
// Clean stray leading asterisks if odd number of asterisks
|
|
147
|
+
if (itemText.startsWith('**')) {
|
|
148
|
+
const count = (itemText.match(/\*\*/g) || []).length;
|
|
149
|
+
if (count % 2 === 1) {
|
|
150
|
+
itemText = itemText.replace(/^\*\*\s*/, '');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
145
153
|
|
|
146
154
|
const listItemNode = {
|
|
147
155
|
type: 'listItem',
|
|
@@ -247,3 +255,121 @@ export function unwrapCardLists(children) {
|
|
|
247
255
|
}
|
|
248
256
|
}
|
|
249
257
|
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Recursively cleans stray unclosed leading asterisks (`**`) from paragraphs
|
|
261
|
+
* and list items. CommonMark leaves unmatched asterisks as literal text in
|
|
262
|
+
* the first text node when the closing asterisks cannot be paired.
|
|
263
|
+
*/
|
|
264
|
+
export function cleanStrayLeadingAsterisks(node) {
|
|
265
|
+
if (!node) return;
|
|
266
|
+
if (node.type === 'paragraph' || node.type === 'listItem') {
|
|
267
|
+
if (Array.isArray(node.children)) {
|
|
268
|
+
const firstChild = node.children[0];
|
|
269
|
+
if (
|
|
270
|
+
firstChild?.type === 'text' &&
|
|
271
|
+
typeof firstChild.value === 'string' &&
|
|
272
|
+
/^\s*\*\*([^*]|$)/.test(firstChild.value)
|
|
273
|
+
) {
|
|
274
|
+
firstChild.value = firstChild.value.replace(/^\s*\*\*\s*/, '');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (Array.isArray(node.children)) {
|
|
279
|
+
node.children.forEach(cleanStrayLeadingAsterisks);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Checks if a string starts with a citation reference marker like `[2] Title`
|
|
285
|
+
*/
|
|
286
|
+
export function isCitationText(text) {
|
|
287
|
+
return typeof text === 'string' && /^\[\d+\]\s+/.test(text.trimStart());
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Normalizes citation paragraphs following a list into list items so that
|
|
292
|
+
* reference lists like `[2] ... \n [3] ...` do not break out of an unordered
|
|
293
|
+
* or ordered list (such as under `### Limitations`). Also converts standalone
|
|
294
|
+
* citation paragraphs into lists.
|
|
295
|
+
*/
|
|
296
|
+
export function normalizeCitationParagraphsToLists(children) {
|
|
297
|
+
if (!Array.isArray(children)) return;
|
|
298
|
+
|
|
299
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
300
|
+
const curr = children[i];
|
|
301
|
+
|
|
302
|
+
// Case 1: Existing list followed by citation paragraph(s)
|
|
303
|
+
if (curr.type === 'list' && Array.isArray(curr.children)) {
|
|
304
|
+
while (i + 1 < children.length) {
|
|
305
|
+
const next = children[i + 1];
|
|
306
|
+
if (!next || next.type !== 'paragraph') break;
|
|
307
|
+
const fullText = getNodeText(next).trim();
|
|
308
|
+
if (!isCitationText(fullText)) break;
|
|
309
|
+
|
|
310
|
+
// If single text node with multiple newline-separated lines
|
|
311
|
+
if (
|
|
312
|
+
Array.isArray(next.children) &&
|
|
313
|
+
next.children.length === 1 &&
|
|
314
|
+
next.children[0].type === 'text'
|
|
315
|
+
) {
|
|
316
|
+
const lines = next.children[0].value
|
|
317
|
+
.split(/\r?\n/)
|
|
318
|
+
.filter((l) => l.trim());
|
|
319
|
+
if (lines.every(isCitationText)) {
|
|
320
|
+
lines.forEach((line) => {
|
|
321
|
+
curr.children.push({
|
|
322
|
+
type: 'listItem',
|
|
323
|
+
children: [
|
|
324
|
+
{
|
|
325
|
+
type: 'paragraph',
|
|
326
|
+
children: [{ type: 'text', value: line.trim() }],
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
children.splice(i + 1, 1);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
curr.children.push({
|
|
337
|
+
type: 'listItem',
|
|
338
|
+
children: [next],
|
|
339
|
+
});
|
|
340
|
+
children.splice(i + 1, 1);
|
|
341
|
+
}
|
|
342
|
+
} else if (curr.type === 'paragraph') {
|
|
343
|
+
// Case 2: Standalone citation paragraph not preceded by a list
|
|
344
|
+
const fullText = getNodeText(curr).trim();
|
|
345
|
+
if (isCitationText(fullText)) {
|
|
346
|
+
if (
|
|
347
|
+
Array.isArray(curr.children) &&
|
|
348
|
+
curr.children.length === 1 &&
|
|
349
|
+
curr.children[0].type === 'text'
|
|
350
|
+
) {
|
|
351
|
+
const lines = curr.children[0].value
|
|
352
|
+
.split(/\r?\n/)
|
|
353
|
+
.filter((l) => l.trim());
|
|
354
|
+
if (lines.every(isCitationText)) {
|
|
355
|
+
const listItems = lines.map((line) => ({
|
|
356
|
+
type: 'listItem',
|
|
357
|
+
children: [
|
|
358
|
+
{
|
|
359
|
+
type: 'paragraph',
|
|
360
|
+
children: [{ type: 'text', value: line.trim() }],
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
}));
|
|
364
|
+
children.splice(i, 1, {
|
|
365
|
+
type: 'list',
|
|
366
|
+
ordered: false,
|
|
367
|
+
children: listItems,
|
|
368
|
+
});
|
|
369
|
+
i += listItems.length - 1;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DOC_MARKER_RE,
|
|
3
|
+
cleanStrayLeadingAsterisks,
|
|
3
4
|
deduplicateAdjacentHeadings,
|
|
5
|
+
isCitationText,
|
|
4
6
|
isNextStepsHeading,
|
|
7
|
+
normalizeCitationParagraphsToLists,
|
|
5
8
|
parseMarkdownLines,
|
|
6
9
|
shouldUnwrapCodeNode,
|
|
7
10
|
unwrapCardLists,
|
|
@@ -205,3 +208,207 @@ describe('unwrapCardLists', () => {
|
|
|
205
208
|
expect(children[1].value).toBe('Tool 2');
|
|
206
209
|
});
|
|
207
210
|
});
|
|
211
|
+
|
|
212
|
+
describe('cleanStrayLeadingAsterisks', () => {
|
|
213
|
+
it('strips unmatched leading ** from paragraphs and list items preceding strong elements', () => {
|
|
214
|
+
const node = {
|
|
215
|
+
type: 'listItem',
|
|
216
|
+
children: [
|
|
217
|
+
{
|
|
218
|
+
type: 'paragraph',
|
|
219
|
+
children: [
|
|
220
|
+
{
|
|
221
|
+
type: 'text',
|
|
222
|
+
value: '**Run a rapid, multi-hazard screening with the ',
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
type: 'strong',
|
|
226
|
+
children: [
|
|
227
|
+
{
|
|
228
|
+
type: 'text',
|
|
229
|
+
value: 'Pathways2Resilience Climate Toolbox',
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
type: 'text',
|
|
235
|
+
value: ' to identify climate threats.',
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
cleanStrayLeadingAsterisks(node);
|
|
243
|
+
const textNode = node.children[0].children[0];
|
|
244
|
+
expect(textNode.value).toBe(
|
|
245
|
+
'Run a rapid, multi-hazard screening with the ',
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('leaves paragraphs without leading ** untouched', () => {
|
|
250
|
+
const node = {
|
|
251
|
+
type: 'paragraph',
|
|
252
|
+
children: [
|
|
253
|
+
{
|
|
254
|
+
type: 'text',
|
|
255
|
+
value: 'Normal text without asterisks',
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
cleanStrayLeadingAsterisks(node);
|
|
261
|
+
expect(node.children[0].value).toBe('Normal text without asterisks');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('isCitationText', () => {
|
|
266
|
+
it('identifies citation markers', () => {
|
|
267
|
+
expect(isCitationText('[2] Pathways2Resilience Climate Toolbox')).toBe(
|
|
268
|
+
true,
|
|
269
|
+
);
|
|
270
|
+
expect(isCitationText(' [15] Multi-digit citation')).toBe(true);
|
|
271
|
+
expect(isCitationText('Normal paragraph text')).toBe(false);
|
|
272
|
+
expect(isCitationText('1. Numbered list item')).toBe(false);
|
|
273
|
+
expect(isCitationText(null)).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('normalizeCitationParagraphsToLists', () => {
|
|
278
|
+
it('appends newline-separated citation lines following a list as list items', () => {
|
|
279
|
+
const children = [
|
|
280
|
+
{
|
|
281
|
+
type: 'list',
|
|
282
|
+
ordered: false,
|
|
283
|
+
children: [
|
|
284
|
+
{
|
|
285
|
+
type: 'listItem',
|
|
286
|
+
children: [
|
|
287
|
+
{
|
|
288
|
+
type: 'paragraph',
|
|
289
|
+
children: [
|
|
290
|
+
{
|
|
291
|
+
type: 'text',
|
|
292
|
+
value: 'The Greek Climate Change Adaptation Hub limitation',
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
type: 'paragraph',
|
|
302
|
+
children: [
|
|
303
|
+
{
|
|
304
|
+
type: 'text',
|
|
305
|
+
value:
|
|
306
|
+
'[2] Pathways2Resilience meta-tool\n[3] NATURE DEMO DST analysis\n[4] Greek Hub resilience audit',
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
];
|
|
311
|
+
|
|
312
|
+
normalizeCitationParagraphsToLists(children);
|
|
313
|
+
// Paragraph should be merged into list
|
|
314
|
+
expect(children).toHaveLength(1);
|
|
315
|
+
const list = children[0];
|
|
316
|
+
expect(list.type).toBe('list');
|
|
317
|
+
expect(list.children).toHaveLength(4);
|
|
318
|
+
expect(list.children[0].children[0].children[0].value).toBe(
|
|
319
|
+
'The Greek Climate Change Adaptation Hub limitation',
|
|
320
|
+
);
|
|
321
|
+
expect(list.children[1].children[0].children[0].value).toBe(
|
|
322
|
+
'[2] Pathways2Resilience meta-tool',
|
|
323
|
+
);
|
|
324
|
+
expect(list.children[2].children[0].children[0].value).toBe(
|
|
325
|
+
'[3] NATURE DEMO DST analysis',
|
|
326
|
+
);
|
|
327
|
+
expect(list.children[3].children[0].children[0].value).toBe(
|
|
328
|
+
'[4] Greek Hub resilience audit',
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('appends consecutive citation paragraphs to a preceding list', () => {
|
|
333
|
+
const children = [
|
|
334
|
+
{
|
|
335
|
+
type: 'list',
|
|
336
|
+
ordered: false,
|
|
337
|
+
children: [
|
|
338
|
+
{
|
|
339
|
+
type: 'listItem',
|
|
340
|
+
children: [
|
|
341
|
+
{
|
|
342
|
+
type: 'paragraph',
|
|
343
|
+
children: [{ type: 'text', value: 'Initial bullet' }],
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
type: 'paragraph',
|
|
351
|
+
children: [{ type: 'text', value: '[2] Second item' }],
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
type: 'paragraph',
|
|
355
|
+
children: [{ type: 'text', value: '[3] Third item' }],
|
|
356
|
+
},
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
normalizeCitationParagraphsToLists(children);
|
|
360
|
+
expect(children).toHaveLength(1);
|
|
361
|
+
expect(children[0].children).toHaveLength(3);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('converts standalone citation paragraphs into a list', () => {
|
|
365
|
+
const children = [
|
|
366
|
+
{
|
|
367
|
+
type: 'paragraph',
|
|
368
|
+
children: [
|
|
369
|
+
{
|
|
370
|
+
type: 'text',
|
|
371
|
+
value: '[2] Item A\n[3] Item B',
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
},
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
normalizeCitationParagraphsToLists(children);
|
|
378
|
+
expect(children).toHaveLength(1);
|
|
379
|
+
expect(children[0].type).toBe('list');
|
|
380
|
+
expect(children[0].children).toHaveLength(2);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('leaves non-citation paragraphs untouched', () => {
|
|
384
|
+
const children = [
|
|
385
|
+
{
|
|
386
|
+
type: 'paragraph',
|
|
387
|
+
children: [{ type: 'text', value: 'Regular paragraph' }],
|
|
388
|
+
},
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
normalizeCitationParagraphsToLists(children);
|
|
392
|
+
expect(children).toHaveLength(1);
|
|
393
|
+
expect(children[0].type).toBe('paragraph');
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe('parseMarkdownLines enhancements', () => {
|
|
398
|
+
it('parses [2] marker lines as list items and cleans stray leading asterisks', () => {
|
|
399
|
+
const markdown =
|
|
400
|
+
'1. **Run a screening with the **Pathways2Resilience Toolbox** for risks\n[2] Citation line description';
|
|
401
|
+
const nodes = parseMarkdownLines(markdown);
|
|
402
|
+
expect(nodes).toHaveLength(2);
|
|
403
|
+
expect(nodes[0].type).toBe('list');
|
|
404
|
+
expect(nodes[0].ordered).toBe(true);
|
|
405
|
+
// Unmatched leading ** was cleaned, leaving the properly matched **Toolbox**
|
|
406
|
+
expect(nodes[0].children[0].children[0].children[0].value).toBe(
|
|
407
|
+
'Run a screening with the **Pathways2Resilience Toolbox** for risks',
|
|
408
|
+
);
|
|
409
|
+
expect(nodes[1].type).toBe('list');
|
|
410
|
+
expect(nodes[1].children[0].children[0].children[0].value).toBe(
|
|
411
|
+
'Citation line description',
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
|
|
20
20
|
import {
|
|
21
21
|
DOC_MARKER_RE,
|
|
22
|
+
cleanStrayLeadingAsterisks,
|
|
22
23
|
deduplicateAdjacentHeadings,
|
|
24
|
+
normalizeCitationParagraphsToLists,
|
|
23
25
|
parseMarkdownLines,
|
|
24
26
|
shouldUnwrapCodeNode,
|
|
25
27
|
unwrapCardLists,
|
|
@@ -132,6 +134,12 @@ export const remarkCcaDocCards = function () {
|
|
|
132
134
|
// 5. Unwrap paragraphs that only contain ccaDocCard nodes
|
|
133
135
|
unwrapCardParagraphs(tree.children);
|
|
134
136
|
|
|
137
|
+
// 6. Normalize citation paragraphs following lists into list items
|
|
138
|
+
normalizeCitationParagraphsToLists(tree.children);
|
|
139
|
+
|
|
140
|
+
// 7. Clean stray unclosed leading asterisks in list items and paragraphs
|
|
141
|
+
cleanStrayLeadingAsterisks(tree);
|
|
142
|
+
|
|
135
143
|
return tree;
|
|
136
144
|
};
|
|
137
145
|
};
|
|
@@ -249,4 +249,76 @@ describe('remarkCcaDocCards', () => {
|
|
|
249
249
|
expect(result.children[1].type).toBe('ccaDocCard');
|
|
250
250
|
expect(result.children[1].value).toBe('Tool B');
|
|
251
251
|
});
|
|
252
|
+
|
|
253
|
+
it('normalizes citation paragraphs following a list so they become list items', () => {
|
|
254
|
+
const tree = {
|
|
255
|
+
type: 'root',
|
|
256
|
+
children: [
|
|
257
|
+
{
|
|
258
|
+
type: 'list',
|
|
259
|
+
ordered: false,
|
|
260
|
+
children: [
|
|
261
|
+
{
|
|
262
|
+
type: 'listItem',
|
|
263
|
+
children: [
|
|
264
|
+
{
|
|
265
|
+
type: 'paragraph',
|
|
266
|
+
children: [
|
|
267
|
+
{ type: 'text', value: 'National context limitation' },
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
type: 'paragraph',
|
|
276
|
+
children: [
|
|
277
|
+
{
|
|
278
|
+
type: 'text',
|
|
279
|
+
value:
|
|
280
|
+
'[2] Pathways2Resilience Toolbox\n[3] NATURE DEMO DST analysis',
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const result = applyPlugin(tree);
|
|
288
|
+
expect(result.children).toHaveLength(1);
|
|
289
|
+
expect(result.children[0].type).toBe('list');
|
|
290
|
+
expect(result.children[0].children).toHaveLength(3);
|
|
291
|
+
expect(result.children[0].children[1].children[0].children[0].value).toBe(
|
|
292
|
+
'[2] Pathways2Resilience Toolbox',
|
|
293
|
+
);
|
|
294
|
+
expect(result.children[0].children[2].children[0].children[0].value).toBe(
|
|
295
|
+
'[3] NATURE DEMO DST analysis',
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('cleans stray leading asterisks in list items and paragraphs', () => {
|
|
300
|
+
const tree = {
|
|
301
|
+
type: 'root',
|
|
302
|
+
children: [
|
|
303
|
+
{
|
|
304
|
+
type: 'paragraph',
|
|
305
|
+
children: [
|
|
306
|
+
{
|
|
307
|
+
type: 'text',
|
|
308
|
+
value: '**Run a rapid screening with the ',
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
type: 'strong',
|
|
312
|
+
children: [{ type: 'text', value: 'Tool A' }],
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const result = applyPlugin(tree);
|
|
320
|
+
expect(result.children[0].children[0].value).toBe(
|
|
321
|
+
'Run a rapid screening with the ',
|
|
322
|
+
);
|
|
323
|
+
});
|
|
252
324
|
});
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
NEXT_STEPS_HEADING_RE,
|
|
13
|
+
cleanStrayLeadingAsterisks,
|
|
13
14
|
getNodeText,
|
|
14
15
|
isNextStepsHeading,
|
|
15
16
|
parseMarkdownLines,
|
|
@@ -61,6 +62,9 @@ function transformContainer(children) {
|
|
|
61
62
|
children: collected,
|
|
62
63
|
};
|
|
63
64
|
|
|
65
|
+
// Clean unclosed leading asterisks on list items inside container
|
|
66
|
+
cleanStrayLeadingAsterisks(containerNode);
|
|
67
|
+
|
|
64
68
|
// Replace heading and all collected siblings with the container node
|
|
65
69
|
children.splice(i, 1 + (j - (i + 1)), containerNode);
|
|
66
70
|
} else if (
|