Skip to content

Commit 7b3f3d3

Browse files
Add Beehiiv table conversion for VS Code markdown (#126)
### Motivation - Allow Markdown/GFM tables copied from VS Code to be converted into Beehiiv-ready rich HTML blocks so table content appears correctly in Beehiiv. ### Description - Add a `TABLE_STYLE` constant and a `convertTable` function that emits Beehiiv-compatible `<table>` markup with `table-fixed`, `data-id`, `<colgroup>`, `<tbody>`, and per-cell `<p data-id>` wrappers while preserving `colspan`/`rowspan`. - Introduce an `applyInlineFormatting` helper to normalize inline formatting (links and inline code language) and reuse it across paragraphs, blockquotes, and table cells. - Integrate table handling into `convertMarkdownToBeehiiv` so `TABLE` nodes are converted and appended to the output blocks. - Update the tool documentation (`vscode-to-beehiiv.docs.md`) to mention GFM table support. ------ [Codex Task](https://chatgpt.com/codex/tasks/task_e_69d3f3826890832591a479e654846c9c)
1 parent edbef93 commit 7b3f3d3

2 files changed

Lines changed: 81 additions & 21 deletions

File tree

vscode-to-beehiiv.docs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Convert Markdown copied from VS Code into Beehiiv-ready rich HTML, with a one-click clipboard copy.
1+
Convert Markdown copied from VS Code into Beehiiv-ready rich HTML (including GFM tables), with a one-click clipboard copy.

vscode-to-beehiiv.html

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ <h2>Beehiiv output</h2>
121121
const BLOCK_STYLE = 'font-style: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration: none; caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0);';
122122
const HEADING_STYLE = BLOCK_STYLE.replace('font-weight: 400; ', '');
123123
const BLOCKQUOTE_STYLE = 'font-style: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-line: none; text-decoration-thickness: auto; text-decoration-style: solid; caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0);';
124+
const TABLE_STYLE = `${BLOCKQUOTE_STYLE} min-width: 75px;`;
124125

125126
const updateStatus = (message) => {
126127
status.textContent = message;
@@ -148,6 +149,75 @@ <h2>Beehiiv output</h2>
148149
return !hasMedia && text === '';
149150
};
150151

152+
const applyInlineFormatting = (element) => {
153+
element.querySelectorAll('a').forEach(anchor => {
154+
anchor.setAttribute('target', '_blank');
155+
anchor.setAttribute('rel', 'noopener noreferrer nofollow');
156+
anchor.setAttribute('class', 'link');
157+
});
158+
159+
element.querySelectorAll('code').forEach(code => {
160+
normalizeCodeLanguage(code);
161+
});
162+
};
163+
164+
const convertTable = (tableNode) => {
165+
const rows = Array.from(tableNode.querySelectorAll('tr'));
166+
if (!rows.length) {
167+
return '';
168+
}
169+
170+
const columnCount = rows.reduce((maxColumns, row) => {
171+
const columnTotal = Array.from(row.children).reduce((sum, cell) => {
172+
const span = Number.parseInt(cell.getAttribute('colspan') || '1', 10);
173+
return sum + (Number.isFinite(span) && span > 0 ? span : 1);
174+
}, 0);
175+
return Math.max(maxColumns, columnTotal);
176+
}, 0);
177+
178+
const table = document.createElement('table');
179+
table.setAttribute('class', 'table-fixed');
180+
table.setAttribute('data-id', crypto.randomUUID());
181+
table.setAttribute('style', TABLE_STYLE);
182+
183+
const colgroup = document.createElement('colgroup');
184+
const safeColumnCount = Math.max(columnCount, 1);
185+
for (let i = 0; i < safeColumnCount; i += 1) {
186+
const col = document.createElement('col');
187+
col.setAttribute('style', 'min-width: 25px;');
188+
colgroup.appendChild(col);
189+
}
190+
table.appendChild(colgroup);
191+
192+
const tbody = document.createElement('tbody');
193+
194+
rows.forEach((sourceRow) => {
195+
const row = document.createElement('tr');
196+
197+
Array.from(sourceRow.children).forEach((sourceCell) => {
198+
const targetTag = sourceCell.tagName === 'TH' ? 'th' : 'td';
199+
const targetCell = document.createElement(targetTag);
200+
const colspan = sourceCell.getAttribute('colspan') || '1';
201+
const rowspan = sourceCell.getAttribute('rowspan') || '1';
202+
targetCell.setAttribute('colspan', colspan);
203+
targetCell.setAttribute('rowspan', rowspan);
204+
205+
const paragraph = document.createElement('p');
206+
paragraph.setAttribute('data-id', crypto.randomUUID());
207+
paragraph.innerHTML = sourceCell.innerHTML;
208+
applyInlineFormatting(paragraph);
209+
210+
targetCell.appendChild(paragraph);
211+
row.appendChild(targetCell);
212+
});
213+
214+
tbody.appendChild(row);
215+
});
216+
217+
table.appendChild(tbody);
218+
return table.outerHTML;
219+
};
220+
151221
const convertMarkdownToBeehiiv = (markdown) => {
152222
if (!markdown.trim()) {
153223
return '';
@@ -188,16 +258,7 @@ <h2>Beehiiv output</h2>
188258
}
189259
paragraph.setAttribute('style', BLOCK_STYLE);
190260
paragraph.innerHTML = node.innerHTML;
191-
192-
paragraph.querySelectorAll('a').forEach(anchor => {
193-
anchor.setAttribute('target', '_blank');
194-
anchor.setAttribute('rel', 'noopener noreferrer nofollow');
195-
anchor.setAttribute('class', 'link');
196-
});
197-
198-
paragraph.querySelectorAll('code').forEach(code => {
199-
normalizeCodeLanguage(code);
200-
});
261+
applyInlineFormatting(paragraph);
201262

202263
blocks.push(paragraph.outerHTML);
203264
return;
@@ -247,16 +308,7 @@ <h2>Beehiiv output</h2>
247308
const paragraph = document.createElement('p');
248309
paragraph.setAttribute('data-id', crypto.randomUUID());
249310
paragraph.innerHTML = sourceParagraph.innerHTML;
250-
251-
paragraph.querySelectorAll('a').forEach(anchor => {
252-
anchor.setAttribute('target', '_blank');
253-
anchor.setAttribute('rel', 'noopener noreferrer nofollow');
254-
anchor.setAttribute('class', 'link');
255-
});
256-
257-
paragraph.querySelectorAll('code').forEach(code => {
258-
normalizeCodeLanguage(code);
259-
});
311+
applyInlineFormatting(paragraph);
260312

261313
quote.appendChild(paragraph);
262314
});
@@ -266,6 +318,14 @@ <h2>Beehiiv output</h2>
266318
return;
267319
}
268320

321+
if (node.tagName === 'TABLE') {
322+
const tableHtml = convertTable(node);
323+
if (tableHtml) {
324+
blocks.push(tableHtml);
325+
}
326+
return;
327+
}
328+
269329
if (node.tagName.startsWith('H')) {
270330
const level = Number.parseInt(node.tagName.replace('H', ''), 10);
271331
const heading = document.createElement(node.tagName.toLowerCase());

0 commit comments

Comments
 (0)