|
37 | 37 | }); |
38 | 38 | turndownService.escape = (string) => string; |
39 | 39 |
|
| 40 | + // Turndown renders <br> as a Markdown " \n" hard break, leaving invisible |
| 41 | + // trailing spaces on every line. Strip it, but only after real content and |
| 42 | + // never inside fenced code blocks where trailing whitespace can be significant. |
| 43 | + const stripTrailingWhitespace = (md: string): string => |
| 44 | + md |
| 45 | + .split(/(```[\s\S]*?```|~~~[\s\S]*?~~~)/g) |
| 46 | + .map((part, i) => |
| 47 | + i % 2 |
| 48 | + ? part |
| 49 | + : part.replace(/([^\n \t])[ \t]+\n/g, '$1\n').replace(/([^\n \t])[ \t]+$/g, '$1') |
| 50 | + ) |
| 51 | + .join(''); |
| 52 | +
|
40 | 53 | // Produce single newlines between paragraphs instead of double. |
41 | 54 | // TipTap wraps every line in <p> tags; the default Turndown rule emits |
42 | 55 | // \n\n around each paragraph which then required a destructive |
|
711 | 724 | try { |
712 | 725 | // Try parsing the value |
713 | 726 | return marked.parse(value.replaceAll(`\n<br/>`, `<br/>`), { |
714 | | - breaks: false |
| 727 | + breaks: true |
715 | 728 | }); |
716 | 729 | } catch (error) { |
717 | 730 | // If no attempts remain, fallback to plain text |
|
885 | 898 | jsonValue = editor.getJSON(); |
886 | 899 |
|
887 | 900 | if (richText) { |
888 | | - mdValue = turndownService |
889 | | - .turndown( |
890 | | - htmlValue |
891 | | - .replace(/<p><\/p>/g, '<br/>') |
892 | | - .replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0')) |
893 | | - ) |
894 | | - .replace(/\u00a0/g, ' '); |
| 901 | + mdValue = stripTrailingWhitespace( |
| 902 | + turndownService |
| 903 | + .turndown( |
| 904 | + htmlValue |
| 905 | + .replace(/<p><\/p>/g, '<br/>') |
| 906 | + .replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0')) |
| 907 | + ) |
| 908 | + .replace(/\u00a0/g, ' ') |
| 909 | + ); |
895 | 910 | } else { |
896 | | - mdValue = turndownService |
897 | | - .turndown( |
898 | | - htmlValue |
899 | | - // Replace empty paragraphs with line breaks |
900 | | - .replace(/<p><\/p>/g, '<br/>') |
901 | | - // Replace multiple spaces with non-breaking spaces |
902 | | - .replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0')) |
903 | | - // Replace tabs with non-breaking spaces (preserve indentation) |
904 | | - .replace(/\t/g, '\u00a0\u00a0\u00a0\u00a0') // 1 tab = 4 spaces |
905 | | - ) |
906 | | - // Convert non-breaking spaces back to regular spaces for markdown |
907 | | - .replace(/\u00a0/g, ' '); |
| 911 | + mdValue = stripTrailingWhitespace( |
| 912 | + turndownService |
| 913 | + .turndown( |
| 914 | + htmlValue |
| 915 | + // Replace empty paragraphs with line breaks |
| 916 | + .replace(/<p><\/p>/g, '<br/>') |
| 917 | + // Replace multiple spaces with non-breaking spaces |
| 918 | + .replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0')) |
| 919 | + // Replace tabs with non-breaking spaces (preserve indentation) |
| 920 | + .replace(/\t/g, '\u00a0\u00a0\u00a0\u00a0') // 1 tab = 4 spaces |
| 921 | + ) |
| 922 | + // Convert non-breaking spaces back to regular spaces for markdown |
| 923 | + .replace(/\u00a0/g, ' ') |
| 924 | + ); |
908 | 925 | } |
909 | 926 |
|
910 | 927 | onChange({ |
|
1267 | 1284 |
|
1268 | 1285 | const jsonValue = editor.getJSON(); |
1269 | 1286 | const htmlValue = editor.getHTML(); |
1270 | | - let mdValue = turndownService |
1271 | | - .turndown( |
1272 | | - (preserveBreaks ? htmlValue.replace(/<p><\/p>/g, '<br/>') : htmlValue).replace( |
1273 | | - / {2,}/g, |
1274 | | - (m) => m.replace(/ /g, '\u00a0') |
| 1287 | + let mdValue = stripTrailingWhitespace( |
| 1288 | + turndownService |
| 1289 | + .turndown( |
| 1290 | + (preserveBreaks ? htmlValue.replace(/<p><\/p>/g, '<br/>') : htmlValue).replace( |
| 1291 | + / {2,}/g, |
| 1292 | + (m) => m.replace(/ /g, '\u00a0') |
| 1293 | + ) |
1275 | 1294 | ) |
1276 | | - ) |
1277 | | - .replace(/\u00a0/g, ' '); |
| 1295 | + .replace(/\u00a0/g, ' ') |
| 1296 | + ); |
1278 | 1297 |
|
1279 | 1298 | if (value === '') { |
1280 | 1299 | editor.commands.clearContent(); // Clear content if value is empty |
|
1300 | 1319 | preserveBreaks |
1301 | 1320 | ? value |
1302 | 1321 | : marked.parse(value.replaceAll(`\n<br/>`, `<br/>`), { |
1303 | | - breaks: false |
| 1322 | + breaks: true |
1304 | 1323 | }) |
1305 | 1324 | ); |
1306 | 1325 |
|
|
0 commit comments