1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-08 23:30:53 +00:00
FrankerFaceZ/src/utilities/rich_tokens.js

1557 lines
38 KiB
JavaScript
Raw Normal View History

'use strict';
// ============================================================================
// Rich Content Tokens
// ============================================================================
import {has} from 'utilities/object';
import Markdown from 'markdown-it';
import MILA from 'markdown-it-link-attributes';
4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
2023-12-19 16:24:33 -05:00
import { FFZEvent } from './events';
export const VERSION = 9;
export const TOKEN_TYPES = {};
const validate = (input, valid) => valid.includes(input) ? input : null;
const VALID_WEIGHTS = ['regular', 'bold', 'semibold'],
VALID_COLORS = ['base', 'alt', 'alt-2', 'link'],
4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
2023-12-19 16:24:33 -05:00
VALID_COLORS_TWO = ['youtube'],
VALID_SIZES = ['1', '2', '3', '4', '5', '6', '7', '8'],
VALID_WRAPS = ['nowrap', 'pre-wrap'],
VALID_PADDING = {
small: '05',
normal: '1',
large: '2',
huge: '3'
};
// ============================================================================
// Markdown
// ============================================================================
const md = new Markdown({
html: false,
linkify: true
}).disable('image');
md.use(MILA, {
attrs: {
class: 'ffz-tooltip',
target: '_blank',
rel: 'noopener',
'data-tooltip-type': 'link'
}
});
// ============================================================================
// Render Tokens
// ============================================================================
function applySpacing(term, token, classes, styles) {
for(const mode of ['', '-x','-y','-t','-r','-b','-l']) {
const key = `${term}${mode}`,
value = token[key];
if ( value ) {
if ( VALID_PADDING[value] )
classes.push(`tw-${term}${mode}-${VALID_PADDING[value]}`);
else if ( styles ) {
const thing = term === 'pd' ? 'padding' : 'margin';
if ( mode === '' )
styles[thing] = value;
if ( mode === '-x' || mode === '-l' )
styles[`${thing}-left`] = value;
if ( mode === '-x' || mode === '-r' )
styles[`${thing}-right`] = value;
if ( mode === '-y' || mode === '-t' )
styles[`${thing}-top`] = value;
if ( mode === '-y' || mode === '-b' )
styles[`${thing}-bottom`] = value;
}
}
}
}
export function getRoundClass(value) {
let klass;
if ( value === -1 )
klass = 'rounded';
else if ( value === 1 )
klass = 'small';
else if ( value === 2 )
klass = 'medium';
else if ( value === 3 )
klass = 'large';
return klass ? `tw-border-radius-${klass}` : '';
}
// TODO: Mess with this more.
// (It's a function for wrapping React's createElement in a function
// that accepts the same input as Vue's createElement, letting us
// deduplicate a ton of code in here.)
/*export function wrapReactCreate(createElement) {
return (tag, opts, children) => {
if ( typeof tag !== 'string' )
throw new Error('invalid tag');
if ( opts ) {
if ( opts.class ) {
if ( typeof opts.class === 'string' )
opts.className = opts.class;
else if ( Array.isArray(opts.class) )
opts.className = opts.class.join(' ');
else if ( typeof opts.class === 'object' ) {
const bits = [];
for(const [key, val] of Object.entries(opts.class))
if ( val )
bits.push(key);
opts.className = bits.join(' ');
}
opts.class = undefined;
}
if ( opts.attrs ) {
for(const [key, val] of Object.entries(opts.attrs) )
opts[key] = val;
opts.attrs = undefined;
}
if ( opts.props )
throw new Error('props unsupported');
if ( opts.domProps )
throw new Error('domProps unsupported');
if ( opts.nativeOn )
throw new Error('nativeOn unsupported');
if ( opts.on ) {
for(const [key, val] of Object.entries(opts.on) )
opts[`on${key.charAt(0).toUpperCase()}${key.slice(1)}`] = val;
opts.on = undefined;
}
if ( opts.style && typeof opts.style !== 'object' )
opts.style = undefined;
}
return createElement(tag, opts, children);
}
}*/
export function renderWithCapture(tokens, createElement, ctx, markdown) {
const old_capture = ctx.text_capture, old_markdown = ctx.markdown;
ctx.text_capture = [];
if ( markdown )
ctx.markdown = true;
const content = renderTokens(tokens, createElement, ctx);
let title = ctx.text_capture.join('').trim();
if ( ! title.length )
title = null;
ctx.text_capture = old_capture;
ctx.markdown = old_markdown;
return {
content,
title
}
}
export function renderTokens(tokens, createElement, ctx, markdown) {
if ( tokens == null )
return null;
const old_markdown = ctx.markdown;
if ( markdown )
ctx.markdown = true;
let out = [];
if ( ! Array.isArray(tokens) )
tokens = [tokens];
for(const token of tokens) {
if ( token == null )
continue;
else if ( Array.isArray(token) )
out = out.concat(renderTokens(token, createElement, ctx));
else if ( typeof token !== 'object' ) {
const val = String(token);
if ( ctx.text_capture )
ctx.text_capture.push(val);
if ( ctx.markdown ) {
const content = md.render(val);
if ( content === val )
out.push(val);
else if ( ctx.vue )
out.push(createElement('span', {
domProps: {
innerHTML: content
}
}));
else
out.push(createElement('span', {
dangerouslySetInnerHTML: {
__html: content
}
}));
} else
out.push(val);
}
else {
const type = token.type,
handler = TOKEN_TYPES[type];
if ( ! handler ) {
console.warn('Skipping unknown token type', type, token);
continue;
}
const result = handler(token, createElement, ctx);
if ( Array.isArray(result) )
out = out.concat(result);
else if ( result )
out.push(result);
}
}
ctx.markdown = old_markdown;
if ( ! out.length )
return null;
return out;
}
export default renderTokens;
// ============================================================================
// Token Type: Reference
// ============================================================================
function resolveToken(token, ctx) {
if ( token?.type === 'ref' ) {
return ctx.fragments?.[token.name] ?? null;
}
return token;
}
TOKEN_TYPES.ref = function(token, createElement, ctx) {
const frag = ctx.fragments?.[token.name];
if ( frag )
return renderTokens(frag, createElement, ctx);
}
// ============================================================================
// Token Type: Box
// ============================================================================
TOKEN_TYPES.box = function(token, createElement, ctx) {
const classes = [], style = {};
if ( VALID_WRAPS.includes(token.wrap) )
classes.push(`tw-white-space-${token.wrap}`);
if ( token.ellipsis )
classes.push('tw-ellipsis');
if ( token.lines ) {
classes.push('ffz--line-clamp');
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);
const capture = token.ellipsis || token.lines,
markdown = token.markdown;
let content, title = null;
if ( capture ) {
const out = renderWithCapture(token.content, createElement, ctx, markdown);
content = out.content; title = out.title;
} else
content = renderTokens(token.content, createElement, ctx, markdown);
if ( ctx.vue )
return createElement('div', {class: classes, style, attrs: {title}}, content);
return createElement('div', {className: classes.join(' '), style, title}, content);
}
4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
2023-12-19 16:24:33 -05:00
// ============================================================================
// Token Type: open_settings
// ============================================================================
TOKEN_TYPES.open_settings = function(token, createElement, ctx) {
const handler = event => {
event.preventDefault();
4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
2023-12-19 16:24:33 -05:00
const evt = new FFZEvent({
item: token.item,
event,
errored: false
});
window.FrankerFaceZ.get().emit('main_menu:open', evt);
}
const markdown = token.markdown,
content = renderTokens(token.content, createElement, ctx, markdown);
4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
2023-12-19 16:24:33 -05:00
if ( ctx.vue )
return createElement('a', {
class: 'tw-link',
href: '#',
4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
2023-12-19 16:24:33 -05:00
on: {
click: handler
}
}, content);
4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
2023-12-19 16:24:33 -05:00
return createElement('a', {
class: 'tw-link',
href: '#',
4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
2023-12-19 16:24:33 -05:00
onClick: handler
}, content);
4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
2023-12-19 16:24:33 -05:00
}
// ============================================================================
// Token Type: Conditional
// ============================================================================
TOKEN_TYPES.conditional = function(token, createElement, ctx) {
let passed = true;
if ( has(token, 'media') && token.media != ctx.allow_media )
passed = false;
if ( token.nsfw && ! ctx.allow_unsafe )
passed = false;
if ( token.skip_nsfw && ctx.allow_unsafe )
passed = false;
if ( token.tooltip && ! ctx.tooltip )
passed = false;
else if ( token.tooltip === false && ctx.tooltip )
passed = false;
if ( passed )
return renderTokens(token.content, createElement, ctx);
return renderTokens(token.alternative, createElement, ctx);
}
// ============================================================================
// Token Type: Fieldset
// ============================================================================
TOKEN_TYPES.fieldset = function(token, createElement, ctx) {
if ( ! Array.isArray(token.fields) )
return null;
const fields = [];
for(const field of token.fields) {
if ( ! field )
continue;
const name = renderTokens(field.name, 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;
if ( ctx.vue )
fields.push(createElement('div', {
class: [
'ffz--field',
field.inline ? 'ffz--field-inline' : false,
icon ? 'ffz--field-icon' : false
]
}, [
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' : ''} ${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)
]));
}
if ( ! fields.length )
return null;
if ( ctx.vue )
return createElement('div', {
class: 'ffz--fields'
}, fields);
return createElement('div', {
className: 'ffz--fields'
}, fields);
}
// ============================================================================
// Token Type: Flex
// ============================================================================
const ALIGNMENTS = ['start', 'end', 'center', 'between', 'around'];
TOKEN_TYPES.flex = function(token, createElement, ctx) {
const classes = [], style = {};
if ( token.inline )
classes.push('tw-flex-inline');
else
classes.push('tw-flex');
const overflow = validate(token.overflow, ['hidden', 'auto']);
if ( overflow )
classes.push(`tw-overflow-${overflow}`);
const direction = validate(token.direction, ['column', 'row', 'column-reverse', 'row-reverse']);
if ( direction )
classes.push(`tw-flex-${direction}`);
const wrap = validate(token.wrap, ['wrap', 'nowrap', 'wrap-reverse']);
if ( wrap )
classes.push(`tw-flex-${wrap}`);
let align = validate(token['align-content'], ALIGNMENTS)
if ( align )
classes.push(`tw-align-content-${align}`);
align = validate(token['justify-content'], ALIGNMENTS);
if ( align )
classes.push(`tw-justify-content-${align}`);
align = validate(token['align-items'], ALIGNMENTS)
if ( align )
classes.push(`tw-align-items-${align}`);
align = validate(token['align-self'], ALIGNMENTS)
if ( align )
classes.push(`tw-align-self-${align}`);
applySpacing('pd', token, classes, style);
applySpacing('mg', token, classes, style);
const content = renderTokens(token.content, createElement, ctx, token.markdown);
if ( ctx.vue )
return createElement('div', {class: classes, style}, content);
return createElement('div', {className: classes.join(' '), style}, content);
}
// ============================================================================
// Token Type: Format
// ============================================================================
TOKEN_TYPES.format = function(token, createElement, ctx) {
const type = token.format, val = token.value, opt = token.options;
let out;
if ( type === 'date' )
out = ctx.i18n.formatDate(val, opt);
else if ( type === 'time' )
out = ctx.i18n.formatTime(val, opt);
else if ( type === 'datetime' )
out = ctx.i18n.formatDateTime(val, opt)
else if ( type === 'relative' )
out = ctx.i18n.toRelativeTime(val, opt);
else if ( type === 'duration' )
out = ctx.i18n.formatDuration(val, opt);
else if ( type === 'number' )
out = ctx.i18n.formatNumber(val, opt);
else {
console.warn('Unknown format type:', type);
out = String(val);
}
if ( ctx.text_capture )
ctx.text_capture.push(out);
return out;
}
// ============================================================================
// Token Type: Gallery
// ============================================================================
TOKEN_TYPES.gallery = function(token, createElement, ctx) {
if ( ! Array.isArray(token.items) || ! token.items.length )
return null;
let first_column = [],
second_column = [],
first = true,
i = 0;
for(const item of token.items) {
const content = renderTokens(item, createElement, ctx);
if ( content ) {
(first ? first_column : second_column).push(content);
first = ! first;
i++;
if ( i >= 4 )
break;
}
}
if ( second_column.length && first_column.length > second_column.length )
second_column.push(first_column.pop());
if ( ! i )
return null
const columns = [];
columns.push(ctx.vue ?
createElement('div', {
class: 'ffz--gallery-column',
attrs: {
'data-items': first_column.length
}
}, first_column) :
createElement('div', {
className: 'ffz--gallery-column',
'data-items': first_column.length
}, first_column)
);
if ( second_column.length )
columns.push(ctx.vue ?
createElement('div', {
class: 'ffz--gallery-column',
attrs: {
'data-items': second_column.length
}
}, second_column) :
createElement('div', {
className: 'ffz--gallery-column',
'data-items': second_column.length
}, second_column)
);
if ( ctx.vue )
return createElement('div', {
class: 'ffz--rich-gallery',
attrs: {
'data-items': first_column.length + second_column.length
}
}, columns);
return createElement('div', {
className: 'ffz--rich-gallery',
'data-items': first_column.length + second_column.length
}, columns);
}
// ============================================================================
// Token Type: Heading
// ============================================================================
function header_vue(token, h, ctx) {
let content = [];
let background;
if ( token.title ) {
const out = renderWithCapture(token.title, h, ctx, token.markdown);
content.push(h('div', {
class: 'tw-ellipsis tw-semibold tw-mg-x-05',
attrs: {
title: out.title
}
}, out.content));
}
if ( token.subtitle ) {
const out = renderWithCapture(token.subtitle, h, ctx, token.markdown);
content.push(h('div', {
class: 'tw-ellipsis tw-c-text-alt-2 tw-mg-x-05',
attrs: {
title: out.title
}
}, out.content));
}
if ( token.extra ) {
const out = renderWithCapture(token.extra, h, ctx, token.markdown);
content.push(h('div', {
class: 'tw-ellipsis tw-c-text-alt-2 tw-mg-x-05',
attrs: {
title: out.title
}
}, 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
}, 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;
}
2025-06-07 16:09:55 -06:00
const subtok = resolveToken(token.sub_logo, ctx);
4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
2023-12-19 16:24:33 -05:00
if ( ! token.compact && subtok && canShowImage(subtok, ctx) ) {
const aspect = subtok.aspect;
let image;
if ( subtok.type === 'image' )
image = render_image({
...subtok,
aspect: undefined
}, h, ctx);
if ( subtok.type === 'icon' )
image = h('figure', {
class: `ffz-i-${subtok.name}`
});
if ( image ) {
image = h('div', {
class: `ffz--header-sublogo tw-flex-shrink-0 ${subtok.extra_pad ? 'tw-mg-l-05 tw-mg-r-1' : 'tw-mg-r-05'}${aspect ? ' ffz--header-aspect' : ''}`,
4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
2023-12-19 16:24:33 -05:00
style: {
width: aspect ? `${aspect * 2}rem` : null
}
}, [image]);
const title = content.shift();
content = [
title,
h('div', {
class: 'tw-flex tw-full-width tw-align-items-center'
}, [
image,
h('div', {
class: `tw-flex tw-full-width tw-overflow-hidden tw-justify-content-center tw-flex-column tw-flex-grow-1`
}, content)
])
];
}
}
content = h('div', {
class: [
'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);
let imtok = resolveToken(token.sfw_image, ctx);
const nsfw_token = resolveToken(token.image, ctx);
if ( nsfw_token && canShowImage(nsfw_token, ctx) )
imtok = nsfw_token;
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}`
});
const right = token.image_side === 'right';
if ( image ) {
image = h('div', {
class: [
'ffz--header-image tw-flex-shrink-0 tw-mg-x-05',
aspect ? 'ffz--header-aspect' : null
],
style: {
width: aspect ? `${aspect * (token.compact ? 2.4 : 4.8)}rem` : null
}
}, [image]);
if ( token.compact ) {
if ( right )
content.children.push(image);
else
content.children.unshift(image);
} else {
content = h('div', {
class: 'tw-flex ffz--rich-header'
}, [
right ? content : null,
image,
right ? null : content
])
}
} else if ( ! token.compact )
content = h('div', {
class: 'tw-flex ffz--rich-header'
}, [
h('div', {class: 'ffz--header-image tw-mg-x-05'}),
content
]);
} else if ( ! token.compact )
content = h('div', {
class: 'tw-flex ffz--rich-header'
}, [
h('div', {class: 'ffz--header-image tw-mg-x-05'}),
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);
content.push(createElement('div', {
className: `tw-ellipsis tw-semibold ${token.compact ? 'tw-mg-r-1' : ''}`,
title: out.title
}, out.content));
}
if ( token.subtitle ) {
const out = renderWithCapture(token.subtitle, createElement, ctx, token.markdown);
content.push(createElement('div', {
className: `tw-ellipsis tw-c-text-alt-2`,
title: out.title
}, out.content));
}
if ( token.extra ) {
const out = renderWithCapture(token.extra, createElement, ctx, token.markdown);
content.push(createElement('div', {
className: 'tw-ellipsis tw-c-text-alt-2',
title: out.title
}, 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;
}
2025-06-07 16:09:55 -06:00
const subtok = resolveToken(token.sub_logo, ctx);
4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
2023-12-19 16:24:33 -05:00
if ( ! token.compact && subtok && canShowImage(subtok, ctx) ) {
const aspect = subtok.aspect;
let image;
if ( subtok.type === 'image' )
image = render_image({
...subtok,
aspect: undefined
}, createElement, ctx);
if ( subtok.type === 'icon' )
image = createElement('figure', {
className: `ffz-i-${subtok.name}`
});
if ( image ) {
image = createElement('div', {
className: `ffz--header-sublogo tw-flex-shrink-0 ${subtok.youtube_dumb ? 'tw-mg-l-05 tw-mg-r-1' : 'tw-mg-r-05'}${aspect ? ' ffz--header-aspect' : ''}`,
style: {
width: aspect ? `${aspect * 2}rem` : null
}
}, image);
const title = content.shift();
content = [
title,
createElement('div', {
className: 'tw-flex tw-full-width tw-align-items-center'
}, [
image,
createElement('div', {
className: `tw-flex tw-full-width tw-overflow-hidden tw-justify-content-center tw-flex-column tw-flex-grow-1`
}, 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);
let imtok = resolveToken(token.sfw_image, ctx);
const nsfw_token = resolveToken(token.image, ctx);
if ( nsfw_token && canShowImage(nsfw_token, ctx) )
imtok = nsfw_token;
if ( imtok ) {
const aspect = imtok.aspect;
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 ) {
image = createElement('div', {
className: `ffz--header-image tw-flex-shrink-0 tw-mg-x-05${aspect ? ' ffz--header-aspect' : ''}`,
style: {
width: aspect ? `${aspect * (token.compact ? 2.4 : 4.8)}rem` : null
}
}, image);
if ( token.compact ) {
// We need to do some weird pushy stuff~
// This varies if we're running with React or native.
if ( content instanceof Node ) {
if ( right )
content.appendChild(image);
else
content.insertBefore(image, content.firstChild);
} else if ( Array.isArray(content?.props?.children) ) {
if ( right )
content.props.children.push(image);
else
content.props.children.unshift(image);
}
} else {
content = createElement('div', {
className: 'tw-flex ffz--rich-header'
}, [right ? content : null, image, right ? null : content])
}
} else if ( ! token.compact )
content = createElement('div', {
className: 'tw-flex ffz--rich-header'
}, [
createElement('div', {className: 'ffz--header-image tw-mg-x-05'}),
content
]);
} else if ( ! token.compact )
content = createElement('div', {
className: 'tw-flex ffz--rich-header'
}, [
createElement('div', {className: 'ffz--header-image tw-mg-x-05'}),
content
]);
if ( background )
content = createElement('div', {
className: 'ffz--rich-header--background'
}, [
createElement('div', {
className: 'ffz--rich-header__background'
}, background),
content
]);
return content;
}
TOKEN_TYPES.header = function(token, createElement, ctx) {
if ( ! token.title && ! token.subtitle && ! token.image && ! token.extra )
return null;
return ctx.vue ?
header_vue(token, createElement, ctx) :
header_normal(token, createElement, ctx);
}
// ============================================================================
// Token Type: Icon
// ============================================================================
TOKEN_TYPES.icon = function(token, createElement, ctx) {
if ( ! token.name )
return null;
return ctx.vue ?
createElement('span', {class: `ffz-i-${token.name}`}) :
createElement('span', {className: `ffz-i-${token.name}`});
}
// ============================================================================
// Token Type: Image
// ============================================================================
function canShowImage(token, ctx) {
return !(has(token, 'sfw') && ! token.sfw && ! ctx.allow_unsafe);
}
function render_image(token, createElement, ctx) {
if ( ! token.url || ! canShowImage(token, ctx) )
return null;
const round = getRoundClass(token.rounding);
let aspect;
if ( token.aspect )
aspect = token.aspect
else if ( token.height > 0 && token.width > 0 )
aspect = token.width / token.height;
if ( ctx.vue ) {
const stuff = {
class: [
token.class,
round
],
style: {
width: token.width,
height: token.height
},
attrs: {
src: token.url,
title: token.title
}
};
if ( token.contain )
stuff.style.objectFit = 'contain';
if ( ctx.onload )
stuff.on = {load: ctx.onload};
const image = createElement('img', stuff);
if ( ! aspect )
return image;
return createElement('aspect', {
props: {
ratio: aspect,
align: 'center'
}
}, [image]);
}
const image = createElement('img', {
className: `${token.class || ''} ${round}`,
src: token.url,
alt: token.alt || token.title || '',
title: token.title || '',
onLoad: ctx.onload,
style: {
width: token.width,
height: token.height
}
});
if ( token.contain )
image.style.objectFit = 'contain';
if ( ! aspect )
return image;
return createElement('div', {
className: 'ffz-aspect ffz-aspect--align-center'
}, [
createElement('div', {
className: 'ffz-aspect__spacer',
style: {
paddingTop: `${100 * (1 / (aspect || 1))}%`
}
}),
image
]);
}
TOKEN_TYPES.image = render_image;
// ============================================================================
// Token Type: I18n
// ============================================================================
TOKEN_TYPES.i18n = function(token, createElement, ctx) {
if ( ! token.phrase ) {
console.warn('Skipping i18n tag with no phrase');
return null;
}
let key = token.key;
if ( ctx.i18n_prefix )
key = `${ctx.i18n_prefix}.${key}`;
return renderTokens(
ctx.i18n.tList(key, token.phrase, token.content),
createElement,
ctx,
token.markdown
);
}
// ============================================================================
// Token Type: I18n Select
// ============================================================================
function findMatchingLocale(locale, list) {
// Is the locale present exactly?
for(const item of list) {
if ( item.localeCompare(locale, undefined, {sensitivity: 'accent'}) === 0 )
return locale;
}
// What about partials?
2025-06-07 16:09:55 -06:00
const prefixed = `${locale.toLowerCase()}-`;
for(const item of list) {
if ( item.toLowerCase().startsWith(prefixed) )
return item;
}
// Last resort, do we have a - in the locale?
const idx = locale.indexOf('-');
if ( idx !== -1 )
return findMatchingLocale(locale.slice(0, idx), list);
}
TOKEN_TYPES.i18n_select = function(token, createElement, ctx) {
// What locale and choices do we have.
const choices = token.choices || {};
2025-06-07 16:09:55 -06:00
const locale = ctx.i18n?.locale ?? 'en';
// Try to find a valid match, or use the default.
let selected = findMatchingLocale(locale, Object.keys(choices));
if ( ! selected )
selected = token.default;
// Render it.
return renderTokens(
choices[selected],
createElement,
ctx,
token.markdown
);
}
// ============================================================================
// Token Type: Link
// ============================================================================
TOKEN_TYPES.link = function(token, createElement, ctx) {
if ( token.content === undefined )
token.content = token.url;
const content = renderTokens(token.content, createElement, ctx, token.markdown);
const klass = [];
if ( token.interactive )
klass.push(`ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive`);
if ( token.tooltip !== false )
klass.push('ffz-tooltip');
if ( token.embed )
klass.push(`tw-block tw-border tw-border-radius-large tw-mg-y-05 tw-pd-05`);
if ( token.no_color )
klass.push(`ffz-link--inherit`);
if ( ctx.vue ) {
2025-06-07 16:09:55 -06:00
const on = {};
if ( ctx.link_click_handler )
on.click = ctx.link_click_handler;
return createElement('a', {
class: klass,
attrs: {
rel: 'noopener noreferrer',
target: '_blank',
'data-tooltip-type': 'link',
href: token.url
},
on
}, content);
}
return createElement('a', {
className: klass.join(' '),
rel: 'noopener noreferrer',
target: '_blank',
'data-tooltip-type': 'link',
href: token.url,
onClick: ctx.link_click_handler
}, content);
}
// ============================================================================
// Token Type: Overlay
// ============================================================================
TOKEN_TYPES.overlay = function(token, createElement, ctx) {
const content = renderTokens(token.content, createElement, ctx, token.markdown);
if ( ! content )
return null;
const corners = [];
for(const corner of ['top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right']) {
const stuff = renderTokens(token[corner], createElement, ctx, token.markdown);
if ( stuff )
corners.push(ctx.vue ?
createElement('div', {class: `ffz--overlay__bit`, attrs:{'data-side':corner}}, stuff) :
createElement('div', {className: `ffz--overlay__bit`, 'data-side':corner}, stuff)
);
}
const classes = ['ffz--overlay'];
const style = {};
if ( token.background ) {
if ( VALID_COLORS.includes(token.background) )
classes.push(`tw-c-background-${token.background}`);
else
style.backgroundColor = token.background;
}
if ( token.color ) {
if ( VALID_COLORS.includes(token.color) )
classes.push(`tw-c-text-${token.color}`);
else
style.color = token.color;
}
if ( ctx.vue )
return createElement('div', {
class: classes,
style
}, [
createElement('div', {class: 'ffz--overlay__content'}, content),
...corners
]);
return createElement('div', {
className: classes.join(' '),
style
}, [
createElement('div', {className: 'ffz--overlay__content'}, content),
...corners
]);
}
// ============================================================================
// Token Type: Player
// ============================================================================
function handlePlayerClick(token, id, ctx, event) {
//console.log('clicked player', token, id, ctx, event);
const target = event.currentTarget,
is_av = target instanceof HTMLVideoElement || target instanceof HTMLAudioElement;
if ( is_av || ctx.togglePlayer ) {
event.preventDefault();
event.stopPropagation();
}
if ( is_av ) {
if ( target.paused )
target.play();
else
target.pause();
return;
}
if ( ctx.togglePlayer )
ctx.togglePlayer(id);
}
TOKEN_TYPES.player = function(token, createElement, ctx) {
// Make a unique ID for this player, within the context.
const id = ctx.last_player = (ctx.last_player || 0) + 1,
active = ctx.player_state?.[id];
const handler = handlePlayerClick.bind(this, token, id, ctx);
if ( ! active && (token.content || token.iframe) )
return render_player_content(id, handler, token, createElement, ctx);
if ( token.iframe )
return render_player_iframe(id, active ?? false, handler, token, createElement, ctx);
if ( ! token.sources )
return null;
const autoplay = token.autoplay ?? false,
loop = token.loop ?? false,
playing = active ?? autoplay,
controls = ! token.silent || ! autoplay;
const muted = token.silent ? true : (active == null && autoplay);
const style = {};
const aspect = token.active_aspect ?? token.aspect;
2025-06-07 16:09:55 -06:00
if ( aspect )
style.aspectRatio = aspect;
if ( ctx.vue )
return createElement(token.audio ? 'audio' : 'video', {
style,
attrs: {
autoplay: playing,
loop,
controls,
poster: token.poster
},
domProps: {
muted
},
on: {
click: handler
}
}, token.sources.map(source => createElement('source', {
attrs: {
type: source.type,
src: source.src
}
})));
return createElement(token.audio ? 'audio' : 'video', {
style,
muted,
autoplay: playing,
loop,
poster: token.poster,
controls,
onClick: handler
}, token.sources.map(source => createElement('source', {
type: source.type,
src: source.src
})));
}
function render_player_content(id, handler, token, createElement, ctx) {
const content = renderTokens(token.content, createElement, ctx, token.markdown),
classes = ['ffz--rich-player'],
style = {};
if ( token.aspect )
style.aspectRatio = token.aspect;
if ( ctx.vue )
return createElement('div', {
class: classes,
style,
on: {
click: handler
}
}, content);
return createElement('div', {
className: classes.join(' '),
onClick: handler,
style
}, content);
}
function render_player_iframe(id, active, handler, token, createElement, ctx) {
const style = {},
attrs = {
src: token.iframe,
frameborder: 0,
allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share',
allowfullscreen: true
};
const aspect = token.active_aspect ?? token.aspect;
if ( aspect )
style.aspectRatio = aspect;
if ( ctx.vue )
return createElement('iframe', {
style,
attrs
});
return createElement('iframe', {
style,
...attrs
});
}
// ============================================================================
// Token Type: Style
// ============================================================================
TOKEN_TYPES.style = function(token, createElement, ctx) {
const classes = [], style = {};
if ( token.weight ) {
if ( VALID_WEIGHTS.includes(token.weight) )
classes.push(`tw-${token.weight}`);
else
style.weight = token.weight;
}
if ( token.italic )
classes.push('tw-italic');
if ( token.strike )
classes.push('tw-strikethrough');
if ( token.underline )
classes.push('tw-underline');
if ( token.tabular )
classes.push('tw-tabular-nums');
if ( token.size ) {
if ( typeof token.size === 'string' ) {
if ( VALID_SIZES.includes(token.size) )
classes.push(`tw-font-size-${token.size}`);
else
style.fontSize = token.size;
} else
style.fontSize = `${token.size}px`;
}
if ( token.color ) {
if ( VALID_COLORS.includes(token.color) )
classes.push(`tw-c-text-${token.color}`);
4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
2023-12-19 16:24:33 -05:00
else if ( VALID_COLORS_TWO.includes(token.color) )
classes.push(`ffz-c-text-${token.color}`);
else
style.color = token.color;
}
if ( VALID_WRAPS.includes(token.wrap) )
classes.push(`tw-white-space-${token.wrap}`);
if ( token.ellipsis )
classes.push('tw-ellipsis');
applySpacing('pd', token, classes, style);
applySpacing('mg', token, classes, style);
const capture = token.ellipsis;
let content, title = null;
if ( capture ) {
const out = renderWithCapture(token.content, createElement, ctx, token.markdown);
content = out.content; title = out.title;
} else
content = renderTokens(token.content, createElement, ctx, token.markdown);
if ( ctx.vue )
return createElement('span', {class: classes, style, attrs: {title}}, content);
return createElement('span', {className: classes.join(' '), style, title}, content);
}
// ============================================================================
// Token Type: Tag (Deprecated)
// ============================================================================
export const ALLOWED_TAGS = [
'a', 'abbr', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'blockquote', 'br',
'caption', 'code', 'col', 'colgroup', 'data', 'dd', 'div', 'dl', 'dt', 'em',
'figcaption', 'figure', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header',
'hr', 'i', 'img', 'li', 'main', 'nav', 'ol', 'p', 'picture', 'pre', 's', 'section',
'source', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot',
'th', 'thead', 'time', 'tr', 'track', 'u', 'ul', 'video', 'wbr'
];
export const ALLOWED_ATTRS = {
a: ['href'],
audio: ['autoplay', 'controls', 'loop', 'muted', 'preload', 'src'],
bdo: ['dir'],
col: ['span'],
colgroup: ['span'],
data: ['value'],
img: ['alt', 'height', 'sizes', 'src', 'srcset', 'width'],
source: ['src', 'srcset', 'type', 'media', 'sizes'],
td: ['colspan', 'headers', 'rowspan'],
th: ['abbr', 'colspan', 'headers', 'rowspan', 'scope'],
time: ['datetime'],
track: ['default', 'kind', 'label', 'src', 'srclang'],
video: ['autoplay', 'controls', 'height', 'loop', 'muted', 'poster', 'preload', 'src', 'width'],
};
export const PROPS = [
'muted'
];
export const GLOBAL_ATTRS = ['style', 'title'];
TOKEN_TYPES.tag = function(token, createElement, ctx) {
const tag = String(token.tag || 'span').toLowerCase();
if ( ! ALLOWED_TAGS.includes(tag) ) {
console.warn('Skipping disallowed tag:', tag);
return null;
}
const attrs = {}, props = {};
if ( token.attrs ) {
const allowed = ALLOWED_ATTRS[tag] || [];
for(const [key, val] of Object.entries(token.attrs)) {
if ( ! allowed.includes(key) && ! key.startsWith('data-') && ! GLOBAL_ATTRS.includes(key) )
console.warn(`Skipping disallowed attribute for tag ${tag}:`, key);
else if ( ctx.vue && PROPS.includes(key) )
props[key] = val;
else
attrs[key] = val;
}
}
if ( tag === 'img' || tag === 'picture' )
attrs.onload = ctx.onload;
if ( tag === 'video' || tag === 'audio' )
attrs.loadedmetadata = ctx.onload;
const content = renderTokens(token.content, createElement, ctx, token.markdown);
if ( ctx.vue )
return createElement(tag, {
class: token.class || '',
domProps: props,
attrs
}, content);
return createElement(tag, {
...attrs,
className: token.class || ''
}, content);
4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
2023-12-19 16:24:33 -05:00
}