|
99 | 99 | const [ tableCols, setTableCols ] = useState( 3 ); |
100 | 100 | const [ tableRows, setTableRows ] = useState( 2 ); |
101 | 101 | const [ cursorInTable, setCursorInTable ] = useState( false ); |
| 102 | + const [ showImportModal, setShowImportModal ] = useState( false ); |
| 103 | + const [ markdownInput, setMarkdownInput ] = useState( '' ); |
| 104 | + const [ djotPreview, setDjotPreview ] = useState( '' ); |
102 | 105 | const textareaRef = useRef( null ); |
103 | 106 | const [ selectionStart, setSelectionStart ] = useState( 0 ); |
104 | 107 | const [ selectionEnd, setSelectionEnd ] = useState( 0 ); |
|
548 | 551 | restoreFocus( textarea, newCursorPos ); |
549 | 552 | } |
550 | 553 |
|
| 554 | + // Convert Markdown to Djot |
| 555 | + function convertMarkdownToDjot( md ) { |
| 556 | + var result = md; |
| 557 | + |
| 558 | + // Protect code blocks first (store and replace with placeholders) |
| 559 | + var codeBlocks = []; |
| 560 | + result = result.replace( /```[\s\S]*?```/g, function( match ) { |
| 561 | + codeBlocks.push( match ); |
| 562 | + return '%%CODEBLOCK' + ( codeBlocks.length - 1 ) + '%%'; |
| 563 | + } ); |
| 564 | + |
| 565 | + // Protect inline code |
| 566 | + var inlineCode = []; |
| 567 | + result = result.replace( /`[^`]+`/g, function( match ) { |
| 568 | + inlineCode.push( match ); |
| 569 | + return '%%INLINECODE' + ( inlineCode.length - 1 ) + '%%'; |
| 570 | + } ); |
| 571 | + |
| 572 | + // Convert indented code blocks to fenced (4 spaces or 1 tab) |
| 573 | + result = result.replace( /^((?:(?: |\t).+\n?)+)/gm, function( match ) { |
| 574 | + var code = match.replace( /^( |\t)/gm, '' ).trimEnd(); |
| 575 | + return '```\n' + code + '\n```\n'; |
| 576 | + } ); |
| 577 | + |
| 578 | + // Bold: **text** or __text__ → *text* |
| 579 | + result = result.replace( /\*\*([^*]+)\*\*/g, '*$1*' ); |
| 580 | + result = result.replace( /__([^_]+)__/g, '*$1*' ); |
| 581 | + |
| 582 | + // Italic: *text* or _text_ → _text_ (but not inside words) |
| 583 | + // Only convert *text* that's not already bold |
| 584 | + result = result.replace( /(?<!\*)\*([^*]+)\*(?!\*)/g, '_$1_' ); |
| 585 | + |
| 586 | + // Strikethrough: ~~text~~ → {~text~} |
| 587 | + result = result.replace( /~~([^~]+)~~/g, '{~$1~}' ); |
| 588 | + |
| 589 | + // Highlight: ==text== → {=text=} |
| 590 | + result = result.replace( /==([^=]+)==/g, '{=$1=}' ); |
| 591 | + |
| 592 | + // Headers: ensure space after # (Djot requires it) |
| 593 | + result = result.replace( /^(#{1,6})([^ #\n])/gm, '$1 $2' ); |
| 594 | + |
| 595 | + // Remove trailing # from headers (Djot treats them as content) |
| 596 | + result = result.replace( /^(#{1,6} .+?)\s*#+\s*$/gm, '$1' ); |
| 597 | + |
| 598 | + // Setext-style headers: convert to ATX style |
| 599 | + // H1: text followed by line of === |
| 600 | + result = result.replace( /^(.+)\n=+$/gm, '# $1' ); |
| 601 | + // H2: text followed by line of --- |
| 602 | + result = result.replace( /^(.+)\n-+$/gm, '## $1' ); |
| 603 | + |
| 604 | + // Ensure blank line after headers (Djot requires it) |
| 605 | + result = result.replace( /^(#{1,6} .+)$(\n?)(?!\n)/gm, '$1\n\n' ); |
| 606 | + |
| 607 | + // Link titles: [text](url "title") → [text](url){title="title"} |
| 608 | + result = result.replace( /\[([^\]]+)\]\(([^)"]+)\s+"([^"]+)"\)/g, '[$1]($2){title="$3"}' ); |
| 609 | + result = result.replace( /\[([^\]]+)\]\(([^)']+)\s+'([^']+)'\)/g, "[$1]($2){title='$3'}" ); |
| 610 | + |
| 611 | + // Hard line breaks: trailing two spaces → backslash |
| 612 | + result = result.replace( / $/gm, '\\' ); |
| 613 | + |
| 614 | + // Blockquotes: ensure space after > (unless followed by newline) |
| 615 | + result = result.replace( /^>([^ \n>])/gm, '> $1' ); |
| 616 | + |
| 617 | + // Ensure blank line before blockquotes |
| 618 | + result = result.replace( /([^\n])\n(>)/gm, '$1\n\n$2' ); |
| 619 | + |
| 620 | + // Ensure blank line before lists |
| 621 | + result = result.replace( /([^\n])\n([-*+] |\d+\. )/gm, '$1\n\n$2' ); |
| 622 | + |
| 623 | + // Raw HTML: wrap in djot raw syntax |
| 624 | + result = result.replace( /<([a-z][a-z0-9]*)([ >])/gi, function( match, tag, after ) { |
| 625 | + // Skip common safe tags that might be intentional |
| 626 | + var safeTags = [ 'http', 'https' ]; |
| 627 | + if ( safeTags.indexOf( tag.toLowerCase() ) >= 0 ) { |
| 628 | + return match; |
| 629 | + } |
| 630 | + return '`<' + tag + after.trimEnd() + '`{=html}'; |
| 631 | + } ); |
| 632 | + |
| 633 | + // Restore inline code |
| 634 | + inlineCode.forEach( function( code, idx ) { |
| 635 | + result = result.replace( '%%INLINECODE' + idx + '%%', code ); |
| 636 | + } ); |
| 637 | + |
| 638 | + // Restore code blocks |
| 639 | + codeBlocks.forEach( function( block, idx ) { |
| 640 | + result = result.replace( '%%CODEBLOCK' + idx + '%%', block ); |
| 641 | + } ); |
| 642 | + |
| 643 | + // Clean up excessive blank lines (more than 2 consecutive) |
| 644 | + result = result.replace( /\n{3,}/g, '\n\n' ); |
| 645 | + |
| 646 | + return result; |
| 647 | + } |
| 648 | + |
| 649 | + // Open import modal |
| 650 | + function onImportMarkdown() { |
| 651 | + setMarkdownInput( '' ); |
| 652 | + setDjotPreview( '' ); |
| 653 | + setShowImportModal( true ); |
| 654 | + } |
| 655 | + |
| 656 | + // Update preview when markdown input changes |
| 657 | + function onMarkdownInputChange( value ) { |
| 658 | + setMarkdownInput( value ); |
| 659 | + setDjotPreview( convertMarkdownToDjot( value ) ); |
| 660 | + } |
| 661 | + |
| 662 | + // Insert converted djot at cursor position |
| 663 | + function onInsertImported() { |
| 664 | + if ( ! djotPreview.trim() ) { |
| 665 | + setShowImportModal( false ); |
| 666 | + return; |
| 667 | + } |
| 668 | + |
| 669 | + var textarea = textareaRef.current ? textareaRef.current.querySelector( 'textarea' ) : null; |
| 670 | + var text = content || ''; |
| 671 | + var start = textarea ? textarea.selectionStart : text.length; |
| 672 | + |
| 673 | + // Add newlines if needed |
| 674 | + var prefix = ''; |
| 675 | + if ( start > 0 && text[ start - 1 ] !== '\n' ) { |
| 676 | + prefix = '\n\n'; |
| 677 | + } else if ( start > 1 && text[ start - 2 ] !== '\n' ) { |
| 678 | + prefix = '\n'; |
| 679 | + } |
| 680 | + |
| 681 | + var suffix = '\n\n'; |
| 682 | + if ( start < text.length - 1 && text[ start ] === '\n' && text[ start + 1 ] === '\n' ) { |
| 683 | + suffix = ''; |
| 684 | + } else if ( start < text.length && text[ start ] === '\n' ) { |
| 685 | + suffix = '\n'; |
| 686 | + } |
| 687 | + |
| 688 | + var newText = text.substring( 0, start ) + prefix + djotPreview + suffix + text.substring( start ); |
| 689 | + var newCursorPos = start + prefix.length + djotPreview.length; |
| 690 | + |
| 691 | + setAttributes( { content: newText } ); |
| 692 | + setShowImportModal( false ); |
| 693 | + setMarkdownInput( '' ); |
| 694 | + setDjotPreview( '' ); |
| 695 | + |
| 696 | + if ( textarea ) { |
| 697 | + restoreFocus( textarea, newCursorPos ); |
| 698 | + } |
| 699 | + } |
| 700 | + |
551 | 701 | // Keyboard shortcut handler for textarea |
552 | 702 | function handleTextareaKeyDown( e ) { |
553 | 703 | const isMod = e.ctrlKey || e.metaKey; |
|
882 | 1032 | wp.element.createElement( 'div', null, wp.element.createElement( 'kbd', null, 'Ctrl+Shift+X' ), ' Strikethrough' ), |
883 | 1033 | wp.element.createElement( 'div', null, wp.element.createElement( 'kbd', null, 'ESC' ), ' Exit preview' ) |
884 | 1034 | ) |
| 1035 | + ), |
| 1036 | + wp.element.createElement( |
| 1037 | + PanelBody, |
| 1038 | + { title: __( 'Tools', 'wp-djot' ), initialOpen: false }, |
| 1039 | + wp.element.createElement( Button, { |
| 1040 | + variant: 'secondary', |
| 1041 | + onClick: onImportMarkdown, |
| 1042 | + style: { width: '100%' }, |
| 1043 | + }, __( 'Import Markdown', 'wp-djot' ) ) |
885 | 1044 | ) |
886 | 1045 | ), |
887 | 1046 | // Link Modal |
|
983 | 1142 | }, __( 'Cancel', 'wp-djot' ) ) |
984 | 1143 | ) |
985 | 1144 | ), |
| 1145 | + // Import Markdown Modal |
| 1146 | + showImportModal && wp.element.createElement( |
| 1147 | + Modal, |
| 1148 | + { |
| 1149 | + title: __( 'Import Markdown', 'wp-djot' ), |
| 1150 | + onRequestClose: function() { setShowImportModal( false ); }, |
| 1151 | + style: { width: '600px', maxWidth: '90vw' }, |
| 1152 | + }, |
| 1153 | + wp.element.createElement( 'div', { style: { display: 'flex', gap: '16px' } }, |
| 1154 | + wp.element.createElement( 'div', { style: { flex: 1 } }, |
| 1155 | + wp.element.createElement( 'label', { style: { display: 'block', marginBottom: '8px', fontWeight: 600 } }, __( 'Markdown Input', 'wp-djot' ) ), |
| 1156 | + wp.element.createElement( 'textarea', { |
| 1157 | + value: markdownInput, |
| 1158 | + onChange: function( e ) { onMarkdownInputChange( e.target.value ); }, |
| 1159 | + style: { width: '100%', height: '200px', fontFamily: 'monospace', fontSize: '13px', padding: '8px' }, |
| 1160 | + placeholder: __( 'Paste your Markdown here...', 'wp-djot' ), |
| 1161 | + } ) |
| 1162 | + ), |
| 1163 | + wp.element.createElement( 'div', { style: { flex: 1 } }, |
| 1164 | + wp.element.createElement( 'label', { style: { display: 'block', marginBottom: '8px', fontWeight: 600 } }, __( 'Djot Preview', 'wp-djot' ) ), |
| 1165 | + wp.element.createElement( 'textarea', { |
| 1166 | + value: djotPreview, |
| 1167 | + readOnly: true, |
| 1168 | + style: { width: '100%', height: '200px', fontFamily: 'monospace', fontSize: '13px', padding: '8px', background: '#f9f9f9' }, |
| 1169 | + placeholder: __( 'Converted Djot will appear here...', 'wp-djot' ), |
| 1170 | + } ) |
| 1171 | + ) |
| 1172 | + ), |
| 1173 | + wp.element.createElement( 'p', { style: { marginTop: '12px', fontSize: '12px', color: '#666' } }, |
| 1174 | + __( 'Converts: **bold** → *bold*, *italic* → _italic_, ~~strike~~ → {~strike~}, ==highlight== → {=highlight=}', 'wp-djot' ) |
| 1175 | + ), |
| 1176 | + wp.element.createElement( |
| 1177 | + 'div', |
| 1178 | + { style: { marginTop: '16px' } }, |
| 1179 | + wp.element.createElement( Button, { |
| 1180 | + variant: 'primary', |
| 1181 | + onClick: onInsertImported, |
| 1182 | + disabled: ! djotPreview.trim(), |
| 1183 | + }, __( 'Insert', 'wp-djot' ) ), |
| 1184 | + wp.element.createElement( Button, { |
| 1185 | + variant: 'secondary', |
| 1186 | + onClick: function() { setShowImportModal( false ); }, |
| 1187 | + style: { marginLeft: '8px' }, |
| 1188 | + }, __( 'Cancel', 'wp-djot' ) ) |
| 1189 | + ) |
| 1190 | + ), |
986 | 1191 | // Main content area |
987 | 1192 | content || isPreviewMode |
988 | 1193 | ? wp.element.createElement( |
|
0 commit comments