fix(#4945): keep native copy for cell sub-selections

This commit is contained in:
Rod Boev authored and nesquena-hermes committed 2026-06-26 17:51:31 +00:00
1 parent 0256b98a3e
commit 8c09c8dfde
2 files changed
+116 -11

No files matched your search

+27 -3
View File
@@ -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(headerRowCount<rows.length){
const cells=Array.from(rows[headerRowCount].cells||[]);
if(!cells.length||!cells.every((cell)=>cell&&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 `<tr>${cells}</tr>`;
}).join('');
const headerRows=headerRowCount?renderRows(rows.slice(0, headerRowCount)):'';
const bodyRows=renderRows(rows.slice(headerRowCount));
const tableSections=[
headerRows?`<thead>${headerRows}</thead>`:'',
bodyRows?`<tbody>${bodyRows}</tbody>`:'',
].join('');
const plainRows=rows.map((row)=>{
return Array.from(row.cells||[])
@@ -506,7 +518,7 @@ function _markdownTableCopyPayloadForTable(table){
.join('\t');
}).join('\n');
return {html:`<table>${htmlRows}</table>`, plain:plainRows};
return {html:`<table>${tableSections}</table>`, 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&&current.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);
+89 -8
View File
@@ -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 "<thead><tr><th>Product</th><th>Price</th></tr></thead>" in out["html"]
assert "<tbody><tr><td>Widget</td><td>12</td></tr></tbody>" in out["html"]
assert "<th>Product</th>" in out["html"]
assert "<th>Price</th>" 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 "<th>Product</th>" 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"] == {}