Commit 27083f7e authored by Simon Knox's avatar Simon Knox

Merge branch '338271-footnotes' into 'master'

Render footnotes in the Content Editor

See merge request gitlab-org/gitlab!71067
parents 90f114fb bb84fda5
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';
import Emoji from '../extensions/emoji';
import Figure from '../extensions/figure';
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 Gapcursor from '../extensions/gapcursor';
import HardBreak from '../extensions/hard_break';
......@@ -94,6 +97,9 @@ export const createContentEditor = ({
Emoji,
Figure,
FigureCaption,
FootnoteDefinition,
FootnoteReference,
FootnotesSection,
Frontmatter,
Gapcursor,
HardBreak,
......
......@@ -17,6 +17,9 @@ import Division from '../extensions/division';
import Emoji from '../extensions/emoji';
import Figure from '../extensions/figure';
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 HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading';
......@@ -156,6 +159,15 @@ const defaultSerializerConfig = {
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) => {
const { language } = node.attrs;
const syntax = {
......
......@@ -429,6 +429,38 @@
</figcaption>
</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
markdown: |-
;;;
......
......@@ -11,6 +11,9 @@ import Division from '~/content_editor/extensions/division';
import Emoji from '~/content_editor/extensions/emoji';
import Figure from '~/content_editor/extensions/figure';
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 Heading from '~/content_editor/extensions/heading';
import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
......@@ -46,6 +49,9 @@ const tiptapEditor = createTestEditor({
DetailsContent,
Division,
Emoji,
FootnoteDefinition,
FootnoteReference,
FootnotesSection,
Figure,
FigureCaption,
HardBreak,
......@@ -81,6 +87,9 @@ const {
descriptionItem,
descriptionList,
emoji,
footnoteDefinition,
footnoteReference,
footnotesSection,
figure,
figureCaption,
heading,
......@@ -117,6 +126,9 @@ const {
emoji: { markType: Emoji.name },
figure: { nodeType: Figure.name },
figureCaption: { nodeType: FigureCaption.name },
footnoteDefinition: { nodeType: FootnoteDefinition.name },
footnoteReference: { nodeType: FootnoteReference.name },
footnotesSection: { nodeType: FootnotesSection.name },
hardBreak: { nodeType: HardBreak.name },
heading: { nodeType: Heading.name },
horizontalRule: { nodeType: HorizontalRule.name },
......@@ -1105,4 +1117,22 @@ there
`.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