diff --git a/src/Blocks/Footnote/FootnotesBlockView.jsx b/src/Blocks/Footnote/FootnotesBlockView.jsx
index dd28841..758bbc6 100644
--- a/src/Blocks/Footnote/FootnotesBlockView.jsx
+++ b/src/Blocks/Footnote/FootnotesBlockView.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { escapeRegExp } from 'lodash';
+
import {
openAccordionOrTabIfContainsFootnoteReference,
getAllBlocksAndSlateFields,
@@ -8,10 +8,9 @@ import {
import './less/public.less';
import { UniversalLink } from '@plone/volto/components';
+import { renderTextWithLinks } from '../../editor/utils';
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
-const urlRegex =
- /\b((http|https|ftp):\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(\/[^\s]*)?\b/g;
/**
* @summary The React component that displays the list of footnotes inserted
@@ -78,47 +77,6 @@ const FootnotesBlockView = (props) => {
startList = citationIndice;
}
- const renderTextWithLinks = (text) => {
- if (!text) return null;
- const links = text.match(urlRegex);
- if (!links) {
- return (
-
{title}
@@ -141,7 +99,7 @@ const FootnotesBlockView = (props) => {
key={`footnote-${zoteroId || uid}`}
id={`footnote-${zoteroId || uid}`}
>
-
{renderTextWithLinks(footnoteText)}
+
{renderTextWithLinks(footnoteText, zoteroId)}
{refsList ? (
<>
{/** some footnotes are never parent so we need the parent to reference */}
diff --git a/src/Blocks/Footnote/FootnotesBlockView.test.jsx b/src/Blocks/Footnote/FootnotesBlockView.test.jsx
new file mode 100644
index 0000000..7b65609
--- /dev/null
+++ b/src/Blocks/Footnote/FootnotesBlockView.test.jsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import FootnotesBlockView from './FootnotesBlockView';
+
+jest.mock('@plone/volto/components', () => ({
+ UniversalLink: ({ children, href }) =>
{children},
+}));
+
+jest.mock('@eeacms/volto-slate-footnote/editor/utils', () => ({
+ openAccordionOrTabIfContainsFootnoteReference: jest.fn(),
+ renderTextWithLinks: jest.fn(),
+ getAllBlocksAndSlateFields: jest.fn(() => [
+ { id: 'block1', footnote: 'Footnote with no link' },
+ { id: 'block2', footnote: 'Footnote with link http://example.com' },
+ { id: 'block3', footnote: 'Footnote with
HTML' },
+ ]),
+ makeFootnoteListOfUniqueItems: jest.fn((blocks) => ({
+ note1: {
+ uid: '1',
+ footnote: 'First note with a reference',
+ zoteroId: 'zotero1',
+ refs: { ref1: 'ref1' },
+ },
+ note2: {
+ uid: '2',
+ footnote: 'Second note with multiple references',
+ zoteroId: null,
+ refs: { ref2: 'ref2', ref3: 'ref3' },
+ },
+ note3: {
+ uid: '3',
+ footnote: '
Note with HTML',
+ zoteroId: 'zotero3',
+ refs: {},
+ },
+ })),
+}));
+
+describe('FootnotesBlockView', () => {
+ const propsVariations = [
+ {
+ description: 'renders with global metadata',
+ props: {
+ data: { title: 'Global Metadata', global: true, placeholder: 'Global' },
+ properties: { test: 'metadata' },
+ tabData: null,
+ content: null,
+ metadata: { test: 'metadata' },
+ },
+ },
+ {
+ description: 'renders with tabData',
+ props: {
+ data: { title: 'Tab Data', global: false, placeholder: 'Tab' },
+ properties: { test: 'tabProperties' },
+ tabData: { test: 'tabData' },
+ content: null,
+ },
+ },
+ {
+ description: 'renders with content',
+ props: {
+ data: { title: 'Content Data', global: false, placeholder: 'Content' },
+ properties: { test: 'contentProperties' },
+ tabData: null,
+ content: { test: 'contentData' },
+ },
+ },
+ {
+ description: 'renders with no metadata',
+ props: {
+ data: { title: 'No Metadata', global: false, placeholder: 'Default' },
+ properties: { test: 'defaultProperties' },
+ tabData: null,
+ content: null,
+ },
+ },
+ ];
+
+ test.each(propsVariations)('$description', ({ props }) => {
+ render(
);
+ });
+});
diff --git a/src/editor/render.jsx b/src/editor/render.jsx
index 6cdd7ff..79b1e33 100644
--- a/src/editor/render.jsx
+++ b/src/editor/render.jsx
@@ -1,6 +1,7 @@
import React from 'react';
import { Popup, List } from 'semantic-ui-react';
import { useEditorContext } from '@plone/volto-slate/hooks';
+
import { getAllBlocksAndSlateFields } from '@eeacms/volto-slate-footnote/editor/utils';
import {
makeFootnoteListOfUniqueItems,
@@ -8,7 +9,9 @@ import {
} from './utils';
import { isEmpty } from 'lodash';
import { useSelector } from 'react-redux';
-import { UniversalLink } from '@plone/volto/components';
+
+import { renderTextWithLinks } from './utils';
+import { useHistory } from 'react-router-dom';
/**
* Removes '' from footnote
@@ -16,14 +19,13 @@ import { UniversalLink } from '@plone/volto/components';
* @returns {string} formatted footnote
*/
-const urlRegex = /https?:\/\/[^\s]+/g;
-
export const FootnoteElement = (props) => {
const { attributes, children, element, mode, extras } = props;
const { data = {} } = element;
const { uid, zoteroId } = data;
const editor = useEditorContext();
const ref = React.useRef();
+ const history = useHistory();
const initialFormData = useSelector((state) => state?.content?.data || {});
const blockProps = editor?.getBlockProps ? editor.getBlockProps() : null;
@@ -51,35 +53,6 @@ export const FootnoteElement = (props) => {
: // no extra citations (no multiples)
`[${Object.keys(notesObjResult).indexOf(zoteroId) + 1}]`;
- const renderTextWithLinks = (text) => {
- if (!text) return null;
- const parts = text.split(urlRegex);
- const links = text.match(urlRegex);
- let result = [];
-
- parts.forEach((part, index) => {
- result.push(
-
,
- );
-
- if (links && links[index]) {
- result.push(
-
- {links[index]}
- ,
- );
- }
- });
- return result;
- };
const citationIndice = zoteroId // ZOTERO
? indiceIfZoteroId
: // FOOTNOTES
@@ -142,16 +115,20 @@ export const FootnoteElement = (props) => {
+ onClick={(e) => {
openAccordionOrTabIfContainsFootnoteReference(
`#footnote-${citationRefId}`,
- )
- }
+ );
+ if (e.target.tagName !== 'A') {
+ e.preventDefault();
+ history.push(`#footnote-${citationRefId}`);
+ }
+ }}
key={`#footnote-${citationRefId}`}
>
- {renderTextWithLinks(footnoteText)}
+ {renderTextWithLinks(footnoteText, zoteroId)}
@@ -164,16 +141,22 @@ export const FootnoteElement = (props) => {
return (
+ onClick={(e) => {
openAccordionOrTabIfContainsFootnoteReference(
`#footnote-${item.zoteroId || item.uid}`,
- )
- }
+ );
+ if (e.target.tagName !== 'A') {
+ e.preventDefault();
+ history.push(
+ `#footnote-${item.zoteroId || item.uid}`,
+ );
+ }
+ }}
key={`#footnote-${item.zoteroId || item.uid}`}
>
- {renderTextWithLinks(footnoteText)}
+ {renderTextWithLinks(footnoteText, item.zoteroId)}
@@ -202,11 +185,15 @@ export const FootnoteElement = (props) => {
+ onClick={(e) => {
openAccordionOrTabIfContainsFootnoteReference(
`#footnote-${citationRefId}`,
- )
- }
+ );
+ if (e.target.tagName !== 'A') {
+ e.preventDefault();
+ history.push(`#footnote-${citationRefId}`);
+ }
+ }}
key={`#footnote-${citationRefId}`}
>
@@ -219,11 +206,15 @@ export const FootnoteElement = (props) => {
data.extra.map((item) => (
+ onClick={(e) => {
openAccordionOrTabIfContainsFootnoteReference(
`#footnote-${item.zoteroId || item.uid}`,
- )
- }
+ );
+ if (e.target.tagName !== 'A') {
+ e.preventDefault();
+ history.push(`#footnote-${citationRefId}`);
+ }
+ }}
key={`#footnote-${item.zoteroId || item.uid}`}
>
diff --git a/src/editor/utils.js b/src/editor/utils.js
index 5a6dedc..f938333 100644
--- a/src/editor/utils.js
+++ b/src/editor/utils.js
@@ -1,6 +1,11 @@
import config from '@plone/volto/registry';
import { Node } from 'slate';
import { getAllBlocks } from '@plone/volto-slate/utils';
+import { escapeRegExp } from 'lodash';
+import { UniversalLink } from '@plone/volto/components';
+const urlRegex =
+ /\b((http|https|ftp):\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(\/[^\s<>)]*)?(?=\s|$|<|>|\))/g;
+
/**
* retrive all slate children of nested objects
* @param {object} path - the keys that we want to extract the slate children from
@@ -9,7 +14,7 @@ import { getAllBlocks } from '@plone/volto-slate/utils';
* path:{items:'value'}
* @returns string
*/
-const retriveValuesOfSlateFromNestedPath = (path, value) => {
+export const retriveValuesOfSlateFromNestedPath = (path, value) => {
if (Array.isArray(value)) {
let allSlateValue = [];
value.forEach((element) => {
@@ -276,3 +281,72 @@ const iterateFootnoteObj = (notesObjResultTemp, node, parentUid) => {
};
}
};
+
+export function isValidHTML(htmlString) {
+ if (
+ __CLIENT__ &&
+ typeof window !== 'undefined' &&
+ typeof DOMParser !== 'undefined'
+ ) {
+ // The environment is client-side, and DOMParser is available
+ const parser = new DOMParser();
+ const parsedDocument = parser.parseFromString(htmlString, 'text/html');
+ const errors = parsedDocument.querySelectorAll('parsererror');
+ return errors.length === 0;
+ }
+ return false;
+}
+
+export const renderTextWithLinks = (text, zoteroId) => {
+ if (!text) return null;
+
+ const links = text.match(urlRegex);
+ let isValid = false;
+ if (zoteroId && isValidHTML(text)) isValid = true;
+
+ if (!links) {
+ if (isValid)
+ return (
+
+ );
+ else return text;
+ }
+ let result = [];
+ const parts = text.split(
+ new RegExp(`(${links.map((link) => escapeRegExp(link)).join('|')})`),
+ );
+ parts.forEach((part, index) => {
+ if (links.includes(part) && zoteroId) {
+ result.push(`
+
+ ${part}
+ `);
+ return;
+ } else if (links.includes(part)) {
+ result.push(
+
+ {part}
+ ,
+ );
+ return;
+ } else result.push(part);
+ });
+
+ if (isValid)
+ return (
+ acc + c, ''),
+ }}
+ />
+ );
+ else return {result}
;
+};
diff --git a/src/editor/utils.test.js b/src/editor/utils.test.js
index 02970d0..7ccf6b5 100644
--- a/src/editor/utils.test.js
+++ b/src/editor/utils.test.js
@@ -1,6 +1,8 @@
import {
openAccordionOrTabIfContainsFootnoteReference,
getAllBlocksAndSlateFields,
+ isValidHTML,
+ retriveValuesOfSlateFromNestedPath,
} from './utils';
import { getAllBlocks } from '@plone/volto-slate/utils';
@@ -8,6 +10,47 @@ jest.mock('@plone/volto-slate/utils', () => ({
getAllBlocks: jest.fn(),
}));
+describe('retriveValuesOfSlateFromNestedPath', () => {
+ test('should return values for a given string path in an object', () => {
+ const obj = { key: ['value1', 'value2'] };
+ expect(retriveValuesOfSlateFromNestedPath('key', obj)).toEqual([
+ 'value1',
+ 'value2',
+ ]);
+ });
+
+ test('should return an empty array when the path is not found', () => {
+ const obj = { key: [] };
+ expect(retriveValuesOfSlateFromNestedPath('key', obj)).toEqual([]);
+ });
+
+ test('should return values from an array of objects', () => {
+ const objArray = [{ key: ['value1'] }, { key: ['value2'] }];
+ expect(retriveValuesOfSlateFromNestedPath('key', objArray)).toEqual([
+ 'value1',
+ 'value2',
+ ]);
+ });
+
+ test('should handle nested object paths', () => {
+ const obj = { level1: { level2: ['value'] } };
+ expect(
+ retriveValuesOfSlateFromNestedPath({ level1: 'level2' }, obj),
+ ).toEqual(['value']);
+ });
+
+ test('should return an empty array for invalid inputs', () => {
+ expect(retriveValuesOfSlateFromNestedPath('key', null)).toEqual([]);
+ expect(retriveValuesOfSlateFromNestedPath('key', undefined)).toEqual([]);
+ expect(retriveValuesOfSlateFromNestedPath({}, {})).toEqual([]);
+ });
+
+ test('should return empty array if given an empty path object', () => {
+ expect(retriveValuesOfSlateFromNestedPath({}, { key: 'value' })).toEqual(
+ [],
+ );
+ });
+});
describe('openAccordionOrTabIfContainsFootnoteReference', () => {
it('should open accordion if it contains footnote reference', () => {
document.body.innerHTML = `
@@ -197,3 +240,29 @@ describe('getAllBlocksAndSlateFields', () => {
expect(result).toEqual(expected);
});
});
+
+describe('isValidHTML', () => {
+ beforeAll(() => {
+ global.DOMParser = class {
+ parseFromString(str, type) {
+ const doc = {
+ querySelectorAll: (selector) => {
+ if (selector === 'parsererror' && str.includes('')) {
+ return [{}]; // Simulate an error
+ }
+ return [];
+ },
+ };
+ return doc;
+ }
+ };
+ });
+
+ test('returns true for valid HTML', () => {
+ expect(isValidHTML('Hello
')).toBe(true);
+ });
+
+ test('returns false for invalid HTML', () => {
+ expect(isValidHTML('Invalid HTML')).toBe(false);
+ });
+});