~linuxgoose/bocpress

ref: 3f126040356e379bea84cbe62d97d265efeb5d05 bocpress/main/templates/assets/markdown-autoformat.js -rw-r--r-- 7.0 KiB
3f126040Jordan Robinson update wording for optional markdown editor auto formatting 2 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
// get textarea element
var bodyElem = document.querySelector('textarea[name="body"]');

// --- Paste handler: convert selection + pasted URL into Markdown link ---
function markdownAutoFormat(event) {
    const clipboardData = event.clipboardData || window.clipboardData;
    const pastedData = clipboardData.getData('text');

    const start = bodyElem.selectionStart;
    const end = bodyElem.selectionEnd;

    if (start !== end) {
        event.preventDefault();

        const selectedText = bodyElem.value.substring(start, end);
        const before = bodyElem.value.substring(0, start);
        const after = bodyElem.value.substring(end);

        const markdownLink = `[${selectedText}](${pastedData})`;

        bodyElem.value = before + markdownLink + after;

        const newCursorPosition = before.length + markdownLink.length;
        bodyElem.setSelectionRange(newCursorPosition, newCursorPosition);
    }
}

// --- Keyboard shortcuts for formatting ---
function handleKeyDown(event) {
    const start = bodyElem.selectionStart;
    const end = bodyElem.selectionEnd;
    const selectedText = bodyElem.value.substring(start, end);

    const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
    const ctrlKey = isMac ? event.metaKey : event.ctrlKey;

    // --- Inline code: Ctrl/Cmd+Shift+C (Mac-safe) ---
    if (ctrlKey && event.shiftKey && event.key.toLowerCase() === 'c') {
        event.preventDefault();
        wrapSelection('`', '`');
        return;
    }

    // --- Bold: Ctrl/Cmd+B ---
    if (ctrlKey && event.key.toLowerCase() === 'b') {
        event.preventDefault();
        wrapSelection('**', '**');
        return;
    }

    // --- Italics: Ctrl/Cmd+I ---
    if (ctrlKey && event.key.toLowerCase() === 'i') {
        event.preventDefault();
        wrapSelection('*', '*');
        return;
    }

    // --- Strikethrough: Ctrl+Shift+S ---
    if (ctrlKey && event.shiftKey && event.key.toLowerCase() === 's') {
        event.preventDefault();
        wrapSelection('~~', '~~');
        return;
    }

    // --- Headings: Ctrl/Cmd+1..6 ---
    if (ctrlKey && !event.shiftKey) {
        const headingLevel = parseInt(event.key);
        if (headingLevel >= 1 && headingLevel <= 6) {
            event.preventDefault();
            insertAtLineStart('#'.repeat(headingLevel) + ' ');
            return;
        }
    }

    // --- Insert link: Ctrl/Cmd+K ---
    if (ctrlKey && event.key.toLowerCase() === 'k') {
        event.preventDefault();
        const url = prompt('Enter URL:');
        if (url) wrapSelection('[', `](${url})`);
        return;
    }

    // --- Insert image: Ctrl/Cmd+Shift+L ---
    if (ctrlKey && event.shiftKey && event.key.toLowerCase() === 'l') {
        event.preventDefault();
        const url = prompt('Enter image URL:');
        if (url) wrapSelection('![', `](${url})`);
        return;
    }

    // --- Insert footnote: Ctrl/Cmd+Shift+F ---
    if (ctrlKey && event.shiftKey && event.key.toLowerCase() === 'f') {
        event.preventDefault();
        const footnoteId = prompt('Enter footnote label or number:');
        if (footnoteId) wrapSelection(`[^${footnoteId}]`, '');
        return;
    }

    // --- Insert table template: Ctrl/Cmd+Shift+T ---
    if (ctrlKey && event.shiftKey && event.key.toLowerCase() === 't') {
        event.preventDefault();
        const tableTemplate = '| Header 1 | Header 2 |\n| --- | --- |\n| Cell 1 | Cell 2 |\n';
        insertAtCursor(tableTemplate);
        return;
    }

    // --- Blockquote / list indentation ---
    if (event.key === 'Tab') {
        event.preventDefault();
        const lines = getSelectedLines();
        if (event.shiftKey) {
            // remove >
            replaceLines(lines, line => line.replace(/^>\s?/, ''));
            // remove list indentation
            replaceLines(lines, line => line.replace(/^ {1,2}/, ''));
        } else {
            // add >
            replaceLines(lines, line => '> ' + line);
            // add list indentation
            replaceLines(lines, line => '  ' + line);
        }
        return;
    }
}

// --- Helper to wrap selection with prefix/suffix ---
function wrapSelection(prefix, suffix) {
    const start = bodyElem.selectionStart;
    const end = bodyElem.selectionEnd;
    const selectedText = bodyElem.value.substring(start, end);

    const before = bodyElem.value.substring(0, start);
    const after = bodyElem.value.substring(end);

    bodyElem.value = before + prefix + selectedText + suffix + after;

    bodyElem.setSelectionRange(
        start + prefix.length,
        end + prefix.length
    );
}

// --- Insert text at cursor (no selection) ---
function insertAtCursor(text) {
    const start = bodyElem.selectionStart;
    const end = bodyElem.selectionEnd;

    const before = bodyElem.value.substring(0, start);
    const after = bodyElem.value.substring(end);

    bodyElem.value = before + text + after;

    const cursorPos = start + text.length;
    bodyElem.setSelectionRange(cursorPos, cursorPos);
}

// --- Insert at start of selected lines ---
function insertAtLineStart(prefix) {
    const lines = getSelectedLines();
    replaceLines(lines, line => prefix + line);
}

// --- Helper to get selected lines ---
function getSelectedLines() {
    const start = bodyElem.selectionStart;
    const end = bodyElem.selectionEnd;
    const value = bodyElem.value;

    const before = value.substring(0, start);
    const after = value.substring(end);

    const startLineIndex = before.lastIndexOf('\n') + 1;
    const endLineIndex = end + after.indexOf('\n');
    const selectedText = value.substring(startLineIndex, endLineIndex === -1 ? value.length : endLineIndex);

    return { startLineIndex, endLineIndex: endLineIndex === -1 ? value.length : endLineIndex, text: selectedText };
}

// --- Helper to replace each line in a selection ---
function replaceLines(linesObj, fn) {
    const { startLineIndex, endLineIndex, text } = linesObj;
    const before = bodyElem.value.substring(0, startLineIndex);
    const after = bodyElem.value.substring(endLineIndex);

    const newText = text.split('\n').map(fn).join('\n');
    bodyElem.value = before + newText + after;

    bodyElem.setSelectionRange(startLineIndex, startLineIndex + newText.length);
}

// --- Event listeners for Undo/Redo ---
bodyElem.addEventListener('paste', markdownAutoFormat);
bodyElem.addEventListener('keydown', handleKeyDown);

function wrapSelection(prefix, suffix) {
    const start = bodyElem.selectionStart;
    const end = bodyElem.selectionEnd;
    const selectedText = bodyElem.value.substring(start, end);

    const wrapped = prefix + selectedText + suffix;

    // Use execCommand to keep undo stack
    bodyElem.focus();
    bodyElem.setSelectionRange(start, end);
    if (document.queryCommandSupported('insertText')) {
        document.execCommand('insertText', false, wrapped);
    } else {
        // fallback
        const before = bodyElem.value.substring(0, start);
        const after = bodyElem.value.substring(end);
        bodyElem.value = before + wrapped + after;
        bodyElem.setSelectionRange(start + prefix.length, end + prefix.length);
    }
}