feat: add MarkdownTables plugin (#1089)

* feat: add MarkdownTables plugin

* bleh

---------

Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
yafyx 2026-05-19 19:52:53 +07:00 • committed by GitHub
parent 8156840270
commit 768b932570
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 1034 additions and 0 deletions

View file

@ -0,0 +1,374 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { EquicordDevs } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import definePlugin from "@utils/types";
import { waitFor } from "@webpack";
import { Parser, useLayoutEffect, useRef, useState } from "@webpack/common";
import type { ReactNode } from "react";
import { type MarkdownTableMatch, parseMarkdownTableMatch, type TableAlignment } from "./parser";
import managedStyle from "./styles.css?managed";
const TABLE_RULE = "markdownTable";
const cl = classNameFactory("vc-markdownTables-");
type ScrollDirection = "left" | "right" | null;
type ParsedCell = unknown[];
type ParserState = Record<string, unknown> & { inline?: boolean; messageId?: string; };
type ParserParse = (source: string, state: ParserState) => ParsedCell;
type ParserOutput = (node: unknown, state: ParserState) => ReactNode;
type MarkdownTableCapture = RegExpExecArray & { markdownTable: MarkdownTableMatch; };
interface MarkdownTableNode {
before: unknown[];
raw: string;
header: ParsedCell[];
alignments: TableAlignment[];
rows: ParsedCell[][];
}
interface MarkdownRule {
order: number;
match(source: string, state: ParserState): MarkdownTableCapture | null;
parse(capture: MarkdownTableCapture, parse: ParserParse, state: ParserState): MarkdownTableNode;
react(node: MarkdownTableNode, output: ParserOutput, state: ParserState): ReactNode;
}
interface ScrollMaskState {
left: boolean;
right: boolean;
direction: ScrollDirection;
}
interface MarkdownTableRendererProps {
node: MarkdownTableNode;
output: ParserOutput;
state: ParserState;
}
let shouldInstallTableRule = false;
let installedRules: MarkdownRules | null = null;
type MarkdownRules = Record<string, Partial<MarkdownRule> | undefined>;
function alignmentClass(alignment: TableAlignment) {
if (alignment === "left") return cl("align-left");
if (alignment === "center") return cl("align-center");
if (alignment === "right") return cl("align-right");
return undefined;
}
function createTableCapture(markdownTable: MarkdownTableMatch, input: string): MarkdownTableCapture {
return Object.assign([markdownTable.raw] as RegExpExecArray, {
index: 0,
input,
groups: undefined,
markdownTable,
});
}
function renderParsedNodes(nodes: ParsedCell, output: ParserOutput, state: ParserState) {
return nodes.map(node => output(node, state));
}
function TableBlock({
output,
outputState,
table,
}: {
output: ParserOutput;
outputState: ParserState;
table: MarkdownTableNode;
}) {
const [showRaw, setShowRaw] = useState(false);
return (
<div className={cl("root")}>
<div className={cl("toolbar")} role="group" aria-label="Markdown table view">
<button
aria-pressed={!showRaw}
className={cl("toggle", { "toggle-active": !showRaw })}
onClick={event => {
event.preventDefault();
event.stopPropagation();
setShowRaw(false);
}}
type="button"
>
Table
</button>
<button
aria-pressed={showRaw}
className={cl("toggle", { "toggle-active": showRaw })}
onClick={event => {
event.preventDefault();
event.stopPropagation();
setShowRaw(true);
}}
type="button"
>
Raw
</button>
</div>
{showRaw
? (
<pre className={cl("raw")}>
<code>{table.raw}</code>
</pre>
)
: <TableScrollFrame output={output} outputState={outputState} table={table} />}
</div>
);
}
function TableScrollFrame({
output,
outputState,
table,
}: {
output: ParserOutput;
outputState: ParserState;
table: MarkdownTableNode;
}) {
const scrollRef = useRef<HTMLDivElement>(null);
const lastScrollLeftRef = useRef(0);
const frameRef = useRef(0);
const directionResetRef = useRef(0);
const [maskState, setMaskState] = useState<ScrollMaskState>({
left: false,
right: false,
direction: null,
});
useLayoutEffect(() => {
const scrollElement = scrollRef.current;
if (!scrollElement) return;
function updateMask(trackDirection = false) {
const maxScrollLeft = Math.max(0, scrollElement!.scrollWidth - scrollElement!.clientWidth);
const scrollLeft = Math.min(Math.max(scrollElement!.scrollLeft, 0), maxScrollLeft);
let nextDirection: ScrollDirection = null;
if (trackDirection && scrollLeft > lastScrollLeftRef.current) {
nextDirection = "right";
} else if (trackDirection && scrollLeft < lastScrollLeftRef.current) {
nextDirection = "left";
}
lastScrollLeftRef.current = scrollLeft;
setMaskState(previousState => {
const nextState: ScrollMaskState = {
left: scrollLeft > 1,
right: maxScrollLeft - scrollLeft > 1,
direction: nextDirection ?? previousState.direction,
};
if (!nextState.left && !nextState.right) {
nextState.direction = null;
}
return previousState.left === nextState.left
&& previousState.right === nextState.right
&& previousState.direction === nextState.direction
? previousState
: nextState;
});
if (nextDirection) {
window.clearTimeout(directionResetRef.current);
directionResetRef.current = window.setTimeout(() => {
setMaskState(previousState => previousState.direction
? { ...previousState, direction: null }
: previousState);
}, 450);
}
}
function scheduleMaskUpdate(trackDirection = false) {
window.cancelAnimationFrame(frameRef.current);
frameRef.current = window.requestAnimationFrame(() => updateMask(trackDirection));
}
function handleScroll() {
scheduleMaskUpdate(true);
}
function handleResize() {
scheduleMaskUpdate();
}
const resizeObserver = new ResizeObserver(() => scheduleMaskUpdate());
resizeObserver.observe(scrollElement);
if (scrollElement.firstElementChild) resizeObserver.observe(scrollElement.firstElementChild);
updateMask();
scrollElement.addEventListener("scroll", handleScroll, { passive: true });
window.addEventListener("resize", handleResize);
return () => {
resizeObserver.disconnect();
scrollElement.removeEventListener("scroll", handleScroll);
window.removeEventListener("resize", handleResize);
window.cancelAnimationFrame(frameRef.current);
window.clearTimeout(directionResetRef.current);
};
}, []);
return (
<div
className={cl(
"scrollFrame",
{
"mask-left": maskState.left,
"mask-right": maskState.right,
"scroll-left": maskState.direction === "left",
"scroll-right": maskState.direction === "right",
},
)}
>
<div className={cl("scroll")} ref={scrollRef}>
<table className={cl("table")}>
{table.header.length > 0 && (
<thead>
<tr>
{table.header.map((cell, cellIndex) => (
<th className={alignmentClass(table.alignments[cellIndex])} key={cellIndex} scope="col">
{renderParsedNodes(cell, output, outputState)}
</th>
))}
</tr>
</thead>
)}
<tbody>
{table.rows.map((row, rowIndex) => (
<tr key={rowIndex}>
{row.map((cell, cellIndex) => (
<td className={alignmentClass(table.alignments[cellIndex])} key={cellIndex}>
{renderParsedNodes(cell, output, outputState)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
function parseCellContent(cells: string[], parse: ParserParse, state: ParserState) {
const inlineState: ParserState = {
...state,
inline: true,
};
return cells.map(cell => parse(cell, inlineState));
}
function shouldSkipTableRule(state: ParserState) {
return state.inline && !state.messageId;
}
const MarkdownTableRenderer = ErrorBoundary.wrap(function MarkdownTableRenderer({
node,
output,
state,
}: MarkdownTableRendererProps) {
return (
<>
{renderParsedNodes(node.before, output, state)}
<TableBlock output={output} outputState={state} table={node} />
</>
);
}, { noop: true });
function createTableRule(order: number): MarkdownRule {
return {
order,
match(source, state) {
if (shouldSkipTableRule(state)) return null;
const parsed = parseMarkdownTableMatch(source);
return parsed ? createTableCapture(parsed, source) : null;
},
parse(capture, parse, state) {
const parsed = capture.markdownTable;
return {
before: parsed.leadingMarkdown ? parse(parsed.leadingMarkdown, state) : [],
raw: parsed.tableRaw,
alignments: parsed.table.alignments,
header: parseCellContent(parsed.table.header, parse, state),
rows: parsed.table.rows.map(row => parseCellContent(row, parse, state)),
};
},
react(node, output, state) {
return <MarkdownTableRenderer node={node} output={output} state={state} />;
},
};
}
function scheduleTableRuleInstall(parser: { defaultRules?: MarkdownRules; }) {
window.setTimeout(() => installTableRuleForParser(parser), 0);
}
function installTableRuleForParser(parser: { defaultRules?: MarkdownRules; }) {
if (!shouldInstallTableRule) return;
const rules = parser.defaultRules;
if (!rules || rules[TABLE_RULE]) return;
installedRules = rules;
const paragraphOrder = typeof rules.paragraph?.order === "number" ? rules.paragraph.order : 1;
rules[TABLE_RULE] = createTableRule(paragraphOrder - 0.5);
}
export default definePlugin({
name: "MarkdownTables",
description: "Render GitHub-style markdown tables in Discord messages.",
tags: ["Chat", "Appearance"],
authors: [EquicordDevs.yafyx],
managedStyle,
patches: [
{
find: "simple-markdown: Invalid order for rule",
replacement: {
match: /paragraph:\{order:/,
replace: "markdownTable:$self.getTableRule(),$&",
},
},
{
find: "Unknown markdown rule:",
replacement: {
match: /paragraph:{type:/,
replace: 'markdownTable:{type:"block"},$&',
},
},
],
start() {
shouldInstallTableRule = true;
if (Parser?.defaultRules) {
scheduleTableRuleInstall(Parser);
}
waitFor("parseTopic", scheduleTableRuleInstall);
},
stop() {
shouldInstallTableRule = false;
if (installedRules) delete installedRules[TABLE_RULE];
installedRules = null;
},
getTableRule(paragraphOrder = 1) {
return createTableRule(paragraphOrder - 0.5);
},
});

View file

@ -0,0 +1,410 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type TableAlignment = "left" | "center" | "right" | null;
export interface MarkdownTable {
header: string[];
alignments: TableAlignment[];
rows: string[][];
startLine: number;
endLine: number;
}
export interface MarkdownTableBlock {
raw: string;
table: MarkdownTable;
}
export interface MarkdownTableMatch {
raw: string;
leadingMarkdown: string;
tableRaw: string;
table: MarkdownTable;
}
interface SeparatorParseResult {
alignments: TableAlignment[];
}
const separatorCellRe = /^:?-{3,}:?$/;
function isEscapedAt(value: string, index: number) {
let slashCount = 0;
for (let i = index - 1; i >= 0 && value[i] === "\\"; i--) {
slashCount++;
}
return slashCount % 2 === 1;
}
function countEscapedPipes(line: string) {
let count = 0;
for (let i = 0; i < line.length; i++) {
if (line[i] === "|" && isEscapedAt(line, i)) count++;
}
return count;
}
function hasUnescapedTableDelimiter(line: string) {
for (let i = 0; i < line.length; i++) {
if (line[i] === "|" && !isEscapedAt(line, i)) return true;
}
return false;
}
function hasEscapedTableDelimiters(line: string) {
return countEscapedPipes(line) >= 2;
}
function hasTableDelimiter(line: string) {
return hasUnescapedTableDelimiter(line) || hasEscapedTableDelimiters(line);
}
function isIndentedCodeLine(line: string) {
return /^(?: {4,}|\t)/.test(line);
}
function isFenceLine(line: string) {
return /^(?: {0,3})(`{3,}|~{3,})/.test(line);
}
function canBeTableLine(line: string) {
return line.trim() !== ""
&& !isIndentedCodeLine(line)
&& !isFenceLine(line)
&& hasTableDelimiter(line);
}
function stripOuterPipes(line: string) {
const trimmed = line.trim();
let start = 0;
let end = trimmed.length;
if (trimmed[start] === "|") start++;
if (trimmed[end - 1] === "|" && !isEscapedAt(trimmed, end - 1)) end--;
return trimmed.slice(start, end);
}
function hasOuterTablePipes(line: string) {
const trimmed = line.trim();
return (trimmed.startsWith("|") || trimmed.startsWith("\\|"))
&& (trimmed.endsWith("|") || trimmed.endsWith("\\|"));
}
export function splitTableRow(line: string) {
const cells: string[] = [];
const row = stripOuterPipes(hasUnescapedTableDelimiter(line) ? line : line.replace(/\\\|/g, "|"));
let current = "";
for (let i = 0; i < row.length; i++) {
const char = row[i];
if (char === "\\" && row[i + 1] === "|" && !isEscapedAt(row, i)) {
current += "|";
i++;
continue;
}
if (char === "|" && !isEscapedAt(row, i)) {
cells.push(current.trim());
current = "";
continue;
}
current += char;
}
cells.push(current.trim());
return cells;
}
function parseAlignment(cell: string): TableAlignment | undefined {
const value = cell.trim();
if (!separatorCellRe.test(value)) return undefined;
if (value.startsWith(":") && value.endsWith(":")) return "center";
if (value.startsWith(":")) return "left";
if (value.endsWith(":")) return "right";
return null;
}
function parseSeparator(line: string): SeparatorParseResult | null {
if (!canBeTableLine(line)) return null;
const cells = splitTableRow(line);
if (cells.length < 2) return null;
const alignments = cells.map(parseAlignment);
if (alignments.some(alignment => alignment === undefined)) return null;
return {
alignments: alignments as TableAlignment[],
};
}
function normaliseCells(cells: string[], width: number) {
return Array.from({ length: width }, (_, index) => cells[index] ?? "");
}
function isBlankLine(line: string) {
return line.trim() === "";
}
function nextContentLine(lines: string[], cursor: number, fenced?: boolean[]) {
for (let index = cursor; index < lines.length; index++) {
if (fenced?.[index]) return -1;
if (!isBlankLine(lines[index])) return index;
}
return -1;
}
function canStartTableAt(lines: string[], lineIndex: number, fenced?: boolean[]) {
if (lineIndex < 0 || lineIndex >= lines.length - 1) return false;
if (fenced?.[lineIndex] || fenced?.[lineIndex + 1]) return false;
return canBeTableLine(lines[lineIndex]) && parseSeparator(lines[lineIndex + 1]) !== null;
}
function getFenceMask(lines: string[]) {
const mask = new Array<boolean>(lines.length).fill(false);
let fenceChar: "`" | "~" | null = null;
let fenceLength = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const openingMatch = /^(?: {0,3})(`{3,}|~{3,})/.exec(line);
if (!fenceChar) {
if (openingMatch) {
fenceChar = openingMatch[1][0] as "`" | "~";
fenceLength = openingMatch[1].length;
mask[i] = true;
}
continue;
}
mask[i] = true;
const trimmed = line.trimStart();
let closingLength = 0;
while (trimmed[closingLength] === fenceChar) {
closingLength++;
}
if (closingLength >= fenceLength && trimmed.slice(closingLength).trim() === "") {
fenceChar = null;
fenceLength = 0;
}
}
return mask;
}
function parseTableAtLine(lines: string[], lineIndex: number, fenced?: boolean[]) {
if (lineIndex >= lines.length - 1) return null;
if (fenced?.[lineIndex] || fenced?.[lineIndex + 1]) return null;
if (!canBeTableLine(lines[lineIndex]) || !canBeTableLine(lines[lineIndex + 1])) return null;
const headerCells = splitTableRow(lines[lineIndex]);
if (headerCells.length < 2) return null;
const separator = parseSeparator(lines[lineIndex + 1]);
if (!separator) return null;
const width = separator.alignments.length;
const rows: string[][] = [];
let cursor = lineIndex + 2;
while (cursor < lines.length && !fenced?.[cursor]) {
const line = lines[cursor];
if (canBeTableLine(line)) {
rows.push(normaliseCells(splitTableRow(line), width));
cursor++;
continue;
}
if (rows.length > 0 && isBlankLine(line)) {
const nextLineIndex = nextContentLine(lines, cursor + 1, fenced);
const nextLine = nextLineIndex >= 0 ? lines[nextLineIndex] : "";
if (nextLineIndex >= 0 && canBeTableLine(nextLine) && !canStartTableAt(lines, nextLineIndex, fenced) && !parseSeparator(nextLine)) {
cursor = nextLineIndex;
continue;
}
}
const nextLineIndex = nextContentLine(lines, cursor + 1, fenced);
const nextLine = nextLineIndex >= 0 ? lines[nextLineIndex] : "";
if (rows.length > 0 && !isBlankLine(line) && nextLineIndex >= 0 && canBeTableLine(nextLine) && !canStartTableAt(lines, nextLineIndex, fenced) && !parseSeparator(nextLine)) {
rows[rows.length - 1][width - 1] = `${rows[rows.length - 1][width - 1]}\n${line.trim()}`.trim();
cursor++;
continue;
}
break;
}
while (cursor > lineIndex + 2 && isBlankLine(lines[cursor - 1])) {
cursor--;
}
if (rows.length === 0) {
return null;
}
return {
table: {
header: normaliseCells(headerCells, width),
alignments: separator.alignments,
rows,
startLine: lineIndex,
endLine: cursor - 1,
},
nextLineIndex: cursor,
};
}
function parseLooseRowsAtLine(lines: string[], lineIndex: number, fenced?: boolean[]) {
if (fenced?.[lineIndex] || !canBeTableLine(lines[lineIndex]) || !hasOuterTablePipes(lines[lineIndex])) return null;
const firstRow = splitTableRow(lines[lineIndex]);
const width = firstRow.length;
const rows: string[][] = [];
let cursor = lineIndex;
if (width < 3) return null;
while (cursor < lines.length && !fenced?.[cursor] && canBeTableLine(lines[cursor]) && hasOuterTablePipes(lines[cursor])) {
const cells = splitTableRow(lines[cursor]);
if (cells.length < 3) break;
rows.push(normaliseCells(cells, width));
cursor++;
}
if (rows.length < 2) return null;
return {
table: {
header: [],
alignments: Array.from({ length: width }, () => null),
rows,
startLine: lineIndex,
endLine: cursor - 1,
},
nextLineIndex: cursor,
};
}
function stripLineEnding(line: string) {
return line.endsWith("\r\n")
? line.slice(0, -2)
: line.endsWith("\n")
? line.slice(0, -1)
: line.endsWith("\r")
? line.slice(0, -1)
: line;
}
function sourceLines(markdown: string) {
const lines: Array<{ raw: string; text: string; }> = [];
let start = 0;
for (let i = 0; i < markdown.length; i++) {
if (markdown[i] !== "\n") continue;
const raw = markdown.slice(start, i + 1);
lines.push({
raw,
text: stripLineEnding(raw),
});
start = i + 1;
}
if (start < markdown.length) {
const raw = markdown.slice(start);
lines.push({
raw,
text: stripLineEnding(raw),
});
}
return lines;
}
function rawTableBlock(lines: Array<{ raw: string; }>, nextLineIndex: number) {
return lines
.slice(0, nextLineIndex)
.map((line, index) => index === nextLineIndex - 1 ? stripLineEnding(line.raw) : line.raw)
.join("");
}
export function parseMarkdownTableBlock(markdown: string): MarkdownTableBlock | null {
const lines = sourceLines(markdown);
const parsed = parseTableAtLine(lines.map(line => line.text), 0);
if (!parsed) return null;
return {
raw: rawTableBlock(lines, parsed.nextLineIndex),
table: parsed.table,
};
}
export function parseMarkdownTableMatch(markdown: string): MarkdownTableMatch | null {
const lines = sourceLines(markdown);
const textLines = lines.map(line => line.text);
const fenced = getFenceMask(textLines);
for (let lineIndex = 0; lineIndex < textLines.length - 1; lineIndex++) {
const parsed = parseTableAtLine(textLines, lineIndex, fenced) ?? parseLooseRowsAtLine(textLines, lineIndex, fenced);
if (!parsed) continue;
const leadingMarkdown = lines
.slice(0, lineIndex)
.map(line => line.raw)
.join("");
const tableRaw = rawTableBlock(lines.slice(lineIndex), parsed.nextLineIndex - lineIndex);
return {
raw: `${leadingMarkdown}${tableRaw}`,
leadingMarkdown,
tableRaw,
table: parsed.table,
};
}
return null;
}
export function parseMarkdownTables(markdown: string): MarkdownTable[] {
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
const fenced = getFenceMask(lines);
const tables: MarkdownTable[] = [];
for (let lineIndex = 0; lineIndex < lines.length - 1; lineIndex++) {
const parsed = parseTableAtLine(lines, lineIndex, fenced);
if (!parsed) continue;
tables.push(parsed.table);
lineIndex = parsed.nextLineIndex - 1;
}
return tables;
}

View file

@ -0,0 +1,246 @@
.vc-markdownTables-root {
display: flex;
flex-direction: column;
gap: 6px;
max-width: 100%;
margin: 0.25rem 0;
}
.vc-markdownTables-toolbar {
display: inline-flex;
align-self: flex-start;
padding: 2px;
border: 1px solid var(--background-modifier-accent);
border-radius: 4px;
background: var(--background-secondary);
}
.vc-markdownTables-toggle {
min-width: 44px;
padding: 2px 8px;
border: 0;
border-radius: 3px;
color: var(--text-muted);
font: inherit;
font-size: 0.75rem;
line-height: 1.25rem;
background: transparent;
cursor: pointer;
}
.vc-markdownTables-toggle:hover {
color: var(--text-normal);
background: var(--background-modifier-hover);
}
.vc-markdownTables-toggle-active,
.vc-markdownTables-toggle-active:hover {
color: var(--interactive-active);
background: var(--background-modifier-selected);
}
.vc-markdownTables-raw {
max-width: 100%;
margin: 0;
padding: 8px 10px;
border: 1px solid var(--background-modifier-accent);
border-radius: 4px;
overflow-x: auto;
color: inherit;
background: var(--background-secondary);
font-size: 0.875rem;
line-height: 1.25rem;
scrollbar-width: thin;
scrollbar-color: gray transparent;
white-space: pre;
}
.vc-markdownTables-scrollFrame {
--vc-markdown-tables-mask-size: 42px;
--vc-markdown-tables-fade-color: var(--background-primary);
--vc-markdown-tables-fade-mid: color-mix(in srgb, var(--vc-markdown-tables-fade-color) 74%, transparent);
--vc-markdown-tables-fade-soft: color-mix(in srgb, var(--vc-markdown-tables-fade-color) 36%, transparent);
position: relative;
max-width: 100%;
border: 1px solid var(--background-modifier-accent);
border-radius: 4px;
background: transparent;
overflow: hidden;
}
.vc-markdownTables-scrollFrame::before,
.vc-markdownTables-scrollFrame::after {
position: absolute;
top: 0;
bottom: 0;
z-index: 1;
width: var(--vc-markdown-tables-mask-size);
content: "";
opacity: 0;
pointer-events: none;
transition:
opacity 180ms ease,
width 180ms ease;
}
.vc-markdownTables-scrollFrame::before {
left: 0;
background: linear-gradient(to right,
var(--vc-markdown-tables-fade-color) 0,
var(--vc-markdown-tables-fade-mid) 42%,
var(--vc-markdown-tables-fade-soft) 70%,
transparent 100%);
}
.vc-markdownTables-scrollFrame::after {
right: 0;
background: linear-gradient(to left,
var(--vc-markdown-tables-fade-color) 0,
var(--vc-markdown-tables-fade-mid) 42%,
var(--vc-markdown-tables-fade-soft) 70%,
transparent 100%);
}
.vc-markdownTables-mask-left::before,
.vc-markdownTables-mask-right::after {
opacity: 1;
}
.vc-markdownTables-scroll-left::before,
.vc-markdownTables-scroll-right::after {
width: calc(var(--vc-markdown-tables-mask-size) + 10px);
}
.vc-markdownTables-scroll {
max-width: 100%;
overflow-x: auto;
background: transparent;
mask-repeat: no-repeat;
mask-size: 100% 100%;
scrollbar-width: thin;
scrollbar-color: gray transparent;
}
.vc-markdownTables-mask-left .vc-markdownTables-scroll {
mask-image: linear-gradient(to right,
transparent 0,
black var(--vc-markdown-tables-mask-size),
black 100%);
}
.vc-markdownTables-mask-right .vc-markdownTables-scroll {
mask-image: linear-gradient(to right,
black 0,
black calc(100% - var(--vc-markdown-tables-mask-size)),
transparent 100%);
}
.vc-markdownTables-mask-left.vc-markdownTables-mask-right .vc-markdownTables-scroll {
mask-image: linear-gradient(to right,
transparent 0,
black var(--vc-markdown-tables-mask-size),
black calc(100% - var(--vc-markdown-tables-mask-size)),
transparent 100%);
}
.vc-markdownTables-table {
width: max-content;
min-width: 100%;
border-collapse: collapse;
color: inherit;
font-size: 0.9375rem;
line-height: 1.25rem;
}
.vc-markdownTables-table th,
.vc-markdownTables-table td {
max-width: 440px;
padding: 6px 10px;
border-right: 1px solid var(--background-modifier-accent);
border-bottom: 1px solid var(--background-modifier-accent);
vertical-align: top;
white-space: pre-wrap;
}
.vc-markdownTables-table th {
font-weight: 600;
background: var(--background-modifier-hover);
}
.vc-markdownTables-table tbody tr:hover {
background: var(--background-modifier-hover);
}
.vc-markdownTables-table th:last-child,
.vc-markdownTables-table td:last-child {
border-right: 0;
}
.vc-markdownTables-table tr:last-child td {
border-bottom: 0;
}
@supports (color: color-mix(in srgb, white, black)) {
.vc-markdownTables-scrollFrame {
border-color: color-mix(in srgb, currentcolor 24%, transparent);
}
.vc-markdownTables-toolbar,
.vc-markdownTables-raw {
border-color: color-mix(in srgb, currentcolor 18%, transparent);
}
.vc-markdownTables-table th,
.vc-markdownTables-table td {
border-right-color: color-mix(in srgb, currentcolor 18%, transparent);
border-bottom-color: color-mix(in srgb, currentcolor 18%, transparent);
}
.vc-markdownTables-table th {
background: color-mix(in srgb, currentcolor 7%, transparent);
}
.vc-markdownTables-table tbody tr:hover {
background: color-mix(in srgb, currentcolor 5%, transparent);
}
}
.vc-markdownTables-scrollFrame:is(.theme-dark *) {
--vc-markdown-tables-fade-color: color-mix(in srgb, var(--background-primary) 88%, black);
}
.vc-markdownTables-scrollFrame:is(.theme-light *) {
--vc-markdown-tables-fade-color: color-mix(in srgb, var(--background-primary) 88%, white);
}
.vc-markdownTables-raw::-webkit-scrollbar,
.vc-markdownTables-scroll::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.vc-markdownTables-raw::-webkit-scrollbar-thumb,
.vc-markdownTables-scroll::-webkit-scrollbar-thumb {
border-radius: 999px;
background-color: gray;
}
.vc-markdownTables-raw::-webkit-scrollbar-track,
.vc-markdownTables-scroll::-webkit-scrollbar-track,
.vc-markdownTables-raw::-webkit-scrollbar-corner,
.vc-markdownTables-scroll::-webkit-scrollbar-corner {
background: transparent;
}
.vc-markdownTables-align-left {
text-align: left;
}
.vc-markdownTables-align-center {
text-align: center;
}
.vc-markdownTables-align-right {
text-align: right;
}

View file

@ -664,6 +664,10 @@ export const EquicordDevs = Object.freeze({
name: "nobody",
id: 0n
},
yafyx: {
name: "kiniyaku",
id: 658209494609821699n
},
thororen: {
name: "thororen",
id: 848339671629299742n