From 768b9325708b4f765d723fd3d04ce0d86967c006 Mon Sep 17 00:00:00 2001 From: yafyx Date: Tue, 19 May 2026 19:52:53 +0700 Subject: [PATCH] feat: add MarkdownTables plugin (#1089) * feat: add MarkdownTables plugin * bleh --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> --- src/equicordplugins/markdownTables/index.tsx | 374 ++++++++++++++++ src/equicordplugins/markdownTables/parser.ts | 410 ++++++++++++++++++ src/equicordplugins/markdownTables/styles.css | 246 +++++++++++ src/utils/constants.ts | 4 + 4 files changed, 1034 insertions(+) create mode 100644 src/equicordplugins/markdownTables/index.tsx create mode 100644 src/equicordplugins/markdownTables/parser.ts create mode 100644 src/equicordplugins/markdownTables/styles.css diff --git a/src/equicordplugins/markdownTables/index.tsx b/src/equicordplugins/markdownTables/index.tsx new file mode 100644 index 000000000..aa8df3ddb --- /dev/null +++ b/src/equicordplugins/markdownTables/index.tsx @@ -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 & { 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 | 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 ( +
+
+ + +
+ {showRaw + ? ( +
+                        {table.raw}
+                    
+ ) + : } +
+ ); +} + +function TableScrollFrame({ + output, + outputState, + table, +}: { + output: ParserOutput; + outputState: ParserState; + table: MarkdownTableNode; +}) { + const scrollRef = useRef(null); + const lastScrollLeftRef = useRef(0); + const frameRef = useRef(0); + const directionResetRef = useRef(0); + const [maskState, setMaskState] = useState({ + 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 ( +
+
+ + {table.header.length > 0 && ( + + + {table.header.map((cell, cellIndex) => ( + + ))} + + + )} + + {table.rows.map((row, rowIndex) => ( + + {row.map((cell, cellIndex) => ( + + ))} + + ))} + +
+ {renderParsedNodes(cell, output, outputState)} +
+ {renderParsedNodes(cell, output, outputState)} +
+
+
+ ); +} + +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)} + + + ); +}, { 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 ; + }, + }; +} +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); + }, +}); diff --git a/src/equicordplugins/markdownTables/parser.ts b/src/equicordplugins/markdownTables/parser.ts new file mode 100644 index 000000000..5e0786de7 --- /dev/null +++ b/src/equicordplugins/markdownTables/parser.ts @@ -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(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; +} diff --git a/src/equicordplugins/markdownTables/styles.css b/src/equicordplugins/markdownTables/styles.css new file mode 100644 index 000000000..866f537ca --- /dev/null +++ b/src/equicordplugins/markdownTables/styles.css @@ -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; +} \ No newline at end of file diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 7af8f8d13..da7e30958 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -664,6 +664,10 @@ export const EquicordDevs = Object.freeze({ name: "nobody", id: 0n }, + yafyx: { + name: "kiniyaku", + id: 658209494609821699n + }, thororen: { name: "thororen", id: 848339671629299742n