From 3a4827a6c7a7172194c17136796c48a4f6cad8ae Mon Sep 17 00:00:00 2001 From: astanley Date: Thu, 11 Jun 2026 16:06:00 -0300 Subject: [PATCH] naming convention --- Changelog.txt | 85 ---- MERGE_NOTES.md | 25 + README.txt | 57 --- assets/css/footnotes-alternative_layout.css | 75 --- assets/css/footnotes.css | 82 --- assets/js/ckeditor/dialogs/footnotes.js | 60 --- assets/js/ckeditor/icons/fn_icon2.png | Bin 262 -> 0 bytes assets/js/ckeditor/icons/footnotes.png | Bin 263 -> 0 bytes assets/js/ckeditor/plugin.js | 151 ------ bibcite_footnotes.ckeditor5.yml | 5 +- bibcite_footnotes.info.yml | 1 + bibcite_footnotes_2.ckeditor5.plugin.yml | 9 - bibcite_footnotes_2.ckeditor5.yml | 17 - bibcite_footnotes_2.info.yml | 7 - bibcite_footnotes_2.libraries.yml | 19 - composer.json | 53 +- config/schema/bibcite_footnotes.schema.yml | 21 + footnotes.info.yml | 10 - footnotes.install | 45 -- footnotes.libraries.yml | 5 - footnotes.module | 92 ---- js/ckeditor5/reference-footnotes-icon.svg | 4 - js/ckeditor5/reference-footnotes.js | 198 -------- .../CKEditor5Plugin/ReferenceFootnotes.php | 19 - src/Plugin/CKEditorPlugin/Footnotes.php | 54 -- src/Plugin/Filter/FootnotesFilter.php | 474 ++++++++---------- templates/footnote-link.html.twig | 2 - templates/footnote-list.html.twig | 23 - .../Functional/FootnotesFilterPluginTest.php | 168 ------- .../FootnotesCkeditorPluginTest.php | 176 ------- 30 files changed, 268 insertions(+), 1669 deletions(-) delete mode 100644 Changelog.txt delete mode 100644 README.txt delete mode 100644 assets/css/footnotes-alternative_layout.css delete mode 100644 assets/css/footnotes.css delete mode 100644 assets/js/ckeditor/dialogs/footnotes.js delete mode 100644 assets/js/ckeditor/icons/fn_icon2.png delete mode 100644 assets/js/ckeditor/icons/footnotes.png delete mode 100644 assets/js/ckeditor/plugin.js delete mode 100644 bibcite_footnotes_2.ckeditor5.plugin.yml delete mode 100644 bibcite_footnotes_2.ckeditor5.yml delete mode 100644 bibcite_footnotes_2.info.yml delete mode 100644 bibcite_footnotes_2.libraries.yml delete mode 100644 footnotes.info.yml delete mode 100644 footnotes.install delete mode 100644 footnotes.libraries.yml delete mode 100644 footnotes.module delete mode 100644 js/ckeditor5/reference-footnotes-icon.svg delete mode 100644 js/ckeditor5/reference-footnotes.js delete mode 100644 src/Plugin/CKEditor5Plugin/ReferenceFootnotes.php delete mode 100644 src/Plugin/CKEditorPlugin/Footnotes.php delete mode 100644 templates/footnote-link.html.twig delete mode 100644 templates/footnote-list.html.twig delete mode 100755 tests/src/Functional/FootnotesFilterPluginTest.php delete mode 100755 tests/src/FunctionalJavascript/FootnotesCkeditorPluginTest.php diff --git a/Changelog.txt b/Changelog.txt deleted file mode 100644 index 62206ac..0000000 --- a/Changelog.txt +++ /dev/null @@ -1,85 +0,0 @@ -2010-03-xx Footnotes 7.x-2.5 - - - First Drupal 7 release. No major feature changes over 6.x-2.5. - -2010-12-31 Footnotes 2.5 - -Major features - - - Add new addon module: Footnotes with Views by AlexisWilke. [#939738] - -Smaller fixes - - - Remove "DEPRECATED" text from Better URL filter and instead have a small - "Note" about it not being available in Footnotes 7.x-x.x. [#1002436] - - Correctly ignore also a tag containing linebreak between the > and <. - Fixes bug [#1002434] - - Add textarea to ignored tags. Anything inside a textarea will be ignored. - Bug [#974760] - -The intention is to branch a Drupal 7 version out of this release. - -2010-10-03 Footnotes 2.4 - -Major features - - - Add Footnotes Wysiwyg module with TinyMCE AND CKEditor support. - This deprecates Footnotes TinyMCE module, which is kept around for backward - compatibility. - [#728642] - - Add i18n support via Drupal.t() also to TinyMCE module. [#672034] - - New feature (option): Collapse identical footnotes into one, as if using same - value="". [#808214] - - Implement [#728658] Highlight footnote when clicking the link. Add mention in - README.txt how to change the highlight color if needed (Footnotes cannot know - what is an appropriate color, I picked #eeeeee as the safest choice). - -Smaller fixes - - - Bug [#761390] - Two small improper CSS names - ...was fixed by changing underscores to dashes in css selectors/classes. - - Deprecate Better URL filter as it is committed to Drupal 7 now. [#296208] - - Bug [#761664] - Footnotes are double numbered when CSS is not used, such as in RSS feeds. - (Due to using OL list) - ...was fixed by migrating to UL list. This is also appropriate since after - introduction of the value="" parameter the footnotes needn't comprise an - ordered list. - - -2010-02-25 Footnotes 2.3 - - - Reset $used_values in _footnotes_replace_callback() after use. [#723446] - -2010-01-17 Footnotes 2.2 - - - Add TinyMCE support as a separate plugin tinymce_footnotes - (thanks elgreg #464066) - - Can have multiple references to same footnote in body by repeating value="" - (#636808) -Small fixes - - Move translations from "po" to "translations" subdirectory. #430656 - - Rename footnotes-alternative_layout.css due to typo in filename. - - fix css: Use "ol.footnotes li" instead of "ol.footnotes" as selector for - "list-style-type: none." Makes it stronger. - - fix html: columns="" should be cols="" bug: #687244 - -2008-09-07 Footnotes 2.1 - - - Add "clear: both" to css of footnotes section. http://drupal.org/node/303828 - -2008-07-30 Footnotes 2.0 - - - Add support for using [fn]square brackets[/fn] (268026) - - Change documentation to talk about [fn] by default, but is still - supported - - Add support for specifying "value" attribute. (emfabric 282104) - - Mention http://drupal.org/node/279420 in known issues - - Adding Better URL filter (fork from core). http://drupal.org/node/161217 - - There appears to also be a French translation now. Thanks Beginner! - (Japanese already done earlier.) - - Fix bug where teaser might cut into the middle of a footnote: - http://drupal.org/node/253326 - - Start using the Drupal theme system, footnotes can now be themed by site - admins (emfabric 221156) diff --git a/MERGE_NOTES.md b/MERGE_NOTES.md index 7ae69c1..20cccfc 100644 --- a/MERGE_NOTES.md +++ b/MERGE_NOTES.md @@ -17,3 +17,28 @@ This package merges the original `bibcite_footnotes` module and the CKEditor 5-o The optional `bibcite_footnotes_article_with_citations` submodule is still a CKEditor 4 example configuration because its installed editor config uses `editor: ckeditor` and the `reference_footnotes` CKEditor 4 toolbar button. On a CKEditor 5 site, enable the main module and add the `Footnote/Citation` button to the desired text format's CKEditor 5 toolbar. Make sure the text format allows the custom `` element and the `filter_reference_footnotes` filter is enabled. + +## 2026-06-11 CKEditor 5 toolbar fix + +Restored the CKEditor 5 plugin PHP class/assets and `reference_footnotes` library from the CKEditor 5 module, with namespaces and library references changed to `bibcite_footnotes`. Also added an explicit `drupal:ckeditor5` dependency so Drupal discovers the CKEditor 5 plugin definitions reliably. + +## CKEditor 5 toolbar fix + +The CKEditor 5 plugin definition now lists the base `` tag and each attribute form separately: + +- `` +- `` +- `` +- `` + +Drupal CKEditor 5 plugin validation treats tag creation and attribute creation separately, so declaring only `` can prevent the toolbar item from being available. + + +## Compatibility fix + +This package includes both filter plugin IDs: + +- `filter_reference_footnotes` via `ReferenceFootnotesFilter` +- `filter_footnotes` via `FootnotesFilter` + +The second class is a compatibility alias for existing text formats whose active config already points at `filter_footnotes`. diff --git a/README.txt b/README.txt deleted file mode 100644 index 746ee2f..0000000 --- a/README.txt +++ /dev/null @@ -1,57 +0,0 @@ -INTRODUCTION ------------------- - -The Footnotes module is used to easily create automatically numbered footnote references in an article or post (such as a reference to a URL). - - * For a full description of the module, visit the project page: - https://www.drupal.org/project/footnotes - - * To submit bug reports and feature suggestions, or track changes: - https://www.drupal.org/project/issues/footnotes - -REQUIREMENTS ------------------- - -The Footnote module for Drupal 8 requires the following modules and plugins: - - * FakeObjects (https://www.drupal.org/project/fakeobjects) - * CKEditor plugin (http://ckeditor.com/addon/fakeobjects) - -INSTALLATION ----------------- -* Before you can use the FakeObjects module, you need to download the plugin from http://ckeditor.com/addon/fakeobjects and place it in /libraries/fakeobjects. - -* In all other steps, install the module as you would normally install a contributed Drupal module. Visit -https://www.drupal.org/docs/8/extending-drupal-8/installing-drupal-8-modules for further information. - -CONFIGURATION -------------------- -* To use the footnotes filter in some input formats, go to Configuration -> - Text formats. - -* For the Text formats you want to support footnotes markup, select configure and activate a suitable footnotes filter. - -* In the place where you want to add a footnote enclose the footnote text within an fn tag:[fn]like this[/fn]. By default, footnotes are placed at the end of the text. You can also use a [footnotes] or [footnotes /] tag to position it anywhere you want. - -* The filter will take the text within the tag and move it to a footnote at the -bottom of the page. In it's place it will place a number which is also a link to -the footnote. Footnotes supports both [fn]square brackets[/fn] and angle brackets. - -* You can also use a "value" attribute to a) set the numbering to start from the given value, or b) to set an arbitrary text string as label. - -Ex: - - [fn value="5"]This becomes footnote #5. Subsequent are #6, #7...[/fn] - [fn value="*"]This footnote is assigned the label "*"[/fn] - -Using value="" you can have multiple references to the same footnote in the text body. - - [fn value="5"]This becomes footnote #5.[/fn] - [fn value="5"]This is a reference to the same footnote #5, this text itself is discarded.[/fn] - -TROUBLESHOOTING & FAQ ------------------------------- - -Q: When trying to install the Footnotes module, I get the message: Before you can use the FakeObjects module, you need to download the plugin from ckeditor.com and place it in /libraries/fakeobjects." - -A: To avoid this error message, please follow the guidelines in the Required modules and the Installation sections of this Readme file. Please mind that the Drupal 8.x-2.x branch of the Footnotes module only supports the CKEditor and does not support the TinyMCE. diff --git a/assets/css/footnotes-alternative_layout.css b/assets/css/footnotes-alternative_layout.css deleted file mode 100644 index f80d105..0000000 --- a/assets/css/footnotes-alternative_layout.css +++ /dev/null @@ -1,75 +0,0 @@ -/* - * CSS specific to Footnotes module. - * - * This is an alternative layout, it is not so nice but overcomes - * the layout bugs on IE. http://drupal.org/node/166628 - * To use this layout, just rename this file to footnotes.css. - */ - -/* Add empty space before footnotes and a black line on top. */ -.footnotes { - clear: both; - margin-top: 4em; - margin-bottom: 2em; - border-top: 1px solid #000; -} -/* Make footnotes appear in a smaller font */ -.footnotes { - font-size: 0.9em; -} -/* - Make the footnote a supertext^1 -*/ -.see-footnote { - vertical-align: top; - position: relative; - top: -0.25em; - font-size: 0.9em; -} -/* Hide the bullet of the UL list of footnotes */ - -ul.footnotes { - list-style-type: none; - margin-left: 0; - padding-left: 0; -} -ul.footnotes li { - margin-left: 0.5em; - list-style-type: none; - background: none; /* Garland theme sets a bullet via background image, this must be unset! See bug 861634 */ -} -.footnotes .footnote-label { - vertical-align: top; - position: relative; - top: -0.35em; - left: -0.35em; - font-size: 0.8em; -} -/* Highlight the target footnote (or ref number, if a backlink was used) when user clicks a footnote. */ -.see-footnote:target, -.footnotes .footnote:target { - background-color: #eee; -} -.see-footnote:target { - border: solid 1px #aaa; -} -/* - Make the multiple backlinks a supertext^1 -*/ -.footnotes .footnote-multi { - vertical-align: top; - position: relative; - top: -0.25em; - font-size: 0.75em; -} -/* - * Textile Footnotes - */ -/* First footnote */ -#fn1 { - border-top: 1px solid #000; - margin-top: 3em; -} -.footnote { - font-size: 0.9em; -} diff --git a/assets/css/footnotes.css b/assets/css/footnotes.css deleted file mode 100644 index 14b2b3f..0000000 --- a/assets/css/footnotes.css +++ /dev/null @@ -1,82 +0,0 @@ -/* - * CSS specific to Footnotes module. -* -* Thanks to binford2k@lug.wsu.edu for this tip and drinkypoo -* for the question leading up to it. http://drupal.org/node/80538 -*/ - -/* Add empty space before footnotes and a black line on top. */ -.footnotes { - clear: both; - margin-top: 4em; - margin-bottom: 2em; - border-top: 1px solid #000; -} -/* Make footnotes appear in a smaller font */ -.footnotes { - font-size: 0.9em; -} -/* - Make the footnote a supertext^1 -*/ -.see-footnote { - vertical-align: top; - position: relative; - top: -0.25em; - font-size: 0.9em; -} -/* Hide the bullet of the UL list of footnotes */ - -ul.footnotes { - list-style-type: none; - margin-left: 0; - padding-left: 0; -} -ul.footnotes li { - margin-left: 2.5em; - list-style-type: none; - background: none; /* Garland theme sets a bullet via background image, this must be unset! See bug 861634 */ -} -/* Move the footnote number outside of the margin for footnote text (hanging indent) */ -ul.footnotes { - /* This is apparently very needed for the "position: absolute;" below to work correctly */ - position: relative; -} -.footnotes .footnote-label { - position: absolute; - left: 0; - z-index: 2; -} -/* Highlight the target footnote (or ref number, if a backlink was used) when user clicks a footnote. */ -.see-footnote:target, -.footnotes .footnote:target { - background-color: #eee; -} -.see-footnote:target { - border: solid 1px #aaa; -} -/* Note: This CSS has a minor bug on all versions of IE in that the footnote numbers -are aligned with the absolute bottom of their space, thus being a couple of pixels -lower than their corresponding line of text. IE5.5 has a serious bug in that the numbers -are not shifted left at all, thus being garbled together with the start of their text. */ - -/* - Make the multiple backlinks a supertext^1 -*/ -.footnotes .footnote-multi { - vertical-align: top; - position: relative; - top: -0.25em; - font-size: 0.75em; -} -/* - * Textile Footnotes - */ -/* First footnote */ -#fn1 { - border-top: 1px solid #000; - margin-top: 3em; -} -.footnote { - font-size: 0.9em; -} diff --git a/assets/js/ckeditor/dialogs/footnotes.js b/assets/js/ckeditor/dialogs/footnotes.js deleted file mode 100644 index 4314944..0000000 --- a/assets/js/ckeditor/dialogs/footnotes.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @file - */ - -function footnotesDialog(editor, isEdit) { - return { - title: Drupal.t("Footnotes Dialog"), - minWidth: 500, - minHeight: 50, - contents: [ - { - id: "info", - label: Drupal.t("Add a footnote"), - title: Drupal.t("Add a footnote"), - elements: [ - { - id: "footnote", - type: "text", - label: Drupal.t("Footnote text :"), - setup(element) { - if (isEdit) { - this.setValue(element.getHtml()); - } - } - }, - { - id: "value", - type: "text", - label: Drupal.t("Value :"), - setup(element) { - if (isEdit) { - this.setValue(element.getAttribute("value")); - } - } - } - ] - } - ], - onShow() { - if (isEdit) { - this.fakeObj = CKEDITOR.plugins.footnotes.getSelectedFootnote(editor); - this.realObj = editor.restoreRealElement(this.fakeObj); - } - this.setupContent(this.realObj); - }, - onOk() { - CKEDITOR.plugins.footnotes.createFootnote( - editor, - this.realObj, - this.getValueOf("info", "footnote"), - this.getValueOf("info", "value") - ); - delete this.fakeObj; - delete this.realObj; - } - }; -} - -CKEDITOR.dialog.add("createfootnotes", editor => footnotesDialog(editor)); -CKEDITOR.dialog.add("editfootnotes", editor => footnotesDialog(editor, 1)); diff --git a/assets/js/ckeditor/icons/fn_icon2.png b/assets/js/ckeditor/icons/fn_icon2.png deleted file mode 100644 index 6a3f89668ca36c6ccc9018d05f5531639ec52935..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 262 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QL70(Y)*K0-AbW|YuPgg44t7ya)u6vGSb#!po-U3d7N?UF z76=$537eGP3p;w?z=Z=qP*t>ET-lB}Vu92@V*>*N1x>w_goFUQo!4I*>h&7f&XCcm zscAEKXe2q)to)wY)jO59%Zs1qmcDpp$kR~CeD?iqNePKH3*rvvGw^Nr`Xp}sXC59N xrWDBnpt0u+5*K+{9T4huIm&QYLqdgtVU>;H!j(JY9spg$;OXk;vd$@?2>_&fSZn|Q diff --git a/assets/js/ckeditor/icons/footnotes.png b/assets/js/ckeditor/icons/footnotes.png deleted file mode 100644 index f0e8224485cd4cac6db09bca4eec5a69ec1159c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 263 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QL70(Y)*K0-AbW|YuPgga9wt!*uAd9?UIB&LJzX3_EKXky zzR1;}z+-cn1+^Dfy>NK;pltg|mbzAfMJ)nPH+sw~7w!At)3f}A z)={qWI~WQ|uIss}?sPi9ut8+gnREv2_gNEWEMDrIDd~FB_N1n^NLgU3Cd)SA=F`)z zo=yMEU+$*Sx$G(LoyGv;PqrI+f>Xug8h<`%ew)atwkfl3_9>u~7(8A5T-G@yGywo$ C=w2lN diff --git a/assets/js/ckeditor/plugin.js b/assets/js/ckeditor/plugin.js deleted file mode 100644 index 8f15e3d..0000000 --- a/assets/js/ckeditor/plugin.js +++ /dev/null @@ -1,151 +0,0 @@ -/** - * @file - * A CKeditor plugin to insert footnotes as in-place elements (consumed by Footnotes module in Drupal). - * - * This is a rather sophisticated plugin to show a dialog to insert - * footnotes or edit existing ones. It produces and understands - * the angle bracket variant and uses the fakeObjects API to - * show a nice icon to the user, while producing proper tags when - * the text is saved or View Source is pressed. - * - * If a text contains footnotes of the [fn]square bracket[/fn] variant, - * they will be visible in the text and this plugin will not react to them. - * - * This plugin uses Drupal.t() to translate strings and will not as such - * work outside of Drupal. (But removing those functions would be the only - * change needed.) While being part of a Wysiwyg compatible module, it could - * also be used together with the CKEditor module. - * - * Drupal Wysiwyg requirement: The first argument to CKEDITOR.plugins.add() - * must be equal to the key used in $plugins[] in hook_wysiwyg_plugin(). - */ - -CKEDITOR.plugins.add("footnotes", { - requires: ["fakeobjects", "dialog"], - icons: "footnotes", - onLoad() { - const iconPath = `${window.location.origin + this.path}icons/fn_icon2.png`; - CKEDITOR.addCss( - `${".cke_footnote{background-image: url("}${CKEDITOR.getUrl( - iconPath - )});` + - `background - position: center center;` + - `background - repeat: no - repeat;` + - `width: 16px;` + - `height: 16px;` + - `}` - ); - }, - init(editor) { - editor.addCommand( - "createfootnotes", - new CKEDITOR.dialogCommand("createfootnotes", { - allowedContent: "fn[value]" - }) - ); - editor.addCommand( - "editfootnotes", - new CKEDITOR.dialogCommand("editfootnotes", { - allowedContent: "fn[value]" - }) - ); - - // Drupal Wysiwyg requirement: The first argument to editor.ui.addButton() - // must be equal to the key used in $plugins[]['buttons'][] - // in hook_wysiwyg_plugin(). - if (editor.ui.addButton) { - editor.ui.addButton("footnotes", { - label: Drupal.t("Add a footnote"), - command: "createfootnotes", - icon: "footnotes" - }); - } - - if (editor.addMenuItems) { - editor.addMenuGroup("footnotes", 100); - editor.addMenuItems({ - footnotes: { - label: Drupal.t("Edit footnote"), - command: "editfootnotes", - icon: "footnotes", - group: "footnotes" - } - }); - } - if (editor.contextMenu) { - editor.contextMenu.addListener(element => { - if (!element || element.data("cke-real-element-type") !== "fn") { - return null; - } - return { footnotes: CKEDITOR.TRISTATE_ON }; - }); - } - - editor.on("doubleclick", evt => { - if (CKEDITOR.plugins.footnotes.getSelectedFootnote(editor)) { - evt.data.dialog = "editfootnotes"; - } - }); - - CKEDITOR.dialog.add("createfootnotes", `${this.path}dialogs/footnotes.js`); - CKEDITOR.dialog.add("editfootnotes", `${this.path}dialogs/footnotes.js`); - }, - afterInit(editor) { - const { dataProcessor } = editor; - const { dataFilter } = dataProcessor; - - if (dataFilter) { - dataFilter.addRules({ - elements: { - fn(element) { - return editor.createFakeParserElement( - element, - "cke_footnote", - "hiddenfield", - false - ); - } - } - }); - } - } -}); - -CKEDITOR.plugins.footnotes = { - createFootnote(editor, origElement, text, value) { - let realElement; - if (!origElement) { - realElement = CKEDITOR.dom.element.createFromHtml(""); - } else { - realElement = origElement; - } - - if (text && text.length > 0) { - realElement.setHtml(text); - } - if (value && value.length > 0) { - realElement.setAttribute("value", value); - } - - const fakeElement = editor.createFakeElement( - realElement, - "cke_footnote", - "hiddenfield", - false - ); - editor.insertElement(fakeElement); - }, - - getSelectedFootnote(editor) { - const selection = editor.getSelection(); - const element = selection.getSelectedElement(); - const seltype = selection.getType(); - - if ( - seltype === CKEDITOR.SELECTION_ELEMENT && - element.data("cke-real-element-type") === "hiddenfield" - ) { - return element; - } - } -}; diff --git a/bibcite_footnotes.ckeditor5.yml b/bibcite_footnotes.ckeditor5.yml index c41b5c8..3ff5ccb 100644 --- a/bibcite_footnotes.ckeditor5.yml +++ b/bibcite_footnotes.ckeditor5.yml @@ -11,4 +11,7 @@ bibcite_footnotes_footnote_picker: label: 'Footnote' icon: '' elements: - - + - + - + - + - diff --git a/bibcite_footnotes.info.yml b/bibcite_footnotes.info.yml index 4d25572..dddd712 100644 --- a/bibcite_footnotes.info.yml +++ b/bibcite_footnotes.info.yml @@ -7,4 +7,5 @@ dependencies: - bibcite:bibcite - bibcite:bibcite_entity - drupal:editor + - drupal:ckeditor5 - drupal:filter diff --git a/bibcite_footnotes_2.ckeditor5.plugin.yml b/bibcite_footnotes_2.ckeditor5.plugin.yml deleted file mode 100644 index 32a11b3..0000000 --- a/bibcite_footnotes_2.ckeditor5.plugin.yml +++ /dev/null @@ -1,9 +0,0 @@ -reference_footnotes: - ckeditor5: - plugins: - - ReferenceFootnotes - drupal: - label: 'Reference Footnotes' - toolbar_items: - referenceFootnotes: - label: 'Reference Footnotes' diff --git a/bibcite_footnotes_2.ckeditor5.yml b/bibcite_footnotes_2.ckeditor5.yml deleted file mode 100644 index a3674da..0000000 --- a/bibcite_footnotes_2.ckeditor5.yml +++ /dev/null @@ -1,17 +0,0 @@ -bibcite_footnotes_2_footnote_picker: - ckeditor5: - plugins: - - footnotepicker2.FootnotePicker2 - - drupal: - label: 'Footnote/Citation (Test)' - library: bibcite_footnotes_2/footnote_picker - admin_library: bibcite_footnotes_2/footnote_picker - - toolbar_items: - footnotePicker2: - label: 'Footnoe' - icon: '' - - elements: - - diff --git a/bibcite_footnotes_2.info.yml b/bibcite_footnotes_2.info.yml deleted file mode 100644 index 0b67b5f..0000000 --- a/bibcite_footnotes_2.info.yml +++ /dev/null @@ -1,7 +0,0 @@ -name: 'Bibcite Footnotes CKEditor 5' -type: module -description: 'Provides a CKEditor 5 plugin for inserting and editing reference footnotes.' -package: 'Custom' -core_version_requirement: '^10.2 || ^11' -dependencies: - - drupal:ckeditor5 diff --git a/bibcite_footnotes_2.libraries.yml b/bibcite_footnotes_2.libraries.yml deleted file mode 100644 index 311dfaf..0000000 --- a/bibcite_footnotes_2.libraries.yml +++ /dev/null @@ -1,19 +0,0 @@ -reference_footnotes: - js: - js/ckeditor5/reference-footnotes.js: { type: module } - css: - theme: - css/reference-footnotes.css: {} - -footnote_picker: - css: - theme: - css/footnote_picker.admin.css: { } - css/footnote_picker.dialog.css: {} - js: - js/ckeditor5_plugins/footnotepicker2/build/footnotepicker2.js: {} - dependencies: - - core/ckeditor5 - - core/drupal - - core/drupal.dialog - - core/jquery diff --git a/composer.json b/composer.json index 0820745..18d9142 100644 --- a/composer.json +++ b/composer.json @@ -1,38 +1,23 @@ { - "name": "drupal/footnotes", - "description": "Add automatically numbered footnotes to your posts.", - "type": "drupal-module", - "homepage": "https://drupal.org/project/footnotes", - "authors": [ - { - "name": "Andrii Aleksandrov (id.aleks)", - "homepage": "https://www.drupal.org/u/idaleks", - "role": "Maintainer" + "name": "roblib/bibcite_footnotes", + "description": "Inline footnote links for BibCite References with CKEditor 4 and CKEditor 5 support.", + "type": "drupal-module", + "homepage": "https://drupal.org/project/bibcite_footnotes", + "authors": [ + { + "name": "Alexander O'Neill (alxp)", + "homepage": "https://www.drupal.org/u/alxp", + "role": "Maintainer" + } + ], + "support": { + "issues": "https://drupal.org/project/issues/bibcite_footnotes", + "source": "https://cgit.drupalcode.org/bibcite_footnotes" }, - { - "name": "Oleksandr Dekhteruk (pifagor)", - "homepage": "https://www.drupal.org/u/pifagor", - "role": "Maintainer" - }, - { - "name": "Fernando Conceição (yukare)", - "homepage": "https://www.drupal.org/u/yukare", - "role": "Maintainer" - }, - { - "name": "Henrik Ingo (hingo)", - "homepage": "https://www.drupal.org/u/hingo", - "role": "Maintainer" + "license": "GPL-2.0+", + "minimum-stability": "dev", + "require": { + "drupal/core": ">=8.6", + "drupal/fakeobjects": "^1.0" } - ], - "support": { - "issues": "https://drupal.org/project/issues/footnotes", - "source": "https://cgit.drupalcode.org/footnotes" - }, - "license": "GPL-2.0+", - "minimum-stability": "dev", - "require": { - "drupal/core": ">=8.6", - "drupal/fakeobjects": "^1.0" - } } diff --git a/config/schema/bibcite_footnotes.schema.yml b/config/schema/bibcite_footnotes.schema.yml index 00d4414..2e5a569 100644 --- a/config/schema/bibcite_footnotes.schema.yml +++ b/config/schema/bibcite_footnotes.schema.yml @@ -17,3 +17,24 @@ filter_settings.filter_reference_footnotes: works_cited_sort_by: type: string label: 'Sort works cited by' + + +filter_settings.filter_footnotes: + type: mapping + label: 'Footnotes filter settings' + mapping: + footnotes_collapse: + type: boolean + label: 'Collapse identical footnotes' + footnotes_ibid: + type: boolean + label: 'Use Ibid.' + notes_section_label: + type: label + label: 'Notes section heading label' + reference_dont_show_backlink_text: + type: boolean + label: "Don't show note value text in reference list" + works_cited_sort_by: + type: string + label: 'Sort works cited by' diff --git a/footnotes.info.yml b/footnotes.info.yml deleted file mode 100644 index 3069bf3..0000000 --- a/footnotes.info.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: Footnotes -description: 'Add automatically numbered footnotes to your posts.' - -type: module -core_version_requirement: ^8 || ^9 - -dependencies: - - 'fakeobjects:fakeobjects' -test_dependencies: - - 'fakeobjects:fakeobjects' diff --git a/footnotes.install b/footnotes.install deleted file mode 100644 index 170fcac..0000000 --- a/footnotes.install +++ /dev/null @@ -1,45 +0,0 @@ -moduleExists('fakeobjects'); - if ($fakeobjects_exist) { - $fakeobjects_requirements = fakeobjects_requirements($phase); - if ($fakeobjects_requirements['fakeobjects']['severity'] === REQUIREMENT_OK) { - $requirements['footnotes'] = [ - 'title' => t('Footnotes'), - 'value' => t('Footnotes requirements are OK.'), - 'severity' => REQUIREMENT_OK, - ]; - } - else { - $requirements['footnotes'] = [ - 'title' => t('Footnotes'), - 'value' => t('Footnotes requirements are not properly configured. Please check Fakeobjects module requirements.'), - 'severity' => REQUIREMENT_ERROR, - ]; - } - } - else { - $requirements['footnotes'] = [ - 'title' => t('Footnotes'), - 'value' => t("Fakeobjects module isn't installed/enabled.", [':href' => 'https://www.drupal.org/project/fakeobjects']), - 'severity' => REQUIREMENT_ERROR, - 'description' => t('Footnotes module has a dependency on Fakeobjects module. Ensure that Fakeobjects module is enabled and configured.'), - ]; - } - - return $requirements; -} diff --git a/footnotes.libraries.yml b/footnotes.libraries.yml deleted file mode 100644 index d3ab9ef..0000000 --- a/footnotes.libraries.yml +++ /dev/null @@ -1,5 +0,0 @@ -footnotes: - version: VERSION - css: - component: - assets/css/footnotes.css: {} diff --git a/footnotes.module b/footnotes.module deleted file mode 100644 index 86e9d9d..0000000 --- a/footnotes.module +++ /dev/null @@ -1,92 +0,0 @@ -here", [ - ':href' => Url::fromRoute('filter.admin_overview')->toString(), - ]); - } -} - -/** - * Implements hook_theme(). - * - * Thanks to emfabric for this implementation. http://drupal.org/node/221156 - */ -function footnotes_theme() { - return [ - 'footnote_link' => [ - 'variables' => [ - 'fn' => NULL, - ], - ], - 'footnote_list' => [ - 'variables' => [ - 'footnotes'=> NULL, - ], - ], - ]; -} - - -/** - * Helper for other filters, check if Footnotes is present in your filter chain. - * - * Note: Due to changes in Filter API, the arguments to this function have - * changed in Drupal 7. - * - * Other filters may leverage the Footnotes functionality in a simple way: - * by outputting markup with ... tags within. - * - * This creates a dependency, the Footnotes filter must be present later in - * "Input format". By calling this helper function the other filters that - * depend on Footnotes may check whether Footnotes is present later in the chain - * of filters in the current Input format. - * - * If this function returns true, the caller may depend on Footnotes. Function - * returns false if caller may not depend on Footnotes. - * - * You should also put "dependencies = footnotes" in your module.info file. - * - * Example usage: - * - * _filter_example_process( $text, $filter, $format ) { - * ... - * if(footnotes_is_footnotes_later($format, $filter)) { - * //output markup which may include [fn] tags - * } - * else { - * // must make do without footnotes features - * // can also emit warning/error that user should install and configure - * // footnotes module - * } - * ... - * } - * - * - * @param object $format - * The text format object caller is part of. - * @param object $caller_filter - * The filter object representing the caller (in this text format). - * - * @return True - * If Footnotes is present after $caller in $format. - */ -function footnotes_is_footnotes_later($format, $caller_filter) { - return $format['filter_footnotes']['weight'] > $caller_filter['weight']; -} diff --git a/js/ckeditor5/reference-footnotes-icon.svg b/js/ckeditor5/reference-footnotes-icon.svg deleted file mode 100644 index 48f2f3c..0000000 --- a/js/ckeditor5/reference-footnotes-icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - fn - diff --git a/js/ckeditor5/reference-footnotes.js b/js/ckeditor5/reference-footnotes.js deleted file mode 100644 index 73af071..0000000 --- a/js/ckeditor5/reference-footnotes.js +++ /dev/null @@ -1,198 +0,0 @@ -import { Plugin } from 'ckeditor5/src/core'; -import { ButtonView } from 'ckeditor5/src/ui'; -import { Widget, toWidget } from 'ckeditor5/src/widget'; -import icon from './reference-footnotes-icon.svg'; - -/** - * ReferenceFootnotes CKEditor 5 plugin. - * - * Provides: - * - A toolbar button that inserts a element. - * - Double-click editing of existing elements. - * - Simple browser-prompt based dialog for editing attributes. - * - * This keeps the implementation simple and reliable in Drupal's environment. - * If you later want a full ContextualBalloon-based dialog, this is the place - * to extend. - */ -export default class ReferenceFootnotes extends Plugin { - static get requires() { - return [ Widget ]; - } - - static get pluginName() { - return 'ReferenceFootnotes'; - } - - init() { - const editor = this.editor; - - // --- SCHEMA ------------------------------------------------- - // Model element: text - editor.model.schema.register('fn', { - allowWhere: '$text', - allowContentOf: '$text', - isInline: true, - isObject: true, - allowAttributes: [ 'value', 'page', 'reference' ] - }); - - // --- MODEL → VIEW (editing) -------------------------------- - // Show an inline widget with a class that you can style with an icon. - editor.conversion.for('editingDowncast').elementToElement({ - model: 'fn', - view: (modelElement, { writer }) => { - const viewElement = writer.createContainerElement('span', { - class: 'reference-footnote', - 'data-value': modelElement.getAttribute('value') || '', - 'data-reference': modelElement.getAttribute('reference') || '', - 'data-page': modelElement.getAttribute('page') || '' - }); - - return toWidget(viewElement, writer, { label: editor.t('Reference footnote') }); - } - }); - - // --- MODEL → VIEW (data/HTML) ------------------------------- - // Persist as TEXT in the saved HTML. - editor.conversion.for('dataDowncast').elementToElement({ - model: 'fn', - view: (modelElement, { writer }) => { - const attrs = { - value: modelElement.getAttribute('value') || '', - reference: modelElement.getAttribute('reference') || '', - page: modelElement.getAttribute('page') || '' - }; - - const fnElement = writer.createContainerElement('fn', attrs); - - // Put inner text into the element. - const firstChild = modelElement.getChild(0); - const textData = firstChild && firstChild.is( 'text' ) ? firstChild.data : ''; - if (textData) { - const textNode = writer.createText(textData); - writer.insert(textNode, fnElement, 0); - } - - return fnElement; - } - }); - - // --- VIEW → MODEL (upcast) --------------------------------- - // Convert tags back into model elements. - editor.conversion.for('upcast').elementToElement({ - view: { - name: 'fn' - }, - model: (viewElement, { writer }) => { - return writer.createElement('fn', { - value: viewElement.getAttribute('value') || '', - reference: viewElement.getAttribute('reference') || '', - page: viewElement.getAttribute('page') || '' - }); - } - }); - - // --- TOOLBAR BUTTON ----------------------------------------- - editor.ui.componentFactory.add('referenceFootnotes', locale => { - const view = new ButtonView(locale); - - view.set({ - label: editor.t('Add reference footnote'), - icon, - tooltip: true - }); - - view.on('execute', () => { - this.openFootnoteDialog(null); - }); - - return view; - }); - - // --- DOUBLE CLICK HANDLER ----------------------------------- - editor.editing.view.document.on('dblclick', (evt, data) => { - const viewElement = data.target; - const modelElement = editor.editing.mapper.toModelElement(viewElement); - - if (modelElement && modelElement.name === 'fn') { - this.openFootnoteDialog(modelElement); - } - }); - } - - /** - * Opens a simple "dialog" (browser prompts) to create or edit a footnote. - * - * @param {?module:engine/model/element~Element} existingFn - * The existing model element, or null for a new one. - */ - openFootnoteDialog(existingFn) { - const editor = this.editor; - - // --- Read existing values (if editing) ---------------------- - let currentText = ''; - let currentValue = ''; - let currentReference = ''; - let currentPage = ''; - - if (existingFn) { - const firstChild = existingFn.getChild(0); - currentText = firstChild && firstChild.is('text') ? firstChild.data : ''; - - currentValue = existingFn.getAttribute('value') || ''; - currentReference = existingFn.getAttribute('reference') || ''; - currentPage = existingFn.getAttribute('page') || ''; - } - - // --- Simple prompts for now -------------------------------- - const text = window.prompt(editor.t('Footnote text:'), currentText || ''); - if (text === null) { - return; // user cancelled - } - - const value = window.prompt(editor.t('Footnote value (e.g. 1, 2, 3):'), currentValue || ''); - if (value === null) { - return; - } - - const reference = window.prompt(editor.t('Reference (optional):'), currentReference || ''); - if (reference === null) { - return; - } - - const page = window.prompt(editor.t('Page (optional):'), currentPage || ''); - if (page === null) { - return; - } - - // --- Write changes to the model ----------------------------- - editor.model.change(writer => { - if (!existingFn) { - // Insert a brand new element at the selection. - const fnElement = writer.createElement('fn', { - value: value, - reference: reference, - page: page - }); - - // Insert the element and then its visible text. - editor.model.insertObject(fnElement, editor.model.document.selection); - writer.insertText(text, fnElement, 0); - } - else { - // Update attributes. - writer.setAttribute('value', value, existingFn); - writer.setAttribute('reference', reference, existingFn); - writer.setAttribute('page', page, existingFn); - - // Replace inner text. - const children = Array.from(existingFn.getChildren()); - for (const child of children) { - writer.remove(child); - } - writer.insertText(text, existingFn, 0); - } - }); - } -} diff --git a/src/Plugin/CKEditor5Plugin/ReferenceFootnotes.php b/src/Plugin/CKEditor5Plugin/ReferenceFootnotes.php deleted file mode 100644 index d50695f..0000000 --- a/src/Plugin/CKEditor5Plugin/ReferenceFootnotes.php +++ /dev/null @@ -1,19 +0,0 @@ -', - ], -)] -class ReferenceFootnotes extends CKEditor5PluginDefault { - -} diff --git a/src/Plugin/CKEditorPlugin/Footnotes.php b/src/Plugin/CKEditorPlugin/Footnotes.php deleted file mode 100644 index 4534163..0000000 --- a/src/Plugin/CKEditorPlugin/Footnotes.php +++ /dev/null @@ -1,54 +0,0 @@ - [ - 'label' => $this->t('Footnotes'), - 'image' => drupal_get_path('module', 'footnotes') . '/assets/js/ckeditor/icons/footnotes.png', - ], - ]; - } - - /** - * {@inheritdoc} - */ - public function getConfig(Editor $editor) { - return []; - } - -} diff --git a/src/Plugin/Filter/FootnotesFilter.php b/src/Plugin/Filter/FootnotesFilter.php index 7c51161..5569dc3 100644 --- a/src/Plugin/Filter/FootnotesFilter.php +++ b/src/Plugin/Filter/FootnotesFilter.php @@ -1,192 +1,146 @@ renderer = \Drupal::service('renderer'); + + $this->renderer = $renderer; + $this->config = $configFactory->get('reference_footnotes.settings'); + $this->configEditable = $configFactory->getEditable('reference_footnotes.settings'); } - /** - * Get the tips for the filter. - * - * @param bool $long - * If get the long or short tip. - * - * @return string - * The tip to show for the user. - */ - public function tips($long = FALSE) { - if ($long) { - return $this->t('You can insert footnotes directly into texts with [fn]This text becomes a footnote.[/fn]. This will be replaced with a running number (the footnote reference) and the text within the [fn] tags will be moved to the bottom of the page (the footnote). See Footnotes Readme page for additional usage options.', [':link' => 'http://drupal.org/project/footnotes">']); - } - else { - return $this->t('Use [fn]...[/fn] (or <fn>...</fn>) to insert automatically numbered footnotes.'); - } + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self { + return new self( + $configuration, + (string) $plugin_id, + $plugin_definition, + $container->get('renderer'), + $container->get('config.factory'), + ); } - /** - * {@inheritdoc} - */ - public function process($text, $langcode) { - // Supporting both [fn] and now. Thanks to fletchgqc - // http://drupal.org/node/268026. - // Convert all square brackets to angle brackets. This way all further code - // just manipulates angle brackets. (Angle brackets are preferred here for - // the simple reason that square brackets are more tedious to use in - // regexps). - if (is_array($text)) { - implode($text); - } - $text = preg_replace('|\[fn([^\]]*)\]|', '', $text); - $text = preg_replace('|\[/fn\]|', '', $text); - $text = preg_replace('|\[footnotes([^\]]*)\]|', '', $text); - - // Check that there are an even number of open and closing tags. - // If there is one closing tag missing, append this to the end. - // If there is more disparity, throw a warning and continue. - // A closing tag may sometimes be missing when we are processing a teaser - // and it has been cut in the middle of the footnote. - // See http://drupal.org/node/253326 - $foo = []; - $open_tags = preg_match_all("|]*)>|", $text, $foo); - $close_tags = preg_match_all("||", $text, $foo); + public function settingsForm(array $form, FormStateInterface $form_state): array { + $settings['footnotes_collapse'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Collapse reference footnotes with identical content'), + '#default_value' => (bool) ($this->settings['footnotes_collapse'] ?? FALSE), + '#description' => $this->t('If two reference footnotes have the exact same content, they will be collapsed into one as if using the same value="" attribute.'), + ]; - if ($open_tags == $close_tags + 1) { - $text = $text . ''; - } - elseif ($open_tags > $close_tags + 1) { - trigger_error($this->t("You have unclosed fn tags. This is invalid and will - produce unpredictable results.")); - } + $settings['footnotes_ibid'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Display subsequent instances of multiple references with \'Ibid.\''), + '#default_value' => (bool) ($this->settings['footnotes_ibid'] ?? FALSE), + ]; - // Before doing the replacement, the callback function needs to know which - // options to use. - $this->replaceCallback($this->settings, 'prepare'); + $settings['notes_section_label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Notes section heading label'), + '#default_value' => (string) ($this->settings['notes_section_label'] ?? 'Notes'), + ]; - $pattern = '|]*)>(.*?)|s'; - $text = preg_replace_callback($pattern, [ - $this, - 'replaceCallback', - ], $text); - - // Replace tag with the list of footnotes. - // If tag is not present, by default add the footnotes at the end. - // Thanks to acp on drupal.org for this idea. see - // http://drupal.org/node/87226. - $footer = $this->replaceCallback(NULL, 'output footer'); - $pattern = '|()|'; - if (preg_match($pattern, $text) > 0) { - $text = preg_replace($pattern, $footer, $text, 1); - } - elseif (!empty($footer)) { - $text .= "\n\n" . $footer; - } - $result = new FilterProcessResult($text); - $result->setAttachments([ - 'library' => [ - 'footnotes/footnotes', + $settings['reference_dont_show_backlink_text'] = [ + '#type' => 'checkbox', + '#title' => $this->t("Don't show note 'value' text in reference list."), + '#description' => $this->t("Suitable for MLA-style citations, like (Smith, 33-22)"), + '#default_value' => (bool) ($this->settings['reference_dont_show_backlink_text'] ?? FALSE), + ]; + + $settings['works_cited_sort_by'] = [ + '#type' => 'select', + '#title' => $this->t('Sort Works Cited list by'), + '#options' => [ + 'weight' => $this->t('Manually'), + 'alphabetical' => $this->t('Alphabetically'), ], - ]); - return $result; + '#default_value' => (string) ($this->settings['works_cited_sort_by'] ?? 'weight'), + ]; + + return $settings; } /** - * Helper function called from preg_replace_callback() above. + * Helper function called from preg_replace_callback(). * * Uses static vars to temporarily store footnotes found. * This is not threadsafe, but PHP isn't. - * - * @param mixed $matches - * Elements from array: - * - 0: complete matched string. - * - 1: tag name. - * - 2: tag attributes. - * - 3: tag content. - * @param string $op - * Operation. - * - * @return string - * Return the string processed by geshi library. */ - protected function replaceCallback($matches, $op = '') { + protected function replaceCallback(array $matches, string $op = ''): string|int { static $opt_collapse = 0; - static $opt_html = 0; static $n = 0; static $store_matches = []; static $used_values = []; - $str = ''; - if ($op == 'prepare') { - // In the 'prepare' case, the first argument contains the options to use. - // The name 'matches' is incorrect, we just use the variable anyway. - $opt_collapse = $matches['footnotes_collapse']; - $opt_html = $matches['footnotes_html']; + if ($op === 'prepare') { + // In the 'prepare' case, $matches contains the options to use. + $opt_collapse = !empty($matches['footnotes_collapse']); return 0; } - if ($op == 'output footer') { + if ($op === 'output footer') { + $str = ''; + if (count($store_matches) > 0) { - // Only if there are stored fn matches, pass the array of fns to be - // themed as a list Drupal 7 requires we use "render element" which - // just introduces a wrapper around the old array. - // @FIXME - // theme() has been renamed to _theme() and should NEVER be called - // directly. Calling _theme() directly can alter the expected output and - // potentially introduce security issues - // (see https://www.drupal.org/node/2195739). You should use renderable - // arrays instead. @see https://www.drupal.org/node/2195739 + if (!empty($this->settings['footnotes_ibid'])) { + $this->ibidemify($store_matches); + } + $markup = [ - '#theme' => 'footnote_list', - '#footnotes' => $store_matches, + '#theme' => 'bibcite_footnote_list', + '#notes' => $store_matches, + '#config' => $this->settings, ]; - $str = $this->renderer->render($markup); + $str = (string) $this->renderer->render($markup, FALSE); } - // Reset the static variables so they can be used again next time. + + // Reset static variables. $n = 0; $store_matches = []; $used_values = []; @@ -194,185 +148,153 @@ class FootnotesFilter extends FilterBase { return $str; } - // Default op: act as called by preg_replace_callback() - // Random string used to ensure footnote id's are unique, even - // when contents of multiple nodes reside on same page. - // (fixes http://drupal.org/node/194558). + // Default op: act as called by preg_replace_callback(). $randstr = $this->randstr(); - $value = ''; - // Did the pattern match anything in the tag? - if ($matches[1]) { - // See if value attribute can parsed, either well-formed in quotes eg - // . - if (preg_match('|value=["\'](.*?)["\']|', $matches[1], $value_match)) { - $value = $value_match[1]; - // Or without quotes eg . - } - elseif (preg_match('|value=(\S*)|', $matches[1], $value_match)) { - $value = $value_match[1]; - } - } + $value = $this->extractAttribute($matches, 'value'); + $page = $this->extractAttribute($matches, 'page'); + $reference = $this->extractAttribute($matches, 'reference'); - if ($value) { - // A value label was found. If it is numeric, record it in $n so further - // notes can increment from there. - // After adding support for multiple references to same footnote in the - // body (http://drupal.org/node/636808) also must check that $n is - // monotonously increasing. - if (is_numeric($value) && $n < $value) { - $n = $value; + if ($value !== '') { + if (is_numeric($value) && $n < (int) $value) { + $n = (int) $value; } } - elseif ($opt_collapse and $value_existing = $this->findFootnote($matches[2], $store_matches)) { - // An identical footnote already exists. Set value to the previously - // existing value. - $value = $value_existing; + elseif ($opt_collapse && ($value_existing = $this->findFootnote($matches[2], $reference, $store_matches)) !== FALSE) { + $value = (string) $value_existing; } else { - // No value label, either a plain or unparsable attributes. Increment - // the footnote counter, set label equal to it. $n++; - $value = $n; + $value = (string) $n; } - // Remove illegal characters from $value so it can be used as an HTML id - // attribute. - $value_id = preg_replace('|[^\w\-]|', '', $value); + $value_id = preg_replace('|[^\w\-]|', '', $value) ?? ''; - // Create a sanitized version of $text that is suitable for using as HTML - // attribute value. (In particular, as the title attribute to the footnote - // link). + // Sanitize content for title attribute. $allowed_tags = []; - $text_clean = Xss::filter($matches['2'], $allowed_tags); - // HTML attribute cannot contain quotes. - $text_clean = str_replace('"', """, $text_clean); - // Remove newlines. Browsers don't support them anyway and they'll confuse - // line break converter in filter.module. - $text_clean = str_replace("\n", " ", $text_clean); - $text_clean = str_replace("\r", "", $text_clean); - - // Create a footnote item as an array. + $text_clean = Xss::filter($matches[2], $allowed_tags); + $text_clean = str_replace('"', '"', $text_clean); + $text_clean = str_replace("\n", ' ', $text_clean); + $text_clean = str_replace("\r", '', $text_clean); + $fn = [ 'value' => $value, - 'text' => $opt_html ? html_entity_decode($matches[2]) : $matches[2], + 'text' => $matches[2], 'text_clean' => $text_clean, + 'page' => $page, + 'reference' => $reference, 'fn_id' => 'footnote' . $value_id . '_' . $randstr, 'ref_id' => 'footnoteref' . $value_id . '_' . $randstr, ]; - // We now allow to repeat the footnote value label, in which case the link - // to the previously existing footnote is returned. Content of the current - // footnote is ignored. See http://drupal.org/node/636808 . - if (!in_array($value, $used_values)) { - // This is the normal case, add the footnote to $store_matches. - // Store the footnote item. - array_push($store_matches, $fn); - array_push($used_values, $value); + if (!in_array($value, $used_values, TRUE)) { + $store_matches[] = $fn; + $used_values[] = $value; } else { - // A footnote with the same label already exists. - // Use the text and id from the first footnote with this value. - // Any text in this footnote is discarded. - $i = array_search($value, $used_values); + $i = array_search($value, $used_values, TRUE); $fn['text'] = $store_matches[$i]['text']; $fn['text_clean'] = $store_matches[$i]['text_clean']; $fn['fn_id'] = $store_matches[$i]['fn_id']; - // Push the new ref_id into the first occurrence of this footnote label - // The stored footnote thus holds a list of ref_id's rather than just one - // id. + $ref_array = is_array($store_matches[$i]['ref_id']) ? $store_matches[$i]['ref_id'] : [$store_matches[$i]['ref_id']]; - array_push($ref_array, $fn['ref_id']); + $ref_array[] = $fn['ref_id']; $store_matches[$i]['ref_id'] = $ref_array; } - // Return the item themed into a footnote link. - // Drupal 7 requires we use "render element" which just introduces a wrapper - // around the old array. - $fn = [ - '#theme' => 'footnote_link', - '#fn' => $fn, + $render_array = [ + '#theme' => 'bibcite_footnote_link', + 'fn' => $fn, ]; - $result = $this->renderer->render($fn); - return $result; + return (string) $this->renderer->render($render_array, FALSE); } - /** - * Helper function to return a random text string. - * - * @return string - * Random (lowercase) alphanumeric string. - */ - public function randstr() { - $chars = "abcdefghijklmnopqrstuwxyz1234567890"; - $str = ""; - - // Seeding with srand() not necessary in modern PHP versions. - for ($i = 0; $i < 7; $i++) { - $n = rand(0, strlen($chars) - 1); - $str .= substr($chars, $n, 1); + private function findFootnote(string $text, string $reference, array &$store_matches): string|false { + foreach ($store_matches as $fn) { + if (($fn['text'] ?? NULL) === $text && ($fn['reference'] ?? NULL) === $reference) { + return (string) $fn['value']; + } } - return $str; + return FALSE; } - /** - * Create the settings form for the filter. - * - * @param array $form - * A minimally prepopulated form array. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The state of the (entire) configuration form. - * - * @return array - * The $form array with additional form elements for the settings of - * this filter. The submitted form values should match $this->settings. - * - * @todo Add validation of submited form values, it already exists for - * drupal 7, must update it only. - */ - public function settingsForm(array $form, FormStateInterface $form_state) { - $settings['footnotes_collapse'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Collapse footnotes with identical content'), - '#default_value' => $this->settings['footnotes_collapse'], - '#description' => $this->t('If two footnotes have the exact same content, they will be collapsed into one as if using the same value="" attribute.'), - ]; - $settings['footnotes_html'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Handle footnote text as HTML'), - '#default_value' => $this->settings['footnotes_html'], - '#description' => $this->t('If not checked, a HTML tag in the footnote text will be shown as-is to the user.'), - ]; - return $settings; + protected function extractAttribute(array $matches, string $attribute): string { + $value = ''; + if (!empty($matches[1])) { + if (preg_match('|' . preg_quote($attribute, '|') . '=["\'](.*?)["\']|', $matches[1], $value_match)) { + $value = $value_match[1]; + } + elseif (preg_match('|' . preg_quote($attribute, '|') . '=(\S*)|', $matches[1], $value_match)) { + $value = $value_match[1]; + } + } + return $value; } - /** - * Search the $store_matches array for footnote text that matches. - * - * Note: This does a linear search on the $store_matches array. For a large - * list of footnotes it would be more efficient to maintain a separate array - * with the footnote content as key, in order to do a hash lookup at this - * stage. Since you typically only have a handful of footnotes, this simple - * search is assumed to be more efficient, but was not tested. - * - * @param string $text - * The footnote text. - * @param array $store_matches - * The matches array. - * - * @return string|false - * The value of the existing footnote, FALSE otherwise. - */ - private function findFootnote($text, array &$store_matches) { - if (!empty($store_matches)) { - foreach ($store_matches as &$fn) { - if ($fn['text'] == $text) { - return $fn['value']; - } + protected function ibidemify(array &$footnotes): void { + $prev_reference_id = FALSE; + foreach ($footnotes as $index => $fn) { + if ($prev_reference_id && ($fn['reference'] ?? NULL) === $prev_reference_id) { + $footnotes[$index]['ibid'] = TRUE; + continue; } + $prev_reference_id = $fn['reference'] ?? FALSE; } - return FALSE; + } + + public function process($text, $langcode): FilterProcessResult { + if (is_array($text)) { + $text = implode('', $text); + } + + $text = preg_replace('|\[fn([^\]]*)\]|', '', (string) $text); + $text = preg_replace('|\[/fn\]|', '', (string) $text); + $text = preg_replace('|\[footnotes([^\]]*)\]|', '', (string) $text); + + $foo = []; + $open_tags = preg_match_all("|]*)>|", $text, $foo); + $close_tags = preg_match_all("||", $text, $foo); + + if ($open_tags === $close_tags + 1) { + $text .= ''; + } + elseif ($open_tags > $close_tags + 1) { + // Keep behavior, but avoid translating inside trigger_error message. + trigger_error('You have unclosed fn tags. This is invalid and will produce unpredictable results.'); + } + + // Let the callback know which options to use. + $this->replaceCallback($this->settings, 'prepare'); + + $pattern = '|]*)>(.*?)|s'; + $text = preg_replace_callback($pattern, [$this, 'replaceCallback'], $text); + + $footer = $this->replaceCallback([], 'output footer'); + $pattern = '|()|'; + + if (preg_match($pattern, $text) > 0) { + $text = preg_replace($pattern, (string) $footer, $text, 1); + } + elseif (!empty($footer)) { + $text .= "\n\n" . $footer; + } + + $result = new FilterProcessResult((string) $text); + $result->setAttachments([ + 'library' => ['footnotes/footnotes'], + ]); + return $result; + } + + public function randstr(): string { + $chars = 'abcdefghijklmnopqrstuwxyz1234567890'; + $str = ''; + for ($i = 0; $i < 7; $i++) { + $n = rand(0, strlen($chars) - 1); + $str .= substr($chars, $n, 1); + } + return $str; } } diff --git a/templates/footnote-link.html.twig b/templates/footnote-link.html.twig deleted file mode 100644 index aa32cae..0000000 --- a/templates/footnote-link.html.twig +++ /dev/null @@ -1,2 +0,0 @@ -{# footnotes/footnote-link.html.twig #} -{{ fn.value }} diff --git a/templates/footnote-list.html.twig b/templates/footnote-list.html.twig deleted file mode 100644 index 1802716..0000000 --- a/templates/footnote-list.html.twig +++ /dev/null @@ -1,23 +0,0 @@ -{# footnotes/footnote-list.html.twig #} -
    - {% for fn in footnotes %} - {% if fn.ref_id is iterable %} - {# - // Output footnote that has more than one reference to it in the body. - // The only difference is to insert backlinks to all references. - // Helper: we need to enumerate a, b, c... - #} - {% set abc = "abcdefghijklmnopqrstuvwxyz"|split('') %} - {% set i = 0 %} -
  • {{ fn.value }} - {% for ref in fn.ref_id %} - {{ attribute(abc, i) }} - {% set i = i + 1 %} - {% endfor %} - {{ fn.text|raw }}
  • - {% else %} - {# Output normal footnote. #} -
  • {{ fn.value }}{{ fn.text|raw }}
  • - {% endif %} - {% endfor %} -
diff --git a/tests/src/Functional/FootnotesFilterPluginTest.php b/tests/src/Functional/FootnotesFilterPluginTest.php deleted file mode 100755 index 65d6f43..0000000 --- a/tests/src/Functional/FootnotesFilterPluginTest.php +++ /dev/null @@ -1,168 +0,0 @@ -adminUser = $this->drupalCreateUser($permissions); - $this->formatName = strtolower($this->randomMachineName()); - - $this->drupalLogin($this->adminUser); - $this->createTextFormat(); - $this->drupalCreateContentType(['type' => 'page']); - } - - /** - * Tests CKEditor Filter plugin functionality. - */ - public function testDefaultFunctionality() { - // Verify a title with HTML entities is properly escaped. - $text1 = 'This is the note one.'; - $note1 = '[fn]' . $text1 . '[/fn]'; - $text2 = 'And this is the note two.'; - $note2 = "$text2"; - - $body = '

' . $this->randomMachineName(100) . $note1 . '

' . - $this->randomMachineName(100) . $note2 . '

'; - - // Create a node. - $node = $this->drupalCreateNode([ - 'title' => $this->randomString(), - 'body' => [ - 0 => [ - 'value' => $body, - 'format' => $this->formatName, - ], - ], - ]); - - $this->drupalGet('node/' . $node->id()); - - // Footnote with [fn]. - $this->assertNoRaw($note1); - $this->assertText($text1); - - // Footnote with . - $this->assertNoRaw($note2); - $this->assertText($text2); - - // Css file: - $this->assertRaw('/assets/css/footnotes.css'); - // @todo currently additional settings doesn't work as expected. - // So we don't check additional settings for now. - // $this->createTextFormat(TRUE); - $text1 = 'This is the note one.'; - $note1 = "[fn value='1']{$text1}[/fn]"; - $text2 = 'And this is the note two.'; - $note2 = "{$text2}"; - - $body = '

' . $this->randomMachineName(100) . $note1 . '

' . - $this->randomMachineName(100) . $note2 . '

'; - - // Create a node. - $node = $this->drupalCreateNode([ - 'title' => $this->randomString(), - 'body' => [ - 0 => [ - 'value' => $body, - 'format' => $this->formatName, - ], - ], - ]); - - $this->drupalGet('node/' . $node->id()); - - // Footnote with [fn]. - $this->assertNoRaw($note1); - $this->assertText($text1); - - // Elements with the same value should be collapsed. - // @todo This should work only if footnotes_collapse setting is enabled. - $this->assertNoRaw($note2); - $this->assertNoText($text2); - } - - /** - * Create a new text format. - * - * @param bool $additional_settings - * Indicates if filter settings should be enabled. - */ - protected function createTextFormat($additional_settings = FALSE) { - $button_groups = json_encode([ - [ - [ - 'name' => 'Tools', - 'items' => ['Source', 'footnotes'], - ], - ], - ]); - - $edit = [ - 'format' => $this->formatName, - 'name' => $this->formatName, - 'roles[' . AccountInterface::AUTHENTICATED_ROLE . ']' => TRUE, - 'editor[editor]' => 'ckeditor', - 'filters[filter_footnotes][status]' => TRUE, - ]; - $this->drupalGet("admin/config/content/formats/add"); - // Keep the "CKEditor" editor selected and click the "Configure" button. - $this->drupalPostForm(NULL, $edit, 'editor_configure'); - $edit['editor[settings][toolbar][button_groups]'] = $button_groups; - $edit['filters[filter_footnotes][settings][footnotes_collapse]'] = $button_groups; - if ($additional_settings) { - $edit['filters[filter_footnotes][settings][footnotes_collapse]'] = 1; - $edit['filters[filter_footnotes][settings][footnotes_html]'] = 1; - } - $this->drupalPostForm(NULL, $edit, $this->t('Save configuration')); - $this->assertText($this->t('Added text format @format.', ['@format' => $this->formatName])); - } - -} diff --git a/tests/src/FunctionalJavascript/FootnotesCkeditorPluginTest.php b/tests/src/FunctionalJavascript/FootnotesCkeditorPluginTest.php deleted file mode 100755 index 2eaede1..0000000 --- a/tests/src/FunctionalJavascript/FootnotesCkeditorPluginTest.php +++ /dev/null @@ -1,176 +0,0 @@ -filterFormat = FilterFormat::create([ - 'format' => 'filtered_html', - 'name' => 'Filtered HTML', - 'filters' => [ - 'filter_footnotes' => [ - 'status' => TRUE, - 'settings' => [ - 'footnotes_collapse' => 0, - 'footnotes_html' => 0 - ], - ], - ], - ]); - $this->filterFormat->save(); - - Editor::create([ - 'format' => 'filtered_html', - 'editor' => 'ckeditor', - 'settings' => [ - 'toolbar' => [ - 'rows' => [ - [ - [ - 'name' => 'All the things', - 'items' => [ - 'Source', - 'Bold', - 'Italic', - 'footnotes', - ], - ], - ], - ], - ], - ], - ])->save(); - - // Create a node type for testing. - NodeType::create(['type' => 'page', 'name' => 'page'])->save(); - - $field_storage = FieldStorageConfig::loadByName('node', 'body'); - - // Create a body field instance for the 'page' node type. - FieldConfig::create([ - 'field_storage' => $field_storage, - 'bundle' => 'page', - 'label' => 'Body', - 'settings' => ['display_summary' => TRUE], - 'required' => TRUE, - ])->save(); - - // Assign widget settings for the 'default' form mode. - EntityFormDisplay::create([ - 'targetEntityType' => 'node', - 'bundle' => 'page', - 'mode' => 'default', - 'status' => TRUE, - ])->setComponent('body', ['type' => 'text_textarea_with_summary']) - ->save(); - - $this->account = $this->drupalCreateUser([ - 'administer nodes', - 'create page content', - 'use text format filtered_html', - ]); - $this->drupalLogin($this->account); - } - - /** - * Tests CKEditor plugin functionality for body field. - */ - public function testUi() - { - $session = $this->getSession(); - $assert_session = $this->assertSession(); - - $this->drupalGet("node/add/page"); - $page = $session->getPage(); - - $this->waitForEditor(); - $this->pressEditorButton('footnotes'); - $this->assertNotEmpty( - $assert_session->waitForElementVisible('css', '.cke_1.cke_editor_edit-body-0-value_dialog') - ); - $assert_session->elementTextContains('css', 'table.cke_dialog .cke_dialog_title', $this->t('Footnotes Dialog')); - $assert_session->elementTextContains('css', '.cke_dialog_page_contents table tr:first-child', $this->t('Footnote text :')); - $assert_session->elementTextContains('css', '.cke_dialog_page_contents table tr:last-child', $this->t('Value :')); - $page->find('css', 'a.cke_dialog_ui_button_cancel')->click(); - - $this->assertEmpty($assert_session->elementExists('css', '.cke_1.cke_editor_edit-body-0-value_dialog')->isVisible()); - - $texts = ['Text one.', 'Text two.', 'Text tree', 'Text four', 'Text five']; - foreach ($texts as $key => $value) { - $this->pressEditorButton('footnotes'); - $this->assertNotEmpty( - $assert_session->waitForElementVisible('css', '.cke_1.cke_editor_edit-body-0-value_dialog') - ); - $assert_session->elementExists('css', '.cke_dialog_page_contents table tr:last-child input')->setValue($key); - $assert_session->elementExists('css', '.cke_dialog_page_contents table tr:first-child input')->setValue($value); - $page->find('css', 'a.cke_dialog_ui_button_ok')->click(); - - $this->assertEmpty($assert_session->elementExists('css', '.cke_1.cke_editor_edit-body-0-value_dialog')->isVisible()); - } - $this->pressEditorButton('source'); - $body_value = $assert_session->elementExists('css', '.cke .cke_contents .cke_source')->getValue(); - - $body_value = str_replace(["\r\n", "\r", "\n"], "", $body_value); - $body_value = trim($body_value); - - $expected_value = '

'; - foreach ($texts as $key => $value) { - $expected_value .= '' . $value . ''; - } - $expected_value .= '

'; - - $this->assertEqual($body_value, $expected_value, $this->t('String, formed by CKEditor, is correct.')); - } - -}