mirror of
https://github.com/laurent22/joplin.git
synced 2026-03-13 08:09:59 +08:00
This commit is contained in:
@@ -45,6 +45,10 @@ describe('HtmlToMd', () => {
|
||||
htmlToMdOptions.preserveColorStyles = true;
|
||||
}
|
||||
|
||||
if (htmlFilename.indexOf('table_with') === 0 || htmlFilename.indexOf('table_default') === 0) {
|
||||
htmlToMdOptions.preserveTableStyles = true;
|
||||
}
|
||||
|
||||
const html = await readFile(htmlPath, 'utf8');
|
||||
let expectedMd = await readFile(mdPath, 'utf8');
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<table style="border-collapse: collapse; width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">Name</th>
|
||||
<th style="width: 50%;">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 50%;">Cell A</td>
|
||||
<td style="width: 50%;">Cell B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 50%;">Cell C</td>
|
||||
<td style="width: 50%;">Cell D</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,4 @@
|
||||
| Name | Value |
|
||||
| --- | --- |
|
||||
| Cell A | Cell B |
|
||||
| Cell C | Cell D |
|
||||
@@ -0,0 +1,18 @@
|
||||
<table bgcolor="#f0f0f0" cellpadding="8">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Cell A</td>
|
||||
<td>Cell B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cell C</td>
|
||||
<td>Cell D</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1 @@
|
||||
<div class="joplin-table-wrapper"><table bgcolor="#f0f0f0" cellpadding="8"><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody><tr><td>Cell A</td><td>Cell B</td></tr><tr><td>Cell C</td><td>Cell D</td></tr></tbody></table></div>
|
||||
@@ -0,0 +1,18 @@
|
||||
<table style="border-collapse: collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="background-color: #e03e2d">Red cell</td>
|
||||
<td style="padding: 10px 15px">Padded cell</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border-color: #2dc26b; border-style: solid">Green border</td>
|
||||
<td>Normal cell</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1 @@
|
||||
<div class="joplin-table-wrapper"><table style="border-collapse: collapse"><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody><tr><td style="background-color: #e03e2d">Red cell</td><td style="padding: 10px 15px">Padded cell</td></tr><tr><td style="border-color: #2dc26b; border-style: solid">Green border</td><td>Normal cell</td></tr></tbody></table></div>
|
||||
@@ -1335,13 +1335,35 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onSetAttrib = (event: EditorEvent<any>) => {
|
||||
// Dispatch onChange when a link is edited
|
||||
// Dispatch onChange when a link or table-related formatting is edited
|
||||
const target = Array.isArray(event.attrElm) ? event.attrElm[0] : event.attrElm;
|
||||
if (!target) return;
|
||||
|
||||
if (target.nodeName === 'A') {
|
||||
if (event.attrName === 'title' || event.attrName === 'href' || event.attrName === 'rel') {
|
||||
onChangeHandler();
|
||||
}
|
||||
}
|
||||
|
||||
if (['TABLE', 'TR', 'TD', 'TH'].includes(target.nodeName)) {
|
||||
const attributeName = (event.attrName ?? '').toLowerCase();
|
||||
if (
|
||||
attributeName === 'style' ||
|
||||
attributeName === 'class' ||
|
||||
attributeName === 'bgcolor' ||
|
||||
attributeName === 'bordercolor' ||
|
||||
attributeName === 'background' ||
|
||||
attributeName === 'cellpadding' ||
|
||||
attributeName === 'cellspacing'
|
||||
) {
|
||||
onChangeHandler();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Table plugin fires this on structure/style changes from dialogs.
|
||||
const onTableModified = () => {
|
||||
onChangeHandler();
|
||||
};
|
||||
|
||||
// Keypress means that a printable key (letter, digit, etc.) has been
|
||||
@@ -1490,6 +1512,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
editor.on(TinyMceEditorEvents.Redo, onChangeHandler);
|
||||
editor.on(TinyMceEditorEvents.ExecCommand, onExecCommand);
|
||||
editor.on(TinyMceEditorEvents.SetAttrib, onSetAttrib);
|
||||
editor.on('TableModified', onTableModified);
|
||||
|
||||
return () => {
|
||||
try {
|
||||
@@ -1506,6 +1529,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
editor.off(TinyMceEditorEvents.Redo, onChangeHandler);
|
||||
editor.off(TinyMceEditorEvents.ExecCommand, onExecCommand);
|
||||
editor.off(TinyMceEditorEvents.SetAttrib, onSetAttrib);
|
||||
editor.off('TableModified', onTableModified);
|
||||
} catch (error) {
|
||||
console.warn('Error removing events', error);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export async function htmlToMarkdown(markupLanguage: number, html: string, origi
|
||||
newBody = htmlToMd.parse(html, {
|
||||
preserveImageTagsWithSize: true,
|
||||
preserveNestedTables: true,
|
||||
preserveTableStyles: true,
|
||||
preserveColorStyles: true,
|
||||
...parseOptions,
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ const convertHtmlToMarkdown = (html: string|HTMLElement) => {
|
||||
codeBlockStyle: 'fenced',
|
||||
preserveImageTagsWithSize: true,
|
||||
preserveNestedTables: true,
|
||||
preserveTableStyles: true,
|
||||
preserveColorStyles: true,
|
||||
bulletListMarker: '-',
|
||||
emDelimiter: '*',
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface ParseOptions {
|
||||
anchorNames?: string[];
|
||||
preserveImageTagsWithSize?: boolean;
|
||||
preserveNestedTables?: boolean;
|
||||
preserveTableStyles?: boolean;
|
||||
preserveColorStyles?: boolean;
|
||||
baseUrl?: string;
|
||||
disableEscapeContent?: boolean;
|
||||
@@ -26,6 +27,7 @@ export default class HtmlToMd {
|
||||
codeBlockStyle: 'fenced',
|
||||
preserveImageTagsWithSize: !!options.preserveImageTagsWithSize,
|
||||
preserveNestedTables: !!options.preserveNestedTables,
|
||||
preserveTableStyles: !!options.preserveTableStyles,
|
||||
preserveColorStyles: !!options.preserveColorStyles,
|
||||
bulletListMarker: '-',
|
||||
emDelimiter: '*',
|
||||
|
||||
@@ -258,3 +258,5 @@ clearsign
|
||||
ligne
|
||||
payant
|
||||
llamacpp
|
||||
bgcolor
|
||||
bordercolor
|
||||
|
||||
@@ -210,6 +210,93 @@ const nodeContains = (node, types) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Style properties that count as user customization.
|
||||
// Excludes TinyMCE/Joplin defaults:
|
||||
// - border-collapse: set by default on all tables
|
||||
// - width: set on every cell by TinyMCE
|
||||
// - text-align: converted to Markdown alignment (:---, :---:, ---:)
|
||||
// - height: false positives from TinyMCE defaults
|
||||
const customStyleProperties = [
|
||||
'background-color', 'background',
|
||||
'border-color', 'border',
|
||||
'border-top', 'border-right', 'border-bottom', 'border-left',
|
||||
'border-style', 'border-width',
|
||||
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
||||
'float', 'margin-left', 'margin-right',
|
||||
];
|
||||
|
||||
// HTML attributes TinyMCE may set instead of CSS.
|
||||
const customAttributeNames = [
|
||||
'bgcolor',
|
||||
'bordercolor',
|
||||
'background',
|
||||
];
|
||||
|
||||
const nodeHasCustomStyle = (node) => {
|
||||
if (!node || !node.getAttribute) return false;
|
||||
const styleAttr = node.getAttribute('style');
|
||||
if (!styleAttr) return false;
|
||||
// Extract property names from the raw style string
|
||||
const properties = styleAttr.split(';')
|
||||
.map(s => s.split(':')[0].trim().toLowerCase())
|
||||
.filter(s => s.length > 0);
|
||||
for (let i = 0; i < properties.length; i++) {
|
||||
if (customStyleProperties.includes(properties[i])) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const hasNonDefaultSpacingAttribute = (node, name) => {
|
||||
if (!node || !node.getAttribute) return false;
|
||||
const value = node.getAttribute(name);
|
||||
if (value === null) return false;
|
||||
const normalisedValue = `${value}`.trim().toLowerCase();
|
||||
if (!normalisedValue) return false;
|
||||
if (normalisedValue === '0' || normalisedValue === '0px') return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const nodeHasCustomAttributes = (node) => {
|
||||
if (!node || !node.getAttribute) return false;
|
||||
|
||||
for (let i = 0; i < customAttributeNames.length; i++) {
|
||||
const value = node.getAttribute(customAttributeNames[i]);
|
||||
if (value !== null && `${value}`.trim() !== '') return true;
|
||||
}
|
||||
|
||||
if (node.nodeName === 'TABLE') {
|
||||
if (hasNonDefaultSpacingAttribute(node, 'cellpadding')) return true;
|
||||
if (hasNonDefaultSpacingAttribute(node, 'cellspacing')) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const nodeHasCustomFormatting = (node) => {
|
||||
return nodeHasCustomStyle(node) || nodeHasCustomAttributes(node);
|
||||
};
|
||||
|
||||
// Returns true if the table or any of its rows/cells have custom formatting.
|
||||
const tableHasCustomStyles = (tableNode) => {
|
||||
if (nodeHasCustomFormatting(tableNode)) return true;
|
||||
|
||||
const rows = tableNode.rows;
|
||||
if (!rows) return false;
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
if (nodeHasCustomFormatting(row)) return true;
|
||||
for (let j = 0; j < row.childNodes.length; j++) {
|
||||
const cell = row.childNodes[j];
|
||||
if ((cell.nodeName === 'TD' || cell.nodeName === 'TH') && nodeHasCustomFormatting(cell)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const tableShouldBeHtml = (tableNode, options) => {
|
||||
const possibleTags = [
|
||||
'UL',
|
||||
@@ -232,7 +319,8 @@ const tableShouldBeHtml = (tableNode, options) => {
|
||||
if (options.preserveNestedTables) possibleTags.push('TABLE');
|
||||
|
||||
return nodeContains(tableNode, 'code') ||
|
||||
nodeContains(tableNode, possibleTags);
|
||||
nodeContains(tableNode, possibleTags) ||
|
||||
(options.preserveTableStyles && tableHasCustomStyles(tableNode));
|
||||
}
|
||||
|
||||
// Various conditions under which a table should be skipped - i.e. each cell
|
||||
|
||||
Reference in New Issue
Block a user