From 62a01ee35f28db23a310cf13889387d0cd42563d Mon Sep 17 00:00:00 2001 From: yafyx Date: Thu, 21 May 2026 00:23:59 +0700 Subject: [PATCH] feat(markdownTables): update MarkdownTables plugin (#1097) * feat(markdownTables): update MarkdownTables plugin * Update index.tsx --------- Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com> --- src/equicordplugins/markdownTables/index.tsx | 87 +++++--- src/equicordplugins/markdownTables/parser.ts | 69 ++++--- src/equicordplugins/markdownTables/styles.css | 189 +++++++++++------- 3 files changed, 214 insertions(+), 131 deletions(-) diff --git a/src/equicordplugins/markdownTables/index.tsx b/src/equicordplugins/markdownTables/index.tsx index aa8df3ddb..ec036d81b 100644 --- a/src/equicordplugins/markdownTables/index.tsx +++ b/src/equicordplugins/markdownTables/index.tsx @@ -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 & { 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 (
+ {hideToggle + ? + : } +
+ ); +} + +function ToggleableTableBlock({ + output, + outputState, + table, +}: TableViewProps) { + const [showRaw, setShowRaw] = useState(false); + + return ( + <>
+ ); } @@ -131,11 +161,7 @@ function TableScrollFrame({ output, outputState, table, -}: { - output: ParserOutput; - outputState: ParserState; - table: MarkdownTableNode; -}) { +}: TableViewProps) { const scrollRef = useRef(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; diff --git a/src/equicordplugins/markdownTables/parser.ts b/src/equicordplugins/markdownTables/parser.ts index 5e0786de7..c9a7c2fec 100644 --- a/src/equicordplugins/markdownTables/parser.ts +++ b/src/equicordplugins/markdownTables/parser.ts @@ -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(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) { diff --git a/src/equicordplugins/markdownTables/styles.css b/src/equicordplugins/markdownTables/styles.css index 866f537ca..19dcd9688 100644 --- a/src/equicordplugins/markdownTables/styles.css +++ b/src/equicordplugins/markdownTables/styles.css @@ -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; -} \ No newline at end of file +}