mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 01:22:11 +00:00
feat(Text): Parse accessibility data
+ Clean up.
This commit is contained in:
131
src/parser/classes/misc/AccessibilityData.ts
Normal file
131
src/parser/classes/misc/AccessibilityData.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type { RawNode } from '../../types/index.js';
|
||||
|
||||
export interface AccessibilityId {
|
||||
accessibility_id_type?:
|
||||
| 'UNKNOWN'
|
||||
| 'MENU_ADD_TO_WATCH_LATER'
|
||||
| 'MENU_REMOVE_FROM_WATCH_LATER'
|
||||
| 'MENU_ADD_TO_PLAYLIST'
|
||||
| 'MENU_REMOVE_FROM_PLAYLIST'
|
||||
| 'MENU_SHARE_VIDEO'
|
||||
| 'MENU_SHARE_PLAYLIST'
|
||||
| 'MENU_OFFLINE_VIDEO'
|
||||
| 'MENU_OFFLINE_PLAYLIST'
|
||||
| 'MENU_DELETE_VIDEO'
|
||||
| 'MENU_DELETE_PLAYLIST'
|
||||
| 'MENU_EDIT_VIDEO_METADATA'
|
||||
| 'MENU_HIDE'
|
||||
| 'MENU_REMOVE_FROM_HISTORY'
|
||||
| 'MENU_LIKE'
|
||||
| 'MENU_INFO'
|
||||
| 'MENU_ADD_TO_REMOTE_QUEUE'
|
||||
| 'MENU_REMOVE_FROM_REMOTE_QUEUE'
|
||||
| 'MENU_CREATE_PLAYLIST'
|
||||
| 'MENU_SETTINGS'
|
||||
| 'MENU_PRIVACY'
|
||||
| 'MENU_FEEDBACK'
|
||||
| 'MENU_HELP'
|
||||
| 'MENU_DELETE_CHANNEL_POST'
|
||||
| 'MENU_PLAYLIST_JOIN_COLLABORATION'
|
||||
| 'MENU_EDIT_PLAYLIST'
|
||||
| 'MENU_OFFLINE_REMOVE'
|
||||
| 'MENU_OFFLINE_PAUSE'
|
||||
| 'MENU_OFFLINE_RESUME'
|
||||
| 'MENU_UNSUBSCRIBE'
|
||||
| 'MENU_GET_ALL_UPDATES'
|
||||
| 'MENU_DISMISS'
|
||||
| 'MENU_CANCEL_UPLOAD'
|
||||
| 'MENU_TAKE_PHOTO'
|
||||
| 'MENU_CHOOSE_PHOTO'
|
||||
| 'MENU_CHOOSE_FROM_CHANNEL_ART_GALLERY'
|
||||
| 'MENU_FILTER_VIDEOS_ONLY'
|
||||
| 'MENU_FILTER_VIDEOS_AND_POSTS'
|
||||
| 'MENU_WATCH_ON_TV'
|
||||
| 'MENU_INSERT_IN_REMOTE_QUEUE'
|
||||
| 'MENU_ADD_UPCOMING_EVENT_REMINDER'
|
||||
| 'MENU_REMOVE_UPCOMING_EVENT_REMINDER'
|
||||
| 'MENU_TOGGLE_DENSITY_MODE'
|
||||
| 'MENU_OFFLINE_UPSELL'
|
||||
| 'MENU_MORE_LIKE_THIS'
|
||||
| 'MENU_CREATE_VIDEO'
|
||||
| 'MENU_CREATE_LIVE_STREAM'
|
||||
| 'MENU_CREATE_REEL_ITEM'
|
||||
| 'MENU_CREATE_POST'
|
||||
| 'MENU_LESS_LIKE_THIS'
|
||||
| 'MENU_REEL_OVERFLOW'
|
||||
| 'MENU_DELETE_REEL'
|
||||
| 'MENU_EDIT_REEL'
|
||||
| 'MENU_REMOVE_FROM_QUEUE'
|
||||
| 'MENU_REEL_SHELF_OVERFLOW'
|
||||
| 'MENU_REEL_SHELF_DISMISS'
|
||||
| 'MENU_SHARE_ARTIST'
|
||||
| 'MENU_ABOUT_RECOMMENDATION'
|
||||
| 'MENU_REPORT'
|
||||
| 'EXPLORE_DESTINATION_TRENDING'
|
||||
| 'EXPLORE_DESTINATION_MUSIC'
|
||||
| 'EXPLORE_DESTINATION_GAMING'
|
||||
| 'EXPLORE_DESTINATION_NEWS'
|
||||
| 'EXPLORE_DESTINATION_MOVIES'
|
||||
| 'EXPLORE_DESTINATION_FASHION'
|
||||
| 'EXPLORE_DESTINATION_LEARNING'
|
||||
| 'EXPLORE_DESTINATION_STAY_HOME'
|
||||
| 'MENU_ABOUT_GAMING_RECOMMENDATAION'
|
||||
| 'EXPLORE_DESTINATION_LIVE'
|
||||
| 'MENU_QUALITY'
|
||||
| 'MENU_CAPTIONS'
|
||||
| 'MENU_PLAYBACK_SPEED'
|
||||
| 'MENU_SHARE_PLAYLIST_UNAVAILABLE'
|
||||
| 'MENU_INFO_CARD'
|
||||
| 'EXPLORE_DESTINATION_SPORTS'
|
||||
| 'MENU_SINGLE_LOOP'
|
||||
| 'MENU_HIDE_VIDEO'
|
||||
| 'MENU_CLEAR_QUEUE'
|
||||
| 'EXPLORE_DESTINATION_SHOPPING'
|
||||
| 'MENU_PLAY_NEXT_IN_QUEUE'
|
||||
| 'MENU_PLAY_LAST_IN_QUEUE'
|
||||
| 'MENU_GO_TO_CHANNEL'
|
||||
| 'EXPLORE_DESTINATION_PODCASTS'
|
||||
| 'MEDIA_GENERATOR_PROMPT_INPUT'
|
||||
| 'MEDIA_GENERATOR_STYLE_SHELF'
|
||||
| 'MEDIA_GENERATOR_STYLE_ITEM'
|
||||
| 'MEDIA_GENERATOR_CREATE_BUTTON'
|
||||
| 'MEDIA_GENERATOR_T2V_ENTRYPOINT'
|
||||
| 'MEDIA_GENERATOR_T2I_ENTRYPOINT'
|
||||
| 'MEDIA_GENERATOR_T2M_ENTRYPOINT'
|
||||
| 'MEDIA_GENERATOR_BACK_BUTTON'
|
||||
| 'MEDIA_GENERATOR_HEADER'
|
||||
| 'MEDIA_GENERATOR_LOADING_PROGRESS'
|
||||
| 'MEDIA_GENERATOR_CANCEL_BUTTON'
|
||||
| 'MEDIA_GENERATOR_IMAGE_PREVIEW'
|
||||
| 'MEDIA_GENERATOR_VIDEO_PREVIEW'
|
||||
| 'MEDIA_GENERATOR_DONE_BUTTON'
|
||||
| 'MEDIA_GENERATOR_IMAGE_SELECTION'
|
||||
| 'MEDIA_GENERATOR_SOUND_METADATA'
|
||||
| 'MEDIA_GENERATOR_AUDIO_SELECT_BUTTON'
|
||||
| 'MEDIA_GENERATOR_T2I2V_ENTRYPOINT'
|
||||
| 'MENU_SAVE_QUEUE_TO_PLAYLIST'
|
||||
| 'MEDIA_GENERATOR_ANIMATE_BUTTON'
|
||||
| 'MEDIA_GENERATOR_SEGMENT_IMPORT_ENTRYPOINT';
|
||||
}
|
||||
|
||||
export default class AccessibilityData {
|
||||
public accessibility_identifier?: string;
|
||||
public identifier?: AccessibilityId;
|
||||
public label?: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
if ('accessibilityIdentifier' in data) {
|
||||
this.accessibility_identifier = data.accessibilityIdentifier;
|
||||
}
|
||||
|
||||
if ('identifier' in data) {
|
||||
this.identifier = {
|
||||
accessibility_id_type: data.identifier.accessibilityIdType
|
||||
};
|
||||
}
|
||||
|
||||
if ('label' in data) {
|
||||
this.label = data.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,17 @@ import type { RawNode } from '../../index.js';
|
||||
import { escape, type Run } from './Text.js';
|
||||
import Thumbnail from './Thumbnail.js';
|
||||
|
||||
export interface Emoji {
|
||||
emoji_id: string;
|
||||
shortcuts: string[];
|
||||
search_terms: string[];
|
||||
image: Thumbnail[];
|
||||
is_custom: boolean;
|
||||
}
|
||||
|
||||
export default class EmojiRun implements Run {
|
||||
text: string;
|
||||
emoji: {
|
||||
emoji_id: string;
|
||||
shortcuts: string[];
|
||||
search_terms: string[];
|
||||
image: Thumbnail[];
|
||||
is_custom: boolean;
|
||||
};
|
||||
public text: string;
|
||||
public emoji: Emoji;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
this.text =
|
||||
|
||||
@@ -3,13 +3,20 @@ import type { RawNode } from '../../index.js';
|
||||
import NavigationEndpoint from '../NavigationEndpoint.js';
|
||||
import EmojiRun from './EmojiRun.js';
|
||||
import TextRun from './TextRun.js';
|
||||
import AccessibilityData from './AccessibilityData.js';
|
||||
|
||||
export interface Run {
|
||||
text: string;
|
||||
|
||||
toString(): string;
|
||||
|
||||
toHTML(): string;
|
||||
}
|
||||
|
||||
export interface FormattedStringSupportedAccessibilityDatas {
|
||||
accessibility_data: AccessibilityData;
|
||||
}
|
||||
|
||||
export function escape(text: string) {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
@@ -19,36 +26,78 @@ export function escape(text: string) {
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Place this here, instead of in a private static property,
|
||||
// To avoid the performance penalty of the private field polyfill
|
||||
const TAG = 'Text';
|
||||
|
||||
/**
|
||||
* Represents text content that may include formatting, emojis, and navigation endpoints.
|
||||
*/
|
||||
export default class Text {
|
||||
text?: string;
|
||||
runs?: (EmojiRun | TextRun)[];
|
||||
endpoint?: NavigationEndpoint;
|
||||
/**
|
||||
* The plain text content.
|
||||
*/
|
||||
public text?: string;
|
||||
|
||||
/**
|
||||
* Individual text segments with their formatting.
|
||||
*/
|
||||
public runs?: (EmojiRun | TextRun)[];
|
||||
|
||||
/**
|
||||
* Navigation endpoint associated with this text.
|
||||
*/
|
||||
public endpoint?: NavigationEndpoint;
|
||||
|
||||
/**
|
||||
* Accessibility data associated with this text.
|
||||
*/
|
||||
public accessibility?: FormattedStringSupportedAccessibilityDatas;
|
||||
|
||||
/**
|
||||
* Indicates if the text is right-to-left.
|
||||
*/
|
||||
public rtl: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
if (typeof data === 'object' && data !== null && Reflect.has(data, 'runs') && Array.isArray(data.runs)) {
|
||||
this.runs = data.runs.map((run: RawNode) => run.emoji ?
|
||||
new EmojiRun(run) :
|
||||
new TextRun(run)
|
||||
if (this.isRunsData(data)) {
|
||||
this.runs = data.runs.map((run: RawNode) =>
|
||||
run.emoji ? new EmojiRun(run) : new TextRun(run)
|
||||
);
|
||||
this.text = this.runs.map((run) => run.text).join('');
|
||||
} else {
|
||||
this.text = data?.simpleText;
|
||||
}
|
||||
if (typeof data === 'object' && data !== null && Reflect.has(data, 'navigationEndpoint')) {
|
||||
|
||||
if (this.isObject(data) && 'accessibility' in data
|
||||
&& 'accessibilityData' in data.accessibility) {
|
||||
this.accessibility = {
|
||||
accessibility_data: new AccessibilityData(data.accessibility.accessibilityData)
|
||||
};
|
||||
}
|
||||
|
||||
this.rtl = !!data?.rtl;
|
||||
|
||||
this.parseEndpoint(data);
|
||||
}
|
||||
|
||||
private isRunsData(data: RawNode): data is { runs: RawNode[] } {
|
||||
return this.isObject(data) &&
|
||||
Reflect.has(data, 'runs') &&
|
||||
Array.isArray(data.runs);
|
||||
}
|
||||
|
||||
private parseEndpoint(data: RawNode): void {
|
||||
if (!this.isObject(data)) return;
|
||||
if ('navigationEndpoint' in data) {
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
}
|
||||
if (typeof data === 'object' && data !== null && Reflect.has(data, 'titleNavigationEndpoint')) {
|
||||
} else if ('titleNavigationEndpoint' in data) {
|
||||
this.endpoint = new NavigationEndpoint(data.titleNavigationEndpoint);
|
||||
} else if ((this.runs?.[0] as TextRun)?.endpoint) {
|
||||
this.endpoint = (this.runs?.[0] as TextRun).endpoint;
|
||||
}
|
||||
if (!this.endpoint) {
|
||||
if ((this.runs?.[0] as TextRun)?.endpoint) {
|
||||
this.endpoint = (this.runs?.[0] as TextRun)?.endpoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isObject(data: RawNode): boolean {
|
||||
return typeof data === 'object' && data !== null;
|
||||
}
|
||||
|
||||
static fromAttributed(data: AttributedText) {
|
||||
@@ -75,120 +124,127 @@ export default class Text {
|
||||
length: run.length ?? content.length
|
||||
}) as StyleRun & ResponseRun);
|
||||
|
||||
if (style_runs || command_runs || attachment_runs) {
|
||||
if (style_runs) {
|
||||
for (const style_run of style_runs) {
|
||||
if (
|
||||
style_run.italic ||
|
||||
style_run.strikethrough === 'LINE_STYLE_SINGLE' ||
|
||||
style_run.weightLabel === 'FONT_WEIGHT_MEDIUM' ||
|
||||
style_run.weightLabel === 'FONT_WEIGHT_BOLD'
|
||||
) {
|
||||
const matching_run = findMatchingRun(runs, style_run);
|
||||
if (style_runs?.length)
|
||||
this.processStyleRuns(runs, style_runs, data);
|
||||
|
||||
if (!matching_run) {
|
||||
Log.warn(TAG, 'Unable to find matching run for style run. Skipping...', {
|
||||
style_run,
|
||||
input_data: data,
|
||||
// For performance reasons, web browser consoles only expand an object, when the user clicks on it,
|
||||
// So if we log the original runs object, it might have changed by the time the user looks at it.
|
||||
// Deep clone, so that we log the exact state of the runs at this point.
|
||||
parsed_runs: JSON.parse(JSON.stringify(runs))
|
||||
});
|
||||
if (command_runs?.length)
|
||||
this.processCommandRuns(runs, command_runs, data);
|
||||
|
||||
continue;
|
||||
}
|
||||
if (attachment_runs?.length)
|
||||
this.processAttachmentRuns(runs, attachment_runs, data);
|
||||
|
||||
// Comments use MEDIUM for bold text and video descriptions use BOLD for bold text
|
||||
insertSubRun(runs, matching_run, style_run, {
|
||||
bold: style_run.weightLabel === 'FONT_WEIGHT_MEDIUM' || style_run.weightLabel === 'FONT_WEIGHT_BOLD',
|
||||
italics: style_run.italic,
|
||||
strikethrough: style_run.strikethrough === 'LINE_STYLE_SINGLE'
|
||||
});
|
||||
} else {
|
||||
Log.debug(TAG, 'Skipping style run as it is doesn\'t have any information that we parse.', {
|
||||
style_run,
|
||||
input_data: data
|
||||
});
|
||||
}
|
||||
return new Text({ runs });
|
||||
}
|
||||
|
||||
private static processStyleRuns(runs: RawRun[], style_runs: (StyleRun & ResponseRun)[], data: AttributedText) {
|
||||
for (const style_run of style_runs) {
|
||||
if (
|
||||
style_run.italic ||
|
||||
style_run.strikethrough === 'LINE_STYLE_SINGLE' ||
|
||||
style_run.weightLabel === 'FONT_WEIGHT_MEDIUM' ||
|
||||
style_run.weightLabel === 'FONT_WEIGHT_BOLD'
|
||||
) {
|
||||
const matching_run = findMatchingRun(runs, style_run);
|
||||
|
||||
if (!matching_run) {
|
||||
Log.warn(TAG, 'Unable to find matching run for style run. Skipping...', {
|
||||
style_run,
|
||||
input_data: data,
|
||||
// For performance reasons, web browser consoles only expand an object, when the user clicks on it,
|
||||
// So if we log the original runs object, it might have changed by the time the user looks at it.
|
||||
// Deep clone, so that we log the exact state of the runs at this point.
|
||||
parsed_runs: JSON.parse(JSON.stringify(runs))
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Comments use MEDIUM for bold text and video descriptions use BOLD for bold text
|
||||
insertSubRun(runs, matching_run, style_run, {
|
||||
bold: style_run.weightLabel === 'FONT_WEIGHT_MEDIUM' || style_run.weightLabel === 'FONT_WEIGHT_BOLD',
|
||||
italics: style_run.italic,
|
||||
strikethrough: style_run.strikethrough === 'LINE_STYLE_SINGLE'
|
||||
});
|
||||
} else {
|
||||
Log.debug(TAG, 'Skipping style run as it is doesn\'t have any information that we parse.', {
|
||||
style_run,
|
||||
input_data: data
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static processCommandRuns(runs: RawRun[], command_runs: CommandRun[], data: AttributedText) {
|
||||
for (const command_run of command_runs) {
|
||||
if (command_run.onTap) {
|
||||
const matching_run = findMatchingRun(runs, command_run);
|
||||
|
||||
if (!matching_run) {
|
||||
Log.warn(TAG, 'Unable to find matching run for command run. Skipping...', {
|
||||
command_run,
|
||||
input_data: data,
|
||||
// For performance reasons, web browser consoles only expand an object, when the user clicks on it,
|
||||
// So if we log the original runs object, it might have changed by the time the user looks at it.
|
||||
// Deep clone, so that we log the exact state of the runs at this point.
|
||||
parsed_runs: JSON.parse(JSON.stringify(runs))
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
insertSubRun(runs, matching_run, command_run, {
|
||||
navigationEndpoint: command_run.onTap
|
||||
});
|
||||
} else {
|
||||
Log.debug(TAG, 'Skipping command run as it is missing the "doTap" property.', {
|
||||
command_run,
|
||||
input_data: data
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static processAttachmentRuns(runs: RawRun[], attachment_runs: AttachmentRun[], data: AttributedText) {
|
||||
for (const attachment_run of attachment_runs) {
|
||||
const matching_run = findMatchingRun(runs, attachment_run);
|
||||
|
||||
if (!matching_run) {
|
||||
Log.warn(TAG, 'Unable to find matching run for attachment run. Skipping...', {
|
||||
attachment_run,
|
||||
input_data: data,
|
||||
// For performance reasons, web browser consoles only expand an object, when the user clicks on it,
|
||||
// So if we log the original runs object, it might have changed by the time the user looks at it.
|
||||
// Deep clone, so that we log the exact state of the runs at this point.
|
||||
parsed_runs: JSON.parse(JSON.stringify(runs))
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command_runs) {
|
||||
for (const command_run of command_runs) {
|
||||
if (command_run.onTap) {
|
||||
const matching_run = findMatchingRun(runs, command_run);
|
||||
if (attachment_run.length === 0) {
|
||||
matching_run.attachment = attachment_run;
|
||||
} else {
|
||||
const offset_start_index = attachment_run.startIndex - matching_run.startIndex;
|
||||
|
||||
if (!matching_run) {
|
||||
Log.warn(TAG, 'Unable to find matching run for command run. Skipping...', {
|
||||
command_run,
|
||||
input_data: data,
|
||||
// For performance reasons, web browser consoles only expand an object, when the user clicks on it,
|
||||
// So if we log the original runs object, it might have changed by the time the user looks at it.
|
||||
// Deep clone, so that we log the exact state of the runs at this point.
|
||||
parsed_runs: JSON.parse(JSON.stringify(runs))
|
||||
});
|
||||
const text = matching_run.text.substring(offset_start_index, offset_start_index + attachment_run.length);
|
||||
|
||||
continue;
|
||||
}
|
||||
const is_custom_emoji = (/^:[^:]+:$/).test(text);
|
||||
|
||||
insertSubRun(runs, matching_run, command_run, {
|
||||
navigationEndpoint: command_run.onTap
|
||||
});
|
||||
} else {
|
||||
Log.debug(TAG, 'Skipping command run as it is missing the "doTap" property.', {
|
||||
command_run,
|
||||
input_data: data
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (attachment_run.element?.type?.imageType?.image && (is_custom_emoji || (/^(?:\p{Emoji}|\u200d)+$/u).test(text))) {
|
||||
const emoji = {
|
||||
image: attachment_run.element.type.imageType.image,
|
||||
isCustomEmoji: is_custom_emoji,
|
||||
shortcuts: is_custom_emoji ? [ text ] : undefined
|
||||
};
|
||||
|
||||
if (attachment_runs) {
|
||||
for (const attachment_run of attachment_runs) {
|
||||
const matching_run = findMatchingRun(runs, attachment_run);
|
||||
|
||||
if (!matching_run) {
|
||||
Log.warn(TAG, 'Unable to find matching run for attachment run. Skipping...', {
|
||||
attachment_run,
|
||||
input_data: data,
|
||||
// For performance reasons, web browser consoles only expand an object, when the user clicks on it,
|
||||
// So if we log the original runs object, it might have changed by the time the user looks at it.
|
||||
// Deep clone, so that we log the exact state of the runs at this point.
|
||||
parsed_runs: JSON.parse(JSON.stringify(runs))
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attachment_run.length === 0) {
|
||||
matching_run.attachment = attachment_run;
|
||||
} else {
|
||||
const offset_start_index = attachment_run.startIndex - matching_run.startIndex;
|
||||
|
||||
const text = matching_run.text.substring(offset_start_index, offset_start_index + attachment_run.length);
|
||||
|
||||
const is_custom_emoji = (/^:[^:]+:$/).test(text);
|
||||
|
||||
if (attachment_run.element?.type?.imageType?.image && (is_custom_emoji || (/^(?:\p{Emoji}|\u200d)+$/u).test(text))) {
|
||||
const emoji = {
|
||||
image: attachment_run.element.type.imageType.image,
|
||||
isCustomEmoji: is_custom_emoji,
|
||||
shortcuts: is_custom_emoji ? [ text ] : undefined
|
||||
};
|
||||
|
||||
insertSubRun(runs, matching_run, attachment_run, { emoji });
|
||||
} else {
|
||||
insertSubRun(runs, matching_run, attachment_run, {
|
||||
attachment: attachment_run
|
||||
});
|
||||
}
|
||||
}
|
||||
insertSubRun(runs, matching_run, attachment_run, { emoji });
|
||||
} else {
|
||||
insertSubRun(runs, matching_run, attachment_run, {
|
||||
attachment: attachment_run
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Text({ runs });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,7 +348,7 @@ interface StyleRun extends Partial<ResponseRun> {
|
||||
value: number
|
||||
}[]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface CommandRun extends ResponseRun {
|
||||
|
||||
@@ -3,25 +3,58 @@ import { escape, type Run } from './Text.js';
|
||||
import type { RawNode } from '../../index.js';
|
||||
|
||||
export default class TextRun implements Run {
|
||||
text: string;
|
||||
endpoint?: NavigationEndpoint;
|
||||
bold: boolean;
|
||||
italics: boolean;
|
||||
strikethrough: boolean;
|
||||
deemphasize: boolean;
|
||||
attachment;
|
||||
public text: string;
|
||||
public text_color?: number;
|
||||
public endpoint?: NavigationEndpoint;
|
||||
public bold: boolean;
|
||||
public bracket: boolean;
|
||||
public dark_mode_text_color?: number;
|
||||
public deemphasize: boolean;
|
||||
public italics: boolean;
|
||||
public strikethrough: boolean;
|
||||
public error_underline: boolean;
|
||||
public underline: boolean;
|
||||
public font_face?:
|
||||
| 'FONT_FACE_UNKNOWN'
|
||||
| 'FONT_FACE_YT_SANS_MEDIUM'
|
||||
| 'FONT_FACE_ROBOTO_MEDIUM'
|
||||
| 'FONT_FACE_YOUTUBE_SANS_LIGHT'
|
||||
| 'FONT_FACE_YOUTUBE_SANS_REGULAR'
|
||||
| 'FONT_FACE_YOUTUBE_SANS_MEDIUM'
|
||||
| 'FONT_FACE_YOUTUBE_SANS_SEMIBOLD'
|
||||
| 'FONT_FACE_YOUTUBE_SANS_BOLD'
|
||||
| 'FONT_FACE_YOUTUBE_SANS_EXTRABOLD'
|
||||
| 'FONT_FACE_YOUTUBE_SANS_BLACK'
|
||||
| 'FONT_FACE_YT_SANS_BOLD'
|
||||
| 'FONT_FACE_ROBOTO_REGULAR';
|
||||
public attachment;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
this.text = data.text;
|
||||
this.bold = Boolean(data.bold);
|
||||
this.bracket = Boolean(data.bracket);
|
||||
this.italics = Boolean(data.italics);
|
||||
this.strikethrough = Boolean(data.strikethrough);
|
||||
this.error_underline = Boolean(data.error_underline);
|
||||
this.underline = Boolean(data.underline);
|
||||
this.deemphasize = Boolean(data.deemphasize);
|
||||
|
||||
if (Reflect.has(data, 'navigationEndpoint')) {
|
||||
if ('textColor' in data) {
|
||||
this.text_color = data.textColor;
|
||||
}
|
||||
|
||||
if ('navigationEndpoint' in data) {
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
}
|
||||
|
||||
if ('darkModeTextColor' in data) {
|
||||
this.dark_mode_text_color = data.darkModeTextColor;
|
||||
}
|
||||
|
||||
if ('fontFace' in data) {
|
||||
this.font_face = data.fontFace;
|
||||
}
|
||||
|
||||
this.attachment = data.attachment;
|
||||
}
|
||||
|
||||
@@ -36,7 +69,9 @@ export default class TextRun implements Run {
|
||||
if (this.italics) tags.push('i');
|
||||
if (this.strikethrough) tags.push('s');
|
||||
if (this.deemphasize) tags.push('small');
|
||||
|
||||
if (this.underline) tags.push('u');
|
||||
if (this.error_underline) tags.push('u');
|
||||
|
||||
if (!this.text?.length)
|
||||
return '';
|
||||
|
||||
@@ -48,13 +83,13 @@ export default class TextRun implements Run {
|
||||
if (this.attachment.element.type.imageType.image.sources.length) {
|
||||
if (this.endpoint) {
|
||||
const { url } = this.attachment.element.type.imageType.image.sources[0];
|
||||
|
||||
|
||||
let image_el = '';
|
||||
|
||||
|
||||
if (url) {
|
||||
image_el = `<img src="${url}" style="vertical-align: middle; height: ${this.attachment.element.properties.layoutProperties.height.value}px; width: ${this.attachment.element.properties.layoutProperties.width.value}px;" alt="">`;
|
||||
}
|
||||
|
||||
|
||||
const nav_url = this.endpoint.toURL();
|
||||
if (nav_url) return `<a href="${nav_url}" class="yt-ch-link">${image_el}${wrapped_text}</a>`;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See ./scripts/build-parser-map.js
|
||||
|
||||
export { default as AccessibilityContext } from './classes/misc/AccessibilityContext.js';
|
||||
export { default as AccessibilityData } from './classes/misc/AccessibilityData.js';
|
||||
export { default as Author } from './classes/misc/Author.js';
|
||||
export { default as ChildElement } from './classes/misc/ChildElement.js';
|
||||
export { default as CommandContext } from './classes/misc/CommandContext.js';
|
||||
|
||||
Reference in New Issue
Block a user