diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/PartnerEventStorePatch.tsx | 193 | ||||
| -rw-r--r-- | src/html2bbcode.js | 1214 | ||||
| -rwxr-xr-x | src/index.tsx | 39 |
3 files changed, 1433 insertions, 13 deletions
diff --git a/src/PartnerEventStorePatch.tsx b/src/PartnerEventStorePatch.tsx new file mode 100644 index 0000000..2307283 --- /dev/null +++ b/src/PartnerEventStorePatch.tsx @@ -0,0 +1,193 @@ +import { findModuleExport } from "@decky/ui"; +import { afterPatch } from "decky-frontend-lib"; +import remarkHtml from "remark-html" +import remarkParse from "remark-parse" +import remarkGfm from "remark-gfm" +import {unified} from "unified" +import html2bbcode from "./html2bbcode"; +import {Mutex} from 'async-mutex'; + +const PartnerEventStore = findModuleExport( + (e) => e?.prototype?.InternalLoadAdjacentPartnerEvents +); + +const SteamID = findModuleExport( + (e) => e?.prototype?.BIsClanAccount + && e?.prototype?.BIsIndividualAccount + && e?.prototype?.BIsValid + && e?.prototype?.ConvertTo64BitString + && e?.prototype?.GetAccountID + && e?.prototype?.GetAccountType + && e?.prototype?.GetInstance + && e?.prototype?.GetUniverse + && e?.prototype?.Render + && e?.prototype?.SetAccountID + && e?.prototype?.SetAccountType + && e?.prototype?.SetFromComponents + && e?.prototype?.SetInstance + && e?.prototype?.SetUniverse +); + +const steamClanSteamID = "103582791470414830"; +const steamClanID = "40893422"; +const steamOSAppId = 1675200; +const githubReleasesURI = "https://api.github.com/repos/ublue-os/bazzite/releases"; + +enum SteamEventType { + SmallUpdate = 12, + Update = 13, + BigUpdate = 14, +} + +const mutex = new Mutex(); +let releases: any[]; +let channel: string; + +export function patchPartnerEventStore() { + return afterPatch( + PartnerEventStore.prototype, + "InternalLoadAdjacentPartnerEvents", + async function(args, ret) { + let [, , , appId, , , c, ] = args; + + if (appId !== steamOSAppId) { + return ret; + } + + ret = await Promise.resolve(ret); + + if (!Array.isArray(ret)) { + return ret; + } + + ret.length = 0; + + await mutex.runExclusive(async () => { + if (releases && releases.length > 0) + return; + + let response: Response; + let responseJson: any; + + try { + response = await fetch(githubReleasesURI); + + if (!response.ok) + return; + + responseJson = await response.json(); + } + catch { + responseJson = []; + } + + if (!Array.isArray(responseJson) || responseJson.length == 0) { + return; + } + + responseJson.sort((a, b) => (new Date(b.created_at)).getTime() - (new Date(a.created_at)).getTime()); + + if (c?.require_tags && c?.require_tags?.includes("stablechannel")) { + releases = responseJson.filter(r => !r.prerelease); + channel = "stablechannel"; + } else if (c?.require_tags && (c?.require_tags?.includes("betachannel") || c?.require_tags?.includes("previewchannel"))) { + releases = responseJson.filter(r => r.prerelease); + channel = "betachannel"; + } else { + releases = responseJson; + channel = "stablechannel"; + } + }); + + if (!releases || releases.length == 0) + return ret; + + for (const release of releases) { + const releaseCreatedAt = Math.floor((new Date(release.created_at)).getTime() / 1000); + + const html = await unified() + .use(remarkParse) + .use(remarkGfm) + .use(remarkHtml) + .process(release.body); + + const converter = new (html2bbcode.HTML2BBCode)(); + const bbcode = converter.feed(html.value); + + // @ts-ignore + const event = { + "gid": String(release.id), + "clan_steamid": steamClanSteamID, + "event_name": release.name, + "event_type": SteamEventType.Update, + "appid": steamOSAppId, + "server_address": "", + "server_password": "", + "rtime32_start_time": releaseCreatedAt, // only used for certain event_type, not used for updates, but anyway + "rtime32_end_time": releaseCreatedAt, // only used for certain event_type, not used for updates, but anyway + "comment_count": 0, + "creator_steamid": "0", + "last_update_steamid": "0", + "event_notes": "see announcement body", + "jsondata": "{\n\t\"localized_subtitle\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_summary\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_title_image\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_capsule_image\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"bSaleEnabled\": false,\n\t\"sale_show_creator\": false,\n\t\"sale_sections\": [\n\n\t]\n\t,\n\t\"sale_browsemore_text\": \"\",\n\t\"sale_browsemore_url\": \"\",\n\t\"sale_browsemore_color\": \"\",\n\t\"sale_browsemore_bgcolor\": \"\",\n\t\"localized_sale_header\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_sale_overlay\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_sale_product_banner\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_sale_product_mobile_banner\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_sale_logo\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"sale_font\": \"\",\n\t\"sale_background_color\": \"\",\n\t\"sale_header_offset\": 150,\n\t\"referenced_appids\": [\n\n\t]\n\t,\n\t\"bBroadcastEnabled\": false,\n\t\"broadcastChatSetting\": \"hide\",\n\t\"default_broadcast_title\": \"#Broadcast_default_title_dev\",\n\t\"localized_broadcast_title\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_broadcast_left_image\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"localized_broadcast_right_image\": [\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull,\n\t\tnull\n\t]\n\t,\n\t\"broadcast_whitelist\": [\n\n\t]\n\t,\n\t\"bScheduleEnabled\": false,\n\t\"scheduleEntries\": [\n\n\t]\n\t,\n\t\"valve_access_log\": [\n\t\t{\n\t\t\t\"strSteamID\": \"76561197979253178\",\n\t\t\t\"rtUpdated\": 1741648377\n\t\t},\n\t\t{\n\t\t\t\"strSteamID\": \"76561198840299494\",\n\t\t\t\"rtUpdated\": 1741740761\n\t\t}\n\t]\n\t,\n\t\"clone_from_event_gid\": \"519706073503894980\",\n\t\"clone_from_sale_enabled\": false,\n\t\"automatically_push_updated_source\": true\n}", + "announcement_body": { + "gid": String(release.id), + "clanid": steamClanID, + "posterid": "0", + "headline": `Bazzite ${release.name}`, + "posttime": releaseCreatedAt, + "updatetime": releaseCreatedAt, + "body": bbcode.toString(), + "commentcount": 0, + "tags": [ + "patchnotes", + channel, + ], + "language": 0, + "hidden": 0, + "forum_topic_id": "0", + "event_gid": "0", + "voteupcount": 0, + "votedowncount": 0, + "ban_check_result": 0, + "banned": 0 + }, + "published": 1, + "hidden": 0, + "rtime32_visibility_start": 0, + "rtime32_visibility_end": 0, + "broadcaster_accountid": 0, + "follower_count": 0, + "ignore_count": 0, + "forum_topic_id": "0", + "rtime32_last_modified": releaseCreatedAt, + "news_post_gid": "0", + "rtime_mod_reviewed": 0, + "featured_app_tagid": 0, + "referenced_appids": [], + "build_id": 0, + "build_branch": "", + "unlisted": 0, + "votes_up": 0, + "votes_down": 0, + "comment_type": "ForumTopic", // haven't found a way to hide likes and comments + "gidfeature": "0", + "gidfeature2": "0" + }; + + // @ts-ignore + if (!this.m_mapExistingEvents.has(event.gid)) { + let steamId = new SteamID(event.clan_steamid); + + // @ts-ignore + this.InsertEventModelFromClanEventData(steamId, event) + } + + // @ts-ignore + ret.push(this.m_mapExistingEvents.get(event.gid)); + } + + return ret; + } + ); +} diff --git a/src/html2bbcode.js b/src/html2bbcode.js new file mode 100644 index 0000000..7399d71 --- /dev/null +++ b/src/html2bbcode.js @@ -0,0 +1,1214 @@ + +(function (name, definition) { + if (typeof exports !== 'undefined' && typeof module !== 'undefined') { + module.exports = definition(); + } else if (typeof define === 'function' && typeof define.amd === 'object') { + define(definition); + } else { + this[name] = definition(); + } + })('html2bbcode', function (html2bbcode) { + + 'use strict'; + + html2bbcode = { version: '1.2.3' }; + + //function HTMLAttribute() + + function HTMLTag() { + this.name = ''; + this.length = 0; + //this.attr = null; + //this.content = null; + } + + HTMLTag.duptags = ['div', 'span']; + HTMLTag.headingtags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + HTMLTag.selfendtags = ['!doctype', 'meta', 'link','img', 'br']; + HTMLTag.newlinetags = ['div', 'p', 'br', 'li', 'tr'].concat(HTMLTag.headingtags); + HTMLTag.noemptytags = ['head', 'style', 'script', + 'span', 'a', 'font', 'color', 'size', 'face', + 'strong', 'b', 'em', 'i', 'del', 's', 'ins', 'u']; + HTMLTag.noemptyattrtags = ['img']; + + HTMLTag.prototype.findquoteend = function (script, start, multiline) { + var end = -1; + var i = start ? start : 0; + var len = script.length; + var d = script[i] === '\"'; + + i++; + while (i < len) { + if (script[i] === '\\') { + i++; + switch (script[i]) { + case 'u': + // \uXXXX + i += 5; + break; + case 'x': + // \xXX + i += 3; + break; + default: + // \n ... + i++; + break; + } + } else if ((d && script[i] === '\"') || (!d && script[i] === '\'')) { + end = i; + break; + } else if (script[i] === '\n' && !multiline) { + // not allow change line + break; + } else { + i++; + } + } + + return end; + }; + + HTMLTag.prototype.findscriptend = function (script, start) { + var end = -1; + var i = start ? start : 0; + var len = script.length; + var freg = /(['"]|<\s*?\/\s*?script\s*?>)/ig; + + while (i < len) { + if (script[i] === '\"' || script[i] === '\'') { + var qi = this.findquoteend(script, i, true); + if (qi === -1) { + break; + } + i = qi + 1; + } else { + freg.lastIndex = i; + var m = freg.exec(script); + if (!m || m.length <= 0) { + break; + } else if (m[0][0] === '<') { + //script here + end = freg.lastIndex - m[0].length; + break; + } + // quote + i = freg.lastIndex - 1; + //console.log(i, script.substr(i, 5)); + continue; + } + } + return end; + }; + + HTMLTag.prototype.quote = function (quotation) { + // convert string type + if (quotation[0] === '\'') { + var s = '"'; + var i = 1; + var len = quotation.length - 1; // last is \' + var start = i; + while (i < len) { + if (quotation[i] === '\\') { + i++; + switch (quotation[i]) { + case 'u': + // \uXXXX + i += 5; + break; + case 'x': + // \xXX + i += 3; + break; + default: + // \n ... + i++; + break; + } + } else if (quotation[i] === '\"') { + s += quotation.substr(start, i - start); + s += '\\"'; + i++; + start = i; + break; + } else { + i++; + } + } + if (start < len) { + s += quotation.substr(start, len - start); + } + s += '"'; + return s; + } else { + return quotation; + } + }; + + HTMLTag.prototype.parseStyle = function (style) { + var ss = style.split(';'); + var r_style = {}; + var count = 0; + for (var i = 0; i < ss.length; i++) { + var s = ss[i].split(':'); + if (s.length >= 2) { + count++; + var val; + if (s.length > 2) { + // eg. url(http://example.com) + val = s.slice(1).join(':').trim(); + } else { + val = s[1].trim(); + } + if (val[0] === '\'' && val[val.length - 1] === '\'') { + try { + val = JSON.parse(this.quote(val)); + } catch (err) { + } + } + r_style[s[0].trim().toLowerCase()] = val; + } + } + if (count > 0) { + return r_style; + } else { + return undefined; + } + }; + + HTMLTag.prototype.parseAttributes = function (attr) { + attr = attr.trim(); + var blank = /\s/; + var i = 0; + var len = attr.length; + var start = i; + var lastkey = null; + var invalue = false; + var r_attr = {}; + var add_attr = function (k, v) { + if (typeof v === 'undefined') { + v = null; + } + k = k.trim().toLowerCase(); + r_attr[k] = v; + }; + while (i < len) { + if (attr[i] === '=') { + // TODO: check lastkey, currently drop previous lastkey + lastkey = attr.substr(start, i - start); + invalue = false; + } else if (blank.test(attr[i])) { + if (lastkey && invalue) { + add_attr(lastkey, attr.substr(start, i - start)); + invalue = false; + lastkey = null; + } else if (i - start > 0) { + lastkey = attr.substr(start, i - start); + add_attr(lastkey); + lastkey = null; + } + start = i + 1; + } else if (lastkey && !invalue) { + start = i; + if (attr[i] === '"' || attr[i] === '\'') { + var b = attr[i] === '\''; + i = this.findquoteend(attr, i); + if (i === -1) { + break; + } + var v = attr.substr(start, i + 1 - start); + if (b) { + v = this.quote(v); + } + try { + v = JSON.parse(v); + } catch (e) { + } + add_attr(lastkey, v); + lastkey = null; + start = i + 1; + } else { + invalue = true; + } + } + i++; + } + if (start < len) { + var d = attr.substr(start); + if (lastkey) { + add_attr(lastkey, d); + } else { + add_attr(d); + } + lastkey = null; + } + var count = 0; + for (var k in r_attr) { + count++; + } + if (count > 0) { + if (r_attr.style) { + r_attr.style = this.parseStyle(r_attr.style); + } + this.attr = r_attr; + } + }; + + HTMLTag.prototype.parse = function (html) { + var i = 0; + if (html[i] !== '<') { + throw new Error('not a tag'); + } + var len = html.length; + var blank = /\s/; + //var htmltagq = /[<>]/; + // strip tagname head blank + while (i < len) { + if (html[i] === '<') { + i++; + } else if (html[i] === '>') { + // drop this empty tag + this.length = i + 1; + return this; + } else if (blank.test(html[i])) { + i++; + } else { + break; + } + } + if (i >= len) { + // drop this + this.length = len; + return this; + } + + // name + var start = i; + var tagheadend = false; + while (i < len && !blank.test(html[i])) { + if (html[i] === '>') { + tagheadend = true; + break; + } else if (html[i] === '/') { + break; + } + i++; + } + if (i >= len) { + // drop this + this.length = i; + return this; + } + this.name = html.substr(start, i - start).trim().toLowerCase(); + if (this.name.length > 0 && this.name[0] === '/') { + this.length = i; + this.name = this.name.substr(1); + this.selfend = true; + return this; + } + if (HTMLTag.selfendtags.indexOf(this.name) >= 0) { + this.selfend = true; + } + + // attr + if (!tagheadend) { + start = i; + while (i < len && html[i] !== '>') { + i++; + } + if (i >= len) { + // drop this + this.length = i; + return this; + } else if (i - start > 0) { + var sattr = html.substr(start, i - start).trim(); + var attrlen = sattr.length; + if (attrlen > 0 && sattr[attrlen - 1] === '/') { + this.selfend = true; + sattr = sattr.substr(0, attrlen - 1); + } + this.parseAttributes(sattr); + } + } + i++; // skip '>' + + if (this.selfend) { + this.length = i; + return this; + } + + // content + var that = this; + var add_content = function (html) { + var hstack = new HTMLStack().parse(html); + if (that.content) { + that.content.append(hstack); + } else { + that.content = hstack; + } + return hstack.length; + }; + + if (this.name === 'script') { + var script_len = this.findscriptend(html.substr(i)); + if (script_len < 0) { + this.length = len; + return this; + } + + this.content = new HTMLStack(); + var script = html.substr(i, script_len); + this.content.length = script_len; + this.content.stack = [ script ]; + + i += script_len; + // script tag end + start = html.indexOf('>', i); + if (start < 0) { + // no possible + this.length = len; + return this; + } + + this.length = start + 1; + return this; + } + + var j = 0; + while (i < len) { + // loop to tag end + j++; + start = i; + + while (i < len && blank.test(html[i])) { + i++; + } + + while (i < len && html[i] !== '<') { + i++; + } + + var i_tagend = i; + i++; + while (i < len && blank.test(html[i])) { + i++; + } + + if (i >= len) { + // drop this + this.content = new HTMLStack().parse(html.substr(start)); + this.length = len; + return this; + } else { + if (i < len && html[i] === '/') { + i++; + while (i < len && blank.test(html[i])) { + i++; + } + if (i >= len) { + // drop this + i += add_content(html.substr(start)); + this.length = len; + return this; + } else { + var t_start = i; + var t_tagheadend = false; + while (i < len && !blank.test(html[i])) { + if (html[i] === '>') { + t_tagheadend = true; + break; + } + i++; + } + if (i > t_start) { + if (!t_tagheadend) { + while (i < len && html[i] !== '>') { + i++; + } + } + var ename = html.substr(t_start, i - t_start).trim().toLowerCase(); + i++; //skip '>' + // force stop current tag + /*if (ename === this.name)*/ { + // end of tag + this.length = i; + if (i_tagend > start) { + // add content + add_content(html.substr(start, i_tagend - start)); + } + return this; + } + } + } + } + } + + i = start + add_content(html.substr(start)); + } + + this.length = i; + return this; + }; + + function HTMLStack() { + this.stack = []; + this.length = 0; + } + + HTMLStack.prototype.parse = function (html) { + // check first... + if (!html) { + return this; + } + + var i = 0; + var len = html.length; + var lasttagend = 0; + var blank = /\s/; + var that = this; + var push_plaintext = function (start, end) { + if (start < end) { + that.push(html.substr(start, end - start)); + } + }; + while (i < len) { + switch (html[i]) { + case '<': + push_plaintext(lasttagend, i); + + // check end & drop + var t_i = i + 1; + while (t_i < len && blank.test(html[t_i])) { + t_i++; + } + if (t_i < len && html[t_i] === '/') { + return this; + } + + var tag = new HTMLTag().parse(html.substr(i)); + this.push(tag); + i += tag.length; + lasttagend = i; + break; + case '>': + // TODO: drop the > + i++; + break; + default: + i++; + break; + } + } + push_plaintext(lasttagend, len); + return this; + }; + + HTMLStack.prototype.push = function (data) { + this.length += data.length; + this.stack.push(data); + }; + + HTMLStack.prototype.pop = function () { + return this.stack.pop(); + }; + + HTMLStack.prototype.append = function (hstack) { + this.stack = this.stack.concat(hstack.stack); + this.length += hstack.length; + }; + + (function () { + var dupRegex = new RegExp( + '<\\s*?(' + HTMLTag.duptags.join('|') + ')\\s*?>\\s*?' + + '<\\s*?\\1\\s*?>' + + '(((?!<\\s*?\\1\\s*?>)[\\S\\s])*?)' + + '<\\s*?/\\s*?\\1\\s*?>\\s*?' + + '<\\s*?/\\s*?\\1\\s*?>', 'ig'); + var nlsRegex = new RegExp( + '(<\\s*?(' + HTMLTag.newlinetags.join('|') + ')(\\s[^>]*?)?>)\\s+', 'ig'); + var nleRegex = new RegExp( + '\\s+(<\\s*?/\\s*?(' + HTMLTag.newlinetags.join('|') + ')\\s*?>)', 'ig'); + var empRegex = new RegExp( + '<\\s*?(' + HTMLTag.noemptytags.join('|') + ')(\\s[^>]*?)?>' + + '<\\s*?/\\s*?\\1\\s*?>', 'ig'); + HTMLStack.minify = function (html) { + var preRegex = /<pre(\s.*?)?>/ig; + var endPreRegex = /<\/pre>/ig; + var emptyRegex = /\s{2,}/g; + var m, newHtml = '', preMarkIndex = -1; + html = html.replace(empRegex, ''); + html = html.replace(nlsRegex, '$1'); + html = html.replace(nleRegex, '$1'); + while (m = preRegex.exec(html)) { + if (preMarkIndex < 0) { + preMarkIndex = 0; + } + newHtml += html.substr(preMarkIndex, preRegex.lastIndex - preMarkIndex).replace(emptyRegex, ' '); + preMarkIndex = preRegex.lastIndex; + endPreRegex.lastIndex = preRegex.lastIndex; + if (m = endPreRegex.exec(html)) { + preRegex.lastIndex = endPreRegex.lastIndex; + // no replace for pre content + newHtml += html.substr(preMarkIndex, m.index - preMarkIndex); + preMarkIndex = m.index; + } + } + if (preMarkIndex >= 0) { + html = newHtml + html.substr(preMarkIndex).replace(emptyRegex, ' '); + } else { + html = html.replace(emptyRegex, ' '); + } + while (dupRegex.test(html)) { + html = html.replace(dupRegex, '<$1>$2</$1>'); + } + return html; + }; + })(); + + var escapeMap = { + '&': 'amp', + '<': 'lt', + '>': 'gt', + '"': 'quot', + "'": '#x27', + '`': '#x60' + }; + var unescapeMap = { + 'nbsp': ' ', + 'amp': '&', + 'lt': '<', + 'gt': '>', + 'quot': '"' + }; + + HTMLStack.unescape = function (str, nonbsp) { + var src = '&([a-zA-Z]+?|#[xX][\\da-fA-F]+?|#\\d+?);'; + var testRegexp = new RegExp(src); + var escaper = function (match, m1) { + m1 = m1.toLowerCase(); + if (nonbsp && m1 === 'nbsp') { + return ' '; + } + var m = unescapeMap[m1]; + if (m) { + return m; + } else if (m1[0] === '#') { + var code = 0; + if (m1[1] == 'x') { + code = parseInt(m1.substr(2), 16); + } else { + code = parseInt(m1.substr(1)); + } + if (code) { + return String.fromCharCode(code); + } + } + return ''; + }; + if (testRegexp.test(str)) { + var replaceRegexp = new RegExp(src, 'g'); + str = str.replace(replaceRegexp, escaper); + } + return str; + }; + + HTMLStack.prototype.decode = function (nonbsp) { + for (var i = 0; i < this.stack.length; i++) { + var s = this.stack[i]; + if (typeof s === 'string') { + this.stack[i] = HTMLStack.unescape(s, nonbsp); + } else if (s instanceof HTMLTag && s.content) { + s.content.decode(nonbsp); + } + } + return this; + }; + + HTMLStack.prototype.dedup = function () { + for (var i = 0; i < this.stack.length; i++) { + var s = this.stack[i]; + if (s instanceof HTMLTag && s.content) { + if (HTMLTag.duptags.indexOf(s.name) >= 0 && !s.attr && s.content.stack.length === 1) { + var ts = s.content.stack[0]; + if (ts.name === s.name) { + this.stack[i] = ts; + i--; + continue; + } + } + s.content.dedup(); + } + } + return this; + }; + + HTMLStack.prototype.strip = function (parent, afternewline) { + + if (!afternewline) { + afternewline = (parent && !afternewline) ? (HTMLTag.newlinetags.indexOf(parent.name) >= 0) : true; + } + + var blanks = /^\s*$/; + var k = 0; + var stag = true; + // first recursive + for (var i = 0; i < this.stack.length; i++) { + var s = this.stack[i]; + if (s instanceof HTMLTag) { + stag = true; + if (s.content) { + //check if is after newline + var anl; + if (k <= 0) { + anl = afternewline; + } else { + anl = false; + // fine previous one + for (var j = i - 1; j >= 0; j--) { + var ts = this.stack[j]; + if (ts instanceof HTMLTag) { + anl = (HTMLTag.newlinetags.indexOf(ts.name) >= 0); + //anl = true; + break; + } else if (typeof ts === 'string' && blanks.test(ts)) { + //continue; + } else { + break; + } + } + } + s.content.strip(s, anl); + } + } else if (typeof s === 'string' && blanks.test(s)) { + if (stag) { + continue; + } + } + k++; + } + + stag = true; + var new_stack = []; + var new_len = 0; + for (var i = 0; i < this.stack.length; i++) { + var s = this.stack[i]; + if (typeof s === 'string' && blanks.test(s) && afternewline) { + if (stag) { + continue; + } + afternewline = false; + } else if (s instanceof HTMLTag) { + stag = true; + if (HTMLTag.noemptyattrtags.indexOf(s.name) >= 0) { + // strip like <img src="" /> + if (!s.attr) { + continue; + } + var exists = false; + for (var k1 in s.attr) { + if (s.attr[k1]) { + exists = true; + break; + } + } + if (!exists) { + continue; + } + } + if (HTMLTag.noemptytags.indexOf(s.name) >= 0 && !s.content) { + // null span + continue; + } else if (HTMLTag.newlinetags.indexOf(s.name) >= 0) { + afternewline = true; + /*} else if (s.name === 'span' && afternewline) {*/ + // keep newline flag + } else { + afternewline = false; + } + } else { + // not full empty string + if (afternewline) { + // removehead space after newline + s = s.replace(/^\s+/g, ''); + if (!s) { + // empty string + continue; + } + } + s = s.replace(/\s+/g, ' '); + stag = false; + afternewline = false; + } + new_len++; + new_stack.push(s); + } + + // check last one is empty string + var s = new_stack[new_len - 1]; + if (typeof s === 'string') { + if (new_len >= 2 && blanks.test(s)) { + // remove last empty string + new_stack.splice(new_len - 1, 1); + new_len--; + } else if (/\S\s+$/.test(s)) { + // space follow with a non-space string + new_stack[new_len - 1] = s.replace(/\s+$/, ''); + } + } + + if (new_len <= 0 && parent) { + delete parent.content; + return; + } + + this.stack = new_stack; + return this; + }; + + HTMLStack.prototype.showtree = function (tab, depth) { + if (!tab) tab = ''; + if (!depth) depth = 0; + + for (var i = 0; i < this.stack.length; i++) { + var d = this.stack[i]; + if (d instanceof HTMLTag) { + console.log(tab, d.name, d.attr ? JSON.stringify(d.attr) : ''); + if (d.content) { + d.content.showtree(tab + '--', depth + 1); + } + } else if (typeof d === 'string') { + console.log(tab, JSON.stringify(d)); + } + } + }; + + function BBCode() { + this.s = ''; + this.weaknewline = true; + this.stack = []; + } + + BBCode.maps = { + 'a': { section: 'url', attr: 'href' }, + 'img': { section: 'img', data: 'src', empty: true }, + 'em': { section: 'i' }, + 'i': { section: 'i' }, + 'strong': { section: 'b' }, + 'b': { section: 'b' }, + 'del': { section: 's' }, + 's': { section: 's' }, + 'ins': { section: 'u' }, + 'u': { section: 'u' }, + 'center': { section: 'center' }, + 'ul': { section: 'ul' }, // may need to treat as 'list' + 'ol': { section: 'ol' }, // may need to treat as 'list' + 'li': { section: 'li', newline: 1 }, + 'blockquote': { section: 'quote' }, + 'code': { section: 'b' }, + 'font': { extend: ['color', 'face', 'size'] }, + 'span': { extend: ['color', 'face', 'size'] }, + 'color': { section: 'color', attr: 'color' }, + 'size': { section: 'size', attr: 'size' }, + 'face': { section: 'font', attr: 'face' }, + // new line tags + 'h1': { section: 'h1', newline: 1 }, + 'h2': { section: 'h2', newline: 1 }, + 'h3': { section: 'h3', newline: 1 }, + 'h4': { section: 'h4', newline: 1 }, + 'h5': { section: 'h5', newline: 1 }, + 'h6': { section: 'h6', newline: 1 }, + 'p': { newline: 1 }, + 'br': { newline: 2, empty: true }, + 'table': { section: 'table', newline: 1 }, + 'tr': { section: 'tr', newline: 1 }, + 'th': { section: 'td', newline: 1 }, + 'td': { section: 'td', newline: 1 }, + 'pre': { section: 'code', newline: 1 }, + 'div': { newline: 0 }, + // ignore tags + '!doctype': { ignore: true }, + 'head': { ignore: true }, + 'style': { ignore: true }, + 'script': { ignore: true }, + 'meta': { ignore: true }, + 'link': { ignore: true }, + }; + + BBCode.prototype.open = function (section, attr, data) { + if (!section) { + return; + } + if (section instanceof Array) { + this.stack = this.stack.concat(section); + } else { + this.stack.push({ + section: section, + attr: attr, + data: data + }); + } + }; + + BBCode.prototype.append = function (str) { + this.solidify(); + this._append(str); + }; + + BBCode.prototype._append = function (str) { + if (str) { + this.s += str; + this.weaknewline = false; + } + }; + + BBCode.prototype.solidify = function () { + // write back stack + var i; + for (i = 0; i < this.stack.length; i++) { + var st = this.stack[i]; + var section = st.section; + var attr = st.attr; + var data = st.data; + + var s = '[' + section; + if (typeof attr === 'string') { + s += '=' + attr; + } else { + for (var k in attr) { + s += ' ' + k + '=' + attr[k]; + } + } + s += ']'; + if (data) { + s += data; + } + + this._append(s); + } + if (i > 0) { + this.stack = []; + } + }; + + BBCode.prototype.close = function (section) { + if (!section) { + return; + } + this.solidify(); + this._append('[/' + section + ']'); + }; + + BBCode.prototype.rollback = function () { + this.stack = []; + }; + + BBCode.prototype.newline = function (n) { + if (n === 2) { + // br + this.append('\n'); + this.weaknewline = true; + } else if (n === 1) { + // div, p + if (!this.weaknewline) { + this.append('\n'); + this.weaknewline = true; + } + } else if (!this.weaknewline) { + this.append('\n'); + this.weaknewline = true; + } + }; + + BBCode.prototype.toString = function () { + return this.s; + }; + + // opts: transsize, imagescale + function HTML2BBCode(opts) { + this.opts = opts ? opts : {}; + } + + HTML2BBCode.prototype.color = function (c) { + if (!c) return; + var c1Regex = /rgba?\s*?\(\s*?(\d{1,3})\s*?,\s*?(\d{1,3})\s*?,\s*?(\d{1,3})\s*?.*?\)/i; + if (c1Regex.test(c)) { + var pad2 = function (s) { + if (s.length < 2) { + s = '0' + s; + } + return s; + } + c = c.replace(c1Regex, function (match, r, g, b) { + r = pad2(parseInt(r).toString(16)); + g = pad2(parseInt(g).toString(16)); + b = pad2(parseInt(b).toString(16)); + return '#' + r + g + b; + }); + } + return c; + }; + + HTML2BBCode.prototype.size = function (size) { + if (!size) return; + + var px2size = [0, 12, 14, 16, 18, 24, 32, 48]; + var name2size = [null, 'smaller', 'small', 'medium', 'large', + 'x-large', 'xx-large', '-webkit-xxx-large']; + + if (/^\d+$/.test(size)) { + return size; + } else if (/^\d+?px$/.test(size)) { + size = parseInt(size); + if (!size || size < 0) { + return; + } + if (this.opts.transsize) { + for (var i = px2size.length; i >= 0; i--) { + if (i === 0) { + // smallest + return '1'; + } + if (size >= px2size[i]) { + return i.toString(); + } + } + } else { + return size.toString(); + } + } else { + var ns = name2size.indexOf(size); + if (ns > 0) { + if (this.opts.transsize) { + return ns.toString(); + } else { + return px2size[ns].toString(); + } + } + + // TODO: support other type + return; + } + + return size ? size.toString() : undefined; + }; + + HTML2BBCode.prototype.px = function (px) { + if (!px) return; + px = parseInt(px); + return px ? px.toString() : undefined; + }; + + HTML2BBCode.prototype.convertStyle = function (htag, sec) { + if (!sec) { + return; + } + var bbs = []; + var that = this; + var opts = this.opts; + var addbb = function (sec) { + if (!sec || sec.ignore || + !(sec.section || (sec.extend && sec.extend.length > 0))) { + return; + } + var tsec = { section: sec.section }; + if (sec.attr) { + if (htag.attr) { + switch (sec.section) { + case 'size': + tsec.attr = that.size(htag.attr[sec.attr]); + break; + case 'color': + tsec.attr = that.color(htag.attr[sec.attr]); + break; + default: + tsec.attr = htag.attr[sec.attr]; + break; + } + if (htag.attr.style) { + var ra; + switch (sec.section) { + case 'size': + ra = htag.attr.style['font-size']; + if (ra) ra = that.size(ra); + break; + case 'color': + ra = htag.attr.style['color']; + if (ra) ra = that.color(ra); + break; + case 'font': + ra = htag.attr.style['font-family']; + break; + } + if (ra) { + tsec.attr = ra; + } + } + if (!tsec.attr) { + return; + } + } else { + return; + } + } else if (sec.section === 'img' && opts.imagescale) { + // image attr + var w, h; + if (htag.attr) { + w = that.px(htag.attr['width']); + h = that.px(htag.attr['height']); + if (htag.attr.style) { + var w1, h1; + w1 = that.px(htag.attr.style['width']); + h1 = that.px(htag.attr.style['height']); + if (w1) w = w1; + if (h1) h = h1; + } + if (w && h) { + tsec.attr = w + 'x' + h; + } else if (w || h) { + if (w) { + tsec.attr = { width: w }; + } else { + tsec.attr = { height: h }; + } + } + } + } + if (sec.data) { + tsec.data = htag.attr[sec.data]; + } + bbs.push(tsec); + }; + // check font-weight & text-align + if (htag.attr && htag.attr.style) { + if (htag.name !== 'b' && htag.name !== 'strong') { + var att = htag.attr.style['font-weight']; + if (att === 'bold' || (/^\d+$/.test(att) && parseInt(att) >= 700)) { + addbb(BBCode.maps['b']); + } + } + if (htag.name !== 'center') { + var att = htag.attr.style['text-align']; + if (att === 'center' && !opts.noalign) { + addbb(BBCode.maps['center']); + } + } + if (htag.name !== 'em' && htag.name !== 'i') { + var att = htag.attr.style['font-style']; + if (att === 'italic' || att === 'oblique') { + // italic style + addbb(BBCode.maps['i']); + } + } + } + if (sec.section === 'list' + || sec.section === 'ul' || sec.section === 'ol' + || sec.section === 'li') { + if (opts.nolist) { + return []; + } + } else if (sec.section === 'center') { + if (opts.noalign) { + return []; + } + } else if (/^h\d+$/.test(sec.section)) { + // HTML Headings + if (opts.noheadings) { + // 18.5 -> 19 + var headings2size = [ null, '32px', '24px', '19px', '16px', '14px', '12px' ]; + var m = sec.section.match(/^h(\d+)$/); + var hi = parseInt(m[1]); + if (hi <= 0) { + return []; + } else if (hi >= headings2size.length) { + hi = headings2size.length; + } + bbs.push({ section: 'size', attr: that.size(headings2size[hi]) }); + return bbs; + } + } + + if ('extend' in sec) { + for (var i = 0; i < sec.extend.length; i++) { + var tag = sec.extend[i]; + addbb(BBCode.maps[tag]); + } + } else { + addbb(sec); + } + return bbs; + }; + + HTML2BBCode.prototype.convert = function (hstack) { + var bbcode = new BBCode(); + if (!hstack) { + return bbcode; + } + var that = this; + var recursive = function (hs, anl) { + for (var i = 0; i < hs.length; i++) { + var s = hs[i]; + if (s instanceof HTMLTag) { + if (s.name in BBCode.maps) { + var fnewline = 0; + var sec = BBCode.maps[s.name]; + if (sec.ignore) { + continue; + } + if ('newline' in sec) { + fnewline = sec.newline; + bbcode.newline(sec.newline); + } + if (!s.content && !sec.empty) { + // drop this + continue; + } + var bbs = that.convertStyle(s, sec); + bbcode.open(bbs); + + if (s.content) { + recursive(s.content.stack, fnewline); + } + for (var j = bbs.length - 1; j >= 0; j--) { + bbcode.close(bbs[j].section); + } + if (fnewline) { + // weak new line + bbcode.newline(); + } + } else if (s.content) { + // drop section + recursive(s.content.stack); + } + } else if (typeof s === 'string') { + // force space + //s = s.replace(/ /gi, ' '); + bbcode.append(s); + } + } + }; + recursive(hstack.stack); + return bbcode; + }; + + HTML2BBCode.prototype.parse = function (html) { + return new HTMLStack().parse(html) + .strip().dedup().decode(); + }; + + HTML2BBCode.prototype.feed = function (html) { + var hstack = this.parse(html); + if (this.opts.debug) { + hstack.showtree(); + } + var bbcode = this.convert(hstack); + return bbcode; + }; + + return { + HTMLTag: HTMLTag, + HTMLStack: HTMLStack, + BBCode: BBCode, + HTML2BBCode: HTML2BBCode + }; + + });
\ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 87f3f72..28ae219 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,17 +1,20 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { FaClipboardList } from "react-icons/fa"; import { definePlugin } from "decky-frontend-lib"; -import { marked } from "marked"; -import DOMPurify from "dompurify"; +import remarkHtml from "remark-html" +import remarkParse from "remark-parse" +import remarkGfm from "remark-gfm" +import { unified } from "unified" +import { patchPartnerEventStore } from "./PartnerEventStorePatch"; +import {staticClasses} from "@decky/ui"; function Content() { - const [changelog, setChangelog] = useState<string | null>(null); + const [changelogHtml, setChangelogHtml] = useState<string | null>(null); const [error, setError] = useState<string | null>(null); const [isRefreshing, setIsRefreshing] = useState<boolean>(false); const fetchChangelog = async (signal?: AbortSignal) => { - const url = - "https://api.github.com/repos/ublue-os/bazzite/releases/tags/41.20250106.2"; + const url = "https://api.github.com/repos/ublue-os/bazzite/releases/latest"; try { const response = await fetch(url, { headers: { @@ -25,7 +28,13 @@ function Content() { } const data = await response.json(); - setChangelog(data.body); + const html = await unified() + .use(remarkParse) + .use(remarkGfm) + .use(remarkHtml) + .process(data.body) + + setChangelogHtml(html.value as string); setError(null); } catch (err) { if (err instanceof DOMException && err.name === "AbortError") return; @@ -48,7 +57,7 @@ function Content() { const refreshChangelog = async () => { setIsRefreshing(true); - setChangelog(null); + setChangelogHtml(null); setError(null); try { @@ -115,7 +124,7 @@ function Content() { <p style={{ color: "red" }} aria-live="polite"> {error} </p> - ) : changelog ? ( + ) : changelogHtml ? ( <div style={{ backgroundColor: "#1e1e1e", @@ -161,7 +170,7 @@ function Content() { </style> <div dangerouslySetInnerHTML={{ - __html: DOMPurify.sanitize(marked(changelog)), + __html: changelogHtml, }} ></div> </div> @@ -173,11 +182,15 @@ function Content() { } export default definePlugin(() => { + const patch = patchPartnerEventStore(); + return { name: "Bazzite Changelog Viewer", - title: <div>Bazzite Changelog</div>, + title: <div className={staticClasses.Title}>Bazzite Buddy</div>, icon: <FaClipboardList />, content: <Content />, - onDismount() {}, + onDismount() { + patch.unpatch(); + }, }; -});
\ No newline at end of file +}); |
