Render RSS description as HTML (#3956)

This commit is contained in:
Abe Jellinek 2024-04-18 06:39:17 -04:00 committed by GitHub
parent 2835d6fe83
commit f7dc68c7f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 220 additions and 43 deletions

View file

@ -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();
/**

View file

@ -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",

View file

@ -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"]
});

View file

@ -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);
}
}

View file

@ -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';
}
}

View file

@ -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(`
<collapsible-section data-l10n-id="section-abstract" data-pane="abstract">
<html:div class="body">
<editable-text multiline="true" data-l10n-id="abstract-field" data-l10n-attrs="placeholder" />
<browser type="content" remote="true" messagemanagergroup="feedAbstract" hidden="true" />
</html:div>
</collapsible-section>
`);
@ -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

View file

@ -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);
};

View file

@ -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('<!doctype html>', '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;
},
};

View file

@ -10,7 +10,7 @@ abstract-box {
abstract-box .body {
display: flex;
editable-text {
editable-text, browser {
flex: 1;
}
}

21
scss/feedAbstract.scss Normal file
View file

@ -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%;
}

View file

@ -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 <a xmlns="http://www.w3.org/1999/xhtml" href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>.',
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 <b>tags</b>");
});
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 <b xmlns="http://www.w3.org/1999/xhtml">VASIMR</b> engine would do that.');
});
it('should parse CDATA as text', async () => {