Commit bb84fda5 authored by Himanshu Kapoor's avatar Himanshu Kapoor Committed by Enrique Alcantara

Render footnotes in the Content Editor

Display footnotes in the Content Editor and
makes sure that the Content Editor generates
correct footnotes Markdown when submitting
changes

Changelog: added

Basic footnotes serializer
parent 0f685998
import { mergeAttributes, Node } from '@tiptap/core';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
export default Node.create({
name: 'footnoteDefinition',
content: 'paragraph',
group: 'block',
parseHTML() {
return [
{ tag: 'section.footnotes li' },
{ tag: '.footnote-backref', priority: PARSE_HTML_PRIORITY_HIGHEST, ignore: true },
];
},
renderHTML({ HTMLAttributes }) {
return ['li', mergeAttributes(HTMLAttributes), 0];
},
});
import { Node, mergeAttributes } from '@tiptap/core';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
export default Node.create({
name: 'footnoteReference',
inline: true,
group: 'inline',
atom: true,
draggable: true,
selectable: true,
addAttributes() {
return {
footnoteId: {
default: null,
parseHTML: (element) => element.querySelector('a').getAttribute('id'),
},
footnoteNumber: {
default: null,
parseHTML: (element) => element.textContent,
},
};
},
parseHTML() {
return [{ tag: 'sup.footnote-ref', priority: PARSE_HTML_PRIORITY_HIGHEST }];
},
renderHTML({ HTMLAttributes: { footnoteNumber, footnoteId, ...HTMLAttributes } }) {
return ['sup', mergeAttributes(HTMLAttributes), footnoteNumber];
},
});
import { mergeAttributes, Node } from '@tiptap/core';
export default Node.create({
name: 'footnotesSection',
content: 'footnoteDefinition+',
group: 'block',
isolating: true,
parseHTML() {
return [{ tag: 'section.footnotes > ol' }];
},
renderHTML({ HTMLAttributes }) {
return ['ol', mergeAttributes(HTMLAttributes, { class: 'footnotes gl-font-sm' }), 0];
},
});
...@@ -19,6 +19,9 @@ import Dropcursor from '../extensions/dropcursor'; ...@@ -19,6 +19,9 @@ import Dropcursor from '../extensions/dropcursor';
import Emoji from '../extensions/emoji'; import Emoji from '../extensions/emoji';
import Figure from '../extensions/figure'; import Figure from '../extensions/figure';
import FigureCaption from '../extensions/figure_caption'; import FigureCaption from '../extensions/figure_caption';
import FootnoteDefinition from '../extensions/footnote_definition';
import FootnoteReference from '../extensions/footnote_reference';
import FootnotesSection from '../extensions/footnotes_section';
import Frontmatter from '../extensions/frontmatter'; import Frontmatter from '../extensions/frontmatter';
import Gapcursor from '../extensions/gapcursor'; import Gapcursor from '../extensions/gapcursor';
import HardBreak from '../extensions/hard_break'; import HardBreak from '../extensions/hard_break';
...@@ -94,6 +97,9 @@ export const createContentEditor = ({ ...@@ -94,6 +97,9 @@ export const createContentEditor = ({
Emoji, Emoji,
Figure, Figure,
FigureCaption, FigureCaption,
FootnoteDefinition,
FootnoteReference,
FootnotesSection,
Frontmatter, Frontmatter,
Gapcursor, Gapcursor,
HardBreak, HardBreak,
......
...@@ -17,6 +17,9 @@ import Division from '../extensions/division'; ...@@ -17,6 +17,9 @@ import Division from '../extensions/division';
import Emoji from '../extensions/emoji'; import Emoji from '../extensions/emoji';
import Figure from '../extensions/figure'; import Figure from '../extensions/figure';
import FigureCaption from '../extensions/figure_caption'; import FigureCaption from '../extensions/figure_caption';
import FootnotesSection from '../extensions/footnotes_section';
import FootnoteDefinition from '../extensions/footnote_definition';
import FootnoteReference from '../extensions/footnote_reference';
import Frontmatter from '../extensions/frontmatter'; import Frontmatter from '../extensions/frontmatter';
import HardBreak from '../extensions/hard_break'; import HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading'; import Heading from '../extensions/heading';
...@@ -156,6 +159,15 @@ const defaultSerializerConfig = { ...@@ -156,6 +159,15 @@ const defaultSerializerConfig = {
state.write(`:${name}:`); state.write(`:${name}:`);
}, },
[FootnoteDefinition.name]: (state, node) => {
state.renderInline(node);
},
[FootnoteReference.name]: (state, node) => {
state.write(`[^${node.attrs.footnoteNumber}]`);
},
[FootnotesSection.name]: (state, node) => {
state.renderList(node, '', (index) => `[^${index + 1}]: `);
},
[Frontmatter.name]: (state, node) => { [Frontmatter.name]: (state, node) => {
const { language } = node.attrs; const { language } = node.attrs;
const syntax = { const syntax = {
......
...@@ -429,6 +429,38 @@ ...@@ -429,6 +429,38 @@
</figcaption> </figcaption>
</figure> </figure>
- name: footnotes
substitutions:
# NOTE: We don't care about verifying specific attribute values here, that should be the
# responsibility of unit tests. These tests are about the structure of the HTML.
fn_href_substitution:
- regex: '(href)(=")(.+?)(")'
replacement: '\1\2REF\4'
footnote_id_substitution:
- regex: '(id)(=")(.+?)(")'
replacement: '\1\2ID\4'
pending:
backend: https://gitlab.com/gitlab-org/gitlab/-/issues/346591
markdown: |-
A footnote reference tag looks like this: [^1]
This reference tag is a mix of letters and numbers. [^2]
[^1]: This is the text inside a footnote.
[^2]: This is another footnote.
html: |-
<p data-sourcepos="1:1-1:46" dir="auto">A footnote reference tag looks like this: <sup class="footnote-ref"><a href="#fn-1-2717" id="fnref-1-2717" data-footnote-ref="">1</a></sup></p>
<p data-sourcepos="3:1-3:56" dir="auto">This reference tag is a mix of letters and numbers. <sup class="footnote-ref"><a href="#fn-2-2717" id="fnref-2-2717" data-footnote-ref="">2</a></sup></p>
<section class="footnotes" data-footnotes><ol>
<li id="fn-1-2717">
<p data-sourcepos="5:7-5:41">This is the text inside a footnote. <a href="#fnref-1-2717" aria-label="Back to content" class="footnote-backref" data-footnote-backref=""><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
</li>
<li id="fn-2-2717">
<p data-sourcepos="6:7-6:31">This is another footnote. <a href="#fnref-2-2717" aria-label="Back to content" class="footnote-backref" data-footnote-backref=""><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
</li>
</ol></section>
- name: frontmatter_json - name: frontmatter_json
markdown: |- markdown: |-
;;; ;;;
......
...@@ -11,6 +11,9 @@ import Division from '~/content_editor/extensions/division'; ...@@ -11,6 +11,9 @@ import Division from '~/content_editor/extensions/division';
import Emoji from '~/content_editor/extensions/emoji'; import Emoji from '~/content_editor/extensions/emoji';
import Figure from '~/content_editor/extensions/figure'; import Figure from '~/content_editor/extensions/figure';
import FigureCaption from '~/content_editor/extensions/figure_caption'; import FigureCaption from '~/content_editor/extensions/figure_caption';
import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
import FootnoteReference from '~/content_editor/extensions/footnote_reference';
import FootnotesSection from '~/content_editor/extensions/footnotes_section';
import HardBreak from '~/content_editor/extensions/hard_break'; import HardBreak from '~/content_editor/extensions/hard_break';
import Heading from '~/content_editor/extensions/heading'; import Heading from '~/content_editor/extensions/heading';
import HorizontalRule from '~/content_editor/extensions/horizontal_rule'; import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
...@@ -46,6 +49,9 @@ const tiptapEditor = createTestEditor({ ...@@ -46,6 +49,9 @@ const tiptapEditor = createTestEditor({
DetailsContent, DetailsContent,
Division, Division,
Emoji, Emoji,
FootnoteDefinition,
FootnoteReference,
FootnotesSection,
Figure, Figure,
FigureCaption, FigureCaption,
HardBreak, HardBreak,
...@@ -81,6 +87,9 @@ const { ...@@ -81,6 +87,9 @@ const {
descriptionItem, descriptionItem,
descriptionList, descriptionList,
emoji, emoji,
footnoteDefinition,
footnoteReference,
footnotesSection,
figure, figure,
figureCaption, figureCaption,
heading, heading,
...@@ -117,6 +126,9 @@ const { ...@@ -117,6 +126,9 @@ const {
emoji: { markType: Emoji.name }, emoji: { markType: Emoji.name },
figure: { nodeType: Figure.name }, figure: { nodeType: Figure.name },
figureCaption: { nodeType: FigureCaption.name }, figureCaption: { nodeType: FigureCaption.name },
footnoteDefinition: { nodeType: FootnoteDefinition.name },
footnoteReference: { nodeType: FootnoteReference.name },
footnotesSection: { nodeType: FootnotesSection.name },
hardBreak: { nodeType: HardBreak.name }, hardBreak: { nodeType: HardBreak.name },
heading: { nodeType: Heading.name }, heading: { nodeType: Heading.name },
horizontalRule: { nodeType: HorizontalRule.name }, horizontalRule: { nodeType: HorizontalRule.name },
...@@ -1105,4 +1117,22 @@ there ...@@ -1105,4 +1117,22 @@ there
`.trim(), `.trim(),
); );
}); });
it('correctly serializes footnotes', () => {
expect(
serialize(
paragraph(
'Oranges are orange ',
footnoteReference({ footnoteId: '1', footnoteNumber: '1' }),
),
footnotesSection(footnoteDefinition(paragraph('Oranges are fruits'))),
),
).toBe(
`
Oranges are orange [^1]
[^1]: Oranges are fruits
`.trim(),
);
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment