feat(new tool): implement table to markdown generator

This commit is contained in:
Le Xuan Tien 2026-07-09 08:45:35 +07:00
parent 953c0a6749
commit ed2d047ab0
8 changed files with 1682 additions and 161 deletions

View file

@ -0,0 +1,178 @@
# 2026-07-08 Table to Markdown Generator Design
## Status
Proposed (Pending User Review)
---
## 1. Context & Goals
Markdown tables (GitHub Flavored Markdown) are extremely useful but tedious to write manually. There is a need for a modern, rich-text table editor within `my-it-tools` that allows developers to:
1. Create, edit, and format tables visually.
2. Intercept copy-paste events from spreadsheets (like Excel, Google Sheets) or HTML pages and auto-populate the table.
3. Edit cell contents with standard text formatting (Bold, Italic) visually.
4. Align columns (Left, Center, Right) visually and output the correct GFM alignment indicators.
5. Export clean GFM Markdown with options for compact mode, beautify/padded mode, and proper representation of cell line breaks (using HTML `<br>`).
---
## 2. Requirements
### Core Features
1. **Interactive Table Editor:**
- Strict table header (the first row).
- Spreadsheet-like grid with cells editable using HTML `contenteditable`.
- Selection/Focus tracking for active column/row index.
- **Keyboard Navigation & Editing:**
- `Tab`: Move focus to the next cell. If on the last cell of the table, auto-insert a new row and focus its first cell.
- `Shift + Tab`: Move focus to the previous cell.
- `Enter`: Move focus to the cell directly below. If on the last row, auto-create a new row below and focus the corresponding cell.
- `Shift + Enter`: Insert a line break (`<br>`) inside the current cell.
- **Right-click Actions:** Right-clicking on column headers or row header numbers opens a context menu to insert/delete columns/rows and adjust alignment.
2. **Tabular Copy-Paste Interception:**
- Copying tabular data from Excel, Google Sheets, Word, or web pages (which generate `text/html` `<table>` blocks) will populate the grid automatically.
- Text formats like TSV/CSV or GFM Markdown tables pasted into the editor will be parsed.
3. **Rich Text Formatting:**
- Inline formatting (Bold, Italic) visually rendered inside cells, mapped to GFM syntax (`**` and `*`) upon Markdown generation.
- Keyboard shortcuts (`Ctrl+B` for bold, `Ctrl+I` for italic) and toolbar buttons.
4. **Column & Row Actions:**
- Insert rows (above/below), insert columns (left/right).
- Delete selected rows or columns.
- Transpose the table (swap rows and columns).
- Column alignment: left, center, right.
5. **Output Options & Live Preview:**
- **Compact Mode:** Omit padding spaces around cell contents (e.g., `|Cell 1|Cell 2|`) for a minified, space-saving output format.
- **Beautify Mode (Padded):** Pad column cells with trailing spaces so they align vertically in the raw Markdown text editor (e.g., `| Header 1 | Long Cell 2 |`).
- **Line Break Representation:** Since Markdown tables do not support actual multi-line formatting inside a cell, any cell line breaks (e.g. from Shift+Enter) are translated into `<br>` tags to preserve formatting.
- **Markdown Code Block:** Live Markdown result with a one-click copy button and download option.
- **Rendered Preview:** Live HTML rendering of the generated Markdown to verify the result visually.
---
## 3. Architecture & Data Model
We will build the module in a dedicated folder: `src/tools/table-to-markdown/`.
```
src/tools/table-to-markdown/
├── index.ts # Tool definition & registration
├── table-to-markdown.vue # Main layout wrapper
├── table-editor.vue # Component for editable table grid
└── table-state.ts # Core model & utility methods for table state
```
### Data Representation (`table-state.ts`)
```typescript
export interface Cell {
html: string; // HTML string containing formatting tags (<b>, <i>, <strong>, <em>, code)
}
export type Alignment = 'left' | 'center' | 'right' | null;
export interface TableSnapshot {
headers: Cell[];
rows: Cell[][];
alignments: Alignment[];
}
export class TableState {
public headers: Cell[] = [];
public rows: Cell[][] = [];
public alignments: Alignment[] = [];
// Undo/Redo history stacks
private undoStack: TableSnapshot[] = [];
private redoStack: TableSnapshot[] = [];
constructor(initialRows = 3, initialCols = 3) {
this.reset(initialRows, initialCols);
}
public reset(numRows: number, numCols: number) {
this.headers = Array.from({ length: numCols }, () => ({ html: '' }));
this.rows = Array.from({ length: numRows }, () =>
Array.from({ length: numCols }, () => ({ html: '' }))
);
this.alignments = Array.from({ length: numCols }, () => null);
this.clearHistory();
}
// State operations...
}
```
---
## 4. Key Mechanics & Algorithms
### 1. Clipboard Paste Parser
When pasting data into the editor:
- **HTML Table (`text/html`):** Parse via DOMParser. Extract the first `<table>` element. Map headers (`<th>` or first row `<td>`s) and rows (`<td>`s). Cell HTML is sanitized using DOMPurify to preserve only formatting tags (`<b>`, `<strong>`, `<i>`, `<em>`, `<code>`, `<br>`).
- **CSV / TSV (`text/plain`):** Check if content contains tabs or commas. Split rows by `\n` and cells by `\t` (Excel plain text copy) or `,`.
- **GFM Table (`text/plain`):** Check if content matches markdown table syntax. If yes, parse cell Markdown contents and convert standard GFM to HTML.
### 2. GFM Cell Content Translation
For each cell's HTML content, we perform a lightweight translation:
- Replace `<strong>` / `<b>` elements with `**` wrapping the inner text.
- Replace `em` / `i` elements with `*` wrapping the inner text.
- Replace `code` elements with `` ` `` wrapping the inner text.
- Convert visual line breaks (like `<br>`, block elements, or `\n` characters) to HTML `<br>` tags.
- Escape any raw pipe characters (`|` -> `\|`) to prevent breaking the Markdown table structure.
- Strip any other unexpected HTML elements while keeping their text.
### 3. Markdown Generator
```typescript
public toMarkdown(options: { compact: boolean }): string {
// 1. Convert cell HTML to inline markdown text:
// - Formatting (bold, italic, code) -> Markdown tokens
// - Line breaks -> '<br>'
// - Escape pipe symbols -> '\|'
// 2. If beautifying (compact = false):
// - Calculate the maximum string length of each column's cells (including headers)
// - Pad each cell with spaces to match the maximum length
// 3. Format header row
// 4. Format separator row based on alignments with matching padding:
// - Left: :--- or :--- [padding]
// - Center: :---: or :---: [padding]
// - Right: ---: or [padding] ---:
// 5. Format body rows
// 6. Join all rows with newlines
}
```
---
## 5. UI Layout
The UI will follow the premium dark/light mode aesthetics of `my-it-tools` using Naive UI controls:
1. **Toolbar:**
- **Table Settings:** Preset size dialog, Clear, Transpose.
- **Column Options:** Left/Center/Right Align. Insert Left, Insert Right, Delete Column.
- **Row Options:** Insert Above, Insert Below, Delete Row.
- **Undo/Redo:** Back and Forward history buttons.
2. **Table Editor Grid:**
- Styled HTML `<table>` with borders.
- Header cells and row index handles styled distinctively.
- Each cell contains a `div` with `contenteditable="true"`.
- Right-click handlers on column headers (th) and row indices (td.index-column) to show a context-sensitive `<n-dropdown>` context menu.
- **Column context menu options:** Align Left, Align Center, Align Right, Insert Column Left, Insert Column Right, Delete Column.
- **Row context menu options:** Insert Row Above, Insert Row Below, Delete Row.
- Hover elements to show target add/remove action icons at the border of columns/rows.
3. **Output & Preview:**
- Standard GFM markdown output textarea with copy and download buttons.
- Beautifully rendered tab for visual preview of the markdown.
---
## 6. Testing Strategy
1. **Unit Tests (`table-state.test.ts`):**
- Test table initialization and resizing.
- Test insertion & deletion of rows and columns at specific indexes.
- Test column alignments.
- Test CSV/TSV, HTML table, and GFM markdown pasting/parsing.
- Test markdown translation (bold, italic, and escaping pipe symbols).
- Test Undo/Redo history stack operations.
2. **Component Tests (`table-to-markdown.test.ts`):**
- Verify tool rendering, button clicks, and basic data binding.

View file

@ -1,394 +1,468 @@
'404':
notFound: 404 Not Found
sorry: Sorry, this page does not seem to exist
maybe: Maybe the cache is doing tricky things, try force-refreshing?
backHome: Back home
home:
categories:
newestTools: Newest tools
favoriteTools: 'Your favorite tools'
allTools: 'All the tools'
favoritesDndToolTip: 'Drag and drop to reorder favorites'
subtitle: 'Handy tools for developers'
toggleMenu: 'Toggle menu'
favoriteTools: Your favorite tools
allTools: All the tools
favoritesDndToolTip: Drag and drop to reorder favorites
subtitle: Handy tools for developers
toggleMenu: Toggle menu
home: Home
uiLib: 'UI Lib'
support: 'Support IT-Tools development'
buyMeACoffee: 'Buy me a coffee'
uiLib: UI Lib
support: Support IT-Tools development
buyMeACoffee: Buy me a coffee
follow:
title: 'You like it-tools?'
p1: 'Give us a star on'
githubRepository: 'IT-Tools GitHub repository'
p2: 'or follow us on'
twitterXAccount: 'IT-Tools X account'
thankYou: 'Thank you!'
title: You like it-tools?
p1: Give us a star on
githubRepository: IT-Tools GitHub repository
p2: or follow us on
twitterXAccount: IT-Tools X account
thankYou: Thank you!
nav:
github: 'GitHub repository'
githubRepository: 'IT-Tools GitHub repository'
twitterX: 'X account'
twitterXAccount: 'IT Tools X account'
about: 'About IT-Tools'
aboutLabel: 'About'
darkMode: 'Dark mode'
lightMode: 'Light mode'
mode: 'Toggle dark/light mode'
github: GitHub repository
githubRepository: IT-Tools GitHub repository
twitterX: X account
twitterXAccount: IT Tools X account
about: About IT-Tools
aboutLabel: About
darkMode: Dark mode
lightMode: Light mode
mode: Toggle dark/light mode
about:
content: >
# About IT-Tools
This wonderful website, made with ❤ by [Corentin Thomasset](https://corentin.tech?utm_source=it-tools&utm_medium=about) , aggregates useful tools for developer and people working in IT. If you find it useful, please feel free to share it to people you think may find it useful too and don't forget to bookmark it in your shortcut bar!
This wonderful website, made with ❤ by [Corentin
Thomasset](https://corentin.tech?utm_source=it-tools&utm_medium=about) ,
aggregates useful tools for developer and people working in IT. If you find
it useful, please feel free to share it to people you think may find it
useful too and don't forget to bookmark it in your shortcut bar!
IT Tools is open-source (under the GPL-3.0 license) and free, and will always be, but it costs me money to host and renew the domain name. If you want to support my work, and encourage me to add more tools, please consider supporting by [sponsoring me](https://www.buymeacoffee.com/cthmsst).
IT Tools is open-source (under the GPL-3.0 license) and free, and will
always be, but it costs me money to host and renew the domain name. If you
want to support my work, and encourage me to add more tools, please consider
supporting by [sponsoring me](https://www.buymeacoffee.com/cthmsst).
## Technologies
IT Tools is made in Vue.js (Vue 3) with the the Naive UI component library and is hosted and continuously deployed by Vercel. Third-party open-source libraries are used in some tools, you may find the complete list in the [package.json](https://github.com/tienlx93/my-it-tools/blob/main/package.json) file of the repository.
IT Tools is made in Vue.js (Vue 3) with the the Naive UI component library
and is hosted and continuously deployed by Vercel. Third-party open-source
libraries are used in some tools, you may find the complete list in the
[package.json](https://github.com/tienlx93/my-it-tools/blob/main/package.json)
file of the repository.
## Found a bug? A tool is missing?
If you need a tool that is currently not present here, and you think can be useful, you are welcome to submit a feature request in the [issues section](https://github.com/tienlx93/my-it-tools/issues/new/choose) in the GitHub repository.
If you need a tool that is currently not present here, and you think can be
useful, you are welcome to submit a feature request in the [issues
section](https://github.com/tienlx93/my-it-tools/issues/new/choose) in the
GitHub repository.
And if you found a bug, or something doesn't work as expected, please file a bug report in the [issues section](https://github.com/tienlx93/my-it-tools/issues/new/choose) in the GitHub repository.
404:
notFound: '404 Not Found'
sorry: 'Sorry, this page does not seem to exist'
maybe: 'Maybe the cache is doing tricky things, try force-refreshing?'
backHome: 'Back home'
And if you found a bug, or something doesn't work as expected, please file a
bug report in the [issues
section](https://github.com/tienlx93/my-it-tools/issues/new/choose) in the
GitHub repository.
favoriteButton:
remove: 'Remove from favorites'
add: 'Add to favorites'
remove: Remove from favorites
add: Add to favorites
toolCard:
new: New
search:
label: Search
tools:
categories:
favorite-tools: 'Your favorite tools'
favorite-tools: Your favorite tools
crypto: Crypto
converter: Converter
web: Web
images and videos: 'Images & Videos'
images and videos: Images & Videos
development: Development
network: Network
math: Math
measurement: Measurement
text: Text
data: Data
password-strength-analyser:
title: Password strength analyser
description: Discover the strength of your password with this client-side-only password strength analyser and crack time estimation tool.
description: >-
Discover the strength of your password with this client-side-only password
strength analyser and crack time estimation tool.
chronometer:
title: Chronometer
description: Monitor the duration of a thing. Basically a chronometer with simple chronometer features.
description: >-
Monitor the duration of a thing. Basically a chronometer with simple
chronometer features.
token-generator:
title: Token generator
description: Generate random string with the chars you want, uppercase or lowercase letters, numbers and/or symbols.
description: >-
Generate random string with the chars you want, uppercase or lowercase
letters, numbers and/or symbols.
uppercase: Uppercase (ABC...)
lowercase: Lowercase (abc...)
numbers: Numbers (123...)
symbols: Symbols (!-;...)
length: Length
tokenPlaceholder: 'The token...'
tokenPlaceholder: The token...
copied: Token copied to the clipboard
button:
copy: Copy
refresh: Refresh
percentage-calculator:
title: Percentage calculator
description: Easily calculate percentages from a value to another value, or from a percentage to a value.
description: >-
Easily calculate percentages from a value to another value, or from a
percentage to a value.
svg-placeholder-generator:
title: SVG placeholder generator
description: Generate svg images to use as a placeholder in your applications.
json-to-csv:
title: JSON to CSV
description: Convert JSON to CSV with automatic header detection.
camera-recorder:
title: Camera recorder
description: Take a picture or record a video from your webcam or camera.
keycode-info:
title: Keycode info
description: Find the javascript keycode, code, location and modifiers of any pressed key.
description: >-
Find the javascript keycode, code, location and modifiers of any pressed
key.
emoji-picker:
title: Emoji picker
description: Copy and paste emojis easily and get the unicode and code points value of each emoji.
description: >-
Copy and paste emojis easily and get the unicode and code points value of
each emoji.
color-converter:
title: Color converter
description: Convert color between the different formats (hex, rgb, hsl and css name)
bcrypt:
title: Bcrypt
description: Hash and compare text string using bcrypt. Bcrypt is a password-hashing function based on the Blowfish cipher.
description: >-
Hash and compare text string using bcrypt. Bcrypt is a password-hashing
function based on the Blowfish cipher.
crontab-generator:
title: Crontab generator
description: Validate and generate crontab and get the human-readable description of the cron schedule.
description: >-
Validate and generate crontab and get the human-readable description of
the cron schedule.
http-status-codes:
title: HTTP status codes
description: The list of all HTTP status codes, their name, and their meaning.
sql-prettify:
title: SQL prettify and format
description: Format and prettify your SQL queries online (it supports various SQL dialects).
description: >-
Format and prettify your SQL queries online (it supports various SQL
dialects).
benchmark-builder:
title: Benchmark builder
description: Easily compare execution time of tasks with this very simple online benchmark builder.
description: >-
Easily compare execution time of tasks with this very simple online
benchmark builder.
git-memo:
title: Git cheatsheet
description: Git is a decentralized version management software. With this cheatsheet, you will have quick access to the most common git commands.
description: >-
Git is a decentralized version management software. With this cheatsheet,
you will have quick access to the most common git commands.
slugify-string:
title: Slugify string
description: Make a string url, filename and id safe.
encryption:
title: Encrypt / decrypt text
description: Encrypt clear text and decrypt ciphertext using crypto algorithms like AES, TripleDES, Rabbit or RC4.
description: >-
Encrypt clear text and decrypt ciphertext using crypto algorithms like
AES, TripleDES, Rabbit or RC4.
random-port-generator:
title: Random port generator
description: Generate random port numbers outside of the range of "known" ports (0-1023).
description: >-
Generate random port numbers outside of the range of "known" ports
(0-1023).
yaml-prettify:
title: YAML prettify and format
description: Prettify your YAML string into a friendly, human-readable format.
eta-calculator:
title: ETA calculator
description: An ETA (Estimated Time of Arrival) calculator to determine the approximate end time of a task, for example, the end time and duration of a file download.
description: >-
An ETA (Estimated Time of Arrival) calculator to determine the approximate
end time of a task, for example, the end time and duration of a file
download.
roman-numeral-converter:
title: Roman numeral converter
description: Convert Roman numerals to numbers and convert numbers to Roman numerals.
hmac-generator:
title: Hmac generator
description: Computes a hash-based message authentication code (HMAC) using a secret key and your favorite hashing function.
description: >-
Computes a hash-based message authentication code (HMAC) using a secret
key and your favorite hashing function.
bip39-generator:
title: BIP39 passphrase generator
description: Generate a BIP39 passphrase from an existing or random mnemonic, or get the mnemonic from the passphrase.
description: >-
Generate a BIP39 passphrase from an existing or random mnemonic, or get
the mnemonic from the passphrase.
base64-file-converter:
title: Base64 file converter
description: Convert a string, file, or image into its base64 representation.
list-converter:
title: List converter
description: This tool can process column-based data and apply various changes (transpose, add prefix and suffix, reverse list, sort list, lowercase values, truncate values) to each row.
description: >-
This tool can process column-based data and apply various changes
(transpose, add prefix and suffix, reverse list, sort list, lowercase
values, truncate values) to each row.
base64-string-converter:
title: Base64 string encoder/decoder
description: Simply encode and decode strings into their base64 representation.
toml-to-yaml:
title: TOML to YAML
description: Parse and convert TOML to YAML.
math-evaluator:
title: Math evaluator
description: A calculator for evaluating mathematical expressions. You can use functions like sqrt, cos, sin, abs, etc.
description: >-
A calculator for evaluating mathematical expressions. You can use
functions like sqrt, cos, sin, abs, etc.
json-to-yaml-converter:
title: JSON to YAML converter
description: Simply convert JSON to YAML with this online live converter.
url-parser:
title: URL parser
description: Parse a URL into its separate constituent parts (protocol, origin, params, port, username-password, ...)
description: >-
Parse a URL into its separate constituent parts (protocol, origin, params,
port, username-password, ...)
iban-validator-and-parser:
title: IBAN validator and parser
description: Validate and parse IBAN numbers. Check if an IBAN is valid and get the country, BBAN, if it is a QR-IBAN and the IBAN friendly format.
description: >-
Validate and parse IBAN numbers. Check if an IBAN is valid and get the
country, BBAN, if it is a QR-IBAN and the IBAN friendly format.
user-agent-parser:
title: User-agent parser
description: Detect and parse Browser, Engine, OS, CPU, and Device type/model from an user-agent string.
description: >-
Detect and parse Browser, Engine, OS, CPU, and Device type/model from an
user-agent string.
numeronym-generator:
title: Numeronym generator
description: A numeronym is a word where a number is used to form an abbreviation. For example, "i18n" is a numeronym of "internationalization" where 18 stands for the number of letters between the first i and the last n in the word.
description: >-
A numeronym is a word where a number is used to form an abbreviation. For
example, "i18n" is a numeronym of "internationalization" where 18 stands
for the number of letters between the first i and the last n in the word.
case-converter:
title: Case converter
description: Transform the case of a string and choose between different formats
html-entities:
title: Escape HTML entities
description: Escape or unescape HTML entities (replace characters like <,>, &, " and \' with their HTML version)
description: >-
Escape or unescape HTML entities (replace characters like <,>, &, " and \'
with their HTML version)
json-prettify:
title: JSON prettify and format
description: Prettify your JSON string into a friendly, human-readable format.
docker-run-to-docker-compose-converter:
title: Docker run to Docker compose converter
description: Transforms "docker run" commands into docker-compose files!
mac-address-lookup:
title: MAC address lookup
description: Find the vendor and manufacturer of a device by its MAC address.
mime-types:
title: MIME types
description: Convert MIME types to file extensions and vice-versa.
toml-to-json:
title: TOML to JSON
description: Parse and convert TOML to JSON.
lorem-ipsum-generator:
title: Lorem ipsum generator
description: Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content
description: >-
Lorem ipsum is a placeholder text commonly used to demonstrate the visual
form of a document or a typeface without relying on meaningful content
qrcode-generator:
title: QR Code generator
description: Generate and download a QR code for a URL (or just plain text), and customize the background and foreground colors.
description: >-
Generate and download a QR code for a URL (or just plain text), and
customize the background and foreground colors.
wifi-qrcode-generator:
title: WiFi QR Code generator
description: Generate and download QR codes for quick connections to WiFi networks.
xml-formatter:
title: XML formatter
description: Prettify your XML string into a friendly, human-readable format.
temperature-converter:
title: Temperature converter
description: Degrees temperature conversions for Kelvin, Celsius, Fahrenheit, Rankine, Delisle, Newton, Réaumur, and Rømer.
description: >-
Degrees temperature conversions for Kelvin, Celsius, Fahrenheit, Rankine,
Delisle, Newton, Réaumur, and Rømer.
chmod-calculator:
title: Chmod calculator
description: Compute your chmod permissions and commands with this online chmod calculator.
description: >-
Compute your chmod permissions and commands with this online chmod
calculator.
rsa-key-pair-generator:
title: RSA key pair generator
description: Generate a new random RSA private and public pem certificate key pair.
html-wysiwyg-editor:
title: HTML WYSIWYG editor
description: Online, feature-rich WYSIWYG HTML editor which generates the source code of the content immediately.
description: >-
Online, feature-rich WYSIWYG HTML editor which generates the source code
of the content immediately.
yaml-to-toml:
title: YAML to TOML
description: Parse and convert YAML to TOML.
mac-address-generator:
title: MAC address generator
description: Enter the quantity and prefix. MAC addresses will be generated in your chosen case (uppercase or lowercase)
description: >-
Enter the quantity and prefix. MAC addresses will be generated in your
chosen case (uppercase or lowercase)
json-diff:
title: JSON diff
description: Compare two JSON objects and get the differences between them.
jwt-parser:
title: JWT parser
description: Parse and decode your JSON Web Token (jwt) and display its content.
date-converter:
title: Date-time converter
description: Convert date and time into the various different formats
phone-parser-and-formatter:
title: Phone parser and formatter
description: Parse, validate and format phone numbers. Get information about the phone number, like the country code, type, etc.
description: >-
Parse, validate and format phone numbers. Get information about the phone
number, like the country code, type, etc.
ipv4-subnet-calculator:
title: IPv4 subnet calculator
description: Parse your IPv4 CIDR blocks and get all the info you need about your subnet.
description: >-
Parse your IPv4 CIDR blocks and get all the info you need about your
subnet.
og-meta-generator:
title: Open graph meta generator
description: Generate open-graph and socials HTML meta tags for your website.
ipv6-ula-generator:
title: IPv6 ULA generator
description: Generate your own local, non-routable IP addresses for your network according to RFC4193.
description: >-
Generate your own local, non-routable IP addresses for your network
according to RFC4193.
hash-text:
title: Hash text
description: 'Hash a text string using the function you need : MD5, SHA1, SHA256, SHA224, SHA512, SHA384, SHA3 or RIPEMD160'
description: >-
Hash a text string using the function you need : MD5, SHA1, SHA256,
SHA224, SHA512, SHA384, SHA3 or RIPEMD160
json-to-toml:
title: JSON to TOML
description: Parse and convert JSON to TOML.
device-information:
title: Device information
description: Get information about your current device (screen size, pixel-ratio, user agent, ...)
description: >-
Get information about your current device (screen size, pixel-ratio, user
agent, ...)
pdf-signature-checker:
title: PDF signature checker
description: Verify the signatures of a PDF file. A signed PDF file contains one or more signatures that may be used to determine whether the contents of the file have been altered since the file was signed.
description: >-
Verify the signatures of a PDF file. A signed PDF file contains one or
more signatures that may be used to determine whether the contents of the
file have been altered since the file was signed.
json-minify:
title: JSON minify
description: Minify and compress your JSON by removing unnecessary whitespace.
ulid-generator:
title: ULID generator
description: Generate random Universally Unique Lexicographically Sortable Identifier (ULID).
description: >-
Generate random Universally Unique Lexicographically Sortable Identifier
(ULID).
string-obfuscator:
title: String obfuscator
description: Obfuscate a string (like a secret, an IBAN, or a token) to make it shareable and identifiable without revealing its content.
description: >-
Obfuscate a string (like a secret, an IBAN, or a token) to make it
shareable and identifiable without revealing its content.
base-converter:
title: Integer base converter
description: Convert a number between different bases (decimal, hexadecimal, binary, octal, base64, ...)
description: >-
Convert a number between different bases (decimal, hexadecimal, binary,
octal, base64, ...)
yaml-to-json-converter:
title: YAML to JSON converter
description: Simply convert YAML to JSON with this online live converter.
uuid-generator:
title: UUIDs generator
description: A Universally Unique Identifier (UUID) is a 128-bit number used to identify information in computer systems. The number of possible UUIDs is 16^32, which is 2^128 or about 3.4x10^38 (which is a lot!).
description: >-
A Universally Unique Identifier (UUID) is a 128-bit number used to
identify information in computer systems. The number of possible UUIDs is
16^32, which is 2^128 or about 3.4x10^38 (which is a lot!).
ipv4-address-converter:
title: IPv4 address converter
description: Convert an IP address into decimal, binary, hexadecimal, or even an IPv6 representation of it.
description: >-
Convert an IP address into decimal, binary, hexadecimal, or even an IPv6
representation of it.
text-statistics:
title: Text statistics
description: Get information about a text, the number of characters, the number of words, its size in bytes, ...
description: >-
Get information about a text, the number of characters, the number of
words, its size in bytes, ...
text-to-nato-alphabet:
title: Text to NATO alphabet
description: Transform text into the NATO phonetic alphabet for oral transmission.
basic-auth-generator:
title: Basic auth generator
description: Generate a base64 basic auth header from a username and password.
text-to-unicode:
title: Text to Unicode
description: Parse and convert text to unicode and vice-versa
ipv4-range-expander:
title: IPv4 range expander
description: Given a start and an end IPv4 address, this tool calculates a valid IPv4 subnet along with its CIDR notation.
description: >-
Given a start and an end IPv4 address, this tool calculates a valid IPv4
subnet along with its CIDR notation.
text-diff:
title: Text diff
description: Compare two texts and see the differences between them.
otp-generator:
title: OTP code generator
description: Generate and validate time-based OTP (one time password) for multi-factor authentication.
description: >-
Generate and validate time-based OTP (one time password) for multi-factor
authentication.
url-encoder:
title: Encode/decode URL-formatted strings
description: Encode text to URL-encoded format (also known as "percent-encoded"), or decode from it.
description: >-
Encode text to URL-encoded format (also known as "percent-encoded"), or
decode from it.
text-to-binary:
title: Text to ASCII binary
description: Convert text to its ASCII binary representation and vice-versa.
html-to-markdown:
title: HTML to markdown
description: Convert HTML (either from clipboard) to Markdown
texts:
placeholder-your-html-content: Your HTML content...
label-your-html-to-convert-can-paste-from-clipboard: 'Your HTML to convert (can paste from clipboard):'
label-output-markdown: 'Output markdown:'
label-heading-style: 'Headings Style:'
label-emphasis-style: 'Emphasis Style:'
label-hash: Hash (#)
label-underline: Underline (=/-)
label-surround-heading: Surround (= Heading =)
label-asterisk: Asterisk (*)
label-underscore: Underscore (_)
label-tilde: Tilde (~)
table-to-markdown:
title: Table to markdown generator
description: Create, edit, paste spreadsheet tables and export to GFM Markdown.
alignLeft: Align left
alignCenter: Align center
alignRight: Align right
insertColLeft: Insert column left
insertColRight: Insert column right
deleteCol: Delete column
insertRowAbove: Insert row above
insertRowBelow: Insert row below
deleteRow: Delete row
newTable: New Table
transpose: Transpose
clear: Clear
undo: Undo
redo: Redo
compactMode: Compact Mode
markdownOutput: Markdown Output
visualPreview: Visual Preview
rows: Rows
columns: Columns
cancel: Cancel
create: Create
newTableTitle: Create New Table
texts:
new-table: New table...
markdown-result: 'Markdown result:'

View file

@ -1,6 +1,8 @@
import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as htmlToMarkdown } from './html-to-markdown';
import { tool as tableToMarkdown } from './table-to-markdown';
import { tool as emailNormalizer } from './email-normalizer';
import { tool as asciiTextDrawer } from './ascii-text-drawer';
@ -166,6 +168,10 @@ export const toolsByCategory: ToolCategory[] = [
name: 'Network',
components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, macAddressGenerator, ipv6UlaGenerator],
},
{
name: 'Markdown',
components: [htmlToMarkdown, tableToMarkdown],
},
{
name: 'Math',
components: [mathEvaluator, etaCalculator, percentageCalculator],

View file

@ -0,0 +1,14 @@
import { Table } from '@vicons/tabler';
import { defineTool } from '../tool';
import { translate as t } from '@/plugins/i18n.plugin';
export const tool = defineTool({
name: t('tools.table-to-markdown.title'),
path: '/table-to-markdown',
description: t('tools.table-to-markdown.description'),
keywords: ['table', 'markdown', 'generator', 'editor', 'csv', 'excel'],
component: () => import('./table-to-markdown.vue'),
icon: Table,
createdAt: new Date('2026-07-08'),
category: 'Markdown',
});

View file

@ -0,0 +1,482 @@
<!-- eslint-disable vue/no-mutating-props -->
<script setup lang="ts">
import { computed, nextTick, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import type { TableState } from './table-state';
const props = defineProps<{
state: TableState
}>();
const { t } = useI18n();
const containerRef = ref<HTMLElement | null>(null);
// Dropdown/Context Menu State
const showDropdown = ref(false);
const x = ref(0);
const y = ref(0);
const dropdownType = ref<'column' | 'row' | null>(null);
const targetIndex = ref<number>(-1);
// Focused Cell Tracking State
const focusedCell = ref<{
type: 'header' | 'body'
rowIdx?: number
colIdx: number
} | null>(null);
const initialCellHtml = ref<string>('');
function isCellFocused(type: 'header' | 'body', rowIdx: number | undefined, colIdx: number): boolean {
if (!focusedCell.value) {
return false;
}
if (focusedCell.value.type !== type) {
return false;
}
if (focusedCell.value.colIdx !== colIdx) {
return false;
}
if (type === 'body') {
return focusedCell.value.rowIdx === rowIdx;
}
return true;
}
// Selection options based on right-clicked header or row index
const dropdownOptions = computed(() => {
if (dropdownType.value === 'column') {
return [
{ label: t('tools.table-to-markdown.alignLeft'), key: 'align-left' },
{ label: t('tools.table-to-markdown.alignCenter'), key: 'align-center' },
{ label: t('tools.table-to-markdown.alignRight'), key: 'align-right' },
{ key: 'd1', type: 'divider' },
{ label: t('tools.table-to-markdown.insertColLeft'), key: 'insert-col-left' },
{ label: t('tools.table-to-markdown.insertColRight'), key: 'insert-col-right' },
{ key: 'd2', type: 'divider' },
{
label: t('tools.table-to-markdown.deleteCol'),
key: 'delete-col',
disabled: props.state.headers.length <= 1,
},
];
}
else if (dropdownType.value === 'row') {
return [
{ label: t('tools.table-to-markdown.insertRowAbove'), key: 'insert-row-above' },
{ label: t('tools.table-to-markdown.insertRowBelow'), key: 'insert-row-below' },
{ key: 'd3', type: 'divider' },
{
label: t('tools.table-to-markdown.deleteRow'),
key: 'delete-row',
disabled: props.state.rows.length <= 1,
},
];
}
return [];
});
// Focus helper using DOM query inside table container
function focusCell(type: 'header' | 'body', rIdx: number | undefined, cIdx: number) {
let selector = '';
if (type === 'header') {
selector = `[data-cell-type="header"][data-col="${cIdx}"]`;
}
else {
selector = `[data-cell-type="body"][data-row="${rIdx}"][data-col="${cIdx}"]`;
}
const el = containerRef.value?.querySelector(selector) as HTMLElement | null;
if (el) {
el.focus();
try {
const range = document.createRange();
const sel = window.getSelection();
range.selectNodeContents(el);
range.collapse(false); // Move caret to the end
sel?.removeAllRanges();
sel?.addRange(range);
}
catch (e) {
console.error('Error setting caret selection:', e);
}
}
}
// Handlers for focus and blur
function onCellFocus(type: 'header' | 'body', rowIdx: number | undefined, colIdx: number, event: FocusEvent) {
focusedCell.value = { type, rowIdx, colIdx };
const target = event.target as HTMLElement;
initialCellHtml.value = target.innerHTML;
}
function onCellBlur(type: 'header' | 'body', rowIdx: number | undefined, colIdx: number, event: FocusEvent) {
const target = event.target as HTMLElement;
const currentHtml = target.innerHTML;
if (currentHtml !== initialCellHtml.value) {
props.state.saveHistory();
}
if (focusedCell.value && focusedCell.value.type === type && focusedCell.value.colIdx === colIdx) {
if (type === 'header' || focusedCell.value.rowIdx === rowIdx) {
focusedCell.value = null;
}
}
}
// Handler for keystroke input
function onCellInput(type: 'header' | 'body', rowIdx: number | undefined, colIdx: number, event: Event) {
const target = event.target as HTMLElement;
const newHtml = target.innerHTML;
if (type === 'header') {
props.state.headers[colIdx].html = newHtml;
}
else {
props.state.rows[rowIdx!][colIdx].html = newHtml;
}
}
// Keyboard Shortcuts override
function onCellKeydown(event: KeyboardEvent, type: 'header' | 'body', rowIdx: number | undefined, colIdx: number) {
if (event.key === 'Tab') {
event.preventDefault();
if (event.shiftKey) {
// Shift + Tab (navigate backward)
if (type === 'header') {
if (colIdx > 0) {
focusCell('header', undefined, colIdx - 1);
}
}
else {
if (colIdx > 0) {
focusCell('body', rowIdx, colIdx - 1);
}
else {
if (rowIdx! > 0) {
focusCell('body', rowIdx! - 1, props.state.headers.length - 1);
}
else {
focusCell('header', undefined, props.state.headers.length - 1);
}
}
}
}
else {
// Tab (navigate forward)
if (type === 'header') {
if (colIdx < props.state.headers.length - 1) {
focusCell('header', undefined, colIdx + 1);
}
else if (props.state.rows.length > 0) {
focusCell('body', 0, 0);
}
}
else {
if (colIdx < props.state.headers.length - 1) {
focusCell('body', rowIdx, colIdx + 1);
}
else {
if (rowIdx! < props.state.rows.length - 1) {
focusCell('body', rowIdx! + 1, 0);
}
else {
props.state.insertRow(rowIdx!, 'below');
nextTick(() => {
focusCell('body', rowIdx! + 1, 0);
});
}
}
}
}
}
else if (event.key === 'Enter') {
if (!event.shiftKey) {
event.preventDefault();
// Enter (navigate to cell below)
if (type === 'header') {
if (props.state.rows.length > 0) {
focusCell('body', 0, colIdx);
}
}
else {
if (rowIdx! < props.state.rows.length - 1) {
focusCell('body', rowIdx! + 1, colIdx);
}
else {
props.state.insertRow(rowIdx!, 'below');
nextTick(() => {
focusCell('body', rowIdx! + 1, colIdx);
});
}
}
}
// Shift + Enter is allowed to insert visual <br> (default browser action)
}
}
// Context Menu Handlers
function onHeaderContextMenu(event: MouseEvent, colIdx: number) {
showDropdown.value = false;
dropdownType.value = 'column';
targetIndex.value = colIdx;
nextTick(() => {
x.value = event.clientX;
y.value = event.clientY;
showDropdown.value = true;
});
}
// Context Menu Handlers for Row Indexes
function onRowContextMenu(event: MouseEvent, rowIdx: number) {
showDropdown.value = false;
dropdownType.value = 'row';
targetIndex.value = rowIdx;
nextTick(() => {
x.value = event.clientX;
y.value = event.clientY;
showDropdown.value = true;
});
}
function handleSelect(key: string) {
showDropdown.value = false;
const idx = targetIndex.value;
if (idx === -1) {
return;
}
if (dropdownType.value === 'column') {
if (key === 'align-left') {
props.state.setColumnAlignment(idx, 'left');
}
else if (key === 'align-center') {
props.state.setColumnAlignment(idx, 'center');
}
else if (key === 'align-right') {
props.state.setColumnAlignment(idx, 'right');
}
else if (key === 'insert-col-left') {
props.state.insertColumn(idx, 'left');
}
else if (key === 'insert-col-right') {
props.state.insertColumn(idx, 'right');
}
else if (key === 'delete-col') {
props.state.deleteColumn(idx);
}
}
else if (dropdownType.value === 'row') {
if (key === 'insert-row-above') {
props.state.insertRow(idx, 'above');
}
else if (key === 'insert-row-below') {
props.state.insertRow(idx, 'below');
}
else if (key === 'delete-row') {
props.state.deleteRow(idx);
}
}
}
// Global Grid Paste Handling
function onTablePaste(event: ClipboardEvent) {
const html = event.clipboardData?.getData('text/html') || '';
const text = event.clipboardData?.getData('text/plain') || '';
// Intercept paste if it contains a table structure, TSV tabs or row newlines
const isTablePaste = html.includes('<table') || text.includes('\t') || text.includes('\n');
if (isTablePaste) {
event.preventDefault();
props.state.parsePaste(html, text);
// Blur to refresh active inputs
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}
}
// Custom directive to update innerHTML without resetting cursor selection during active typing
const vSafeHtml = {
mounted(el: HTMLElement, binding: any) {
el.innerHTML = binding.value ?? '';
},
updated(el: HTMLElement, binding: any) {
if (binding.value !== binding.oldValue) {
if (document.activeElement !== el) {
el.innerHTML = binding.value ?? '';
}
}
},
};
</script>
<template>
<div ref="containerRef" class="table-editor-container" @paste="onTablePaste">
<table class="table-editor">
<thead>
<tr>
<th class="row-index-header">
#
</th>
<!-- Contenteditable column headers -->
<th
v-for="(header, colIdx) in state.headers"
:key="`h-${colIdx}`"
v-safe-html="header.html"
contenteditable="true"
data-cell-type="header"
:data-col="colIdx"
:style="{ textAlign: state.alignments[colIdx] || 'left' }"
:class="{ 'is-focused': isCellFocused('header', undefined, colIdx) }"
@focus="onCellFocus('header', undefined, colIdx, $event)"
@blur="onCellBlur('header', undefined, colIdx, $event)"
@input="onCellInput('header', undefined, colIdx, $event)"
@keydown="onCellKeydown($event, 'header', undefined, colIdx)"
@contextmenu.prevent="onHeaderContextMenu($event, colIdx)"
/>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIdx) in state.rows" :key="`r-${rowIdx}`">
<!-- Row index cell containing row number -->
<td
class="row-index-cell"
@contextmenu.prevent="onRowContextMenu($event, rowIdx)"
>
{{ rowIdx + 1 }}
</td>
<!-- Contenteditable body cells -->
<td
v-for="(cell, colIdx) in row"
:key="`c-${colIdx}`"
v-safe-html="cell.html"
contenteditable="true"
data-cell-type="body"
:data-row="rowIdx"
:data-col="colIdx"
:style="{ textAlign: state.alignments[colIdx] || 'left' }"
:class="{ 'is-focused': isCellFocused('body', rowIdx, colIdx) }"
@focus="onCellFocus('body', rowIdx, colIdx, $event)"
@blur="onCellBlur('body', rowIdx, colIdx, $event)"
@input="onCellInput('body', rowIdx, colIdx, $event)"
@keydown="onCellKeydown($event, 'body', rowIdx, colIdx)"
/>
</tr>
</tbody>
</table>
<n-dropdown
placement="bottom-start"
trigger="manual"
:x="x"
:y="y"
:show="showDropdown"
:options="dropdownOptions"
@clickoutside="showDropdown = false"
@select="handleSelect"
/>
</div>
</template>
<style scoped lang="less">
.table-editor-container {
overflow-x: auto;
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: var(--cell-bg);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
margin: 16px 0;
width: 100%;
--border-color: #efeff5;
--header-bg: #fafafc;
--index-bg: #f5f5f7;
--cell-bg: #ffffff;
--text-color: #333639;
--index-text-color: #8e8e93;
--primary-color: #18a058;
}
.dark .table-editor-container {
--border-color: #303033;
--header-bg: #18181c;
--index-bg: #101014;
--cell-bg: #18181c;
--text-color: #d7dae2;
--index-text-color: #767c82;
--primary-color: #18a058;
}
.table-editor {
width: 100%;
border-collapse: collapse;
font-family: inherit;
font-size: 14px;
color: var(--text-color);
th,
td {
border: 1px solid var(--border-color);
padding: 12px 16px;
min-width: 100px;
position: relative;
box-sizing: border-box;
}
th {
background-color: var(--header-bg);
font-weight: 600;
outline: none;
&[contenteditable="true"]:focus {
outline: none;
box-shadow: inset 0 0 0 2px var(--primary-color);
background-color: rgba(24, 160, 88, 0.04);
}
}
td {
outline: none;
&[contenteditable="true"]:focus {
outline: none;
box-shadow: inset 0 0 0 2px var(--primary-color);
background-color: rgba(24, 160, 88, 0.04);
}
}
.row-index-header {
background-color: var(--index-bg);
color: var(--index-text-color);
font-weight: 600;
text-align: center;
width: 45px;
min-width: 45px;
max-width: 45px;
cursor: default;
border-right: 2px solid var(--border-color);
user-select: none;
}
.row-index-cell {
background-color: var(--index-bg);
color: var(--index-text-color);
font-weight: 600;
text-align: center;
width: 45px;
min-width: 45px;
max-width: 45px;
cursor: context-menu;
user-select: none;
border-right: 2px solid var(--border-color);
&:hover {
background-color: rgba(24, 160, 88, 0.08);
color: var(--primary-color);
}
}
}
</style>

View file

@ -0,0 +1,156 @@
import { describe, expect, it } from 'vitest';
import { TableState } from './table-state';
describe('TableState Core Operations', () => {
it('initializes grid with correct dimensions', () => {
const state = new TableState(3, 4);
expect(state.headers.length).toBe(4);
expect(state.rows.length).toBe(3);
expect(state.rows[0].length).toBe(4);
expect(state.alignments.length).toBe(4);
});
it('can set column alignment', () => {
const state = new TableState(3, 3);
state.setColumnAlignment(1, 'center');
expect(state.alignments[1]).toBe('center');
});
it('can insert row and column', () => {
const state = new TableState(2, 2);
state.insertRow(1, 'below');
expect(state.rows.length).toBe(3);
state.insertColumn(0, 'right');
expect(state.headers.length).toBe(3);
expect(state.rows[0].length).toBe(3);
});
it('can delete row and column', () => {
const state = new TableState(3, 3);
state.deleteRow(1);
expect(state.rows.length).toBe(2);
state.deleteColumn(1);
expect(state.headers.length).toBe(2);
expect(state.rows[0].length).toBe(2);
});
it('can transpose a table', () => {
const state = new TableState(2, 3); // 2 rows, 3 cols
state.headers[0].html = 'H1';
state.headers[1].html = 'H2';
state.headers[2].html = 'H3';
state.rows[0][0].html = 'A';
state.rows[1][0].html = 'B';
state.transpose();
expect(state.headers.length).toBe(3);
expect(state.rows.length).toBe(2);
expect(state.rows[0].length).toBe(3);
});
it('handles Undo and Redo states', () => {
const state = new TableState(2, 2);
state.saveHistory();
state.rows[0][0].html = 'Edited';
state.saveHistory();
state.undo();
expect(state.rows[0][0].html).toBe('');
state.redo();
expect(state.rows[0][0].html).toBe('Edited');
});
it('allows undoing the first mutation', () => {
const state = new TableState(2, 2);
state.setColumnAlignment(1, 'center');
expect(state.alignments[1]).toBe('center');
state.undo();
expect(state.alignments[1]).toBeNull();
});
it('does not mutate or save history if indices are out of bounds', () => {
const state = new TableState(2, 2);
// deleteRow with out of bounds index
state.deleteRow(-1);
expect(state.rows.length).toBe(2);
state.deleteRow(5);
expect(state.rows.length).toBe(2);
// deleteColumn with out of bounds index
state.deleteColumn(-1);
expect(state.headers.length).toBe(2);
state.deleteColumn(5);
expect(state.headers.length).toBe(2);
// insertRow with out of bounds index
state.insertRow(-1, 'above');
expect(state.rows.length).toBe(2);
state.insertRow(5, 'above');
expect(state.rows.length).toBe(2);
// insertColumn with out of bounds index
state.insertColumn(-1, 'left');
expect(state.headers.length).toBe(2);
state.insertColumn(5, 'left');
expect(state.headers.length).toBe(2);
// setColumnAlignment with out of bounds index
state.setColumnAlignment(-1, 'center');
expect(state.alignments).toEqual([null, null]);
state.setColumnAlignment(5, 'center');
expect(state.alignments).toEqual([null, null]);
});
describe('Markdown Conversion & Parsing', () => {
it('serializes cell styles to GFM Markdown with formatting', () => {
const state = new TableState(1, 2);
state.headers[0].html = '<b>Header 1</b>';
state.headers[1].html = 'Header 2';
state.rows[0][0].html = '<i>Italic</i>';
state.rows[0][1].html = 'Line 1<br>Line 2';
const markdown = state.toMarkdown({ compact: true });
expect(markdown).toContain('|**Header 1**|Header 2|');
expect(markdown).toContain('|*Italic*|Line 1<br>Line 2|');
});
it('generates padded Markdown in beautified mode', () => {
const state = new TableState(1, 2);
state.headers[0].html = 'H1';
state.headers[1].html = 'LongHeader';
state.rows[0][0].html = 'Value';
state.rows[0][1].html = 'V';
const markdown = state.toMarkdown({ compact: false });
expect(markdown).toContain('| H1 | LongHeader |');
});
it('parses Excel HTML paste tables', () => {
const excelHtml = `
<table>
<tr><th>Col A</th><th>Col B</th></tr>
<tr><td>A1</td><td><b>B1</b></td></tr>
</table>
`;
const state = new TableState(1, 1);
state.parsePaste(excelHtml, '');
expect(state.headers.length).toBe(2);
expect(state.headers[0].html).toBe('Col A');
expect(state.rows[0][1].html).toContain('<b>B1</b>');
});
it('parses CSV/TSV plain text paste', () => {
const plainText = 'Col A\tCol B\nA1\tB1';
const state = new TableState(1, 1);
state.parsePaste('', plainText);
expect(state.headers.length).toBe(2);
expect(state.rows[0][0].html).toBe('A1');
});
});
});

View file

@ -0,0 +1,332 @@
export interface Cell {
html: string
}
export type Alignment = 'left' | 'center' | 'right' | null;
export interface TableSnapshot {
headers: Cell[]
rows: Cell[][]
alignments: Alignment[]
}
export class TableState {
public headers: Cell[] = [];
public rows: Cell[][] = [];
public alignments: Alignment[] = [];
public undoStack: string[] = [];
public redoStack: string[] = [];
public get canUndo(): boolean {
return this.undoStack.length > 1;
}
public get canRedo(): boolean {
return this.redoStack.length > 0;
}
constructor(initialRows = 3, initialCols = 3) {
this.reset(initialRows, initialCols);
}
public reset(numRows: number, numCols: number) {
this.headers = Array.from({ length: numCols }, () => ({ html: '' }));
this.rows = Array.from({ length: numRows }, () =>
Array.from({ length: numCols }, () => ({ html: '' })),
);
this.alignments = Array.from({ length: numCols }, () => null);
this.clearHistory();
this.undoStack.push(this.serialize());
}
public clearHistory() {
this.undoStack = [];
this.redoStack = [];
}
private serialize(): string {
return JSON.stringify({
headers: this.headers,
rows: this.rows,
alignments: this.alignments,
});
}
private deserialize(json: string) {
const data = JSON.parse(json);
this.headers = data.headers;
this.rows = data.rows;
this.alignments = data.alignments;
}
public saveHistory() {
this.undoStack.push(this.serialize());
this.redoStack = [];
}
public undo() {
if (this.undoStack.length > 1) {
const current = this.undoStack.pop();
if (current) {
this.redoStack.push(current);
}
const prev = this.undoStack[this.undoStack.length - 1];
this.deserialize(prev);
}
}
public redo() {
if (this.redoStack.length > 0) {
const next = this.redoStack.pop();
if (next) {
this.undoStack.push(next);
this.deserialize(next);
}
}
}
public setColumnAlignment(colIdx: number, align: Alignment) {
if (colIdx >= 0 && colIdx < this.alignments.length) {
this.alignments[colIdx] = align;
this.saveHistory();
}
}
public insertRow(rowIdx: number, position: 'above' | 'below') {
if (rowIdx >= 0 && rowIdx < this.rows.length) {
const insertAt = position === 'above' ? rowIdx : rowIdx + 1;
const numCols = this.headers.length;
const newRow = Array.from({ length: numCols }, () => ({ html: '' }));
this.rows.splice(insertAt, 0, newRow);
this.saveHistory();
}
}
public deleteRow(rowIdx: number) {
if (rowIdx >= 0 && rowIdx < this.rows.length) {
if (this.rows.length > 1) {
this.rows.splice(rowIdx, 1);
this.saveHistory();
}
}
}
public insertColumn(colIdx: number, position: 'left' | 'right') {
if (colIdx >= 0 && colIdx < this.headers.length) {
const insertAt = position === 'left' ? colIdx : colIdx + 1;
this.headers.splice(insertAt, 0, { html: '' });
this.alignments.splice(insertAt, 0, null);
for (const row of this.rows) {
row.splice(insertAt, 0, { html: '' });
}
this.saveHistory();
}
}
public deleteColumn(colIdx: number) {
if (colIdx >= 0 && colIdx < this.headers.length) {
if (this.headers.length > 1) {
this.headers.splice(colIdx, 1);
this.alignments.splice(colIdx, 1);
for (const row of this.rows) {
row.splice(colIdx, 1);
}
this.saveHistory();
}
}
}
public transpose() {
const oldRowsCount = this.rows.length;
const oldColsCount = this.headers.length;
const newHeaders: Cell[] = [];
const newRows: Cell[][] = [];
const newAlignments: Alignment[] = Array.from({ length: oldRowsCount + 1 }, () => null);
newHeaders.push({ html: this.headers[0].html });
for (let r = 0; r < oldRowsCount; r++) {
newHeaders.push({ html: this.rows[r][0].html });
}
for (let c = 1; c < oldColsCount; c++) {
const row: Cell[] = [{ html: this.headers[c].html }];
for (let r = 0; r < oldRowsCount; r++) {
row.push({ html: this.rows[r][c].html });
}
newRows.push(row);
}
if (newRows.length === 0) {
newRows.push(Array.from({ length: newHeaders.length }, () => ({ html: '' })));
}
this.headers = newHeaders;
this.rows = newRows;
this.alignments = newAlignments;
this.saveHistory();
}
// Helper to translate HTML cell format to Markdown syntax
public static htmlToCellMarkdown(html: string): string {
if (!html) {
return '';
}
let text = html;
text = text.replace(/<(b|strong)>(.*?)<\/\1>/gi, '**$2**');
text = text.replace(/<(i|em)>(.*?)<\/\1>/gi, '*$2*');
text = text.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`');
text = text.replace(/<br\s*\/?>/gi, '___BR___');
text = text.replace(/<\/p><p>/gi, '___BR___');
text = text.replace(/<\/div><div>/gi, '___BR___');
text = text.replace(/<[^>]+>/g, '');
text = text.replace(/___BR___/g, '<br>');
text = text.replace(/\|/g, '\\|');
return text.trim();
}
public toMarkdown(options: { compact: boolean }): string {
const compact = options.compact;
const mdHeaders = this.headers.map(h => TableState.htmlToCellMarkdown(h.html));
const mdRows = this.rows.map(row => row.map(cell => TableState.htmlToCellMarkdown(cell.html)));
const colWidths = this.headers.map((_, colIdx) => {
if (compact) {
return 0;
}
let maxLen = mdHeaders[colIdx].length;
for (const row of mdRows) {
maxLen = Math.max(maxLen, row[colIdx].length);
}
return Math.max(maxLen, 3);
});
const formatCell = (text: string, colIdx: number): string => {
if (compact) {
return text;
}
const width = colWidths[colIdx];
return text.padEnd(width, ' ');
};
if (compact) {
const headerLine = `|${mdHeaders.join('|')}|`;
const separatorLine = `|${this.alignments.map((align) => {
const dashCount = 3;
if (align === 'left') {
return `:${'-'.repeat(dashCount - 1)}`;
}
else if (align === 'center') {
return `:${'-'.repeat(dashCount - 2)}:`;
}
else if (align === 'right') {
return `${'-'.repeat(dashCount - 1)}:`;
}
else {
return '-'.repeat(dashCount);
}
}).join('|')}|`;
const bodyLines = mdRows.map((row) => {
return `|${row.join('|')}|`;
});
return [headerLine, separatorLine, ...bodyLines].join('\n');
}
else {
const headerLine = `| ${mdHeaders.map((h, i) => formatCell(h, i)).join(' | ')} |`;
const separatorLine = `| ${this.alignments.map((align, i) => {
const width = colWidths[i];
const dashCount = width;
if (align === 'left') {
return `:${'-'.repeat(dashCount - 1)}`;
}
else if (align === 'center') {
return `:${'-'.repeat(dashCount - 2)}:`;
}
else if (align === 'right') {
return `${'-'.repeat(dashCount - 1)}:`;
}
else {
return '-'.repeat(dashCount);
}
}).join(' | ')} |`;
const bodyLines = mdRows.map((row) => {
return `| ${row.map((cell, i) => formatCell(cell, i)).join(' | ')} |`;
});
return [headerLine, separatorLine, ...bodyLines].join('\n');
}
}
public parsePaste(html: string, text: string) {
this.saveHistory();
if (html && html.includes('<table')) {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const table = doc.querySelector('table');
if (table) {
const rows = Array.from(table.querySelectorAll('tr'));
if (rows.length > 0) {
const headerRow = rows[0];
const headerCells = Array.from(headerRow.querySelectorAll('th, td'));
const numCols = headerCells.length;
this.headers = headerCells.map(c => ({ html: c.innerHTML.trim() }));
this.alignments = Array.from({ length: numCols }, () => null);
this.rows = rows.slice(1).map((tr) => {
const cells = Array.from(tr.querySelectorAll('th, td'));
return Array.from({ length: numCols }, (_, i) => ({
html: cells[i] ? cells[i].innerHTML.trim() : '',
}));
});
if (this.rows.length === 0) {
this.rows = [[...this.headers]];
this.headers = Array.from({ length: numCols }, () => ({ html: 'Header' }));
}
return;
}
}
}
catch (e) {
console.error('Failed to parse pasted HTML table:', e);
}
}
const content = text || '';
if (content) {
const lines = content.split(/\r?\n/).filter(line => line.trim().length > 0);
if (lines.length > 0) {
const firstLine = lines[0];
const isTab = firstLine.includes('\t');
const delimiter = isTab ? '\t' : ',';
const parseLine = (line: string) => {
return line.split(delimiter).map((cell) => {
let val = cell.trim();
if (val.startsWith('"') && val.endsWith('"')) {
val = val.substring(1, val.length - 1).replace(/""/g, '"');
}
return { html: val };
});
};
const firstRowCells = parseLine(lines[0]);
const numCols = firstRowCells.length;
this.headers = firstRowCells;
this.alignments = Array.from({ length: numCols }, () => null);
this.rows = lines.slice(1).map((line) => {
const cells = parseLine(line);
return Array.from({ length: numCols }, (_, i) => ({
html: cells[i] ? cells[i].html : '',
}));
});
}
}
}
}

View file

@ -0,0 +1,279 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import {
ArrowBack,
ArrowForwardUp,
Exchange,
Plus,
Trash,
} from '@vicons/tabler';
import { TableState } from './table-state';
import TableEditor from './table-editor.vue';
import TextareaCopyable from '@/components/TextareaCopyable.vue';
const { t } = useI18n();
// Initialize TableState
const state = reactive(new TableState(3, 3));
// Compact Mode
const compactMode = ref(false);
// Active Tab for Output: 'markdown' or 'preview'
const activeTab = ref('markdown');
const tabOptions = computed(() => [
{ label: t('tools.table-to-markdown.markdownOutput'), value: 'markdown' },
{ label: t('tools.table-to-markdown.visualPreview'), value: 'preview' },
]);
// Modal State
const showNewTableModal = ref(false);
const newTableRows = ref(3);
const newTableCols = ref(3);
function openNewTableModal() {
newTableRows.value = state.rows.length;
newTableCols.value = state.headers.length;
showNewTableModal.value = true;
}
function confirmNewTable() {
if (newTableRows.value >= 1 && newTableCols.value >= 1) {
state.reset(newTableRows.value, newTableCols.value);
}
showNewTableModal.value = false;
}
// Actions
function transposeTable() {
state.transpose();
}
function clearTable() {
// Clear all cell contents but preserve size and alignments
state.saveHistory();
state.headers.forEach((h) => {
h.html = '';
});
state.rows.forEach((row) => {
row.forEach((cell) => {
cell.html = '';
});
});
}
function undoAction() {
state.undo();
}
function redoAction() {
state.redo();
}
// Computed Markdown output
const markdownOutput = computed(() => {
return state.toMarkdown({ compact: compactMode.value });
});
</script>
<template>
<div class="table-to-markdown-tool">
<!-- Toolbar -->
<c-card class="toolbar-card" mb-4>
<div class="toolbar-container">
<n-space align="center" class="flex-wrap">
<c-button @click="openNewTableModal">
<n-icon :component="Plus" class="mr-1" />
{{ t('tools.table-to-markdown.newTable') }}
</c-button>
<c-button @click="transposeTable">
<n-icon :component="Exchange" class="mr-1" />
{{ t('tools.table-to-markdown.transpose') }}
</c-button>
<c-button @click="clearTable">
<n-icon :component="Trash" class="mr-1" />
{{ t('tools.table-to-markdown.clear') }}
</c-button>
<n-divider vertical />
<c-button :disabled="!state.canUndo" @click="undoAction">
<n-icon :component="ArrowBack" class="mr-1" />
{{ t('tools.table-to-markdown.undo') }}
</c-button>
<c-button :disabled="!state.canRedo" @click="redoAction">
<n-icon :component="ArrowForwardUp" class="mr-1" />
{{ t('tools.table-to-markdown.redo') }}
</c-button>
</n-space>
<n-space align="center">
<span class="text-sm font-medium">{{ t('tools.table-to-markdown.compactMode') }}</span>
<n-switch v-model:value="compactMode" />
</n-space>
</div>
</c-card>
<!-- Table Editor Workspace -->
<c-card class="editor-container-card" mb-4>
<div class="editor-section">
<h3 class="section-title mb-2">
Table Editor
</h3>
<TableEditor :state="state" />
<div class="instructions">
Tip: Right-click column (#) or row headers to add/delete/align columns and rows. Tab/Enter to navigate. You can paste spreadsheet tables or CSV data directly!
</div>
</div>
</c-card>
<!-- Output Workspace -->
<div class="output-container">
<div class="output-header">
<h3 class="section-title">
Output
</h3>
<c-buttons-select v-model:value="activeTab" :options="tabOptions" />
</div>
<div v-show="activeTab === 'markdown'" class="markdown-output-wrapper">
<TextareaCopyable :value="markdownOutput" language="markdown" />
</div>
<div v-show="activeTab === 'preview'" class="preview-output-wrapper">
<c-card class="preview-card">
<div class="preview-content">
<c-markdown :markdown="markdownOutput" />
</div>
</c-card>
</div>
</div>
<!-- New Table Presets Modal -->
<c-modal v-model:open="showNewTableModal">
<c-card :title="t('tools.table-to-markdown.newTableTitle')">
<n-space vertical size="large">
<n-form-item :label="t('tools.table-to-markdown.rows')">
<n-input-number v-model:value="newTableRows" :min="1" :max="100" />
</n-form-item>
<n-form-item :label="t('tools.table-to-markdown.columns')">
<n-input-number v-model:value="newTableCols" :min="1" :max="100" />
</n-form-item>
<n-space justify="end" class="mt-4">
<c-button @click="showNewTableModal = false">
{{ t('tools.table-to-markdown.cancel') }}
</c-button>
<c-button type="primary" @click="confirmNewTable">
{{ t('tools.table-to-markdown.create') }}
</c-button>
</n-space>
</n-space>
</c-card>
</c-modal>
</div>
</template>
<style scoped lang="less">
.table-to-markdown-tool {
max-width: 100%;
}
.toolbar-card {
border-radius: 8px;
background-color: var(--card-bg, #ffffff);
}
.toolbar-container {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.editor-container-card {
margin-top: 16px;
margin-bottom: 24px;
}
.section-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--text-color, #1f2225);
}
.dark .section-title {
color: #e5e7eb;
}
.instructions {
margin-top: 8px;
font-size: 12px;
color: #8e8e93;
}
.output-container {
margin-top: 24px;
}
.output-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.preview-card {
padding: 24px;
border-radius: 8px;
background-color: var(--card-bg, #ffffff);
border: 1px solid var(--border-color, #efeff5);
}
.preview-content {
overflow-x: auto;
font-family: inherit;
:deep(table) {
width: 100%;
border-collapse: collapse;
margin: 1em 0;
th,
td {
border: 1px solid var(--border-color, #efeff5);
padding: 10px 14px;
text-align: left;
}
th {
background-color: var(--header-bg, #fafafc);
font-weight: 600;
}
}
}
.dark {
.preview-card {
border-color: #303033;
background-color: #18181c;
}
.preview-content {
:deep(table) {
th,
td {
border-color: #303033;
}
th {
background-color: #18181c;
}
}
}
}
</style>