diff --git a/chrome/content/zotero/HiddenBrowser.jsm b/chrome/content/zotero/HiddenBrowser.jsm
index 44da7877a3..3cafdf1156 100644
--- a/chrome/content/zotero/HiddenBrowser.jsm
+++ b/chrome/content/zotero/HiddenBrowser.jsm
@@ -29,6 +29,8 @@ var EXPORTED_SYMBOLS = ["HiddenBrowser"];
const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
const { BlockingObserver } = ChromeUtils.import("chrome://zotero/content/BlockingObserver.jsm");
+ChromeUtils.import("chrome://zotero/content/actors/ActorManager.jsm");
+
/* global HiddenFrame, E10SUtils, this */
XPCOMUtils.defineLazyModuleGetters(this, {
E10SUtils: "resource://gre/modules/E10SUtils.jsm",
@@ -40,18 +42,6 @@ ChromeUtils.defineESModuleGetters(this, {
Zotero: "chrome://zotero/content/zotero.mjs"
});
-ChromeUtils.registerWindowActor("PageData", {
- child: {
- moduleURI: "chrome://zotero/content/actors/PageDataChild.jsm"
- }
-});
-
-ChromeUtils.registerWindowActor("SingleFile", {
- child: {
- moduleURI: "chrome://zotero/content/actors/SingleFileChild.jsm"
- }
-});
-
const progressListeners = new Set();
/**
diff --git a/chrome/content/zotero/RemoteTranslate.jsm b/chrome/content/zotero/RemoteTranslate.jsm
index 35a42dc606..22bc4a1da8 100644
--- a/chrome/content/zotero/RemoteTranslate.jsm
+++ b/chrome/content/zotero/RemoteTranslate.jsm
@@ -27,14 +27,7 @@ var EXPORTED_SYMBOLS = ["RemoteTranslate"];
const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-ChromeUtils.registerWindowActor("Translation", {
- parent: {
- moduleURI: "chrome://zotero/content/actors/TranslationParent.jsm"
- },
- child: {
- moduleURI: "chrome://zotero/content/actors/TranslationChild.jsm"
- }
-});
+ChromeUtils.import("chrome://zotero/content/actors/ActorManager.jsm");
ChromeUtils.defineESModuleGetters(this, {
Zotero: "chrome://zotero/content/zotero.mjs",
diff --git a/chrome/content/zotero/actors/ActorManager.jsm b/chrome/content/zotero/actors/ActorManager.jsm
new file mode 100644
index 0000000000..b2e4dc9ee8
--- /dev/null
+++ b/chrome/content/zotero/actors/ActorManager.jsm
@@ -0,0 +1,36 @@
+var EXPORTED_SYMBOLS = [];
+
+ChromeUtils.registerWindowActor("PageData", {
+ child: {
+ moduleURI: "chrome://zotero/content/actors/PageDataChild.jsm"
+ }
+});
+
+ChromeUtils.registerWindowActor("SingleFile", {
+ child: {
+ moduleURI: "chrome://zotero/content/actors/SingleFileChild.jsm"
+ }
+});
+
+ChromeUtils.registerWindowActor("Translation", {
+ parent: {
+ moduleURI: "chrome://zotero/content/actors/TranslationParent.jsm"
+ },
+ child: {
+ moduleURI: "chrome://zotero/content/actors/TranslationChild.jsm"
+ }
+});
+
+ChromeUtils.registerWindowActor("FeedAbstract", {
+ parent: {
+ moduleURI: "chrome://zotero/content/actors/FeedAbstractParent.jsm",
+ },
+ child: {
+ moduleURI: "chrome://zotero/content/actors/FeedAbstractChild.jsm",
+ events: {
+ DOMDocElementInserted: {},
+ click: {},
+ }
+ },
+ messageManagerGroups: ["feedAbstract"]
+});
diff --git a/chrome/content/zotero/actors/FeedAbstractChild.jsm b/chrome/content/zotero/actors/FeedAbstractChild.jsm
new file mode 100644
index 0000000000..3195e07db7
--- /dev/null
+++ b/chrome/content/zotero/actors/FeedAbstractChild.jsm
@@ -0,0 +1,65 @@
+var EXPORTED_SYMBOLS = ["FeedAbstractChild"];
+
+
+class FeedAbstractChild extends JSWindowActorChild {
+ _stylesheet;
+
+ _stylesheetPromise;
+
+ actorCreated() {
+ this._stylesheetPromise = this.sendQuery('getStylesheet');
+ }
+
+ async receiveMessage({ name, data }) {
+ switch (name) {
+ case "setContent": {
+ this.document.documentElement.innerHTML = data;
+ break;
+ }
+ }
+ }
+
+ async handleEvent(event) {
+ switch (event.type) {
+ case "DOMDocElementInserted": {
+ await this._injectStylesheet();
+ new this.contentWindow.ResizeObserver(() => this._sendResize())
+ .observe(this._getResizeRoot());
+ await this._sendResize();
+ break;
+ }
+
+ case "click": {
+ // Prevent default click behavior (link opening, form submission,
+ // and so on) in all cases; open links externally
+ event.preventDefault();
+ if (event.button === 0 && event.target.localName === 'a' && event.target.href) {
+ await this._sendLaunchURL(event.target.href);
+ }
+ break;
+ }
+ }
+ }
+
+ async _sendResize() {
+ let root = this._getResizeRoot();
+ await this.sendAsyncMessage("resize", { offsetWidth: root.offsetWidth, offsetHeight: root.offsetHeight });
+ }
+
+ async _sendLaunchURL(url) {
+ await this.sendAsyncMessage("launchURL", url);
+ }
+
+ _getResizeRoot() {
+ return this.document.documentElement;
+ }
+
+ async _injectStylesheet() {
+ if (!this._stylesheet) {
+ this._stylesheet = new this.contentWindow.CSSStyleSheet();
+ this._stylesheet.replaceSync(await this._stylesheetPromise);
+ }
+
+ this.document.wrappedJSObject.adoptedStyleSheets.push(this._stylesheet);
+ }
+}
diff --git a/chrome/content/zotero/actors/FeedAbstractParent.jsm b/chrome/content/zotero/actors/FeedAbstractParent.jsm
new file mode 100644
index 0000000000..bbcbc91766
--- /dev/null
+++ b/chrome/content/zotero/actors/FeedAbstractParent.jsm
@@ -0,0 +1,31 @@
+var EXPORTED_SYMBOLS = ["FeedAbstractParent"];
+
+ChromeUtils.defineESModuleGetters(this, {
+ Zotero: "chrome://zotero/content/zotero.mjs"
+});
+
+class FeedAbstractParent extends JSWindowActorParent {
+ async receiveMessage({ name, data }) {
+ switch (name) {
+ case "getStylesheet": {
+ return Zotero.File.getResource('chrome://zotero/skin/feedAbstract.css');
+ }
+
+ case "resize": {
+ this._resizeBrowser(data.offsetHeight);
+ return;
+ }
+
+ case "launchURL": {
+ Zotero.launchURL(data);
+ return;
+ }
+ }
+ }
+
+ _resizeBrowser(height) {
+ let browser = this.browsingContext?.embedderElement;
+ if (!browser) return;
+ browser.style.height = height + 'px';
+ }
+}
diff --git a/chrome/content/zotero/elements/abstractBox.js b/chrome/content/zotero/elements/abstractBox.js
index 2670e6b31a..dbc8ad203b 100644
--- a/chrome/content/zotero/elements/abstractBox.js
+++ b/chrome/content/zotero/elements/abstractBox.js
@@ -26,11 +26,16 @@
"use strict";
{
+ ChromeUtils.import("chrome://zotero/content/actors/ActorManager.jsm");
+
+ const SANDBOX_ALL_FLAGS = 0xFFFFF;
+
class AbstractBox extends ItemPaneSectionElementBase {
content = MozXULElement.parseXULToFragment(`
+
`);
@@ -70,6 +75,9 @@
this._abstractField = this.querySelector('editable-text');
this._abstractField.addEventListener('change', () => this.save());
this._abstractField.ariaLabel = Zotero.getString('itemFields.abstractNote');
+
+ this._feedAbstractBrowser = this.querySelector('browser');
+ this._feedAbstractBrowser.browsingContext.sandboxFlags |= SANDBOX_ALL_FLAGS;
this.render();
}
@@ -103,7 +111,34 @@
if (!this.item) return;
if (this._isAlreadyRendered()) return;
+ if (!this.item.isFeedItem) {
+ this._renderRegularItem();
+ }
+ }
+
+ async asyncRender() {
+ if (!this._item) return;
+ if (this._isAlreadyRendered("async")) return;
+
+ if (this.item.isFeedItem) {
+ await this._renderFeedItem();
+ }
+ }
+
+ async _renderFeedItem() {
let abstract = this.item.getField('abstractNote');
+ this._abstractField.hidden = true;
+ this._feedAbstractBrowser.hidden = false;
+ this._section.summary = Zotero.Utilities.cleanTags(abstract);
+
+ let actor = this._feedAbstractBrowser.browsingContext.currentWindowGlobal.getActor('FeedAbstract');
+ await actor.sendQuery('setContent', abstract);
+ }
+
+ _renderRegularItem() {
+ let abstract = this.item.getField('abstractNote');
+ this._abstractField.hidden = false;
+ this._feedAbstractBrowser.hidden = true;
this._section.summary = abstract;
// If focused, update the value that will be restored on Escape;
// otherwise, update the displayed value
diff --git a/chrome/content/zotero/xpcom/feedReader.js b/chrome/content/zotero/xpcom/feedReader.js
index c0bf0c0a67..8cf45063f8 100644
--- a/chrome/content/zotero/xpcom/feedReader.js
+++ b/chrome/content/zotero/xpcom/feedReader.js
@@ -417,7 +417,11 @@ Zotero.FeedReader._getFeedItem = function (feedEntry, feedInfo) {
if (feedEntry.title) item.title = Zotero.FeedReader._getRichText(feedEntry.title, 'title');
if (feedEntry.summary) {
- item.abstractNote = Zotero.FeedReader._getRichText(feedEntry.summary, 'abstractNote');
+ let summaryFragment = feedEntry.summary.createDocumentFragment();
+ if (summaryFragment.querySelectorAll('body').length === 1) {
+ summaryFragment.replaceChildren(...summaryFragment.querySelector('body').childNodes);
+ }
+ item.abstractNote = new XMLSerializer().serializeToString(summaryFragment);
if (!item.title) {
// We will probably have to trim this, so let's use plain text to
@@ -529,8 +533,7 @@ Zotero.FeedReader._getFeedItem = function (feedEntry, feedInfo) {
* Convert HTML-formatted text to Zotero-compatible formatting
*/
Zotero.FeedReader._getRichText = function (feedText, field) {
- let domDiv = Zotero.Utilities.Internal.getDOMDocument().createElement("div");
- let domFragment = feedText.createDocumentFragment(domDiv);
+ let domFragment = feedText.createDocumentFragment();
return Zotero.Utilities.trimInternal(domFragment.textContent);
};
diff --git a/resource/feeds/FeedProcessor.js b/resource/feeds/FeedProcessor.js
index 96a6427d8b..a612a3f538 100644
--- a/resource/feeds/FeedProcessor.js
+++ b/resource/feeds/FeedProcessor.js
@@ -575,12 +575,11 @@ TextConstruct.prototype = {
return this.text;
},
- createDocumentFragment: function (element) {
+ createDocumentFragment: function () {
if (this.type == "text") {
- const doc = element.ownerDocument;
- const docFragment = doc.createDocumentFragment();
- const node = doc.createTextNode(this.text);
- docFragment.appendChild(node);
+ const docFragment = new DOMParser().parseFromString('', 'text/html')
+ .createDocumentFragment();
+ docFragment.append(this.text);
return docFragment;
}
@@ -596,7 +595,9 @@ TextConstruct.prototype = {
}
const parsedDoc = new DOMParser().parseFromString(this.text, parserType);
- return parsedDoc.documentElement;
+ const docFragment = parsedDoc.createDocumentFragment();
+ docFragment.append(parsedDoc.documentElement);
+ return docFragment;
},
};
diff --git a/scss/elements/_abstractBox.scss b/scss/elements/_abstractBox.scss
index b6bb2ed2ee..990fb82c21 100644
--- a/scss/elements/_abstractBox.scss
+++ b/scss/elements/_abstractBox.scss
@@ -10,7 +10,7 @@ abstract-box {
abstract-box .body {
display: flex;
- editable-text {
+ editable-text, browser {
flex: 1;
}
}
diff --git a/scss/feedAbstract.scss b/scss/feedAbstract.scss
new file mode 100644
index 0000000000..fd6d3a0765
--- /dev/null
+++ b/scss/feedAbstract.scss
@@ -0,0 +1,21 @@
+@import "abstracts/variables";
+@import "abstracts/functions";
+@import "abstracts/mixins";
+@import "abstracts/placeholders";
+@import "abstracts/utilities";
+@import "abstracts/split-button";
+@import "abstracts/svgicon";
+
+@import "themes/light";
+@import "themes/dark";
+
+@import "base/base";
+
+html, body {
+ background: var(--material-sidepane);
+ overflow: clip;
+}
+
+img, svg {
+ max-width: 100%;
+}
diff --git a/test/tests/feedReaderTest.js b/test/tests/feedReaderTest.js
index 402c0675a7..84cddf6d26 100644
--- a/test/tests/feedReaderTest.js
+++ b/test/tests/feedReaderTest.js
@@ -123,7 +123,7 @@ describe("Zotero.FeedReader", function () {
it('should parse items correctly for a sparse RSS feed', function* () {
let expected = { guid: 'http://liftoff.msfc.nasa.gov/2003/06/03.html#item573',
title: 'Star City',
- abstractNote: 'How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia\'s Star City.',
+ abstractNote: 'How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia\'s Star City.',
url: 'http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp',
creators: [{ firstName: '', lastName: 'editor@example.com', creatorType: 'author', fieldMode: 1 }],
date: 'Tue, 03 Jun 2003 09:39:21 GMT',
@@ -203,17 +203,7 @@ describe("Zotero.FeedReader", function () {
assert.isNull(item);
});
- it('should decode entities', async () => {
- const fr = new Zotero.FeedReader(richTextRSSFeedURL);
- await fr.process();
- const itemIterator = new fr.ItemIterator();
- const item = await itemIterator.next().value;
-
- assert.equal(item.title, `Encoded "entity"`);
- assert.equal(item.abstractNote, "They take a crash course in language & protocol.");
- });
-
- it('should remove tags', async () => {
+ it('should preserve tags in text fields', async () => {
const fr = new Zotero.FeedReader(richTextRSSFeedURL);
await fr.process();
const itemIterator = new fr.ItemIterator();
@@ -225,8 +215,20 @@ describe("Zotero.FeedReader", function () {
// The entry title is text only, so tags are just more text.
assert.equal(item.title, "Embedded tags");
+ });
+
+ it('should parse HTML fields', async () => {
+ const fr = new Zotero.FeedReader(richTextRSSFeedURL);
+ await fr.process();
+ const itemIterator = new fr.ItemIterator();
+ let item;
+ for (let i = 0; i < 2; i++) {
+ // eslint-disable-next-line no-await-in-loop
+ item = await itemIterator.next().value;
+ }
+
// The entry description is XHTML, so tags are removed there.
- assert.equal(item.abstractNote, "The proposed VASIMR engine would do that.");
+ assert.equal(item.abstractNote, 'The proposed VASIMR engine would do that.');
});
it('should parse CDATA as text', async () => {