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 () => {