feat(markdownTables): update MarkdownTables plugin (#1097)

* feat(markdownTables): update MarkdownTables plugin

* Update index.tsx

---------

Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
yafyx 2026-05-21 00:23:59 +07:00 • committed by GitHub
parent 0c84785080
commit 62a01ee35f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 214 additions and 131 deletions

View file

@ -4,10 +4,12 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { CodeBlock } from "@components/CodeBlock";
import ErrorBoundary from "@components/ErrorBoundary";
import { EquicordDevs } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import definePlugin from "@utils/types";
import definePlugin, { OptionType } from "@utils/types";
import { waitFor } from "@webpack";
import { Parser, useLayoutEffect, useRef, useState } from "@webpack/common";
import type { ReactNode } from "react";
@ -18,6 +20,16 @@ import managedStyle from "./styles.css?managed";
const TABLE_RULE = "markdownTable";
const cl = classNameFactory("vc-markdownTables-");
const settings = definePluginSettings({
hideToggle: {
type: OptionType.BOOLEAN,
default: true,
description: "Hide the Table/Raw toggle and always show rendered tables.",
},
});
const TABLE_BLOCK_SETTING_KEYS: Array<"hideToggle"> = ["hideToggle"];
type ScrollDirection = "left" | "right" | null;
type ParsedCell = unknown[];
type ParserState = Record<string, unknown> & { inline?: boolean; messageId?: string; };
@ -46,6 +58,12 @@ interface ScrollMaskState {
direction: ScrollDirection;
}
interface TableViewProps {
output: ParserOutput;
outputState: ParserState;
table: MarkdownTableNode;
}
interface MarkdownTableRendererProps {
node: MarkdownTableNode;
output: ParserOutput;
@ -81,15 +99,27 @@ function TableBlock({
output,
outputState,
table,
}: {
output: ParserOutput;
outputState: ParserState;
table: MarkdownTableNode;
}) {
const [showRaw, setShowRaw] = useState(false);
}: TableViewProps) {
const { hideToggle } = settings.use(TABLE_BLOCK_SETTING_KEYS);
return (
<div className={cl("root")}>
{hideToggle
? <TableScrollFrame output={output} outputState={outputState} table={table} />
: <ToggleableTableBlock output={output} outputState={outputState} table={table} />}
</div>
);
}
function ToggleableTableBlock({
output,
outputState,
table,
}: TableViewProps) {
const [showRaw, setShowRaw] = useState(false);
return (
<>
<div className={cl("toolbar")} role="group" aria-label="Markdown table view">
<button
aria-pressed={!showRaw}
@ -118,12 +148,12 @@ function TableBlock({
</div>
{showRaw
? (
<pre className={cl("raw")}>
<code>{table.raw}</code>
</pre>
<div className={cl("rawBlock")}>
<CodeBlock content={table.raw} lang="markdown" />
</div>
)
: <TableScrollFrame output={output} outputState={outputState} table={table} />}
</div>
</>
);
}
@ -131,11 +161,7 @@ function TableScrollFrame({
output,
outputState,
table,
}: {
output: ParserOutput;
outputState: ParserState;
table: MarkdownTableNode;
}) {
}: TableViewProps) {
const scrollRef = useRef<HTMLDivElement>(null);
const lastScrollLeftRef = useRef(0);
const frameRef = useRef(0);
@ -149,10 +175,11 @@ function TableScrollFrame({
useLayoutEffect(() => {
const scrollElement = scrollRef.current;
if (!scrollElement) return;
const observedElement: HTMLDivElement = scrollElement;
function updateMask(trackDirection = false) {
const maxScrollLeft = Math.max(0, scrollElement!.scrollWidth - scrollElement!.clientWidth);
const scrollLeft = Math.min(Math.max(scrollElement!.scrollLeft, 0), maxScrollLeft);
function updateMask(element: HTMLDivElement, trackDirection = false) {
const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth);
const scrollLeft = Math.min(Math.max(element.scrollLeft, 0), maxScrollLeft);
let nextDirection: ScrollDirection = null;
if (trackDirection && scrollLeft > lastScrollLeftRef.current) {
@ -184,16 +211,18 @@ function TableScrollFrame({
if (nextDirection) {
window.clearTimeout(directionResetRef.current);
directionResetRef.current = window.setTimeout(() => {
setMaskState(previousState => previousState.direction
? { ...previousState, direction: null }
: previousState);
setMaskState(previousState => {
if (!previousState.direction) return previousState;
return { ...previousState, direction: null };
});
}, 450);
}
}
function scheduleMaskUpdate(trackDirection = false) {
window.cancelAnimationFrame(frameRef.current);
frameRef.current = window.requestAnimationFrame(() => updateMask(trackDirection));
frameRef.current = window.requestAnimationFrame(() => updateMask(observedElement, trackDirection));
}
function handleScroll() {
@ -205,16 +234,16 @@ function TableScrollFrame({
}
const resizeObserver = new ResizeObserver(() => scheduleMaskUpdate());
resizeObserver.observe(scrollElement);
if (scrollElement.firstElementChild) resizeObserver.observe(scrollElement.firstElementChild);
resizeObserver.observe(observedElement);
if (observedElement.firstElementChild) resizeObserver.observe(observedElement.firstElementChild);
updateMask();
scrollElement.addEventListener("scroll", handleScroll, { passive: true });
updateMask(observedElement);
observedElement.addEventListener("scroll", handleScroll, { passive: true });
window.addEventListener("resize", handleResize);
return () => {
resizeObserver.disconnect();
scrollElement.removeEventListener("scroll", handleScroll);
observedElement.removeEventListener("scroll", handleScroll);
window.removeEventListener("resize", handleResize);
window.cancelAnimationFrame(frameRef.current);
window.clearTimeout(directionResetRef.current);
@ -335,6 +364,7 @@ export default definePlugin({
tags: ["Chat", "Appearance"],
authors: [EquicordDevs.yafyx],
managedStyle,
settings,
patches: [
{
find: "simple-markdown: Invalid order for rule",
@ -351,7 +381,6 @@ export default definePlugin({
},
},
],
start() {
shouldInstallTableRule = true;

View file

@ -42,14 +42,17 @@ function isEscapedAt(value: string, index: number) {
return slashCount % 2 === 1;
}
function countEscapedPipes(line: string) {
function hasEscapedTableDelimiters(line: string) {
let count = 0;
for (let i = 0; i < line.length; i++) {
if (line[i] === "|" && isEscapedAt(line, i)) count++;
if (line[i] !== "|" || !isEscapedAt(line, i)) continue;
count++;
if (count >= 2) return true;
}
return count;
return false;
}
function hasUnescapedTableDelimiter(line: string) {
@ -60,10 +63,6 @@ function hasUnescapedTableDelimiter(line: string) {
return false;
}
function hasEscapedTableDelimiters(line: string) {
return countEscapedPipes(line) >= 2;
}
function hasTableDelimiter(line: string) {
return hasUnescapedTableDelimiter(line) || hasEscapedTableDelimiters(line);
}
@ -177,6 +176,17 @@ function canStartTableAt(lines: string[], lineIndex: number, fenced?: boolean[])
return canBeTableLine(lines[lineIndex]) && parseSeparator(lines[lineIndex + 1]) !== null;
}
function canContinueTableAt(lines: string[], lineIndex: number, fenced?: boolean[]) {
if (lineIndex < 0 || fenced?.[lineIndex]) return false;
const line = lines[lineIndex];
if (!canBeTableLine(line) || parseSeparator(line)) return false;
return lineIndex >= lines.length - 1
|| fenced?.[lineIndex + 1]
|| !parseSeparator(lines[lineIndex + 1]);
}
function getFenceMask(lines: string[]) {
const mask = new Array<boolean>(lines.length).fill(false);
let fenceChar: "`" | "~" | null = null;
@ -238,25 +248,29 @@ function parseTableAtLine(lines: string[], lineIndex: number, fenced?: boolean[]
continue;
}
if (rows.length > 0 && isBlankLine(line)) {
const nextLineIndex = nextContentLine(lines, cursor + 1, fenced);
const nextLine = nextLineIndex >= 0 ? lines[nextLineIndex] : "";
if (rows.length > 0) {
if (isBlankLine(line)) {
const nextLineIndex = nextContentLine(lines, cursor + 1, fenced);
if (nextLineIndex >= 0 && canBeTableLine(nextLine) && !canStartTableAt(lines, nextLineIndex, fenced) && !parseSeparator(nextLine)) {
cursor = nextLineIndex;
continue;
if (canContinueTableAt(lines, nextLineIndex, fenced)) {
cursor = nextLineIndex;
continue;
}
} else {
const nextLineIndex = nextContentLine(lines, cursor + 1, fenced);
if (canContinueTableAt(lines, nextLineIndex, fenced)) {
const lastRow = rows[rows.length - 1];
const lastCellIndex = width - 1;
// Treat prose between table-looking rows as wrapped cell text.
lastRow[lastCellIndex] = `${lastRow[lastCellIndex]}\n${line.trim()}`.trim();
cursor++;
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;
}
@ -313,13 +327,10 @@ function parseLooseRowsAtLine(lines: string[], lineIndex: number, fenced?: boole
}
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;
if (line.endsWith("\r\n")) return line.slice(0, -2);
if (line.endsWith("\n") || line.endsWith("\r")) return line.slice(0, -1);
return line;
}
function sourceLines(markdown: string) {

View file

@ -1,7 +1,9 @@
.vc-markdownTables-root {
--vc-markdown-tables-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
display: flex;
flex-direction: column;
gap: 6px;
gap: 4px;
max-width: 100%;
margin: 0.25rem 0;
}
@ -9,57 +11,60 @@
.vc-markdownTables-toolbar {
display: inline-flex;
align-self: flex-start;
padding: 2px;
border: 1px solid var(--background-modifier-accent);
gap: 1px;
padding: 1px;
border: 1px solid transparent;
border-radius: 4px;
background: var(--background-secondary);
background: transparent;
}
.vc-markdownTables-toggle {
min-width: 44px;
padding: 2px 8px;
min-width: 38px;
padding: 1px 6px;
border: 0;
border-radius: 3px;
color: var(--text-muted);
font: inherit;
font-size: 0.75rem;
line-height: 1.25rem;
line-height: 1.125rem;
background: transparent;
cursor: pointer;
transition:
color 140ms var(--vc-markdown-tables-ease-out),
background-color 140ms var(--vc-markdown-tables-ease-out),
transform 140ms var(--vc-markdown-tables-ease-out);
}
.vc-markdownTables-toggle:hover {
.vc-markdownTables-toggle:active {
transform: scale(0.98);
}
.vc-markdownTables-toggle:focus-visible {
outline: 1px solid var(--text-muted);
outline-offset: 2px;
}
.vc-markdownTables-toggle-active {
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);
@media (hover: hover) and (pointer: fine) {
.vc-markdownTables-toggle:hover {
color: var(--text-normal);
background: var(--background-modifier-hover);
}
}
.vc-markdownTables-raw {
.vc-markdownTables-rawBlock {
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-mask-size: 30px;
--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);
--vc-markdown-tables-fade-mid: color-mix(in srgb, var(--vc-markdown-tables-fade-color) 58%, transparent);
--vc-markdown-tables-fade-soft: color-mix(in srgb, var(--vc-markdown-tables-fade-color) 24%, transparent);
position: relative;
max-width: 100%;
@ -80,26 +85,30 @@
opacity: 0;
pointer-events: none;
transition:
opacity 180ms ease,
width 180ms ease;
opacity 160ms var(--vc-markdown-tables-ease-out),
width 160ms var(--vc-markdown-tables-ease-out);
}
.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%);
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%);
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,
@ -109,7 +118,7 @@
.vc-markdownTables-scroll-left::before,
.vc-markdownTables-scroll-right::after {
width: calc(var(--vc-markdown-tables-mask-size) + 10px);
width: calc(var(--vc-markdown-tables-mask-size) + 6px);
}
.vc-markdownTables-scroll {
@ -123,25 +132,31 @@
}
.vc-markdownTables-mask-left .vc-markdownTables-scroll {
mask-image: linear-gradient(to right,
transparent 0,
black var(--vc-markdown-tables-mask-size),
black 100%);
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%);
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%);
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 {
@ -149,14 +164,14 @@
min-width: 100%;
border-collapse: collapse;
color: inherit;
font-size: 0.9375rem;
line-height: 1.25rem;
font-size: 0.875rem;
line-height: 1.2rem;
}
.vc-markdownTables-table th,
.vc-markdownTables-table td {
max-width: 440px;
padding: 6px 10px;
padding: 5px 8px;
border-right: 1px solid var(--background-modifier-accent);
border-bottom: 1px solid var(--background-modifier-accent);
vertical-align: top;
@ -168,8 +183,10 @@
background: var(--background-modifier-hover);
}
.vc-markdownTables-table tbody tr:hover {
background: var(--background-modifier-hover);
@media (hover: hover) and (pointer: fine) {
.vc-markdownTables-table tbody tr:hover {
background: var(--background-modifier-hover);
}
}
.vc-markdownTables-table th:last-child,
@ -183,26 +200,50 @@
@supports (color: color-mix(in srgb, white, black)) {
.vc-markdownTables-scrollFrame {
border-color: color-mix(in srgb, currentcolor 24%, transparent);
border-color: color-mix(in srgb, currentcolor 14%, transparent);
}
.vc-markdownTables-toolbar,
.vc-markdownTables-raw {
border-color: color-mix(in srgb, currentcolor 18%, transparent);
.vc-markdownTables-toolbar {
border-color: color-mix(in srgb, currentcolor 8%, transparent);
}
.vc-markdownTables-toggle-active {
color: color-mix(in srgb, currentcolor 86%, transparent);
background: color-mix(in srgb, currentcolor 7%, transparent);
}
.vc-markdownTables-toggle:focus-visible {
outline-color: color-mix(in srgb, currentcolor 44%, transparent);
}
@media (hover: hover) and (pointer: fine) {
.vc-markdownTables-toggle:hover {
background: color-mix(in srgb, currentcolor 6%, transparent);
}
.vc-markdownTables-toggle-active:hover {
background: color-mix(in srgb, currentcolor 9%, 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);
border-right-color: color-mix(in srgb, currentcolor 12%, transparent);
border-bottom-color: color-mix(in srgb, currentcolor 12%, transparent);
}
.vc-markdownTables-table th {
background: color-mix(in srgb, currentcolor 7%, transparent);
background: color-mix(in srgb, currentcolor 4%, transparent);
}
.vc-markdownTables-table tbody tr:hover {
background: color-mix(in srgb, currentcolor 5%, transparent);
@media (hover: hover) and (pointer: fine) {
.vc-markdownTables-table tbody tr:hover {
background: color-mix(in srgb, currentcolor 4%, transparent);
}
}
.vc-markdownTables-scroll {
scrollbar-color: color-mix(in srgb, currentcolor 24%, transparent) transparent;
}
}
@ -214,21 +255,23 @@
--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,
@supports (color: color-mix(in srgb, white, black)) {
.vc-markdownTables-scroll::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, currentcolor 24%, transparent);
}
}
.vc-markdownTables-scroll::-webkit-scrollbar-track,
.vc-markdownTables-raw::-webkit-scrollbar-corner,
.vc-markdownTables-scroll::-webkit-scrollbar-corner {
background: transparent;
}
@ -243,4 +286,4 @@
.vc-markdownTables-align-right {
text-align: right;
}
}