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 { 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();
|
||||
|
||||
/**
|
||||
|
|
|
@ -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",
|
||||
|
|
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";
|
||||
|
||||
{
|
||||
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
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ abstract-box {
|
|||
abstract-box .body {
|
||||
display: flex;
|
||||
|
||||
editable-text {
|
||||
editable-text, browser {
|
||||
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* () {
|
||||
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 () => {
|
||||
|
|
Loading…
Reference in a new issue