1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
* Added: Chat actions for modding and un-modding users.
* Fixed: Settings not being removed when an add-on is unloaded.
* Changed: Add a few new icons.
* API Added: Add support for header backgrounds for rich token documents.
* API Added: Methods for adding/updating emotes to and removing emotes from an emote set.
* API Added: Context flag to disable FFZ's chat message processing.
* API Changed: Add-ons can now be hot reloaded for development purposes. This feature may be somewhat unstable.
This commit is contained in:
SirStendec 2023-01-19 17:00:09 -05:00
parent 14400e16bc
commit 8e48021c43
35 changed files with 1285 additions and 214 deletions

View file

@ -805,6 +805,68 @@
"css": "volume-up",
"code": 59464,
"src": "elusive"
},
{
"uid": "1fc437d46c5ef828375b6b3de577918d",
"css": "unmod",
"code": 59465,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M350 100A200 200 0 0 0 299.3 493.5C294.4 525.5 266.7 550 233.3 550A133.3 133.3 0 0 0 100 683.4V900H200V683.4C200 664.9 214.9 650 233.3 650 278.8 650 319.9 631.9 350 602.4 380.1 631.9 421.3 650 466.7 650 485.1 650 500 664.9 500 683.4V900H600V683.4A133.3 133.3 0 0 0 466.7 550C433.3 550 405.6 525.5 400.8 493.5A200.1 200.1 0 0 0 350 100ZM250 300A100 100 0 1 0 450 300 100 100 0 0 0 250 300ZM600 420.7L670.7 350 751.8 431.1 833 350 903.7 420.7 822.6 501.8 903.7 583 833 653.7 751.9 572.5 670.7 653.7 600 583 681.1 501.9 600 420.7Z",
"width": 1000
},
"search": [
"unmod"
]
},
{
"uid": "2c1f4d302aa8281c3ed4882568669043",
"css": "mod",
"code": 59466,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M350 100A200 200 0 0 0 299.3 493.5C294.4 525.5 266.7 550 233.3 550A133.3 133.3 0 0 0 100 683.4V900H200V683.4C200 664.9 214.9 650 233.3 650 278.8 650 319.9 631.9 350 602.4 380.1 631.9 421.3 650 466.7 650 485.1 650 500 664.9 500 683.4V900H600V683.4A133.3 133.3 0 0 0 466.7 550C433.3 550 405.6 525.5 400.8 493.5A200.1 200.1 0 0 0 350 100ZM250 300A100 100 0 1 0 450 300 100 100 0 0 0 250 300ZM750 350L900 500 750 650V550H600V450H750V350Z",
"width": 1000
},
"search": [
"mod"
]
},
{
"uid": "e436d990b8c910352dba1fe3e88d9ca3",
"css": "flag",
"code": 59467,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M900 100L700 400 900 700H200V900H100V100H900ZM200 600H713.1L579.8 400 713.1 200H200V600Z",
"width": 1000
},
"search": [
"flag"
]
},
{
"uid": "c56ae110cddeae77e2e904e33f9b9718",
"css": "mange-suspicious",
"code": 59468,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M450 550V650H550V550H450ZM250.2 197.3A500 500 0 0 0 475.9 116L500 100 524 116A500 500 0 0 0 801.5 200H850L825.3 482.1A450 450 0 0 1 623.1 819.6L500 900 376.9 819.6A450 450 0 0 1 174.7 482.1L150 200H198.6C215.9 200 233.1 199.1 250.2 197.3ZM258.9 296.9L274.3 473.4A350 350 0 0 0 431.6 735.9L500 780.6 568.4 735.9A350 350 0 0 0 725.7 473.4L741.2 296.9A600 600 0 0 1 550 244.8V450H450V244.8A600 600 0 0 1 258.8 296.9Z",
"width": 1000
},
"search": [
"mange-suspicious"
]
},
{
"uid": "5408be43f7c42bccee419c6be53fdef5",
"css": "doc-text",
"code": 61686,
"src": "fontawesome"
}
]
}

View file

@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
"version": "4.39.0",
"version": "4.40.0",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true,
"license": "Apache-2.0",

Binary file not shown.

View file

@ -152,6 +152,14 @@
<glyph glyph-name="volume-up" unicode="&#xe848;" d="M0 169l0 360 203 0 307 250 0-858-307 248-203 0z m563 33q62 63 62 151t-62 152l60 65q90-90 92-219 0-125-92-213z m101-105q106 105 106 256t-106 258l66 62q131-133 131-319t-131-321z m100-98q146 147 146 354t-146 353l62 65q82-82 128-190t46-227-46-229-128-190z" horiz-adv-x="1000" />
<glyph glyph-name="unmod" unicode="&#xe849;" d="M350 750a200 200 0 0 1-51-393c-5-32-32-57-66-57a133 133 0 0 1-133-133v-217h100v217c0 18 15 33 33 33 46 0 87 18 117 48 30-30 71-48 117-48 18 0 33-15 33-33v-217h100v217a133 133 0 0 1-133 133c-34 0-61 25-66 57a200 200 0 0 1-51 393z m-100-200a100 100 0 1 1 200 0 100 100 0 0 1-200 0z m350-121l71 71 81-81 81 81 71-71-81-81 81-81-71-71-81 82-81-82-71 71 81 81-81 81z" horiz-adv-x="1000" />
<glyph glyph-name="mod" unicode="&#xe84a;" d="M350 750a200 200 0 0 1-51-393c-5-32-32-57-66-57a133 133 0 0 1-133-133v-217h100v217c0 18 15 33 33 33 46 0 87 18 117 48 30-30 71-48 117-48 18 0 33-15 33-33v-217h100v217a133 133 0 0 1-133 133c-34 0-61 25-66 57a200 200 0 0 1-51 393z m-100-200a100 100 0 1 1 200 0 100 100 0 0 1-200 0z m500-50l150-150-150-150v100h-150v100h150v100z" horiz-adv-x="1000" />
<glyph glyph-name="flag" unicode="&#xe84b;" d="M900 750l-200-300 200-300h-700v-200h-100v800h800z m-700-500h513l-133 200 133 200h-513v-400z" horiz-adv-x="1000" />
<glyph glyph-name="mange-suspicious" unicode="&#xe84c;" d="M450 300v-100h100v100h-100z m-200 353a500 500 0 0 1 226 81l24 16 24-16a500 500 0 0 1 278-84h48l-25-282a450 450 0 0 0-202-338l-123-80-123 80a450 450 0 0 0-202 338l-25 282h49c17 0 34 1 51 3z m9-100l15-176a350 350 0 0 1 158-263l68-45 68 45a350 350 0 0 1 158 263l15 176a600 600 0 0 0-191 52v-205h-100v205a600 600 0 0 0-191-52z" horiz-adv-x="1000" />
<glyph glyph-name="move" unicode="&#xf047;" d="M1000 350q0-14-11-25l-142-143q-11-11-26-11t-25 11-10 25v72h-215v-215h72q14 0 25-10t11-25-11-25l-143-143q-10-11-25-11t-25 11l-143 143q-11 10-11 25t11 25 25 10h72v215h-215v-72q0-14-10-25t-25-11-25 11l-143 143q-11 11-11 25t11 25l143 143q10 11 25 11t25-11 10-25v-72h215v215h-72q-14 0-25 10t-11 25 11 26l143 142q11 11 25 11t25-11l143-142q11-11 11-26t-11-25-25-10h-72v-215h215v72q0 14 10 25t25 11 26-11l142-143q11-10 11-25z" horiz-adv-x="1000" />
<glyph glyph-name="link-ext" unicode="&#xf08e;" d="M786 332v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" />
@ -176,6 +184,8 @@
<glyph glyph-name="upload-cloud" unicode="&#xf0ee;" d="M714 368q0 8-5 13l-196 196q-5 5-13 5t-13-5l-196-196q-5-6-5-13 0-8 5-13t13-5h125v-196q0-8 5-13t12-5h108q7 0 12 5t5 13v196h125q8 0 13 5t5 13z m357-161q0-89-62-151t-152-63h-607q-103 0-177 73t-73 177q0 72 39 134t105 92q-1 17-1 24 0 118 84 202t202 84q87 0 159-49t105-129q40 35 93 35 59 0 101-42t42-101q0-43-23-77 72-17 119-76t46-133z" horiz-adv-x="1071.4" />
<glyph glyph-name="doc-text" unicode="&#xf0f6;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-572 483q0 7 5 12t13 5h393q8 0 13-5t5-12v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36z m411-125q8 0 13-5t5-13v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36q0 8 5 13t13 5h393z m0-143q8 0 13-5t5-13v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36q0 8 5 13t13 5h393z" horiz-adv-x="857.1" />
<glyph glyph-name="reply" unicode="&#xf112;" d="M1000 225q0-93-71-252-1-4-6-13t-7-17-7-12q-7-10-16-10-8 0-13 6t-5 14q0 5 1 15t2 13q3 38 3 69 0 56-10 101t-27 77-45 56-59 39-74 24-86 12-98 3h-125v-143q0-14-10-25t-26-11-25 11l-285 286q-11 10-11 25t11 25l285 286q11 10 25 10t26-10 10-25v-143h125q398 0 488-225 30-75 30-186z" horiz-adv-x="1000" />
<glyph glyph-name="smile" unicode="&#xf118;" d="M633 250q-21-67-77-109t-127-41-128 41-77 109q-4 14 3 27t21 18q14 4 27-2t17-22q14-44 52-72t85-28 84 28 52 72q4 15 18 22t27 2 21-18 2-27z m-276 243q0-30-21-51t-50-21-51 21-21 51 21 50 51 21 50-21 21-50z m286 0q0-30-21-51t-51-21-50 21-21 51 21 50 50 21 51-21 21-50z m143-143q0 73-29 139t-76 114-114 76-138 28-139-28-114-76-76-114-29-139 29-139 76-113 114-77 139-28 138 28 114 77 76 113 29 139z m71 0q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -57,6 +57,8 @@ export default class AddonManager extends Module {
isAddonExternal: id => this.isAddonExternal(id),
enableAddon: id => this.enableAddon(id),
disableAddon: id => this.disableAddon(id),
reloadAddon: id => this.reloadAddon(id),
canReloadAddon: id => this.canReloadAddon(id),
isReloadRequired: () => this.reload_required,
refresh: () => window.location.reload(),
@ -280,6 +282,150 @@ export default class AddonManager extends Module {
return module.external || (module.constructor && module.constructor.external);
}
canReloadAddon(id) {
// Obviously we can't reload it if we don't have it.
if ( ! this.hasAddon(id) )
throw new Error(`Unknown add-on id: ${id}`);
// If the module isn't available, we can't reload it.
let module = this.resolve(`addon.${id}`);
if ( ! module )
return false;
// If the module cannot be disabled, or it cannot be unloaded, then
// we can't reload it.
if ( ! module.canDisable() || ! module.canUnload() )
return false;
// Check each child.
if ( module.children )
for(const child of Object.values(module.children))
if ( ! child.canDisable() || ! child.canUnload() )
return false;
// If we got here, we might be able to reload it.
return true;
}
async fullyUnloadModule(module) {
if ( ! module )
return;
if ( module.children )
for(const child of Object.values(module.children))
await this.fullyUnloadModule(child);
await module.disable();
await module.unload();
// Clean up parent references.
if ( module.parent && module.parent.children[module.name] === module )
delete module.parent.children[module.name];
// Clean up all individual references.
for(const entry of module.references) {
const other = this.resolve(entry[0]),
name = entry[1];
if ( other && other[name] === module )
other[name] = null;
}
// Clean up the global reference.
if ( this.__modules[module.__path] === module )
delete this.__modules[module.__path]; /* = [
module.dependents,
module.load_dependents,
module.references
];*/
// Remove any events we didn't unregister.
this.offContext(null, module);
// Do the same for settings.
for(const ctx of this.settings.__contexts)
ctx.offContext(null, module);
// Clean up all settings.
for(const [key, def] of Array.from(this.settings.definitions.entries())) {
if ( def && def.__source === module.__path ) {
this.settings.remove(key);
}
}
// Clean up the logger too.
module.__log = null;
}
async reloadAddon(id) {
const addon = this.getAddon(id),
button = this.resolve('site.menu_button');
if ( ! addon )
throw new Error(`Unknown add-on id: ${id}`);
const start = performance.now();
// Yeet the module into the abyss.
// This will also yeet all children.
let module = this.resolve(`addon.${id}`);
if ( module )
try {
await this.fullyUnloadModule(module);
} catch(err) {
if ( button )
button.addToast({
title_i18n: 'addons.reload.toast-error',
title: 'Error Reloading Add-On',
text_i18n: 'addons.reload.toast-error.unload',
text: 'Unable to unload existing modules for add-on "{addon_id}":\n\n{error}',
icon: 'ffz-i-attention',
addon_id: id,
error: String(err)
});
throw err;
}
// Is there a script tab?
let el = document.querySelector(`script#ffz-loaded-addon-${addon.id}`);
if ( el )
el.remove();
// Do unnatural things to webpack.
if ( window.ffzAddonsWebpackJsonp )
window.ffzAddonsWebpackJsonp = undefined;
// Now, reload it all~
try {
await this._enableAddon(id);
} catch(err) {
if ( button )
button.addToast({
title_i18n: 'addons.reload.toast-error',
title: 'Error Reloading Add-On',
text_i18n: 'addons.reload.toast-error.reload',
text: 'Unable to load new module for add-on "{addon_id}":\n\n{error}',
error: String(err),
icon: 'ffz-i-attention',
addon_id: id
});
throw err;
}
const end = performance.now();
if ( button )
button.addToast({
title_i18n: 'addons.reload.toast',
title: 'Reloaded Add-On',
text_i18n: 'addons.reload.toast.text',
text: 'Successfully reloaded add-on "{addon_id}" in {duration}ms.',
icon: 'ffz-i-info',
addon_id: id,
timeout: 5000,
duration: Math.round(100 * (end - start)) / 100
});
}
async _enableAddon(id) {
const addon = this.getAddon(id);
if ( ! addon )

View file

@ -1124,6 +1124,8 @@ export default class Actions extends Module {
if ( target._ffz_tooltip )
target._ffz_tooltip.hide();
return data.definition.click.call(this, event, data);
}

View file

@ -499,6 +499,84 @@ export const untimeout = {
}
// ============================================================================
// Mod and Unmod User
// ============================================================================
export const mod = {
presets: [{
appearance: {
type: 'icon',
icon: 'ffz-i-mod'
}
}],
required_context: ['room', 'user'],
title: 'Mod User',
tooltip(data) {
return this.i18n.t('chat.actions.mod.tooltip', 'Mod {user.login}', {user: data.user});
},
hidden(data, message, current_room, current_user, mod_icons, instance) {
// You cannot mod mods.
if ( message.user.type === 'mod' )
return true;
// You cannot mod the broadcaster.
if ( message.user.id === current_room.id )
return true;
// Only the broadcaster can mod, otherwise.
return current_room.id !== current_user.id;
},
click(event, data) {
this.sendMessage(data.room.login, `/mod ${data.user.login}`);
}
};
export const unmod = {
presets: [{
appearance: {
type: 'icon',
icon: 'ffz-i-unmod'
}
}],
required_context: ['room', 'user'],
title: 'Un-Mod User',
tooltip(data) {
return this.i18n.t('chat.actions.unmod.tooltip', 'Un-Mod {user.login}', {user: data.user});
},
hidden(data, message, current_room, current_user, mod_icons, instance) {
// You can only un-mod mods.
if ( message.user.type !== 'mod' )
return true;
// You can unmod yourself.
if ( message.user.id === current_user.id )
return false;
// You cannot unmod the broadcaster.
if ( message.user.id === current_room.id )
return false;
// Only the broadcaster can unmod, otherwise.
return current_room.id !== current_user.id;
},
click(event, data) {
this.sendMessage(data.room.login, `/unmod ${data.user.login}`);
}
};
// ============================================================================
// Whisper
// ============================================================================

View file

@ -23,6 +23,7 @@ export default {
full: null,
unsafe: false,
urls: null,
i18n_prefix: null,
allow_media: false,
allow_unsafe: false
}
@ -114,6 +115,7 @@ export default {
this.fragments = {};
this.unsafe = false;
this.urls = null;
this.i18n_prefix = null;
this.allow_media = false;
this.allow_unsafe = false;
this.load(refresh);
@ -179,6 +181,7 @@ export default {
this.fragments = data.fragments ?? {};
this.unsafe = data.unsafe;
this.urls = data.urls;
this.i18n_prefix = data.i18n_prefix;
this.allow_media = data.allow_media;
this.allow_unsafe = data.allow_unsafe;
},
@ -238,6 +241,7 @@ export default {
i18n: this.getI18n(),
fragments: this.fragments,
i18n_prefix: this.i18n_prefix,
allow_media: this.forceMedia ?? this.allow_media,
allow_unsafe: this.forceUnsafe ?? this.allow_unsafe

View file

@ -197,8 +197,8 @@ export default class Emotes extends Module {
for(const set_id in this.emote_sets)
if ( has(this.emote_sets, set_id) ) {
const emote_set = this.emote_sets[set_id];
if ( emote_set && emote_set.pending_css ) {
this.style.set(`es--${set_id}`, emote_set.pending_css + (emote_set.css || ''));
if ( emote_set && (emote_set.pending_css || emote_set.css) ) {
this.style.set(`es--${set_id}`, (emote_set.pending_css || '') + (emote_set.css || ''));
emote_set.pending_css = null;
}
}
@ -816,6 +816,166 @@ export default class Emotes extends Module {
}
processEmote(emote, set_id) {
if ( ! emote.id || ! emote.name || ! emote.urls )
return null;
emote.set_id = set_id;
emote.src = emote.urls[1];
emote.srcSet = `${emote.urls[1]} 1x`;
if ( emote.urls[2] )
emote.srcSet += `, ${emote.urls[2]} 2x`;
if ( emote.urls[4] )
emote.srcSet += `, ${emote.urls[4]} 4x`;
if ( emote.urls[2] ) {
emote.can_big = true;
emote.src2 = emote.urls[2];
emote.srcSet2 = `${emote.urls[2]} 1x`;
if ( emote.urls[4] )
emote.srcSet2 += `, ${emote.urls[4]} 2x`;
}
if ( emote.animated?.[1] ) {
emote.animSrc = emote.animated[1];
emote.animSrcSet = `${emote.animated[1]} 1x`;
if ( emote.animated[2] ) {
emote.animSrcSet += `, ${emote.animated[2]} 2x`;
emote.animSrc2 = emote.animated[2];
emote.animSrcSet2 = `${emote.animated[2]} 1x`;
if ( emote.animated[4] ) {
emote.animSrcSet += `, ${emote.animated[4]} 4x`;
emote.animSrcSet2 += `, ${emote.animated[4]} 2x`;
}
}
}
emote.token = {
type: 'emote',
id: emote.id,
set: set_id,
provider: 'ffz',
src: emote.src,
srcSet: emote.srcSet,
can_big: !! emote.urls[2],
src2: emote.src2,
srcSet2: emote.srcSet2,
animSrc: emote.animSrc,
animSrcSet: emote.animSrcSet,
animSrc2: emote.animSrc2,
animSrcSet2: emote.animSrcSet2,
text: emote.hidden ? '???' : emote.name,
length: emote.name.length,
height: emote.height
};
if ( has(MODIFIERS, emote.id) )
Object.assign(emote, MODIFIERS[emote.id]);
return emote;
}
addEmoteToSet(set_id, emote) {
const set = this.emote_sets[set_id];
if ( ! set )
throw new Error(`Invalid emote set "${set_id}"`);
let processed = this.processEmote(emote, set_id);
if ( ! processed )
throw new Error("Invalid emote data object.");
// Are we removing an existing emote?
const old_emote = set.emotes[processed.id],
old_css = old_emote && this.generateEmoteCSS(old_emote);
// Store the emote.
set.emotes[processed.id] = processed;
if ( ! old_emote )
set.count++;
// Now we need to update the CSS. If we had old emote CSS, then we
// will need to totally rebuild the CSS.
const style_key = `es--${set_id}`;
if ( old_css && old_css.length ) {
const css = [];
for(const em of Object.values(set.emotes)) {
const emote_css = this.generateEmoteCSS(em);
if ( emote_css && emote_css.length )
css.push(emote_css);
}
if ( this.style && (css.length || set.css) )
this.style.set(style_key, css.join('') + (set.css || ''));
else if ( css.length )
set.pending_css = css.join('');
} else {
const emote_css = this.generateEmoteCSS(processed);
if ( emote_css && emote_css.length ) {
if ( this.style )
this.style.set(style_key, (this.style.get(style_key) || '') + emote_css);
else
set.pending_css = (set.pending_css || '') + emote_css;
}
}
// Send a loaded event because this emote set changed.
this.emit(':loaded', set_id, set);
}
removeEmoteFromSet(set_id, emote_id) {
const set = this.emote_sets[set_id];
if ( ! set )
throw new Error(`Invalid emote set "${set_id}"`);
if ( emote_id && emote_id.id )
emote_id = emote_id.id;
const emote = set.emotes[emote_id];
if ( ! emote )
return;
const emote_css = this.generateEmoteCSS(emote);
const css = (emote_css && emote_css.length) ? [] : null;
// Rebuild the emotes object to avoid gaps.
const new_emotes = {};
let count = 0;
for(const em of Object.values(set.emotes)) {
if ( em.id == emote_id )
continue;
new_emotes[em.id] = em;
count++;
if ( css != null) {
const em_css = this.generateEmoteCSS(em);
if ( em_css && em_css.length )
css.push(em_css);
}
}
set.emotes = new_emotes;
set.count = count;
if ( css != null ) {
const style_key = `es--${set_id}`;
if ( this.style && (css.length || set.css) )
this.style.set(style_key, css.join('') + (set.css || ''));
else if ( css.length )
set.pending_css = css.join('');
}
// Send a loaded event because this emote set changed.
this.emit(':loaded', set_id, set);
}
loadSetData(set_id, data, suppress_log = false) {
const old_set = this.emote_sets[set_id];
if ( ! data ) {
@ -838,70 +998,18 @@ export default class Emotes extends Module {
const bad_emotes = [];
for(const emote of ems) {
if ( ! emote.id || ! emote.name || ! emote.urls ) {
let processed = this.processEmote(emote, set_id);
if ( ! processed ) {
bad_emotes.push(emote);
continue;
}
emote.set_id = set_id;
emote.src = emote.urls[1];
emote.srcSet = `${emote.urls[1]} 1x`;
if ( emote.urls[2] )
emote.srcSet += `, ${emote.urls[2]} 2x`;
if ( emote.urls[4] )
emote.srcSet += `, ${emote.urls[4]} 4x`;
if ( emote.urls[2] ) {
emote.can_big = true;
emote.src2 = emote.urls[2];
emote.srcSet2 = `${emote.urls[2]} 1x`;
if ( emote.urls[4] )
emote.srcSet2 += `, ${emote.urls[4]} 2x`;
}
if ( emote.animated?.[1] ) {
emote.animSrc = emote.animated[1];
emote.animSrcSet = `${emote.animated[1]} 1x`;
if ( emote.animated[2] ) {
emote.animSrcSet += `, ${emote.animated[2]} 2x`;
emote.animSrc2 = emote.animated[2];
emote.animSrcSet2 = `${emote.animated[2]} 1x`;
if ( emote.animated[4] ) {
emote.animSrcSet += `, ${emote.animated[4]} 4x`;
emote.animSrcSet2 += `, ${emote.animated[4]} 2x`;
}
}
}
emote.token = {
type: 'emote',
id: emote.id,
set: set_id,
provider: 'ffz',
src: emote.src,
srcSet: emote.srcSet,
can_big: !! emote.urls[2],
src2: emote.src2,
srcSet2: emote.srcSet2,
animSrc: emote.animSrc,
animSrcSet: emote.animSrcSet,
animSrc2: emote.animSrc2,
animSrcSet2: emote.animSrcSet2,
text: emote.hidden ? '???' : emote.name,
length: emote.name.length,
height: emote.height
};
if ( has(MODIFIERS, emote.id) )
Object.assign(emote, MODIFIERS[emote.id]);
const emote_css = this.generateEmoteCSS(emote);
const emote_css = this.generateEmoteCSS(processed);
if ( emote_css )
css.push(emote_css);
count++;
new_ems[emote.id] = emote;
new_ems[processed.id] = processed;
}
if ( bad_emotes.length )

View file

@ -1668,6 +1668,10 @@ export default class Chat extends Module {
b[item.setID] = item.version;
}
// Validate User Type
if ( user.type == null && msg.badges && msg.badges.moderator )
user.type = 'mod';
// Standardize Timestamp
if ( ! msg.timestamp && msg.sentAt )
msg.timestamp = new Date(msg.sentAt).getTime();
@ -2235,6 +2239,9 @@ export default class Chat extends Module {
}
fixLinkInfo(data) {
if ( ! data )
return data;
if ( data.error && data.message )
data.error = data.message;

View file

@ -11,7 +11,7 @@ const USER_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/([^/]+)$/;
const BAD_USERS = [
'directory', '_deck', 'p', 'downloads', 'jobs', 'turbo', 'settings', 'friends',
'subscriptions', 'inventory', 'wallet'
'subscriptions', 'inventory', 'wallet', 'store', 'drops', 'search', 'prime'
];
import GET_CLIP from './clip_info.gql';

View file

@ -97,6 +97,7 @@ export const Links = {
i18n: this.i18n,
fragments: data.fragments,
i18n_prefix: data.i18n_prefix,
allow_media: show_images,
allow_unsafe: show_unsafe,

View file

@ -5,6 +5,10 @@
<img :src="icon" class="tw-image">
</div>
<div v-if="reloading" class="tw-mg-b-05 ffz-pill">
{{ t('addon.reloading', 'Reloading') }}
</div>
<div v-if="external" class="tw-mg-b-05 ffz-pill">
{{ t('addon.external', 'External') }}
</div>
@ -96,6 +100,20 @@
{{ t('addon.disable', 'Disable') }}
</span>
</button>
<button
v-if="addon.dev && can_reload"
class="tw-button ffz-button--hollow tw-mg-r-1"
:class="{'tw-button--disabled': reloading}"
:disabled="reloading"
@click="reloadAddon()"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-arrows-cw" />
</span>
<span class="tw-button__text">
{{ t('addon.reload', 'Reload') }}
</span>
</button>
<button
v-if="addon.settings"
class="tw-button ffz-button--hollow tw-mg-r-1"
@ -151,6 +169,8 @@ export default {
data() {
return {
enabled: this.item.isAddonEnabled(this.id),
can_reload: this.addon.dev && this.item.canReloadAddon(this.id),
reloading: false,
external: this.item.isAddonExternal(this.id),
version: this.item.getVersion(this.id),
expanded: false
@ -251,6 +271,19 @@ export default {
list.push(`add_ons.${this.addon.name.toSnakeCase()}`);
this.$emit('navigate', ...list);
},
reloadAddon() {
this.reloading = true;
this.item.reloadAddon(this.id)
.then(() => {
this.reloading = false;
this.can_reload = this.item.canReloadAddon(this.id);
})
.catch(err => {
console.error(err);
this.reloading = false;
});
}
}
}

View file

@ -4,6 +4,117 @@
{{ t('setting.experiments.about', 'This feature allows you to override experiment values. Please note that, for most experiments, you may have to refresh the page for your changes to take effect.') }}
</div>
<div class="tw-mg-b-2 tw-flex tw-align-items-center">
<div class="tw-flex-grow-1">
{{ t('setting.experiments.unique-id', 'Unique ID: {id}', {id: unique_id}) }}
</div>
<select
ref="sort_select"
class="tw-border-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-x-05"
@change="onSort"
>
<option :selected="sort_by === 0">
{{ t('setting.experiments.sort-name', 'Sort By: Name') }}
</option>
<option :selected="sort_by === 1">
{{ t('setting.experiments.sort-rarity', 'Sort By: Rarity') }}
</option>
</select>
</div>
<div class="tw-mg-b-2 tw-flex tw-align-items-center">
<div class="tw-flex-grow-1" />
<div class="ffz-checkbox tw-relative">
<input
id="unused"
ref="unused"
v-model="unused"
type="checkbox"
class="ffz-checkbox__input"
>
<label for="unused" class="ffz-checkbox__label">
<span class="tw-mg-l-1">
{{ t('setting.experiments.show-unused', 'Display unused experiments.') }}
</span>
</label>
</div>
</div>
<h3 class="tw-mg-b-1">
<span>
{{ t('setting.experiments.ffz', 'FrankerFaceZ Experiments') }}
</span>
<span v-if="filter" class="tw-mg-l-1 tw-font-size-base tw-regular tw-c-text-alt-2">
{{ t('setting.experiments.visible', '(Showing {visible,number} of {total,number})', {
visible: visible_ffz.length,
total: sorted_ffz.length
}) }}
</span>
</h3>
<div class="ffz--experiment-list">
<section
v-for="({key, exp}) of visible_ffz"
:key="key"
:data-key="key"
>
<div class="tw-elevation-1 tw-c-background-base tw-border tw-pd-y-05 tw-pd-x-1 tw-mg-y-05 tw-flex tw-flex-nowrap">
<div class="tw-flex-grow-1">
<h4>{{ exp.name }}</h4>
<div v-if="exp.description" class="description">
{{ exp.description }}
</div>
</div>
<div class="tw-flex tw-flex-shrink-0 tw-align-items-start">
<select
:data-key="key"
class="tw-border-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-x-05"
@change="onChange($event)"
>
<option
v-for="(i, idx) in exp.groups"
:key="idx"
:selected="i.value === exp.value"
>
{{ t('setting.experiments.entry', '{value,tostring} (weight: {weight,tostring})', i) }}
</option>
</select>
<button
:disabled="exp.default"
:class="{'tw-button--disabled': exp.default}"
class="tw-mg-t-05 tw-button tw-button--text ffz-il-tooltip__container"
@click="reset(key)"
>
<span class="tw-button__text ffz-i-cancel" />
<span class="ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-right">
{{ t('setting.reset', 'Reset to Default') }}
</span>
</button>
</div>
</div>
</section>
<div v-if="! Object.keys(ffz_data).length">
{{ t('setting.experiments.none', 'There are no current experiments.') }}
</div>
<div v-else-if="! visible_ffz.length">
{{ t('setting.experiments.none-filter', 'There are no matching experiments.') }}
</div>
</div>
<h3 class="tw-mg-t-5 tw-mg-b-1">
<span>
{{ t('setting.experiments.twitch', 'Twitch Experiments') }}
</span>
<span v-if="experiments_locked && filter" class="tw-mg-l-1 tw-font-size-base tw-regular tw-c-text-alt-2">
{{ t('setting.experiments.visible', '(Showing {visible,number} of {total,number})', {
visible: visible_twitch.length,
total: sorted_twitch.length
}) }}
</span>
</h3>
<section v-if="experiments_locked">
<div class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-2">
<h3 class="ffz-i-attention">
@ -16,7 +127,7 @@
<input
ref="code"
type="text"
class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05"
class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05 tw-mg-b-5"
autocapitalize="off"
autocorrect="off"
@keydown.enter="enterCode"
@ -25,117 +136,6 @@
</section>
<section v-else>
<div class="tw-mg-b-2 tw-flex tw-align-items-center">
<div class="tw-flex-grow-1">
{{ t('setting.experiments.unique-id', 'Unique ID: {id}', {id: unique_id}) }}
</div>
<select
ref="sort_select"
class="tw-border-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-x-05"
@change="onSort"
>
<option :selected="sort_by === 0">
{{ t('setting.experiments.sort-name', 'Sort By: Name') }}
</option>
<option :selected="sort_by === 1">
{{ t('setting.experiments.sort-rarity', 'Sort By: Rarity') }}
</option>
</select>
</div>
<div class="tw-mg-b-2 tw-flex tw-align-items-center">
<div class="tw-flex-grow-1" />
<div class="ffz-checkbox tw-relative">
<input
id="unused"
ref="unused"
v-model="unused"
type="checkbox"
class="ffz-checkbox__input"
>
<label for="unused" class="ffz-checkbox__label">
<span class="tw-mg-l-1">
{{ t('setting.experiments.show-unused', 'Display unused experiments.') }}
</span>
</label>
</div>
</div>
<h3 class="tw-mg-b-1">
<span>
{{ t('setting.experiments.ffz', 'FrankerFaceZ Experiments') }}
</span>
<span v-if="filter" class="tw-mg-l-1 tw-font-size-base tw-regular tw-c-text-alt-2">
{{ t('setting.experiments.visible', '(Showing {visible,number} of {total,number})', {
visible: visible_ffz.length,
total: sorted_ffz.length
}) }}
</span>
</h3>
<div class="ffz--experiment-list">
<section
v-for="({key, exp}) of visible_ffz"
:key="key"
:data-key="key"
>
<div class="tw-elevation-1 tw-c-background-base tw-border tw-pd-y-05 tw-pd-x-1 tw-mg-y-05 tw-flex tw-flex-nowrap">
<div class="tw-flex-grow-1">
<h4>{{ exp.name }}</h4>
<div v-if="exp.description" class="description">
{{ exp.description }}
</div>
</div>
<div class="tw-flex tw-flex-shrink-0 tw-align-items-start">
<select
:data-key="key"
class="tw-border-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-x-05"
@change="onChange($event)"
>
<option
v-for="(i, idx) in exp.groups"
:key="idx"
:selected="i.value === exp.value"
>
{{ t('setting.experiments.entry', '{value,tostring} (weight: {weight,tostring})', i) }}
</option>
</select>
<button
:disabled="exp.default"
:class="{'tw-button--disabled': exp.default}"
class="tw-mg-t-05 tw-button tw-button--text ffz-il-tooltip__container"
@click="reset(key)"
>
<span class="tw-button__text ffz-i-cancel" />
<span class="ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-right">
{{ t('setting.reset', 'Reset to Default') }}
</span>
</button>
</div>
</div>
</section>
<div v-if="! Object.keys(ffz_data).length">
{{ t('setting.experiments.none', 'There are no current experiments.') }}
</div>
<div v-else-if="! visible_ffz.length">
{{ t('setting.experiments.none-filter', 'There are no matching experiments.') }}
</div>
</div>
<h3 class="tw-mg-t-5 tw-mg-b-1">
<span>
{{ t('setting.experiments.twitch', 'Twitch Experiments') }}
</span>
<span v-if="filter" class="tw-mg-l-1 tw-font-size-base tw-regular tw-c-text-alt-2">
{{ t('setting.experiments.visible', '(Showing {visible,number} of {total,number})', {
visible: visible_twitch.length,
total: sorted_twitch.length
}) }}
</span>
</h3>
<div class="ffz--experiment-list">
<section
v-for="({key, exp}) of visible_twitch"

View file

@ -215,7 +215,12 @@ export default class MainMenu extends Module {
this.on('settings:added-definition', (key, definition) => {
this._addDefinitionToTree(key, definition);
this.scheduleUpdate();
})
});
this.on('settings:removed-definition', key => {
this._removeDefinitionFromTree(key);
this.scheduleUpdate();
});
this.on('socket:command:new_version', version => {
if ( version === window.FrankerFaceZ.version_info.commit )
@ -361,6 +366,7 @@ export default class MainMenu extends Module {
this.log.info('Context proxy gone.');
this.updateContext({proxied: false});
}
});
try {
@ -508,6 +514,58 @@ export default class MainMenu extends Module {
}
_removeDefinitionFromTree(key) {
if ( ! this._settings_tree )
return;
let page;
for(const val of Object.values(this._settings_tree)) {
if ( ! val || ! Array.isArray(val.settings) )
continue;
for(let i = 0; i < val.settings.length; i++) {
const entry = val.settings[i];
if ( entry && entry[0] === key ) {
val.settings.splice(i, 1);
page = val;
break;
}
}
if ( page )
break;
}
// Was it found?
if ( ! page )
return;
this._maybeDeleteSection(page);
}
_maybeDeleteSection(page) {
// Is the section empty?
if ( page.settings && page.settings.length )
return;
const id = page.full_key;
// Check for children.
for(const val of Object.values(this._settings_tree)) {
if ( val.parent === id )
return;
}
// Nope~
delete this._settings_tree[id];
if ( page.parent ) {
const parent = this._settings_tree[page.parent];
if ( parent )
this._maybeDeleteSection(parent);
}
}
_addDefinitionToTree(key, def) {
if ( ! def.ui || ! this._settings_tree )
return;

View file

@ -941,11 +941,45 @@ export default class SettingsManager extends Module {
setContext(context) { return this.main_context.setContext(context) }
// ========================================================================
// Add-On Proxy
// ========================================================================
getAddonProxy(module) {
const path = module.__path;
const add = (key, definition) => {
return this.add(key, definition, path);
}
const addUI = (key, definition) => {
return this.addUI(key, definition, path);
}
const addClearable = (key, definition) => {
return this.addClearable(key, definition, path);
}
const handler = {
get(obj, prop) {
if ( prop === 'add' )
return add;
if ( prop === 'addUI' )
return addUI;
if ( prop === 'addClearable' )
return addClearable;
return Reflect.get(...arguments);
}
}
return new Proxy(this, handler);
}
// ========================================================================
// Definitions
// ========================================================================
add(key, definition) {
add(key, definition, source) {
if ( typeof key === 'object' ) {
for(const k in key)
if ( has(key, k) )
@ -960,6 +994,8 @@ export default class SettingsManager extends Module {
definition.required_by = required_by;
definition.requires = definition.requires || [];
definition.__source = source;
for(const req_key of definition.requires) {
const req = this.definitions.get(req_key);
if ( ! req )
@ -1007,7 +1043,42 @@ export default class SettingsManager extends Module {
}
addUI(key, definition) {
remove(key) {
const definition = this.definitions.get(key);
if ( ! definition )
return;
// If the definition is an array, we're already not defined.
if ( Array.isArray(definition) )
return;
// Remove this definition from the definitions list.
if ( Array.isArray(definition.required_by) && definition.required_by.length > 0 )
this.definitions.set(key, definition.required_by);
else
this.definitions.delete(key);
// Remove it from all the things it required.
if ( Array.isArray(definition.requires) )
for(const req_key of definition.requires) {
let req = this.definitions.get(req_key);
if ( req.required_by )
req = req.required_by;
if ( Array.isArray(req) ) {
const idx = req.indexOf(key);
if ( idx !== -1 )
req.splice(idx, 1);
}
}
if ( definition.changed )
this.off(`:changed:${key}`, definition.changed);
this.emit(':removed-definition', key, definition);
}
addUI(key, definition, source) {
if ( typeof key === 'object' ) {
for(const k in key)
if ( has(key, k) )
@ -1018,6 +1089,8 @@ export default class SettingsManager extends Module {
if ( ! definition.ui )
definition = {ui: definition};
definition.__source = source;
const ui = definition.ui;
ui.path_tokens = ui.path_tokens ?
format_path_tokens(ui.path_tokens) :
@ -1038,14 +1111,16 @@ export default class SettingsManager extends Module {
}
addClearable(key, definition) {
addClearable(key, definition, source) {
if ( typeof key === 'object' ) {
for(const k in key)
if ( has(key, k) )
this.addClearable(k, key[k]);
this.addClearable(k, key[k], source);
return;
}
definition.__source = source;
this.clearables[key] = definition;
}

View file

@ -276,6 +276,24 @@ export default class ChatHook extends Module {
// Settings
this.settings.add('chat.disable-handling', {
default: null,
requires: ['context.disable-chat-processing'],
process(ctx, val) {
if ( val != null )
return ! val;
if ( ctx.get('context.disable-chat-processing') )
return true;
return false;
},
ui: {
path: 'Debugging > Chat >> Processing',
title: 'Enable processing of chat messages.',
component: 'setting-check-box',
force_seen: true
}
});
this.settings.addUI('debug.chat-test', {
path: 'Debugging > Chat >> Chat',
component: 'chat-tester',
@ -887,6 +905,11 @@ export default class ChatHook extends Module {
}
updateDisableHandling() {
this.disable_handling = this.chat.context.get('chat.disable-handling');
}
onEnable() {
this.on('site.web_munch:loaded', this.grabTypes);
this.on('site.web_munch:loaded', this.defineClasses);
@ -909,6 +932,8 @@ export default class ChatHook extends Module {
this.chat.context.on('changed:chat.banners.prediction', this.cleanHighlights, this);
this.chat.context.on('changed:chat.banners.drops', this.cleanHighlights, this);
this.chat.context.on('changed:chat.disable-handling', this.updateDisableHandling, this);
this.chat.context.on('changed:chat.subs.gift-banner', () => this.GiftBanner.forceUpdate(), this);
this.chat.context.on('changed:chat.effective-width', this.updateChatCSS, this);
this.settings.main_context.on('changed:chat.use-width', this.updateChatCSS, this);
@ -992,6 +1017,7 @@ export default class ChatHook extends Module {
this.chat.context.getChanges('chat.input.show-elevate-your-message', val =>
this.css_tweaks.toggleHide('elevate-your-message', ! val));
this.updateDisableHandling();
this.updateChatCSS();
this.updateColors();
this.updateLineBorders();
@ -1325,7 +1351,7 @@ export default class ChatHook extends Module {
});
this.subpump.on(':pubsub-message', event => {
if ( event.prefix !== 'community-points-channel-v1' )
if ( event.prefix !== 'community-points-channel-v1' || this.disable_handling )
return;
const service = this.ChatService.first,
@ -2186,7 +2212,7 @@ export default class ChatHook extends Module {
const old_announce = this.onAnnouncementEvent;
this.onAnnouncementEvent = function(e) {
console.log('announcement', e);
//console.log('announcement', e);
return old_announce.call(this, e);
}
@ -2197,6 +2223,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('Subscription') )
return;
if ( t.disable_handling )
return old_sub.call(i, e);
if ( t.chat.context.get('chat.subs.show') < 3 )
return;
@ -2236,6 +2265,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('Resubscription') )
return;
if ( t.disable_handling )
return old_resub.call(i, e);
if ( t.chat.context.get('chat.subs.show') < 2 && ! e.body )
return;
@ -2267,6 +2299,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('SubGift') )
return;
if ( t.disable_handling )
return old_subgift.call(i, e);
const key = `${e.channel}:${e.user.userID}`,
mystery = mysteries[key];
@ -2316,6 +2351,9 @@ export default class ChatHook extends Module {
const old_communityintro = this.onCommunityIntroductionEvent;
this.onCommunityIntroductionEvent = function(e) {
try {
if ( t.disable_handling )
return old_communityintro.call(this, e);
if ( t.chat.context.get('chat.filtering.blocked-types').has('CommunityIntroduction') ) {
const out = i.convertMessage(e);
return i.postMessageToCurrentChannel(e, out);
@ -2335,6 +2373,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('AnonSubGift') )
return;
if ( t.disable_handling )
return old_anonsubgift.call(i, e);
const key = `${e.channel}:ANON`,
mystery = mysteries[key];
@ -2388,6 +2429,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('SubMysteryGift') )
return;
if ( t.disable_handling )
return old_submystery.call(i, e);
let mystery = null;
if ( e.massGiftCount > t.chat.context.get('chat.subs.merge-gifts') ) {
const key = `${e.channel}:${e.user.userID}`;
@ -2423,6 +2467,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('AnonSubMysteryGift') )
return;
if ( t.disable_handling )
return old_anonsubmystery.call(i, e);
let mystery = null;
if ( e.massGiftCount > t.chat.context.get('chat.subs.merge-gifts') ) {
const key = `${e.channel}:ANON`;
@ -2457,6 +2504,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('Ritual') )
return;
if ( t.disable_handling )
return old_ritual.call(i, e);
const out = i.convertMessage(e);
out.ffz_type = 'ritual';
out.ritual = e.type;
@ -2475,6 +2525,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('ChannelPointsReward') )
return;
if ( t.disable_handling )
return old_points.call(i, e);
const reward = e.rewardID && get(e.rewardID, i.props.rewardMap);
if ( reward ) {
const out = i.convertMessage(e);

View file

@ -39,6 +39,12 @@ export default class ChatLine extends Module {
this.line_types = {};
this.line_types.unknown = {
renderNotice: (msg, current_user, room, inst, e) => {
return `Unknown message type: ${msg.ffz_type}`
}
};
this.line_types.cheer = {
renderNotice: (msg, current_user, room, inst, e) => {
return this.i18n.tList(
@ -741,6 +747,9 @@ other {# messages were deleted by a moderator.}
if ( ! type && msg.bits > 0 && t.chat.context.get('chat.bits.cheer-notice') )
type = t.line_types.cheer;
if ( ! type && msg.ffz_type )
type = t.line_types.unknown;
if ( type ) {
if ( type.render )
return type.render(msg, current_user, current_room, this, e);

View file

@ -192,6 +192,7 @@ export default class RichContent extends Module {
i18n: t.i18n,
fragments: this.state.fragments,
i18n_prefix: this.state.i18n_prefix,
allow_media: t.chat.context.get('tooltip.link-images'),
allow_unsafe: t.chat.context.get('tooltip.link-nsfw-images')

View file

@ -8,6 +8,13 @@ export class Addon extends Module {
this.inject('settings');
}
__processModule(module, name) {
if ( module.getAddonProxy )
return module.getAddonProxy(this);
return module;
}
static register(id, info) {
if ( typeof id === 'object' ) {
info = id;

View file

@ -271,6 +271,13 @@ export class ManagedStyle {
this._style = null;
}
get(key) {
const block = this._blocks[key];
if ( block )
return block.textContent;
return undefined;
}
set(key, value, force) {
const block = this._blocks[key];
if ( block ) {

View file

@ -132,6 +132,36 @@ export class EventEmitter {
this.__dead_events++;
}
offContext(event, ctx) {
if ( event == null ) {
for(const evt in Object.keys(this.__listeners)) {
if ( ! this.__running.has(evt) )
this.offContext(evt, ctx);
}
return;
}
if ( this.__running.has(event) )
throw new Error(`concurrent modification: tried removing event listener while event is running`);
let list = this.__listeners[event];
if ( ! list )
return;
if ( ! fn )
list = null;
else {
list = list.filter(x => x && x[1] !== ctx);
if ( ! list.length )
list = null;
}
this.__listeners[event] = list;
if ( ! list )
this.__dead_events++;
}
events() {
this.__cleanListeners();
return Object.keys(this.__listeners);

View file

@ -106,5 +106,10 @@ export default [
"right-open",
"list-bullet",
"mastodon",
"volume-up"
"volume-up",
"unmod",
"mod",
"flag",
"mange-suspicious",
"doc-text"
];

View file

@ -111,6 +111,13 @@ export class Module extends EventEmitter {
return this.__disable(args, this.__path, []);
}
canUnload() {
return this.__canUnload(this.__path, []);
}
canDisable() {
return this.__canDisable(this.__path, []);
}
__load(args, initial, chain) {
const path = this.__path || this.name,
@ -172,6 +179,43 @@ export class Module extends EventEmitter {
}
__canUnload(initial, chain) {
const path = this.__path || this.name,
state = this.__load_state;
if ( chain.includes(this) )
throw new CyclicDependencyError(`cyclic load requirements when checking if can unload ${initial}`, [...chain, this]);
else if ( this.load_dependents ) {
chain.push(this);
for(const dep of this.load_dependents) {
const module = this.resolve(dep);
if ( module ) {
if ( chain.includes(module) )
throw new CyclicDependencyError(`cyclic load requirements when checking if can unload ${initial}`, [...chain, this, module]);
if ( ! module.__canUnload(initial, Array.from(chain)) )
return false;
}
}
}
if ( state === State.UNLOADING )
return true;
else if ( state === State.UNLOADED )
return true;
else if ( this.onLoad && ! this.onUnload )
return false;
else if ( state === State.LOADING )
return false;
return true;
}
__unload(args, initial, chain) {
const path = this.__path || this.name,
state = this.__load_state;
@ -193,7 +237,7 @@ export class Module extends EventEmitter {
else if ( state === State.UNLOADED )
return Promise.resolve();
else if ( ! this.onUnload )
else if ( this.onLoad && ! this.onUnload )
return Promise.reject(new ModuleError(`attempted to unload module ${path} but module cannot be unloaded`));
else if ( state === State.LOADING )
@ -220,7 +264,9 @@ export class Module extends EventEmitter {
}
this.__time('unload-self');
return this.onUnload(...args);
if ( this.onUnload )
return this.onUnload(...args);
return null;
})().then(ret => {
this.__load_state = State.UNLOADED;
@ -307,6 +353,40 @@ export class Module extends EventEmitter {
}
__canDisable(initial, chain) {
const path = this.__path || this.name,
state = this.__state;
if ( chain.includes(this) )
throw new CyclicDependencyError(`cyclic load requirements when checking if can disable ${initial}`, [...chain, this]);
else if ( this.dependents ) {
chain.push(this);
for(const dep of this.dependents) {
const module = this.resolve(dep);
if ( module ) {
if ( chain.includes(module) )
throw new CyclicDependencyError(`cyclic load requirements when checking if can disable ${initial}`, [...chain, this, module]);
if ( ! module.__canDisable(initial, Array.from(chain)) )
return false;
}
}
}
if ( state === State.DISABLING || state === State.DISABLED )
return true;
else if ( ! this.onDisable )
return false;
else if ( state === State.ENABLING )
return false;
return true;
}
__disable(args, initial, chain) {
const path = this.__path || this.name,
state = this.__state;
@ -516,6 +596,11 @@ export class Module extends EventEmitter {
if ( this.enabled && ! module.enabled )
module.enable();
module.references.push([this.__path, name]);
if ( this.__processModule )
module = this.__processModule(module, name);
return this[name] = module;
}
@ -569,9 +654,15 @@ export class Module extends EventEmitter {
if ( require )
requires.push(module.abs_path('.'));
if ( this.enabled && ! module.enabled )
module.enable();
module.references.push([this.__path, variable]);
if ( this.__processModule )
module = this.__processModule(module, name);
return this[variable] = module;
}
@ -600,6 +691,7 @@ export class Module extends EventEmitter {
inst.dependents = dependents[0];
inst.load_dependents = dependents[1];
inst.references = dependents[2];
if ( inst instanceof SiteModule && ! requires.includes('site') )
requires.push('site');

View file

@ -8,7 +8,7 @@ import {has} from 'utilities/object';
import Markdown from 'markdown-it';
import MILA from 'markdown-it-link-attributes';
export const VERSION = 6;
export const VERSION = 7;
export const TOKEN_TYPES = {};
@ -282,6 +282,34 @@ TOKEN_TYPES.box = function(token, createElement, ctx) {
style['--ffz-lines'] = token.lines;
}
if ( token.border )
classes.push('tw-border');
if ( token.rounding ) {
const round = getRoundClass(token.rounding);
if ( round )
classes.push(round);
}
if ( token.background ) {
if ( token.background === 'text' )
style.backgroundColor = `var(--color-text-base)`;
else if ( token.background === 'text-alt' )
style.backgroundColor = `var(--color-text-alt)`;
else if ( token.background === 'text-alt-2' )
style.backgroundColor = `var(--color-text-alt-2)`;
else if ( VALID_COLORS.includes(token.background) )
classes.push(`tw-c-background-${token.background}`);
else
style.backgroundColor = token.background;
}
if ( token.width )
style.width = token.width;
if ( token.height )
style.height = token.height;
applySpacing('pd', token, classes, style);
applySpacing('mg', token, classes, style);
@ -338,7 +366,8 @@ TOKEN_TYPES.fieldset = function(token, createElement, ctx) {
const name = renderTokens(field.name, createElement, ctx, token.markdown),
value = renderTokens(field.value, createElement, ctx, token.markdown);
value = renderTokens(field.value, createElement, ctx, token.markdown),
icon = renderTokens(field.icon, createElement, ctx, token.markdown);
if ( name == null || value == null )
continue;
@ -347,20 +376,19 @@ TOKEN_TYPES.fieldset = function(token, createElement, ctx) {
fields.push(createElement('div', {
class: [
'ffz--field',
field.inline ? 'ffz--field-inline' : false
field.inline ? 'ffz--field-inline' : false,
icon ? 'ffz--field-icon' : false
]
}, [
createElement('div', {
class: 'ffz--field__name tw-semibold'
}, name),
createElement('div', {
class: 'ffz--field__value tw-c-text-alt'
}, value)
createElement('div', {class: 'ffz--field__icon'}, icon),
createElement('div', {class: 'ffz--field__name tw-semibold'}, name),
createElement('div', {class: 'ffz--field__value tw-c-text-alt'}, value)
]));
else
fields.push(createElement('div', {
className: `ffz--field ${field.inline ? 'ffz--field-inline' : ''}`
className: `ffz--field ${field.inline ? 'ffz--field-inline' : ''} ${icon ? 'ffz--field-icon' : ''}`
}, [
createElement('div', {className: 'ffz--field__icon'}, icon),
createElement('div', {className: 'ffz--field__name tw-semibold'}, name),
createElement('div', {className: 'ffz--field__value tw-c-text-alt'}, value)
]));
@ -531,6 +559,7 @@ TOKEN_TYPES.gallery = function(token, createElement, ctx) {
function header_vue(token, h, ctx) {
let content = [];
let background;
if ( token.title ) {
const out = renderWithCapture(token.title, h, ctx, token.markdown);
@ -569,6 +598,25 @@ function header_vue(token, h, ctx) {
]
}, content);
let bgtoken = resolveToken(token.sfw_background, ctx);
const nsfw_bg_token = resolveToken(token.background, ctx);
if ( nsfw_bg_token && canShowImage(nsfw_bg_token, ctx) )
bgtoken = nsfw_bg_token;
if ( bgtoken ) {
if ( bgtoken.type === 'image' )
background = render_image({
...bgtoken,
aspect: undefined
}, h, ctx);
else if ( bgtoken.type === 'icon' )
background = h('figure', {
class: `ffz-i-${bgtoken.name}`
});
else
background = renderWithCapture(token.background, h, ctx, token.markdown).content;
}
let imtok = resolveToken(token.sfw_image, ctx);
const nsfw_token = resolveToken(token.image, ctx);
if ( nsfw_token && canShowImage(nsfw_token, ctx) )
@ -576,11 +624,19 @@ function header_vue(token, h, ctx) {
if ( imtok ) {
const aspect = imtok.aspect;
let image;
if ( imtok.type === 'image' )
image = render_image({
...imtok,
aspect: undefined
}, h, ctx);
if ( imtok.type === 'icon' )
image = h('figure', {
class: `ffz-i-${imtok.name}`
});
let image = render_image({
...imtok,
aspect: undefined
}, h, ctx);
const right = token.image_side === 'right';
if ( image ) {
@ -626,11 +682,24 @@ function header_vue(token, h, ctx) {
content
]);
if ( background )
content = h('div', {
class: 'ffz--rich-header--background'
}, [
h('div', {
class: 'ffz--rich-header__background'
}, [
background
]),
content
]);
return content;
}
function header_normal(token, createElement, ctx) {
let content = [];
let background;
if ( token.title ) {
const out = renderWithCapture(token.title, createElement, ctx, token.markdown);
@ -656,6 +725,25 @@ function header_normal(token, createElement, ctx) {
}, out.content));
}
let bgtoken = resolveToken(token.sfw_background, ctx);
const nsfw_bg_token = resolveToken(token.background, ctx);
if ( nsfw_bg_token && canShowImage(nsfw_bg_token, ctx) )
bgtoken = nsfw_bg_token;
if ( bgtoken ) {
if ( bgtoken.type === 'image' )
background = render_image({
...bgtoken,
aspect: undefined
}, createElement, ctx);
else if ( bgtoken.type === 'icon' )
background = createElement('figure', {
className: `ffz-i-${bgtoken.name}`
});
else
background = renderWithCapture(token.background, createElement, ctx, token.markdown).content;
}
content = createElement('div', {
className: `tw-flex tw-full-width tw-overflow-hidden ${token.compact ? 'ffz--rich-header ffz--compact-header tw-align-items-center' : 'tw-justify-content-center tw-flex-column tw-flex-grow-1'}`
}, content);
@ -668,10 +756,19 @@ function header_normal(token, createElement, ctx) {
if ( imtok ) {
const aspect = imtok.aspect;
let image = render_image({
...imtok,
aspect: undefined
}, createElement, ctx);
let image;
if ( imtok.type === 'image' )
image = render_image({
...imtok,
aspect: undefined
}, createElement, ctx);
if ( imtok.type === 'icon' )
image = createElement('figure', {
className: `ffz-i-${imtok.name}`
});
const right = token.image_side === 'right';
if ( image ) {
@ -718,6 +815,16 @@ function header_normal(token, createElement, ctx) {
content
]);
if ( background )
content = createElement('div', {
className: 'ffz--rich-header--background'
}, [
createElement('div', {
className: 'ffz--rich-header__background'
}, background),
content
]);
return content;
}
@ -783,6 +890,9 @@ function render_image(token, createElement, ctx) {
}
};
if ( token.contain )
stuff.style.objectFit = 'contain';
if ( ctx.onload )
stuff.on = {load: ctx.onload};
@ -811,6 +921,9 @@ function render_image(token, createElement, ctx) {
}
});
if ( token.contain )
image.style.objectFit = 'contain';
if ( ! aspect )
return image;
@ -840,8 +953,12 @@ TOKEN_TYPES.i18n = function(token, createElement, ctx) {
return null;
}
let key = token.key;
if ( ctx.i18n_prefix )
key = `${ctx.i18n_prefix}.${key}`;
return renderTokens(
ctx.i18n.tList(token.key, token.phrase, token.content),
ctx.i18n.tList(key, token.phrase, token.content),
createElement,
ctx,
token.markdown

View file

@ -48,6 +48,70 @@
margin-right: -0.5rem;
}
.ffz--chat-card {
--ffz-rich-header-outline: var(--color-background-base);
.ffz--rich-header--background {
margin: 0; // overflow hidden is in play
}
}
.ffz__tooltip {
--ffz-rich-header-outline: var(--color-background-tooltip);
.ffz--rich-header--background {
margin: -.8rem;
}
}
.ffz--rich-header--background {
position: relative;
overflow: hidden;
--ffz-rich-header-outline: #000;
--color-background-base: #000;
--color-text-base: #efeff1;
--color-text-alt: #dedee3;
--color-text-alt-2: #adadb8;
--color-background-tooltip: var(--color-background-base);
--color-text-tooltip: var(--color-text-base);
--color-text-tooltip-alt: var(--color-text-alt);
--color-text-tooltip-alt-2: var(--color-text-alt-2);
padding: 1rem;
margin: -1rem;
margin-bottom: 0 !important;
background: var(--color-background-base);
text-shadow: -1px 1px 2px var(--ffz-rich-header-outline),
1px 1px 2px var(--ffz-rich-header-outline),
1px -1px 0 var(--ffz-rich-header-outline),
-1px -1px 0 var(--ffz-rich-header-outline);
& > * {
position: relative;
z-index: 1;
}
.ffz--rich-header__background {
position: absolute !important;
z-index: 0 !important;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0.5;
& > img {
height: 100%;
width: 100%;
object-fit: cover;
}
}
}
.ffz--overlay {
position: relative;
@ -132,6 +196,11 @@
height: 4.8rem;
max-width: 25%;
figure {
line-height: 4.8rem;
font-size: 2.4rem;
}
img {
object-fit: contain;
height: 100%;
@ -145,6 +214,12 @@
.ffz--compact-header .ffz--header-image {
height: 2.4rem;
figure {
line-height: 2.4rem;
font-size: 1.6rem;
}
}
.ffz--rich-gallery, .ffz--compact-header {
@ -200,6 +275,18 @@
width: unset;
min-width: 150px;
}
.ffz--field-icon {
position: relative;
padding-left: 2.5rem;
}
}
.ffz--field__icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
}
.ffz--twitter-badge {

View file

@ -72,6 +72,10 @@
.ffz-i-right-open:before { content: '\e846'; } /* '' */
.ffz-i-mastodon:before { content: '\e847'; } /* '' */
.ffz-i-volume-up:before { content: '\e848'; } /* '' */
.ffz-i-unmod:before { content: '\e849'; } /* '' */
.ffz-i-mod:before { content: '\e84a'; } /* '' */
.ffz-i-flag:before { content: '\e84b'; } /* '' */
.ffz-i-mange-suspicious:before { content: '\e84c'; } /* '' */
.ffz-i-move:before { content: '\f047'; } /* '' */
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
.ffz-i-twitter:before { content: '\f099'; } /* '' */
@ -84,6 +88,7 @@
.ffz-i-chat-empty:before { content: '\f0e6'; } /* '' */
.ffz-i-download-cloud:before { content: '\f0ed'; } /* '' */
.ffz-i-upload-cloud:before { content: '\f0ee'; } /* '' */
.ffz-i-doc-text:before { content: '\f0f6'; } /* '' */
.ffz-i-reply:before { content: '\f112'; } /* '' */
.ffz-i-smile:before { content: '\f118'; } /* '' */
.ffz-i-keyboard:before { content: '\f11c'; } /* '' */

File diff suppressed because one or more lines are too long

View file

@ -72,6 +72,10 @@
.ffz-i-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe846;&nbsp;'); }
.ffz-i-mastodon { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe847;&nbsp;'); }
.ffz-i-volume-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe848;&nbsp;'); }
.ffz-i-unmod { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe849;&nbsp;'); }
.ffz-i-mod { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84a;&nbsp;'); }
.ffz-i-flag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84b;&nbsp;'); }
.ffz-i-mange-suspicious { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84c;&nbsp;'); }
.ffz-i-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf047;&nbsp;'); }
.ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf099;&nbsp;'); }
@ -84,6 +88,7 @@
.ffz-i-chat-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e6;&nbsp;'); }
.ffz-i-download-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ed;&nbsp;'); }
.ffz-i-upload-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ee;&nbsp;'); }
.ffz-i-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0f6;&nbsp;'); }
.ffz-i-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf112;&nbsp;'); }
.ffz-i-smile { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf118;&nbsp;'); }
.ffz-i-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf11c;&nbsp;'); }

View file

@ -83,6 +83,10 @@
.ffz-i-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe846;&nbsp;'); }
.ffz-i-mastodon { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe847;&nbsp;'); }
.ffz-i-volume-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe848;&nbsp;'); }
.ffz-i-unmod { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe849;&nbsp;'); }
.ffz-i-mod { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84a;&nbsp;'); }
.ffz-i-flag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84b;&nbsp;'); }
.ffz-i-mange-suspicious { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84c;&nbsp;'); }
.ffz-i-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf047;&nbsp;'); }
.ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf099;&nbsp;'); }
@ -95,6 +99,7 @@
.ffz-i-chat-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e6;&nbsp;'); }
.ffz-i-download-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ed;&nbsp;'); }
.ffz-i-upload-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ee;&nbsp;'); }
.ffz-i-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0f6;&nbsp;'); }
.ffz-i-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf112;&nbsp;'); }
.ffz-i-smile { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf118;&nbsp;'); }
.ffz-i-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf11c;&nbsp;'); }

View file

@ -1,11 +1,11 @@
@font-face {
font-family: 'ffz-fontello';
src: url('../font/ffz-fontello.eot?19069837');
src: url('../font/ffz-fontello.eot?19069837#iefix') format('embedded-opentype'),
url('../font/ffz-fontello.woff2?19069837') format('woff2'),
url('../font/ffz-fontello.woff?19069837') format('woff'),
url('../font/ffz-fontello.ttf?19069837') format('truetype'),
url('../font/ffz-fontello.svg?19069837#ffz-fontello') format('svg');
src: url('../font/ffz-fontello.eot?30569253');
src: url('../font/ffz-fontello.eot?30569253#iefix') format('embedded-opentype'),
url('../font/ffz-fontello.woff2?30569253') format('woff2'),
url('../font/ffz-fontello.woff?30569253') format('woff'),
url('../font/ffz-fontello.ttf?30569253') format('truetype'),
url('../font/ffz-fontello.svg?30569253#ffz-fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'ffz-fontello';
src: url('../font/ffz-fontello.svg?19069837#ffz-fontello') format('svg');
src: url('../font/ffz-fontello.svg?30569253#ffz-fontello') format('svg');
}
}
*/
@ -127,6 +127,10 @@
.ffz-i-right-open:before { content: '\e846'; } /* '' */
.ffz-i-mastodon:before { content: '\e847'; } /* '' */
.ffz-i-volume-up:before { content: '\e848'; } /* '' */
.ffz-i-unmod:before { content: '\e849'; } /* '' */
.ffz-i-mod:before { content: '\e84a'; } /* '' */
.ffz-i-flag:before { content: '\e84b'; } /* '' */
.ffz-i-mange-suspicious:before { content: '\e84c'; } /* '' */
.ffz-i-move:before { content: '\f047'; } /* '' */
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
.ffz-i-twitter:before { content: '\f099'; } /* '' */
@ -139,6 +143,7 @@
.ffz-i-chat-empty:before { content: '\f0e6'; } /* '' */
.ffz-i-download-cloud:before { content: '\f0ed'; } /* '' */
.ffz-i-upload-cloud:before { content: '\f0ee'; } /* '' */
.ffz-i-doc-text:before { content: '\f0f6'; } /* '' */
.ffz-i-reply:before { content: '\f112'; } /* '' */
.ffz-i-smile:before { content: '\f118'; } /* '' */
.ffz-i-keyboard:before { content: '\f11c'; } /* '' */

View file

@ -0,0 +1,54 @@
.ffz-ct--obj-open,
.ffz-ct--obj-close {
&[depth="1"], &[depth="5"], &[depth="9"] {
color: var(--color-text-alt-2);
}
&[depth="2"], &[depth="6"], &[depth="10"] {
color: var(--color-text-error);
}
&[depth="3"], &[depth="7"], &[depth="11"] {
color: var(--color-text-prime);
}
&[depth="4"], &[depth="8"] {
color: var(--color-text-success);
}
}
.ffz-ct--obj-sep,
.ffz-ct--obj-key-sep,
.ffz-ct--params,
.ffz-ct--prefix,
.ffz-ct--tags {
color: var(--color-text-alt-2);
}
.ffz-ct--obj-key,
.ffz-ct--command,
.ffz-ct--tag {
color: var(--color-text-warn);
}
.ffz-ct--channel,
.ffz-ct--user {
color: var(--color-text-success);
}
.ffz-ct--literal,
.ffz-ct--param {
color: var(--color-text-prime);
}
.ffz-ct--string,
.ffz-ct--tag-value {
color: var(--color-text-base);
}
.ffz-ct--tags,
.ffz-ct--params {
overflow-wrap: anywhere;
}