Skip to content

Commit

Permalink
Merge pull request #2428 from TrimmingFool/rss-desc-bbcode-support
Browse files Browse the repository at this point in the history
Add bbcode support in rss description
  • Loading branch information
stickz committed Jan 29, 2023
2 parents fa0b160 + c8521d9 commit 518e98f
Show file tree
Hide file tree
Showing 4 changed files with 470 additions and 9 deletions.
4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
@import "./css/style.css";
</style>
<script type="text/javascript" src="./js/jquery.js"></script>
<script type="text/javascript" src="./js/sanitize.js"></script>
<script type="text/javascript" src="./js/sanitize.config.js"></script>
<script type="text/javascript" src="./lang/langs.js"></script>
<script type="text/javascript" src="./js/common.js"></script>
<script type="text/javascript" src="./js/objects.js"></script>
Expand All @@ -36,8 +38,6 @@
<script type="text/javascript" src="./js/plugins.js"></script>
<script type="text/javascript" src="./js/rtorrent.js"></script>
<script type="text/javascript" src="./js/webui.js"></script>
<script type="text/javascript" src="./js/sanitize.js"></script>
<script type="text/javascript" src="./js/sanitize.config.js"></script>
</head>
<body>
<div id="preload"></div>
Expand Down
322 changes: 315 additions & 7 deletions plugins/rss/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -1147,14 +1147,322 @@ rTorrentStub.prototype.setrsssettings = function()
this.rssCommon("mode=setsettings&interval="+this.ss[0]+"&delayerrui="+this.ss[1]);
}

rTorrentStub.prototype.getrssdetailsResponse = function(data)
{
const doc = new DOMParser().parseFromString(String(data), 'text/html');
var s = new Sanitize(Sanitize.Config.RESTRICTED);
$("#rsslayout").html(s.clean_node(doc.body));
return(false);
}
theWebUI.mapBBCodeToHTML = function (htmlText) {
const tags = {
...Object.fromEntries(
[ "b", "i", "sup", "sub", "table", "thead", "tbody", "tfoot", "tr", "td", "th", "li" ].map((t) => [t, () => [t]])
),
...Object.fromEntries(
["ul", "ol", "list"].map((name) => [
name,
(_, content) => {
const htmlTag = name === "list" ? "ul" : name;
const ele = $(`<${htmlTag}>`).html(content);
const list = $(`<${htmlTag}>`);
let lastLiNode = $("<li>");
for (const node of ele.contents()) {
if (node.nodeName.toLowerCase() === "li") {
// keep li nodes
lastLiNode = $(node);
} else {
if (node.nodeType === 3) {
// parse list items denoted by [*] and *
const items = String(node.nodeValue)
.replaceAll(/(^|[\s\]])\*\s/g, "[*]")
.split(/\[\*\]/g);
if (!list.children("li").length) {
// set text of empty list
list.text(items.shift());
}
const firstItem = items.shift();
if (firstItem) {
// add textnode to lastLiNode
lastLiNode.append(document.createTextNode(firstItem));
}
for (const item of items) {
list.append(lastLiNode);
lastLiNode = $("<li>").text(item);
}
} else {
// add some node to lastLiNode
lastLiNode.append($(node));
}
}
// add lastLiNode to list (if not added already)
list.append(lastLiNode);
}
return [htmlTag, {}, list[0].innerHTML];
},
])
),
u: () => ["ins"],
s: () => ["del"],
...Object.fromEntries(
["small", "normal", "large"].map((t) => [
t,
() => ["span", { class: `bbcode-size-${t}` }],
])
),
size: (arg) => ["span", { class: `bbcode-size-${arg}` }],
color: (arg) => ["span", { class: `bbcode-color-${arg}` }],
...Object.fromEntries(
["center", "left", "right"].map((t) => [
t,
() => ["span", { class: `bbcode-align-${t}` }],
])
),
...Object.fromEntries(
["font", "face"].map((t) => [
t,
(arg) => ["span", { class: (arg || "").toLowerCase() }],
])
),
style: (_, __, args) => [
"span",
{
class: Object.entries(args)
.map(([k, v]) => `bbcode-${k}-${v}`)
.join(" "),
},
],
img: (arg, content, args) => [
"img",
{
src: content,
...Object.fromEntries(
(arg || "")
.split("x")
.map((v, k) => [["width", "height"][k], Number.parseInt(v)])
.filter(([_, v]) => !Number.isNaN(v))
),
...args,
},
"",
],
url: (arg, content) => ["a", { href: arg == null ? content : arg }],
email: (arg, content) => [
"a",
{ href: `mailto:${arg == null ? content : arg}` },
],
quote: (arg, content, args) => [
"blockquote",
{},
$("<p>").html(content)[0].outerHTML +
$("<span>")
.addClass("bbcode-quote")
.text("-- ")
.append($("<cite>").text(arg || args["author"] || ""))[0].outerHTML,
],
code: () => ["pre", { class: "bbcode-code" }],
spoiler: (arg, content) => [
"details",
{},
$("<summary>").html(arg)[0].outerHTML + content,
],
"bbcode-root": () => ["div"],
};

const trimArg = (arg) =>
arg == null
? null
: arg.startsWith('"')
? arg.substring(1, arg.length - 1)
: arg.trim();
const argsToDict = (args) => {
const dict = {};
for (const match of args.matchAll(
/\s+?(?<name>[a-z]+)=(?<arg>"(.*?)"|[^\s]*)/gi
)) {
const { name, arg } = match.groups;
if (name && arg) {
dict[name] = trimArg(arg);
}
}
return dict;
};

const nodeToElement = (node) => {
const htmlContent = node.children
.map((n) => (n.name ? nodeToElement(n).outerHTML : n))
.join("");
const arg = trimArg(node.arg);
const args = node.args ? argsToDict(node.args) : {};
const [htmlTag, attribs, htmlContentProcessed] = tags[node.name](
arg,
htmlContent,
args
);
const ele = $(`<${htmlTag}>`)
.attr(attribs || {})
.html(htmlContentProcessed || htmlContent)[0];
return ele;
};

const simpleParamPattern = '\\s*?=\\s*?(?<arg>"(.*?)"|.*?)';
const complexParamPattern = '(?<args>(\\s+?[a-z]+=("(.*?)"|[^\\s]*?))+)';
const tagPattern = new RegExp(
"\\[\\/?(?<name>" +
Object.keys(tags).join("|") +
")(" +
simpleParamPattern +
"|" +
complexParamPattern +
")?\\s*?\\]",
"gsi"
);
let nodeStack = [{ name: "bbcode-root", children: [] }];
let offset = 0;
for (const match of htmlText.matchAll(tagPattern)) {
const parent = nodeStack[nodeStack.length - 1];
const { name, arg, args } = match.groups;
const closing = match[0].startsWith("[/");
// add textnode to parent
const textnode = match.input.substring(offset, match.index);
if (textnode) {
parent.children.push(textnode);
}
if (closing) {
if (parent.name === name && nodeStack.length > 1) {
nodeStack.pop();
} else {
// encoutered unexpected close tag
nodeStack = [nodeStack[0]];
}
} else {
const node = { name, arg, args, children: [] };
parent.children.push(node);
// make curnode to parent node
nodeStack.push(node);
}
offset = match.index + match[0].length;
}
nodeStack[nodeStack.length-1].children.push(htmlText.substring(offset, htmlText.length));
const htmlContent = nodeToElement(nodeStack[0]).innerHTML;

// Support for some emoticons from WhatCD/Gazelle (https://github.com/WhatCD/Gazelle/tree/master/static/common/smileys)
// :code: => utf8 emoticon (https://utf8-icons.com/subset/emoticons)
const emoticons = {
smile: "&#128578;",
blank: "&#128528;",
biggrin: "&#128513;",
angry: "&#128545;",
blush: "&#128522;",
cool: "&#128526;",
crying: "&#128546;",
frown: "&#128577;",
unsure: "&#128533;",
lol: "&#128516;",
ninja: "&#129399;",
no: "&#128581;",
ohno: "&#128552;",
ohnoes: "&#128552;",
omg: "&#128576;",
shifty: "&#128530;",
sick: "&#128567;",
wink: "&#128521;",
creepy: "&#128520;",
tongue: "&#128540;",
thumbsup: "&#128077;",
"+1": "&#128077;",
thumbsdown: "&#128078;",
"-1": "&#128078;",
};
const emoticonRegExp = new RegExp(
":(" +
Object.keys(emoticons)
.map((e) => e.replaceAll(/\+/g, "\\+"))
.join("|") +
"):",
"g"
);
return htmlContent.replace(
emoticonRegExp,
(_, iconName) => emoticons[iconName]
);
};

rTorrentStub.prototype.getrssdetailsResponse = function (data) {
const colorNamePattern =
/^(?:aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen)$/;
const colorCodePattern = /^#?[a-f0-9]{6}$/i;
const restirctedFontSize = (value) => {
const size = Math.max(4, Math.min(40, Number.parseInt(value)));
return Number.isNaN(size) ? "normal" : `${size}px`;
};
const bbNodeAttr = {
color: (value) =>
colorNamePattern.test(value)
? { color: value }
: colorCodePattern.test(value)
? { color: value.startsWith("#") ? value : `#${value}` }
: null,
size: (value) =>
["small", "large", "normal"].includes(value)
? "class"
: { "font-size": restirctedFontSize(value) },
align: (value) =>
["left", "right", "center"].includes(value) ? "class" : null,
font: (value) =>
[ "times", "courier", "arial", "serif", "sans", "fantasy", "monospace", "caps", ].includes(value) ? "class" : null,
};
const bbclassTransform = (cfg) => {
const node = cfg.node;
if (!["pre", "span"].includes(node.nodeName.toLowerCase())) {
return null;
}
let styles = {};
let classes = [];
for (const bbClass of (node.attributes.class?.value || "").split(" ")) {
const [bbcode, key, value] = bbClass.split("-");
if (bbcode === "bbcode") {
const style = key in bbNodeAttr ? bbNodeAttr[key](value || "") : null;
if (style !== null) {
if (style !== "class") {
styles = { ...styles, ...style };
}
classes.push(
`${bbcode}-${key}` + (style === "class" ? `-${value}` : "")
);
}
}
}
// replace existing attributes with style and class
[...node.attributes].forEach((attr) => node.removeAttribute(attr.name));
for (const [name, value] of [
["style", Object.entries(styles).map((e) => e.join(": ")).join("; ")],
["class", classes.join(" ")],
]) {
if (value) {
const attr = cfg.dom.createAttribute(name);
attr.value = value;
node.attributes[name] = attr;
}
}
return {
whitelist: Boolean(classes.length),
attr_whitelist: ["class", "style"],
node,
};
};
const cfg = Sanitize.Config.RESTRICTED;
const s = new Sanitize({
elements: [...cfg.elements, "ins", "details", "summary"],
transformers: [bbclassTransform],
});
const rawHTML = String(data);
const dirtyHTML = theWebUI.mapBBCodeToHTML(rawHTML);
const doc = new DOMParser().parseFromString(dirtyHTML, "text/html");
$("#rsslayout")
.empty()
.append(
$("<details>")
.addClass('raw-details')
.text(rawHTML)
.append($("<hr>"))
.append($("<summary>").text("Raw")),
$('<div>').html(s.clean_node(doc.body)));
return false;
};

rTorrentStub.prototype.setfilters = function()
{
Expand Down
20 changes: 20 additions & 0 deletions plugins/rss/rss.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ div#FLT_buttons {clear: both}
#FLTBtn {width: 30px}

#rsslayout { border: 0; display: none; margin: 5px; white-space: pre-line; }
#rsslayout div { max-width: 700px; }
.bbcode-code { padding: 1em; border: 1px solid; }
.bbcode-quote { display: block; padding-left: 1.6em; }
.bbcode-size, .bbcode-size-normal { font-size: normal; }
.bbcode-size-small { font-size: smaller; }
.bbcode-size-large { font-size: larger; }
.bbcode-color { color: inherit; }
.bbcode-align-left { display: block; text-align: start; }
.bbcode-align-right { display: block; text-align: end; }
.bbcode-align-center { display: block; margin-left: auto; margin-right: auto; text-align: center; }
.bbcode-font-times { font-family: 'Times New Roman', Times, serif; }
.bbcode-font-courier { font-family: 'Courier New', Courier, monospace; }
.bbcode-font-arial { font-family: Arial, Helvetica, sans-serif; }
.bbcode-font-serif { font-family: serif; }
.bbcode-font-sans { font-family: sans-serif; }
.bbcode-font-fantasy { font-family: fantasy; }
.bbcode-font-monospace { font-family: monospace, monospace; }
.bbcode-font-caps { font-variant: small-caps; }
#rsslayout .raw-details { font-size: smaller; float: right; padding: 0 0 0.5em 0.5em; max-width: 700px; }
#rsslayout .raw-details summary { opacity: 0.5; text-align: right;}

#dlgAddRSSGroup {width: 320px; height: 300px}
#dlgAddRSSGroup div.dlg-header {background-image: url(./images/rss.gif)}
Expand Down
Loading

0 comments on commit 518e98f

Please sign in to comment.