diff --git a/.gitignore b/.gitignore
index 3c7b1482942..3b60900b38a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,6 +43,7 @@
!/plugins/poll/
!/plugins/styleguide
!/plugins/checklist/
+!/plugins/footnote/
/plugins/*/auto_generated/
/spec/fixtures/plugins/my_plugin/auto_generated
diff --git a/plugins/footnote/README.md b/plugins/footnote/README.md
new file mode 100644
index 00000000000..97982944e6e
--- /dev/null
+++ b/plugins/footnote/README.md
@@ -0,0 +1,8 @@
+## Discourse footnotes plugin
+
+https://meta.discourse.org/t/discourse-footnote/84533
+
+Official footnotes Discourse plugin
+
+Based off: [github.com/markdown-it/markdown-it-footnote](https://github.com/markdown-it/markdown-it-footnote)
+
diff --git a/plugins/footnote/assets/javascripts/initializers/inline-footnotes.js b/plugins/footnote/assets/javascripts/initializers/inline-footnotes.js
new file mode 100644
index 00000000000..e910e1f4a83
--- /dev/null
+++ b/plugins/footnote/assets/javascripts/initializers/inline-footnotes.js
@@ -0,0 +1,130 @@
+import { createPopper } from "@popperjs/core";
+import { withPluginApi } from "discourse/lib/plugin-api";
+import { iconHTML } from "discourse-common/lib/icon-library";
+
+let inlineFootnotePopper;
+
+function applyInlineFootnotes(elem) {
+ const footnoteRefs = elem.querySelectorAll("sup.footnote-ref");
+
+ footnoteRefs.forEach((footnoteRef) => {
+ const refLink = footnoteRef.querySelector("a");
+ if (!refLink) {
+ return;
+ }
+
+ const expandableFootnote = document.createElement("a");
+ expandableFootnote.classList.add("expand-footnote");
+ expandableFootnote.innerHTML = iconHTML("ellipsis-h");
+ expandableFootnote.href = "";
+ expandableFootnote.role = "button";
+ expandableFootnote.dataset.footnoteId = refLink.getAttribute("href");
+
+ footnoteRef.after(expandableFootnote);
+ });
+
+ if (footnoteRefs.length) {
+ elem.classList.add("inline-footnotes");
+ }
+}
+
+function buildTooltip() {
+ let html = `
+
+ `;
+
+ let template = document.createElement("template");
+ html = html.trim();
+ template.innerHTML = html;
+ return template.content.firstChild;
+}
+
+function footNoteEventHandler(event) {
+ inlineFootnotePopper?.destroy();
+
+ const tooltip = document.getElementById("footnote-tooltip");
+
+ // reset state by hidding tooltip, it handles "click outside"
+ // allowing to hide the tooltip when you click anywhere else
+ tooltip?.removeAttribute("data-show");
+
+ // if we didn't actually click a footnote button, exit early
+ if (!event.target.classList.contains("expand-footnote")) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ // append footnote to tooltip body
+ const expandableFootnote = event.target;
+ const cooked = expandableFootnote.closest(".cooked");
+ const footnoteId = expandableFootnote.dataset.footnoteId;
+ const footnoteContent = tooltip.querySelector(".footnote-tooltip-content");
+ let newContent = cooked.querySelector(footnoteId);
+
+ footnoteContent.innerHTML = newContent.innerHTML;
+
+ // display tooltip
+ tooltip.dataset.show = "";
+
+ // setup popper
+ inlineFootnotePopper?.destroy();
+ inlineFootnotePopper = createPopper(expandableFootnote, tooltip, {
+ modifiers: [
+ {
+ name: "arrow",
+ options: { element: tooltip.querySelector("#arrow") },
+ },
+ {
+ name: "preventOverflow",
+ options: {
+ altAxis: true,
+ padding: 5,
+ },
+ },
+ {
+ name: "offset",
+ options: {
+ offset: [0, 12],
+ },
+ },
+ ],
+ });
+}
+
+export default {
+ name: "inline-footnotes",
+
+ initialize(container) {
+ if (!container.lookup("site-settings:main").display_footnotes_inline) {
+ return;
+ }
+
+ document.documentElement.append(buildTooltip());
+
+ window.addEventListener("click", footNoteEventHandler);
+
+ withPluginApi("0.8.9", (api) => {
+ api.decorateCookedElement((elem) => applyInlineFootnotes(elem), {
+ onlyStream: true,
+ id: "inline-footnotes",
+ });
+
+ api.onPageChange(() => {
+ document
+ .getElementById("footnote-tooltip")
+ ?.removeAttribute("data-show");
+ });
+ });
+ },
+
+ teardown() {
+ inlineFootnotePopper?.destroy();
+ window.removeEventListener("click", footNoteEventHandler);
+ document.getElementById("footnote-tooltip")?.remove();
+ },
+};
diff --git a/plugins/footnote/assets/javascripts/lib/discourse-markdown/footnotes.js b/plugins/footnote/assets/javascripts/lib/discourse-markdown/footnotes.js
new file mode 100644
index 00000000000..6dce84d3d99
--- /dev/null
+++ b/plugins/footnote/assets/javascripts/lib/discourse-markdown/footnotes.js
@@ -0,0 +1,26 @@
+export function setup(helper) {
+ helper.registerOptions((opts, siteSettings) => {
+ opts.features["footnotes"] =
+ window.markdownitFootnote && !!siteSettings.enable_markdown_footnotes;
+ });
+
+ helper.allowList([
+ "ol.footnotes-list",
+ "hr.footnotes-sep",
+ "li.footnote-item",
+ "a.footnote-backref",
+ "sup.footnote-ref",
+ ]);
+
+ helper.allowList({
+ custom(tag, name, value) {
+ if ((tag === "a" || tag === "li") && name === "id") {
+ return !!value.match(/^fn(ref)?\d+$/);
+ }
+ },
+ });
+
+ if (window.markdownitFootnote) {
+ helper.registerPlugin(window.markdownitFootnote);
+ }
+}
diff --git a/plugins/footnote/assets/stylesheets/footnotes.scss b/plugins/footnote/assets/stylesheets/footnotes.scss
new file mode 100644
index 00000000000..55476a8c1dc
--- /dev/null
+++ b/plugins/footnote/assets/stylesheets/footnotes.scss
@@ -0,0 +1,106 @@
+.inline-footnotes {
+ a.expand-footnote {
+ user-select: none;
+ padding: 0px 0.5em;
+ margin: 0 0 0 0.25em;
+ color: var(--primary-low-mid-or-secondary-high);
+ background: var(--primary-low);
+ border-radius: 3px;
+ min-height: 20px;
+ display: inline-flex;
+ align-items: center;
+
+ &:hover {
+ background: var(--primary-medium);
+ color: var(--secondary);
+ }
+
+ > * {
+ pointer-events: none;
+ }
+ }
+
+ // This is hack to work with lazy-loading, we will trick the browser
+ // to believe the image is in the DOM and can be loaded
+ .footnotes-list,
+ .footnotes-sep {
+ position: absolute;
+ // the left/right positioning prevents overflow issues
+ // with long words causing overflow on small screens
+ left: 0;
+ right: 0;
+ }
+
+ .footnotes-sep,
+ .footnotes-list,
+ .footnote-ref {
+ display: none;
+ }
+}
+
+#footnote-tooltip {
+ background-color: var(--primary-low);
+ color: var(--primary);
+ padding: 0.5em;
+ font-size: var(--font-down-1);
+ border-radius: 3px;
+ display: none;
+ z-index: z("tooltip");
+ max-width: 400px;
+ overflow-wrap: break-word;
+
+ .footnote-tooltip-content {
+ overflow: hidden;
+
+ .footnote-backref {
+ display: none;
+ }
+
+ img {
+ object-fit: cover;
+ max-width: 385px;
+ }
+
+ p {
+ margin: 0;
+ }
+ }
+}
+
+#footnote-tooltip[data-show] {
+ display: block;
+}
+
+#arrow,
+#arrow::before {
+ position: absolute;
+ width: 10px;
+ height: 10px;
+ background: inherit;
+}
+
+#arrow {
+ visibility: hidden;
+}
+
+#arrow::before {
+ visibility: visible;
+ content: "";
+ transform: rotate(45deg);
+}
+
+#footnote-tooltip[data-popper-placement^="top"] > #arrow {
+ bottom: -4px;
+}
+
+#footnote-tooltip[data-popper-placement^="bottom"] > #arrow {
+ top: -4px;
+}
+
+#footnote-tooltip[data-popper-placement^="left"] > #arrow {
+ right: -4px;
+}
+
+#footnote-tooltip[data-popper-placement^="right"] > #arrow {
+ left: -4px;
+}
diff --git a/plugins/footnote/assets/vendor/javascripts/markdown-it-footnote.js b/plugins/footnote/assets/vendor/javascripts/markdown-it-footnote.js
new file mode 100644
index 00000000000..7f5f9569cfa
--- /dev/null
+++ b/plugins/footnote/assets/vendor/javascripts/markdown-it-footnote.js
@@ -0,0 +1,382 @@
+/* eslint-disable */
+
+/*! markdown-it-footnote 3.0.3 https://github.com//markdown-it/markdown-it-footnote @license MIT */(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.markdownitFootnote = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 0) {
+ n += ':' + tokens[idx].meta.subId;
+ }
+
+ return '[' + n + ']';
+}
+
+function render_footnote_ref(tokens, idx, options, env, slf) {
+ var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
+ var caption = slf.rules.footnote_caption(tokens, idx, options, env, slf);
+ var refid = id;
+
+ if (tokens[idx].meta.subId > 0) {
+ refid += ':' + tokens[idx].meta.subId;
+ }
+
+ return '';
+}
+
+function render_footnote_block_open(tokens, idx, options) {
+ return (options.xhtmlOut ? '\n' : '\n') +
+ '\n';
+}
+
+function render_footnote_open(tokens, idx, options, env, slf) {
+ var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
+
+ if (tokens[idx].meta.subId > 0) {
+ id += ':' + tokens[idx].meta.subId;
+ }
+
+ return '\n';
+}
+
+function render_footnote_anchor(tokens, idx, options, env, slf) {
+ var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
+
+ if (tokens[idx].meta.subId > 0) {
+ id += ':' + tokens[idx].meta.subId;
+ }
+
+ /* ↩ with escape code to prevent display as Apple Emoji on iOS */
+ return ' ';
+}
+
+
+module.exports = function footnote_plugin(md) {
+ var parseLinkLabel = md.helpers.parseLinkLabel,
+ isSpace = md.utils.isSpace;
+
+ md.renderer.rules.footnote_ref = render_footnote_ref;
+ md.renderer.rules.footnote_block_open = render_footnote_block_open;
+ md.renderer.rules.footnote_block_close = render_footnote_block_close;
+ md.renderer.rules.footnote_open = render_footnote_open;
+ md.renderer.rules.footnote_close = render_footnote_close;
+ md.renderer.rules.footnote_anchor = render_footnote_anchor;
+
+ // helpers (only used in other rules, no tokens are attached to those)
+ md.renderer.rules.footnote_caption = render_footnote_caption;
+ md.renderer.rules.footnote_anchor_name = render_footnote_anchor_name;
+
+ // Process footnote block definition
+ function footnote_def(state, startLine, endLine, silent) {
+ var oldBMark, oldTShift, oldSCount, oldParentType, pos, label, token,
+ initial, offset, ch, posAfterColon,
+ start = state.bMarks[startLine] + state.tShift[startLine],
+ max = state.eMarks[startLine];
+
+ // line should be at least 5 chars - "[^x]:"
+ if (start + 4 > max) { return false; }
+
+ if (state.src.charCodeAt(start) !== 0x5B/* [ */) { return false; }
+ if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) { return false; }
+
+ for (pos = start + 2; pos < max; pos++) {
+ if (state.src.charCodeAt(pos) === 0x20) { return false; }
+ if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
+ break;
+ }
+ }
+
+ if (pos === start + 2) { return false; } // no empty footnote labels
+ if (pos + 1 >= max || state.src.charCodeAt(++pos) !== 0x3A /* : */) { return false; }
+ if (silent) { return true; }
+ pos++;
+
+ if (!state.env.footnotes) { state.env.footnotes = {}; }
+ if (!state.env.footnotes.refs) { state.env.footnotes.refs = {}; }
+ label = state.src.slice(start + 2, pos - 2);
+ state.env.footnotes.refs[':' + label] = -1;
+
+ token = new state.Token('footnote_reference_open', '', 1);
+ token.meta = { label: label };
+ token.level = state.level++;
+ state.tokens.push(token);
+
+ oldBMark = state.bMarks[startLine];
+ oldTShift = state.tShift[startLine];
+ oldSCount = state.sCount[startLine];
+ oldParentType = state.parentType;
+
+ posAfterColon = pos;
+ initial = offset = state.sCount[startLine] + pos - (state.bMarks[startLine] + state.tShift[startLine]);
+
+ while (pos < max) {
+ ch = state.src.charCodeAt(pos);
+
+ if (isSpace(ch)) {
+ if (ch === 0x09) {
+ offset += 4 - offset % 4;
+ } else {
+ offset++;
+ }
+ } else {
+ break;
+ }
+
+ pos++;
+ }
+
+ state.tShift[startLine] = pos - posAfterColon;
+ state.sCount[startLine] = offset - initial;
+
+ state.bMarks[startLine] = posAfterColon;
+ state.blkIndent += 4;
+ state.parentType = 'footnote';
+
+ if (state.sCount[startLine] < state.blkIndent) {
+ state.sCount[startLine] += state.blkIndent;
+ }
+
+ state.md.block.tokenize(state, startLine, endLine, true);
+
+ state.parentType = oldParentType;
+ state.blkIndent -= 4;
+ state.tShift[startLine] = oldTShift;
+ state.sCount[startLine] = oldSCount;
+ state.bMarks[startLine] = oldBMark;
+
+ token = new state.Token('footnote_reference_close', '', -1);
+ token.level = --state.level;
+ state.tokens.push(token);
+
+ return true;
+ }
+
+ // Process inline footnotes (^[...])
+ function footnote_inline(state, silent) {
+ var labelStart,
+ labelEnd,
+ footnoteId,
+ token,
+ tokens,
+ max = state.posMax,
+ start = state.pos;
+
+ if (start + 2 >= max) { return false; }
+ if (state.src.charCodeAt(start) !== 0x5E/* ^ */) { return false; }
+ if (state.src.charCodeAt(start + 1) !== 0x5B/* [ */) { return false; }
+
+ labelStart = start + 2;
+ labelEnd = parseLinkLabel(state, start + 1);
+
+ // parser failed to find ']', so it's not a valid note
+ if (labelEnd < 0) { return false; }
+
+ // We found the end of the link, and know for a fact it's a valid link;
+ // so all that's left to do is to call tokenizer.
+ //
+ if (!silent) {
+ if (!state.env.footnotes) { state.env.footnotes = {}; }
+ if (!state.env.footnotes.list) { state.env.footnotes.list = []; }
+ footnoteId = state.env.footnotes.list.length;
+
+ state.md.inline.parse(
+ state.src.slice(labelStart, labelEnd),
+ state.md,
+ state.env,
+ tokens = []
+ );
+
+ // Having a rendered footnote inside a link creates a nested link, which
+ // is not valid HTML, so close the parent tag first before proceeding.
+ const previousToken = state.tokens[state.tokens.length - 1];
+ if (previousToken && previousToken.content && previousToken.content.includes(" max) { return false; }
+
+ if (!state.env.footnotes || !state.env.footnotes.refs) { return false; }
+ if (state.src.charCodeAt(start) !== 0x5B/* [ */) { return false; }
+ if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) { return false; }
+
+ for (pos = start + 2; pos < max; pos++) {
+ if (state.src.charCodeAt(pos) === 0x20) { return false; }
+ if (state.src.charCodeAt(pos) === 0x0A) { return false; }
+ if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
+ break;
+ }
+ }
+
+ if (pos === start + 2) { return false; } // no empty footnote labels
+ if (pos >= max) { return false; }
+ pos++;
+
+ label = state.src.slice(start + 2, pos - 1);
+ if (typeof state.env.footnotes.refs[':' + label] === 'undefined') { return false; }
+
+ if (!silent) {
+ if (!state.env.footnotes.list) { state.env.footnotes.list = []; }
+
+ if (state.env.footnotes.refs[':' + label] < 0) {
+ footnoteId = state.env.footnotes.list.length;
+ state.env.footnotes.list[footnoteId] = { label: label, count: 0 };
+ state.env.footnotes.refs[':' + label] = footnoteId;
+ } else {
+ footnoteId = state.env.footnotes.refs[':' + label];
+ }
+
+ footnoteSubId = state.env.footnotes.list[footnoteId].count;
+ state.env.footnotes.list[footnoteId].count++;
+
+ token = state.push('footnote_ref', '', 0);
+ token.meta = { id: footnoteId, subId: footnoteSubId, label: label };
+ }
+
+ state.pos = pos;
+ state.posMax = max;
+ return true;
+ }
+
+ // Glue footnote tokens to end of token stream
+ function footnote_tail(state) {
+ var i, l, j, t, lastParagraph, list, token, tokens, current, currentLabel,
+ insideRef = false,
+ refTokens = {};
+
+ if (!state.env.footnotes) { return; }
+
+ state.tokens = state.tokens.filter(function (tok) {
+ if (tok.type === 'footnote_reference_open') {
+ insideRef = true;
+ current = [];
+ currentLabel = tok.meta.label;
+ return false;
+ }
+ if (tok.type === 'footnote_reference_close') {
+ insideRef = false;
+ // prepend ':' to avoid conflict with Object.prototype members
+ refTokens[':' + currentLabel] = current;
+ return false;
+ }
+ if (insideRef) { current.push(tok); }
+ return !insideRef;
+ });
+
+ if (!state.env.footnotes.list) { return; }
+ list = state.env.footnotes.list;
+
+ token = new state.Token('footnote_block_open', '', 1);
+ state.tokens.push(token);
+
+ for (i = 0, l = list.length; i < l; i++) {
+ token = new state.Token('footnote_open', '', 1);
+ token.meta = { id: i, label: list[i].label };
+ state.tokens.push(token);
+
+ if (list[i].tokens) {
+ tokens = [];
+
+ token = new state.Token('paragraph_open', 'p', 1);
+ token.block = true;
+ tokens.push(token);
+
+ token = new state.Token('inline', '', 0);
+ token.children = list[i].tokens;
+ token.content = list[i].content;
+ tokens.push(token);
+
+ token = new state.Token('paragraph_close', 'p', -1);
+ token.block = true;
+ tokens.push(token);
+
+ } else if (list[i].label) {
+ tokens = refTokens[':' + list[i].label];
+ }
+
+ if (tokens) state.tokens = state.tokens.concat(tokens);
+ if (state.tokens[state.tokens.length - 1].type === 'paragraph_close') {
+ lastParagraph = state.tokens.pop();
+ } else {
+ lastParagraph = null;
+ }
+
+ t = list[i].count > 0 ? list[i].count : 1;
+ for (j = 0; j < t; j++) {
+ token = new state.Token('footnote_anchor', '', 0);
+ token.meta = { id: i, subId: j, label: list[i].label };
+ state.tokens.push(token);
+ }
+
+ if (lastParagraph) {
+ state.tokens.push(lastParagraph);
+ }
+
+ token = new state.Token('footnote_close', '', -1);
+ state.tokens.push(token);
+ }
+
+ token = new state.Token('footnote_block_close', '', -1);
+ state.tokens.push(token);
+ }
+
+ md.block.ruler.before('reference', 'footnote_def', footnote_def, { alt: [ 'paragraph', 'reference' ] });
+ md.inline.ruler.after('image', 'footnote_inline', footnote_inline);
+ md.inline.ruler.after('footnote_inline', 'footnote_ref', footnote_ref);
+ md.core.ruler.after('inline', 'footnote_tail', footnote_tail);
+};
+
+},{}]},{},[1])(1)
+});
diff --git a/plugins/footnote/config/locales/server.ar.yml b/plugins/footnote/config/locales/server.ar.yml
new file mode 100644
index 00000000000..108b81d2930
--- /dev/null
+++ b/plugins/footnote/config/locales/server.ar.yml
@@ -0,0 +1,10 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ar:
+ site_settings:
+ enable_markdown_footnotes: تفعيل تنسيق Markdown في الحاشية السفلية على هذا الموقع
+ display_footnotes_inline: تفعيل التوسيع المضمَّن للحواشي السفلية
diff --git a/plugins/footnote/config/locales/server.be.yml b/plugins/footnote/config/locales/server.be.yml
new file mode 100644
index 00000000000..2ea77a0d350
--- /dev/null
+++ b/plugins/footnote/config/locales/server.be.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+be:
diff --git a/plugins/footnote/config/locales/server.bg.yml b/plugins/footnote/config/locales/server.bg.yml
new file mode 100644
index 00000000000..52333529d3c
--- /dev/null
+++ b/plugins/footnote/config/locales/server.bg.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+bg:
diff --git a/plugins/footnote/config/locales/server.bs_BA.yml b/plugins/footnote/config/locales/server.bs_BA.yml
new file mode 100644
index 00000000000..828a7e65af8
--- /dev/null
+++ b/plugins/footnote/config/locales/server.bs_BA.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+bs_BA:
diff --git a/plugins/footnote/config/locales/server.ca.yml b/plugins/footnote/config/locales/server.ca.yml
new file mode 100644
index 00000000000..ec737bc1a5b
--- /dev/null
+++ b/plugins/footnote/config/locales/server.ca.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ca:
diff --git a/plugins/footnote/config/locales/server.cs.yml b/plugins/footnote/config/locales/server.cs.yml
new file mode 100644
index 00000000000..041b2f0bd05
--- /dev/null
+++ b/plugins/footnote/config/locales/server.cs.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+cs:
diff --git a/plugins/footnote/config/locales/server.da.yml b/plugins/footnote/config/locales/server.da.yml
new file mode 100644
index 00000000000..f39c727c594
--- /dev/null
+++ b/plugins/footnote/config/locales/server.da.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+da:
diff --git a/plugins/footnote/config/locales/server.de.yml b/plugins/footnote/config/locales/server.de.yml
new file mode 100644
index 00000000000..98b21cfca2b
--- /dev/null
+++ b/plugins/footnote/config/locales/server.de.yml
@@ -0,0 +1,10 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+de:
+ site_settings:
+ enable_markdown_footnotes: Fußnoten-Markdown auf dieser Website aktivieren
+ display_footnotes_inline: Inline-Erweiterung von Fußnoten aktivieren
diff --git a/plugins/footnote/config/locales/server.el.yml b/plugins/footnote/config/locales/server.el.yml
new file mode 100644
index 00000000000..d872d0ecc40
--- /dev/null
+++ b/plugins/footnote/config/locales/server.el.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+el:
diff --git a/plugins/footnote/config/locales/server.en.yml b/plugins/footnote/config/locales/server.en.yml
new file mode 100644
index 00000000000..f671b78d867
--- /dev/null
+++ b/plugins/footnote/config/locales/server.en.yml
@@ -0,0 +1,4 @@
+en:
+ site_settings:
+ enable_markdown_footnotes: enable footnote markdown on this site
+ display_footnotes_inline: enable inline expansion of footnotes
diff --git a/plugins/footnote/config/locales/server.en_GB.yml b/plugins/footnote/config/locales/server.en_GB.yml
new file mode 100644
index 00000000000..2d4fa180ec7
--- /dev/null
+++ b/plugins/footnote/config/locales/server.en_GB.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+en_GB:
diff --git a/plugins/footnote/config/locales/server.es.yml b/plugins/footnote/config/locales/server.es.yml
new file mode 100644
index 00000000000..a4b02da6d1f
--- /dev/null
+++ b/plugins/footnote/config/locales/server.es.yml
@@ -0,0 +1,10 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+es:
+ site_settings:
+ enable_markdown_footnotes: habilitar el marcado de notas a pie de página en este sitio
+ display_footnotes_inline: habilitar la expansión en línea de notas al pie
diff --git a/plugins/footnote/config/locales/server.et.yml b/plugins/footnote/config/locales/server.et.yml
new file mode 100644
index 00000000000..0ea0b6d554b
--- /dev/null
+++ b/plugins/footnote/config/locales/server.et.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+et:
diff --git a/plugins/footnote/config/locales/server.fa_IR.yml b/plugins/footnote/config/locales/server.fa_IR.yml
new file mode 100644
index 00000000000..56512089fb5
--- /dev/null
+++ b/plugins/footnote/config/locales/server.fa_IR.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+fa_IR:
diff --git a/plugins/footnote/config/locales/server.fi.yml b/plugins/footnote/config/locales/server.fi.yml
new file mode 100644
index 00000000000..0fe9460fcfd
--- /dev/null
+++ b/plugins/footnote/config/locales/server.fi.yml
@@ -0,0 +1,10 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+fi:
+ site_settings:
+ enable_markdown_footnotes: ota käyttöön alaviitteiden merkintä tällä sivustolla
+ display_footnotes_inline: ota käyttöön alaviitteiden rivilaajennus
diff --git a/plugins/footnote/config/locales/server.fr.yml b/plugins/footnote/config/locales/server.fr.yml
new file mode 100644
index 00000000000..40728f96669
--- /dev/null
+++ b/plugins/footnote/config/locales/server.fr.yml
@@ -0,0 +1,10 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+fr:
+ site_settings:
+ enable_markdown_footnotes: activer le marquage de la note de bas de page sur ce site
+ display_footnotes_inline: activer le développement intégré des notes de bas de page
diff --git a/plugins/footnote/config/locales/server.gl.yml b/plugins/footnote/config/locales/server.gl.yml
new file mode 100644
index 00000000000..fb911ce1635
--- /dev/null
+++ b/plugins/footnote/config/locales/server.gl.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+gl:
diff --git a/plugins/footnote/config/locales/server.he.yml b/plugins/footnote/config/locales/server.he.yml
new file mode 100644
index 00000000000..90c43f673f6
--- /dev/null
+++ b/plugins/footnote/config/locales/server.he.yml
@@ -0,0 +1,10 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+he:
+ site_settings:
+ enable_markdown_footnotes: הפעלת הערות שוליים ב־Markdown באתר הזה
+ display_footnotes_inline: הפעלת הרחבה בתוך השורה של הערות שוליים
diff --git a/plugins/footnote/config/locales/server.hr.yml b/plugins/footnote/config/locales/server.hr.yml
new file mode 100644
index 00000000000..93343ce6e19
--- /dev/null
+++ b/plugins/footnote/config/locales/server.hr.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+hr:
diff --git a/plugins/footnote/config/locales/server.hu.yml b/plugins/footnote/config/locales/server.hu.yml
new file mode 100644
index 00000000000..76b6e9d8bcb
--- /dev/null
+++ b/plugins/footnote/config/locales/server.hu.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+hu:
diff --git a/plugins/footnote/config/locales/server.hy.yml b/plugins/footnote/config/locales/server.hy.yml
new file mode 100644
index 00000000000..cb18f64d356
--- /dev/null
+++ b/plugins/footnote/config/locales/server.hy.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+hy:
diff --git a/plugins/footnote/config/locales/server.id.yml b/plugins/footnote/config/locales/server.id.yml
new file mode 100644
index 00000000000..596e36b2e13
--- /dev/null
+++ b/plugins/footnote/config/locales/server.id.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+id:
diff --git a/plugins/footnote/config/locales/server.it.yml b/plugins/footnote/config/locales/server.it.yml
new file mode 100644
index 00000000000..797cf1dd9f7
--- /dev/null
+++ b/plugins/footnote/config/locales/server.it.yml
@@ -0,0 +1,10 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+it:
+ site_settings:
+ enable_markdown_footnotes: abilita markdown nelle note a piè di pagina su questo sito
+ display_footnotes_inline: consenti l'espansione inline delle note a piè di pagina
diff --git a/plugins/footnote/config/locales/server.ja.yml b/plugins/footnote/config/locales/server.ja.yml
new file mode 100644
index 00000000000..c1f966ee953
--- /dev/null
+++ b/plugins/footnote/config/locales/server.ja.yml
@@ -0,0 +1,10 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ja:
+ site_settings:
+ enable_markdown_footnotes: このサイトで脚注マークダウンを有効にする
+ display_footnotes_inline: 脚注のインライン展開を有効にする
diff --git a/plugins/footnote/config/locales/server.ko.yml b/plugins/footnote/config/locales/server.ko.yml
new file mode 100644
index 00000000000..18dd77fd3ec
--- /dev/null
+++ b/plugins/footnote/config/locales/server.ko.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ko:
diff --git a/plugins/footnote/config/locales/server.lt.yml b/plugins/footnote/config/locales/server.lt.yml
new file mode 100644
index 00000000000..16bb19758dc
--- /dev/null
+++ b/plugins/footnote/config/locales/server.lt.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+lt:
diff --git a/plugins/footnote/config/locales/server.lv.yml b/plugins/footnote/config/locales/server.lv.yml
new file mode 100644
index 00000000000..59e0ef6f4ed
--- /dev/null
+++ b/plugins/footnote/config/locales/server.lv.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+lv:
diff --git a/plugins/footnote/config/locales/server.nb_NO.yml b/plugins/footnote/config/locales/server.nb_NO.yml
new file mode 100644
index 00000000000..2e2224d1472
--- /dev/null
+++ b/plugins/footnote/config/locales/server.nb_NO.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+nb_NO:
diff --git a/plugins/footnote/config/locales/server.nl.yml b/plugins/footnote/config/locales/server.nl.yml
new file mode 100644
index 00000000000..55d9f99cf41
--- /dev/null
+++ b/plugins/footnote/config/locales/server.nl.yml
@@ -0,0 +1,10 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+nl:
+ site_settings:
+ enable_markdown_footnotes: voetnootmarkdown inschakelen op deze site
+ display_footnotes_inline: inline uitbreiding van voetnoten inschakelen
diff --git a/plugins/footnote/config/locales/server.pl_PL.yml b/plugins/footnote/config/locales/server.pl_PL.yml
new file mode 100644
index 00000000000..6abcf488357
--- /dev/null
+++ b/plugins/footnote/config/locales/server.pl_PL.yml
@@ -0,0 +1,10 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+pl_PL:
+ site_settings:
+ enable_markdown_footnotes: włącz markdown dla przypisów na tej stronie
+ display_footnotes_inline: włącz wbudowane rozszerzenie przypisów
diff --git a/plugins/footnote/config/locales/server.pt.yml b/plugins/footnote/config/locales/server.pt.yml
new file mode 100644
index 00000000000..298ba523c1d
--- /dev/null
+++ b/plugins/footnote/config/locales/server.pt.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+pt:
diff --git a/plugins/footnote/config/locales/server.pt_BR.yml b/plugins/footnote/config/locales/server.pt_BR.yml
new file mode 100644
index 00000000000..128a8e33ad9
--- /dev/null
+++ b/plugins/footnote/config/locales/server.pt_BR.yml
@@ -0,0 +1,10 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+pt_BR:
+ site_settings:
+ enable_markdown_footnotes: ative redução de nota de rodapé neste site
+ display_footnotes_inline: ative expansão em linha das notas de rodapé
diff --git a/plugins/footnote/config/locales/server.ro.yml b/plugins/footnote/config/locales/server.ro.yml
new file mode 100644
index 00000000000..08a77f812ee
--- /dev/null
+++ b/plugins/footnote/config/locales/server.ro.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ro:
diff --git a/plugins/footnote/config/locales/server.ru.yml b/plugins/footnote/config/locales/server.ru.yml
new file mode 100644
index 00000000000..182ccda977b
--- /dev/null
+++ b/plugins/footnote/config/locales/server.ru.yml
@@ -0,0 +1,10 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ru:
+ site_settings:
+ enable_markdown_footnotes: Включить Markdown для сносок на этом сайте
+ display_footnotes_inline: Включить встроенное расширение сносок
diff --git a/plugins/footnote/config/locales/server.sk.yml b/plugins/footnote/config/locales/server.sk.yml
new file mode 100644
index 00000000000..6f815624081
--- /dev/null
+++ b/plugins/footnote/config/locales/server.sk.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sk:
diff --git a/plugins/footnote/config/locales/server.sl.yml b/plugins/footnote/config/locales/server.sl.yml
new file mode 100644
index 00000000000..23489a48b1f
--- /dev/null
+++ b/plugins/footnote/config/locales/server.sl.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sl:
diff --git a/plugins/footnote/config/locales/server.sq.yml b/plugins/footnote/config/locales/server.sq.yml
new file mode 100644
index 00000000000..7f051b7a7cf
--- /dev/null
+++ b/plugins/footnote/config/locales/server.sq.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sq:
diff --git a/plugins/footnote/config/locales/server.sr.yml b/plugins/footnote/config/locales/server.sr.yml
new file mode 100644
index 00000000000..88d63d6ae1a
--- /dev/null
+++ b/plugins/footnote/config/locales/server.sr.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sr:
diff --git a/plugins/footnote/config/locales/server.sv.yml b/plugins/footnote/config/locales/server.sv.yml
new file mode 100644
index 00000000000..c2fa3490619
--- /dev/null
+++ b/plugins/footnote/config/locales/server.sv.yml
@@ -0,0 +1,10 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sv:
+ site_settings:
+ enable_markdown_footnotes: aktivera markdown för fotnot på denna webbplats
+ display_footnotes_inline: aktivera infogad expansion av fotnoter
diff --git a/plugins/footnote/config/locales/server.sw.yml b/plugins/footnote/config/locales/server.sw.yml
new file mode 100644
index 00000000000..0d7cdd075bf
--- /dev/null
+++ b/plugins/footnote/config/locales/server.sw.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sw:
diff --git a/plugins/footnote/config/locales/server.te.yml b/plugins/footnote/config/locales/server.te.yml
new file mode 100644
index 00000000000..03967bdbb07
--- /dev/null
+++ b/plugins/footnote/config/locales/server.te.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+te:
diff --git a/plugins/footnote/config/locales/server.th.yml b/plugins/footnote/config/locales/server.th.yml
new file mode 100644
index 00000000000..7de85ff91c4
--- /dev/null
+++ b/plugins/footnote/config/locales/server.th.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+th:
diff --git a/plugins/footnote/config/locales/server.tr_TR.yml b/plugins/footnote/config/locales/server.tr_TR.yml
new file mode 100644
index 00000000000..557ef306b3f
--- /dev/null
+++ b/plugins/footnote/config/locales/server.tr_TR.yml
@@ -0,0 +1,10 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+tr_TR:
+ site_settings:
+ enable_markdown_footnotes: bu sitede dipnot işaretlemeyi etkinleştirin
+ display_footnotes_inline: dipnotların satır içi genişletilmesini etkinleştirin
diff --git a/plugins/footnote/config/locales/server.uk.yml b/plugins/footnote/config/locales/server.uk.yml
new file mode 100644
index 00000000000..f1390545d1d
--- /dev/null
+++ b/plugins/footnote/config/locales/server.uk.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+uk:
diff --git a/plugins/footnote/config/locales/server.ur.yml b/plugins/footnote/config/locales/server.ur.yml
new file mode 100644
index 00000000000..b4a9c21ee2f
--- /dev/null
+++ b/plugins/footnote/config/locales/server.ur.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ur:
diff --git a/plugins/footnote/config/locales/server.vi.yml b/plugins/footnote/config/locales/server.vi.yml
new file mode 100644
index 00000000000..f629dcf5329
--- /dev/null
+++ b/plugins/footnote/config/locales/server.vi.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+vi:
diff --git a/plugins/footnote/config/locales/server.zh_CN.yml b/plugins/footnote/config/locales/server.zh_CN.yml
new file mode 100644
index 00000000000..468ab90650a
--- /dev/null
+++ b/plugins/footnote/config/locales/server.zh_CN.yml
@@ -0,0 +1,10 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+zh_CN:
+ site_settings:
+ enable_markdown_footnotes: 在此站点上启用脚注 Markdown
+ display_footnotes_inline: 启用脚注内联扩展
diff --git a/plugins/footnote/config/locales/server.zh_TW.yml b/plugins/footnote/config/locales/server.zh_TW.yml
new file mode 100644
index 00000000000..7e15fab0018
--- /dev/null
+++ b/plugins/footnote/config/locales/server.zh_TW.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+zh_TW:
diff --git a/plugins/footnote/config/settings.yml b/plugins/footnote/config/settings.yml
new file mode 100644
index 00000000000..45af30da580
--- /dev/null
+++ b/plugins/footnote/config/settings.yml
@@ -0,0 +1,7 @@
+plugins:
+ enable_markdown_footnotes:
+ default: true
+ client: true
+ display_footnotes_inline:
+ default: true
+ client: true
diff --git a/plugins/footnote/plugin.rb b/plugins/footnote/plugin.rb
new file mode 100644
index 00000000000..89fc270745f
--- /dev/null
+++ b/plugins/footnote/plugin.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+# name: footnote
+# about: Adds markdown.it footnote support to Discourse
+# version: 1.0
+# authors: Discourse Team
+# url: https://github.com/discourse/discourse/tree/main/plugins/footnote
+
+enabled_site_setting :enable_markdown_footnotes
+
+register_asset "vendor/javascripts/markdown-it-footnote.js", :vendored_pretty_text
+
+register_asset "stylesheets/footnotes.scss"
+
+register_svg_icon "ellipsis-h" if respond_to?(:register_svg_icon)
+
+on(:before_post_process_cooked) do |doc, post|
+ doc
+ .css("a.footnote-backref")
+ .each do |backref|
+ href = backref["href"] || ""
+ id = href[6..-1].to_i
+ backref["href"] = "#footnote-ref-#{post.id}-#{id}"
+ end
+
+ doc
+ .css("sup.footnote-ref a")
+ .each do |ref|
+ href = ref["href"] || ""
+ id = href[3..-1].to_i
+ ref["href"] = "#footnote-#{post.id}-#{id}"
+
+ id = ref["id"] || ""
+ id = id[5..-1].to_i
+ ref["id"] = "footnote-ref-#{post.id}-#{id}"
+ end
+
+ doc
+ .css("li.footnote-item")
+ .each do |li|
+ id = li["id"] || ""
+ id = id[2..-1].to_i
+
+ li["id"] = "footnote-#{post.id}-#{id}"
+ end
+end
diff --git a/plugins/footnote/spec/pretty_text_spec.rb b/plugins/footnote/spec/pretty_text_spec.rb
new file mode 100644
index 00000000000..def57a98c44
--- /dev/null
+++ b/plugins/footnote/spec/pretty_text_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe PrettyText do
+ before { SiteSetting.queue_jobs = false }
+
+ it "can be disabled" do
+ SiteSetting.enable_markdown_footnotes = false
+
+ markdown = <<~MD
+ Here is a footnote, [^1]
+
+ [^1]: I am one
+ MD
+
+ html = <<~HTML
+ Here is a footnote, [^1]
\n[^1]: I am one
+ HTML
+
+ cooked = PrettyText.cook markdown.strip
+ expect(cooked).to eq(html.strip)
+ end
+
+ it "supports normal footnotes" do
+ markdown = <<~MD
+ Here is a footnote, [^1] and another. [^test]
+
+ [^1]: I am one
+
+ [^test]: I am one
+
+ test multiline
+ MD
+
+ html = <<~HTML
+ Here is a footnote, and another.
+ test multiline
+
+
+
+ HTML
+
+ cooked = PrettyText.cook markdown.strip
+ expect(cooked).to eq(html.strip)
+ end
+
+ it "applies unique ids to elements after cooking a post" do
+ raw = <<~MD
+ Here is a footnote, [^1] and another. [^test]
+
+ [^1]: I am one
+
+ [^test]: I am one
+
+ test multiline
+ MD
+
+ post = create_post(raw: raw)
+ post.reload
+
+ html = <<~HTML
+ Here is a footnote, and another.
+ test multiline
+
+
+
+ HTML
+
+ expect(post.cooked.strip).to eq(html.strip)
+ end
+
+ it "supports inline footnotes wrapped in elements by ending the elements early" do
+ raw = <<~MD
+ I have a point, see footnote. ^[the point]
+
+ ^[footnote]
+ MD
+
+ post = create_post(raw: raw)
+ post.reload
+
+ html = <<~HTML
+ I have a point, see footnote.
+
+
+
+
+ HTML
+
+ expect(post.cooked.strip).to eq(html.strip)
+ end
+end
diff --git a/plugins/footnote/test/javascripts/acceptance/footnote-test.js b/plugins/footnote/test/javascripts/acceptance/footnote-test.js
new file mode 100644
index 00000000000..4ff4d8f6603
--- /dev/null
+++ b/plugins/footnote/test/javascripts/acceptance/footnote-test.js
@@ -0,0 +1,57 @@
+import { click, visit } from "@ember/test-helpers";
+import { test } from "qunit";
+import topicFixtures from "discourse/tests/fixtures/topic";
+import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
+import { cloneJSON } from "discourse-common/lib/object";
+
+acceptance("Discourse Foonote Plugin", function (needs) {
+ needs.user();
+
+ needs.settings({
+ display_footnotes_inline: true,
+ });
+
+ needs.pretender((server, helper) => {
+ server.get("/t/45.json", () => {
+ let topic = cloneJSON(topicFixtures["/t/28830/1.json"]);
+ topic["post_stream"]["posts"][0]["cooked"] = `
+ Lorem ipsum dolor sit amet
+ Second reference should also work.
+
+
+ `;
+ return helper.response(topic);
+ });
+ });
+
+ test("displays the foonote on click", async function (assert) {
+ await visit("/t/45");
+
+ const tooltip = document.getElementById("footnote-tooltip");
+ assert.ok(exists(tooltip));
+
+ await click(".expand-footnote");
+ assert.equal(
+ tooltip.querySelector(".footnote-tooltip-content").textContent.trim(),
+ "consectetur adipiscing elit ↩︎"
+ );
+ });
+
+ test("clicking a second footnote with same name works", async function (assert) {
+ await visit("/t/45");
+
+ const tooltip = document.getElementById("footnote-tooltip");
+ assert.ok(exists(tooltip));
+
+ await click(".second .expand-footnote");
+
+ assert.equal(
+ tooltip.querySelector(".footnote-tooltip-content").textContent.trim(),
+ "consectetur adipiscing elit ↩︎"
+ );
+ });
+});
diff --git a/translator.yml b/translator.yml
index cfc3387d660..a8215bd30a0 100644
--- a/translator.yml
+++ b/translator.yml
@@ -79,3 +79,10 @@ files:
- source_path: plugins/checklist/config/locales/server.en.yml
destination_path: plugins/checklist/server.yml
label: checklist
+
+ - source_path: plugins/footnote/config/locales/client.en.yml
+ destination_path: plugins/footnote/client.yml
+ label: footnote
+ - source_path: plugins/footnote/config/locales/server.en.yml
+ destination_path: plugins/footnote/server.yml
+ label: footnote