diff --git a/static/messages.js b/static/messages.js index 6f1e6cd0..f92f6554 100644 --- a/static/messages.js +++ b/static/messages.js @@ -486,11 +486,17 @@ function _markdownTableCopyPayloadForTable(table){ if(!table||!table.rows) return null; const rows=Array.from(table.rows||[]); if(!rows.length) return null; + let headerRowCount=0; + while(headerRowCountcell&&cell.tagName==='TH')) break; + headerRowCount++; + } - const htmlRows=rows.map((row)=>{ + const renderRows=(rowSet)=>rowSet.map((row)=>{ const cellTag=(cell)=>String(cell&&cell.tagName?cell.tagName.toLowerCase():'td'); const cells=Array.from(row.cells||[]) - .filter((cell)=>cell&&cell.nodeType===1 || cell&&cell.nodeType===3) + .filter((cell)=>cell&&cell.nodeType===1) .map((cell)=>{ const tag=cellTag(cell); const text=_sanitizeMarkdownTableCellText(cell); @@ -499,6 +505,12 @@ function _markdownTableCopyPayloadForTable(table){ .join(''); return `${cells}`; }).join(''); + const headerRows=headerRowCount?renderRows(rows.slice(0, headerRowCount)):''; + const bodyRows=renderRows(rows.slice(headerRowCount)); + const tableSections=[ + headerRows?`${headerRows}`:'', + bodyRows?`${bodyRows}`:'', + ].join(''); const plainRows=rows.map((row)=>{ return Array.from(row.cells||[]) @@ -506,7 +518,7 @@ function _markdownTableCopyPayloadForTable(table){ .join('\t'); }).join('\n'); - return {html:`${htmlRows}
`, plain:plainRows}; + return {html:`${tableSections}
`, plain:plainRows}; } function _findEnhancedMarkdownTable(node){ @@ -518,6 +530,15 @@ function _findEnhancedMarkdownTable(node){ return null; } +function _findMarkdownTableCell(node){ + let current=node&&node.nodeType===3?node.parentElement:node; + while(current){ + if(current.matches&¤t.matches('th,td')) return current; + current=current.parentElement||current.parentNode; + } + return null; +} + function _findEnhancedMarkdownTableFromRange(range){ if(!range) return null; const found=_findEnhancedMarkdownTable(range.startContainer) @@ -543,6 +564,9 @@ function _handleMarkdownTableCopy(event){ if(!selection||selection.isCollapsed||!selection.rangeCount) return; const range=selection.getRangeAt(0); if(!range) return; + const startCell=_findMarkdownTableCell(range.startContainer); + const endCell=_findMarkdownTableCell(range.endContainer); + if(startCell&&endCell&&startCell===endCell) return; const table=_findEnhancedMarkdownTableFromRange(range); if(!table||!table.matches||!table.matches('table[data-markdown-table-enhanced]')) return; const payload=_markdownTableCopyPayloadForTable(table); diff --git a/tests/test_issue4945_markdown_table_copy.py b/tests/test_issue4945_markdown_table_copy.py index 3d94a803..ae7b1dfc 100644 --- a/tests/test_issue4945_markdown_table_copy.py +++ b/tests/test_issue4945_markdown_table_copy.py @@ -24,15 +24,53 @@ function extractFunc(name) { const re = new RegExp('function\\s+' + name + '\\s*\\('); const start = src.search(re); if (start < 0) throw new Error(name + ' not found'); - let i = src.indexOf('{', start); - let depth = 1; - i++; - while (depth > 0 && i < src.length) { - if (src[i] === '{') depth++; - else if (src[i] === '}') depth--; - i++; + const braceStart = src.indexOf('{', start); + let depth = 0; + let inString = null; + let escaped = false; + let inLineComment = false; + let inBlockComment = false; + for (let i = braceStart; i < src.length; i++) { + const ch = src[i]; + const next = i + 1 < src.length ? src[i + 1] : ''; + if (inLineComment) { + if (ch === '\n') inLineComment = false; + continue; + } + if (inBlockComment) { + if (ch === '*' && next === '/') { + inBlockComment = false; + i++; + } + continue; + } + if (inString) { + if (escaped) escaped = false; + else if (ch === '\\') escaped = true; + else if (ch === inString) inString = null; + continue; + } + if (ch === '/' && next === '/') { + inLineComment = true; + i++; + continue; + } + if (ch === '/' && next === '*') { + inBlockComment = true; + i++; + continue; + } + if (ch === "'" || ch === '"' || ch === '`') { + inString = ch; + continue; + } + if (ch === '{') depth++; + else if (ch === '}') { + depth--; + if (depth === 0) return src.slice(start, i + 1); + } } - return src.slice(start, i); + throw new Error(name + ' brace scan failed'); } const required = [ @@ -41,6 +79,7 @@ const required = [ '_markdownTableCellText', '_sanitizeMarkdownTableCellText', '_findEnhancedMarkdownTable', + '_findMarkdownTableCell', '_findEnhancedMarkdownTableFromRange', '_markdownTableCopyPayloadForTable', '_handleMarkdownTableCopy', @@ -260,6 +299,8 @@ const payload = _markdownTableCopyPayloadForTable(table); console.log(JSON.stringify(payload)); """ ) + assert "ProductPrice" in out["html"] + assert "Widget12" in out["html"] assert "Product" in out["html"] assert "Price" in out["html"] assert "markdown-table-sort" not in out["html"] @@ -434,3 +475,43 @@ console.log(JSON.stringify({ prevented: event.preventDefaultCalled, data: clipbo assert out["prevented"] is True assert "Product" in out["data"]["text/html"] assert out["data"]["text/plain"].startswith("Product\tPrice") + + +def test_single_cell_subselection_leaves_native_copy_unmodified(): + out = _run_js( + """ +const {table, body} = buildEnhancedTableFixture(true); + +const range = { + startContainer: body.cells[0].children[0], + endContainer: body.cells[0].children[0], + commonAncestorContainer: body.cells[0], +}; + +window.getSelection = () => ({ + isCollapsed: false, + rangeCount: 1, + getRangeAt: () => range, +}); + +const clipboard = { + data: {}, + setData(type, value) { + this.data[type] = value; + } +}; + +const event = { + preventDefaultCalled: false, + preventDefault() { + this.preventDefaultCalled = true; + }, + clipboardData: clipboard, +}; + +_handleMarkdownTableCopy(event); +console.log(JSON.stringify({ prevented: event.preventDefaultCalled, data: clipboard.data })); +""" + ) + assert out["prevented"] is False + assert out["data"] == {}