Render RSS description as HTML (#3956)
This commit is contained in:
parent
2835d6fe83
commit
f7dc68c7f4
11 changed files with 220 additions and 43 deletions
|
@ -29,6 +29,8 @@ var EXPORTED_SYMBOLS = ["HiddenBrowser"];
|
||||||
const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
const { BlockingObserver } = ChromeUtils.import("chrome://zotero/content/BlockingObserver.jsm");
|
const { BlockingObserver } = ChromeUtils.import("chrome://zotero/content/BlockingObserver.jsm");
|
||||||
|
|
||||||
|
ChromeUtils.import("chrome://zotero/content/actors/ActorManager.jsm");
|
||||||
|
|
||||||
/* global HiddenFrame, E10SUtils, this */
|
/* global HiddenFrame, E10SUtils, this */
|
||||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||||
E10SUtils: "resource://gre/modules/E10SUtils.jsm",
|
E10SUtils: "resource://gre/modules/E10SUtils.jsm",
|
||||||
|
@ -40,18 +42,6 @@ ChromeUtils.defineESModuleGetters(this, {
|
||||||
Zotero: "chrome://zotero/content/zotero.mjs"
|
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();
|
const progressListeners = new Set();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -27,14 +27,7 @@ var EXPORTED_SYMBOLS = ["RemoteTranslate"];
|
||||||
|
|
||||||
const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
||||||
ChromeUtils.registerWindowActor("Translation", {
|
ChromeUtils.import("chrome://zotero/content/actors/ActorManager.jsm");
|
||||||
parent: {
|
|
||||||
moduleURI: "chrome://zotero/content/actors/TranslationParent.jsm"
|
|
||||||
},
|
|
||||||
child: {
|
|
||||||
moduleURI: "chrome://zotero/content/actors/TranslationChild.jsm"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ChromeUtils.defineESModuleGetters(this, {
|
ChromeUtils.defineESModuleGetters(this, {
|
||||||
Zotero: "chrome://zotero/content/zotero.mjs",
|
Zotero: "chrome://zotero/content/zotero.mjs",
|
||||||
|
|
36
chrome/content/zotero/actors/ActorManager.jsm
Normal file
36
chrome/content/zotero/actors/ActorManager.jsm
Normal 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"]
|
||||||
|
});
|
65
chrome/content/zotero/actors/FeedAbstractChild.jsm
Normal file
65
chrome/content/zotero/actors/FeedAbstractChild.jsm
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
31
chrome/content/zotero/actors/FeedAbstractParent.jsm
Normal file
31
chrome/content/zotero/actors/FeedAbstractParent.jsm
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,11 +26,16 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
{
|
{
|
||||||
|
ChromeUtils.import("chrome://zotero/content/actors/ActorManager.jsm");
|
||||||
|
|
||||||
|
const SANDBOX_ALL_FLAGS = 0xFFFFF;
|
||||||
|
|
||||||
class AbstractBox extends ItemPaneSectionElementBase {
|
class AbstractBox extends ItemPaneSectionElementBase {
|
||||||
content = MozXULElement.parseXULToFragment(`
|
content = MozXULElement.parseXULToFragment(`
|
||||||
<collapsible-section data-l10n-id="section-abstract" data-pane="abstract">
|
<collapsible-section data-l10n-id="section-abstract" data-pane="abstract">
|
||||||
<html:div class="body">
|
<html:div class="body">
|
||||||
<editable-text multiline="true" data-l10n-id="abstract-field" data-l10n-attrs="placeholder" />
|
<editable-text multiline="true" data-l10n-id="abstract-field" data-l10n-attrs="placeholder" />
|
||||||
|
<browser type="content" remote="true" messagemanagergroup="feedAbstract" hidden="true" />
|
||||||
</html:div>
|
</html:div>
|
||||||
</collapsible-section>
|
</collapsible-section>
|
||||||
`);
|
`);
|
||||||
|
@ -70,6 +75,9 @@
|
||||||
this._abstractField = this.querySelector('editable-text');
|
this._abstractField = this.querySelector('editable-text');
|
||||||
this._abstractField.addEventListener('change', () => this.save());
|
this._abstractField.addEventListener('change', () => this.save());
|
||||||
this._abstractField.ariaLabel = Zotero.getString('itemFields.abstractNote');
|
this._abstractField.ariaLabel = Zotero.getString('itemFields.abstractNote');
|
||||||
|
|
||||||
|
this._feedAbstractBrowser = this.querySelector('browser');
|
||||||
|
this._feedAbstractBrowser.browsingContext.sandboxFlags |= SANDBOX_ALL_FLAGS;
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
@ -103,7 +111,34 @@
|
||||||
if (!this.item) return;
|
if (!this.item) return;
|
||||||
if (this._isAlreadyRendered()) 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');
|
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;
|
this._section.summary = abstract;
|
||||||
// If focused, update the value that will be restored on Escape;
|
// If focused, update the value that will be restored on Escape;
|
||||||
// otherwise, update the displayed value
|
// otherwise, update the displayed value
|
||||||
|
|
|
@ -417,7 +417,11 @@ Zotero.FeedReader._getFeedItem = function (feedEntry, feedInfo) {
|
||||||
if (feedEntry.title) item.title = Zotero.FeedReader._getRichText(feedEntry.title, 'title');
|
if (feedEntry.title) item.title = Zotero.FeedReader._getRichText(feedEntry.title, 'title');
|
||||||
|
|
||||||
if (feedEntry.summary) {
|
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) {
|
if (!item.title) {
|
||||||
// We will probably have to trim this, so let's use plain text to
|
// 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
|
* Convert HTML-formatted text to Zotero-compatible formatting
|
||||||
*/
|
*/
|
||||||
Zotero.FeedReader._getRichText = function (feedText, field) {
|
Zotero.FeedReader._getRichText = function (feedText, field) {
|
||||||
let domDiv = Zotero.Utilities.Internal.getDOMDocument().createElement("div");
|
let domFragment = feedText.createDocumentFragment();
|
||||||
let domFragment = feedText.createDocumentFragment(domDiv);
|
|
||||||
return Zotero.Utilities.trimInternal(domFragment.textContent);
|
return Zotero.Utilities.trimInternal(domFragment.textContent);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -575,12 +575,11 @@ TextConstruct.prototype = {
|
||||||
return this.text;
|
return this.text;
|
||||||
},
|
},
|
||||||
|
|
||||||
createDocumentFragment: function (element) {
|
createDocumentFragment: function () {
|
||||||
if (this.type == "text") {
|
if (this.type == "text") {
|
||||||
const doc = element.ownerDocument;
|
const docFragment = new DOMParser().parseFromString('<!doctype html>', 'text/html')
|
||||||
const docFragment = doc.createDocumentFragment();
|
.createDocumentFragment();
|
||||||
const node = doc.createTextNode(this.text);
|
docFragment.append(this.text);
|
||||||
docFragment.appendChild(node);
|
|
||||||
return docFragment;
|
return docFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -596,7 +595,9 @@ TextConstruct.prototype = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedDoc = new DOMParser().parseFromString(this.text, parserType);
|
const parsedDoc = new DOMParser().parseFromString(this.text, parserType);
|
||||||
return parsedDoc.documentElement;
|
const docFragment = parsedDoc.createDocumentFragment();
|
||||||
|
docFragment.append(parsedDoc.documentElement);
|
||||||
|
return docFragment;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ abstract-box {
|
||||||
abstract-box .body {
|
abstract-box .body {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
editable-text {
|
editable-text, browser {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
21
scss/feedAbstract.scss
Normal file
21
scss/feedAbstract.scss
Normal 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%;
|
||||||
|
}
|
|
@ -123,7 +123,7 @@ describe("Zotero.FeedReader", function () {
|
||||||
it('should parse items correctly for a sparse RSS feed', 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',
|
let expected = { guid: 'http://liftoff.msfc.nasa.gov/2003/06/03.html#item573',
|
||||||
title: 'Star City',
|
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',
|
url: 'http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp',
|
||||||
creators: [{ firstName: '', lastName: 'editor@example.com', creatorType: 'author', fieldMode: 1 }],
|
creators: [{ firstName: '', lastName: 'editor@example.com', creatorType: 'author', fieldMode: 1 }],
|
||||||
date: 'Tue, 03 Jun 2003 09:39:21 GMT',
|
date: 'Tue, 03 Jun 2003 09:39:21 GMT',
|
||||||
|
@ -203,17 +203,7 @@ describe("Zotero.FeedReader", function () {
|
||||||
assert.isNull(item);
|
assert.isNull(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should decode entities', async () => {
|
it('should preserve tags in text fields', 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 () => {
|
|
||||||
const fr = new Zotero.FeedReader(richTextRSSFeedURL);
|
const fr = new Zotero.FeedReader(richTextRSSFeedURL);
|
||||||
await fr.process();
|
await fr.process();
|
||||||
const itemIterator = new fr.ItemIterator();
|
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.
|
// The entry title is text only, so tags are just more text.
|
||||||
assert.equal(item.title, "Embedded <b>tags</b>");
|
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.
|
// 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 () => {
|
it('should parse CDATA as text', async () => {
|
||||||
|
|
Loading…
Add table
Reference in a new issue