autumnnote 1.8.3 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/autumnnote.es.js +196 -27
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +204 -27
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +1 -2
- package/src/js/core/markdown.js +171 -36
- package/src/js/index.js +1 -1
- package/src/js/module/Clipboard.js +25 -0
- package/types/index.d.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autumnnote",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "WYSIWYG rich-text editor built with vanilla JavaScript — zero dependencies, no jQuery. Dark mode, @mention, markdown shortcuts, bubble toolbar. React and Vue 3 wrappers included.",
|
|
5
5
|
"main": "dist/autumnnote.umd.js",
|
|
6
6
|
"module": "dist/autumnnote.es.js",
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
},
|
|
15
15
|
"./dist/autumnnote.css": "./dist/autumnnote.css"
|
|
16
16
|
},
|
|
17
|
-
"sideEffects": ["./dist/autumnnote.css", "**/*.css"],
|
|
18
17
|
"files": [
|
|
19
18
|
"dist",
|
|
20
19
|
"src",
|
package/src/js/core/markdown.js
CHANGED
|
@@ -139,7 +139,11 @@ function _domToMd(node, depth = 0) {
|
|
|
139
139
|
* @returns {boolean} `true` if any Markdown-like pattern is present, `false` otherwise.
|
|
140
140
|
*/
|
|
141
141
|
export function isMarkdown(text) {
|
|
142
|
-
return /^#{1,6} [^\s]|^[ \t]*[-*+] [^\s]|^[ \t]*\d+\. [^\s]|^> [^\s]|^```|^\*{2}[^*\n]+\*{2}/m.test(text)
|
|
142
|
+
return /^#{1,6} [^\s]|^[ \t]*[-*+] [^\s]|^[ \t]*\d+\. [^\s]|^> [^\s]|^```|^\*{2}[^*\n]+\*{2}/m.test(text)
|
|
143
|
+
|| /^.+\n=+\s*$/m.test(text)
|
|
144
|
+
|| /^.+\n-{2,}\s*$/m.test(text)
|
|
145
|
+
|| /^---\s*\n(?:[\s\S]*?\n)?(?:---|\.\.\.)\s*(?:\n|$)/.test(text)
|
|
146
|
+
|| /^\|.+\|[ \t]*\n\|[ \t:|-]+\|/m.test(text);
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
/**
|
|
@@ -148,7 +152,12 @@ export function isMarkdown(text) {
|
|
|
148
152
|
* @returns {string}
|
|
149
153
|
*/
|
|
150
154
|
export function markdownToHTML(text) {
|
|
151
|
-
|
|
155
|
+
let lines = text.replaceAll('\r\n', '\n').replaceAll('\r', '\n').split('\n');
|
|
156
|
+
lines = _stripFrontmatter(lines);
|
|
157
|
+
const refs = _extractReferenceDefinitions(lines);
|
|
158
|
+
lines = refs.clean;
|
|
159
|
+
_linkDefs = refs.linkDefs;
|
|
160
|
+
_footnoteIds = refs.footnoteIds;
|
|
152
161
|
const out = [];
|
|
153
162
|
let i = 0;
|
|
154
163
|
|
|
@@ -171,6 +180,20 @@ export function markdownToHTML(text) {
|
|
|
171
180
|
continue;
|
|
172
181
|
}
|
|
173
182
|
|
|
183
|
+
// ---- Setext headings (Title\n=== or Title\n---) -------------------------
|
|
184
|
+
if (line.trim() && !/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line) && !/^#{1,6} /.test(line) && i + 1 < lines.length) {
|
|
185
|
+
if (/^=+\s*$/.test(lines[i + 1])) {
|
|
186
|
+
out.push(`<h1>${_inline(line.trim())}</h1>`);
|
|
187
|
+
i += 2;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (/^-{2,}\s*$/.test(lines[i + 1])) {
|
|
191
|
+
out.push(`<h2>${_inline(line.trim())}</h2>`);
|
|
192
|
+
i += 2;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
174
197
|
// ---- Horizontal rule --- / *** / _________________________________________
|
|
175
198
|
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
|
|
176
199
|
out.push('<hr>');
|
|
@@ -200,41 +223,14 @@ export function markdownToHTML(text) {
|
|
|
200
223
|
|
|
201
224
|
// ---- Checklist or Unordered list - / * / + item ----------------------
|
|
202
225
|
if (/^[-*+] /.test(line)) {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
const listTag = isChecklist ? 'ul class="an-checklist"' : 'ul';
|
|
206
|
-
while (i < lines.length && /^[-*+] /.test(lines[i])) {
|
|
207
|
-
const nextLineIsChecklist = /^[-*+]\s+\[[ xX]\]\s+/.test(lines[i]);
|
|
208
|
-
if (nextLineIsChecklist !== isChecklist) {
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
const itemLine = lines[i];
|
|
212
|
-
const content = itemLine.slice(2);
|
|
213
|
-
if (isChecklist) {
|
|
214
|
-
const cbMatch = /^\[([ xX])\][ \t]+/.exec(content);
|
|
215
|
-
const checked = cbMatch?.[1]?.toLowerCase() === 'x';
|
|
216
|
-
const checkedAttr = checked ? ' checked' : '';
|
|
217
|
-
const cbHtml = `<input type="checkbox" contenteditable="false"${checkedAttr}>`;
|
|
218
|
-
const textContent = cbMatch ? content.slice(cbMatch[0].length) : content;
|
|
219
|
-
items.push(`<li>${cbHtml}${_inline(textContent)}</li>`);
|
|
220
|
-
} else {
|
|
221
|
-
items.push(`<li>${_inline(content)}</li>`);
|
|
222
|
-
}
|
|
223
|
-
i++;
|
|
224
|
-
}
|
|
225
|
-
out.push(`<${listTag}>${items.join('')}</${listTag.split(' ')[0]}>`);
|
|
226
|
-
continue;
|
|
226
|
+
const { html: listHtml, endIdx } = _parseListBlock(lines, i);
|
|
227
|
+
out.push(listHtml); i = endIdx; continue;
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
// ---- Ordered list 1. item ----------------------------------------------
|
|
230
231
|
if (/^\d+\. /.test(line)) {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
items.push(`<li>${_inline(lines[i].replace(/^\d+\. /, ''))}</li>`);
|
|
234
|
-
i++;
|
|
235
|
-
}
|
|
236
|
-
out.push(`<ol>${items.join('')}</ol>`);
|
|
237
|
-
continue;
|
|
232
|
+
const { html: listHtml, endIdx } = _parseListBlock(lines, i);
|
|
233
|
+
out.push(listHtml); i = endIdx; continue;
|
|
238
234
|
}
|
|
239
235
|
|
|
240
236
|
// ---- Blank line ----------------------------------------------------------
|
|
@@ -248,15 +244,25 @@ export function markdownToHTML(text) {
|
|
|
248
244
|
// a separator row (| --- | --- |). We detect and collect all rows.
|
|
249
245
|
if (/^\|.+\|/.test(line) && i + 1 < lines.length && /^\|[\s|:-]+\|/.test(lines[i + 1])) {
|
|
250
246
|
const headerCells = _parseTableRow(line);
|
|
247
|
+
const alignments = _parseTableRow(lines[i + 1]).map((c) => {
|
|
248
|
+
if (c.startsWith(':') && c.endsWith(':')) return 'center';
|
|
249
|
+
if (c.endsWith(':')) return 'right';
|
|
250
|
+
if (c.startsWith(':')) return 'left';
|
|
251
|
+
return null;
|
|
252
|
+
});
|
|
251
253
|
i += 2; // skip header + separator
|
|
252
254
|
const bodyRows = [];
|
|
253
255
|
while (i < lines.length && /^\|.+\|/.test(lines[i])) {
|
|
254
256
|
bodyRows.push(_parseTableRow(lines[i]));
|
|
255
257
|
i++;
|
|
256
258
|
}
|
|
257
|
-
const
|
|
259
|
+
const _cell = (tag, content, align) => {
|
|
260
|
+
const s = align ? ` style="text-align:${align}"` : '';
|
|
261
|
+
return `<${tag}${s}>${_inline(content)}</${tag}>`;
|
|
262
|
+
};
|
|
263
|
+
const thCells = headerCells.map((c, idx) => _cell('th', c, alignments[idx])).join('');
|
|
258
264
|
const thead = `<thead><tr>${thCells}</tr></thead>`;
|
|
259
|
-
const renderRow = (row) => `<tr>${row.map((c) =>
|
|
265
|
+
const renderRow = (row) => `<tr>${row.map((c, idx) => _cell('td', c, alignments[idx])).join('')}</tr>`;
|
|
260
266
|
const tbody = bodyRows.length ? `<tbody>${bodyRows.map(renderRow).join('')}</tbody>` : '';
|
|
261
267
|
out.push(`<table>${thead}${tbody}</table>`);
|
|
262
268
|
continue;
|
|
@@ -268,7 +274,9 @@ export function markdownToHTML(text) {
|
|
|
268
274
|
i < lines.length &&
|
|
269
275
|
lines[i].trim() !== '' &&
|
|
270
276
|
!/^(#{1,6} |> |[-*+] |\d+\. |```|---\s*$|\*{3}\s*$|_{3}\s*$)/.test(lines[i]) &&
|
|
271
|
-
!/^\|.+\|/.test(lines[i])
|
|
277
|
+
!/^\|.+\|/.test(lines[i]) &&
|
|
278
|
+
!(i + 1 < lines.length && /^=+\s*$/.test(lines[i + 1])) &&
|
|
279
|
+
!(i + 1 < lines.length && /^-{2,}\s*$/.test(lines[i + 1]))
|
|
272
280
|
) {
|
|
273
281
|
paraLines.push(lines[i]);
|
|
274
282
|
i++;
|
|
@@ -285,6 +293,68 @@ export function markdownToHTML(text) {
|
|
|
285
293
|
// Inline formatting
|
|
286
294
|
// ---------------------------------------------------------------------------
|
|
287
295
|
|
|
296
|
+
/** Reference-link and footnote definitions collected per markdownToHTML() call. */
|
|
297
|
+
let _linkDefs = new Map();
|
|
298
|
+
let _footnoteIds = new Set();
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Strips a leading YAML frontmatter block (--- ... --- or --- ... ...) from
|
|
302
|
+
* the line array, only when it is the very first line and the enclosed body
|
|
303
|
+
* looks like YAML (key: value / list items / indented continuations) — this
|
|
304
|
+
* disambiguates real frontmatter from a horizontal rule followed by prose.
|
|
305
|
+
* @param {string[]} lines
|
|
306
|
+
* @returns {string[]}
|
|
307
|
+
*/
|
|
308
|
+
function _stripFrontmatter(lines) {
|
|
309
|
+
if ((lines[0] || '').trim() !== '---') return lines;
|
|
310
|
+
let closeIdx = -1;
|
|
311
|
+
for (let j = 1; j < lines.length; j++) {
|
|
312
|
+
const t = lines[j].trim();
|
|
313
|
+
if (t === '---' || t === '...') { closeIdx = j; break; }
|
|
314
|
+
}
|
|
315
|
+
if (closeIdx === -1) return lines;
|
|
316
|
+
|
|
317
|
+
const body = lines.slice(1, closeIdx);
|
|
318
|
+
const looksLikeYAML = body.every((l) =>
|
|
319
|
+
l.trim() === '' ||
|
|
320
|
+
/^[ \t]*[\w$.-]+\s*:(\s|$)/.test(l) ||
|
|
321
|
+
/^[ \t]*-\s+\S/.test(l) ||
|
|
322
|
+
/^[ \t]+\S/.test(l));
|
|
323
|
+
if (!looksLikeYAML) return lines;
|
|
324
|
+
|
|
325
|
+
let start = closeIdx + 1;
|
|
326
|
+
if (lines[start] !== undefined && lines[start].trim() === '') start++;
|
|
327
|
+
return lines.slice(start);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Extracts GFM reference-link definitions (`[ref]: url "title"`) and footnote
|
|
332
|
+
* definitions (`[^id]: text`) from the line array, skipping fenced code
|
|
333
|
+
* regions. Returns the definition-free line array plus lookup maps.
|
|
334
|
+
* @param {string[]} lines
|
|
335
|
+
* @returns {{ clean: string[], linkDefs: Map<string, {href: string, title?: string}>, footnoteIds: Set<string> }}
|
|
336
|
+
*/
|
|
337
|
+
function _extractReferenceDefinitions(lines) {
|
|
338
|
+
const linkDefs = new Map();
|
|
339
|
+
const footnoteIds = new Set();
|
|
340
|
+
const clean = [];
|
|
341
|
+
let inFence = false;
|
|
342
|
+
const linkDefRe = /^\[([^\]]+)\]:\s*(\S+)(?:\s+"([^"]*)")?\s*$/;
|
|
343
|
+
const footnoteDefRe = /^\[\^([^\]]+)\]:\s*(.+)$/;
|
|
344
|
+
|
|
345
|
+
for (const line of lines) {
|
|
346
|
+
if (/^```/.test(line)) { inFence = !inFence; clean.push(line); continue; }
|
|
347
|
+
if (!inFence) {
|
|
348
|
+
const fm = footnoteDefRe.exec(line);
|
|
349
|
+
if (fm) { footnoteIds.add(fm[1]); continue; }
|
|
350
|
+
const lm = linkDefRe.exec(line);
|
|
351
|
+
if (lm) { linkDefs.set(lm[1].trim().toLowerCase(), { href: lm[2], title: lm[3] }); continue; }
|
|
352
|
+
}
|
|
353
|
+
clean.push(line);
|
|
354
|
+
}
|
|
355
|
+
return { clean, linkDefs, footnoteIds };
|
|
356
|
+
}
|
|
357
|
+
|
|
288
358
|
/**
|
|
289
359
|
* Splits a GFM table row string into trimmed cell strings.
|
|
290
360
|
* '| a | b | c |' → ['a', 'b', 'c']
|
|
@@ -299,6 +369,55 @@ function _parseTableRow(row) {
|
|
|
299
369
|
.map((c) => c.trim());
|
|
300
370
|
}
|
|
301
371
|
|
|
372
|
+
function _parseListBlock(lines, startIdx) {
|
|
373
|
+
const baseIndent = (lines[startIdx].match(/^(\s*)/)[1]).length;
|
|
374
|
+
const isOL = /^\s*\d+\. /.test(lines[startIdx]);
|
|
375
|
+
const items = [];
|
|
376
|
+
let firstIsCB = null;
|
|
377
|
+
let i = startIdx;
|
|
378
|
+
|
|
379
|
+
while (i < lines.length) {
|
|
380
|
+
const line = lines[i];
|
|
381
|
+
if (line.trim() === '') break;
|
|
382
|
+
const indent = (line.match(/^(\s*)/)[1]).length;
|
|
383
|
+
if (indent < baseIndent) break;
|
|
384
|
+
|
|
385
|
+
if (indent === baseIndent) {
|
|
386
|
+
if (!/^\s*(?:[-*+]|\d+\.) /.test(line)) break;
|
|
387
|
+
if (/^\s*\d+\. /.test(line) !== isOL) break;
|
|
388
|
+
const raw = isOL ? line.replace(/^\s*\d+\. /, '') : line.replace(/^\s*[-*+] /, '');
|
|
389
|
+
const isCB = !isOL && /^\[[ xX]\]\s+/.test(raw);
|
|
390
|
+
if (firstIsCB === null) firstIsCB = isCB;
|
|
391
|
+
if (isCB !== firstIsCB) break;
|
|
392
|
+
const checked = isCB && raw[1].toLowerCase() === 'x';
|
|
393
|
+
const text = isCB ? raw.replace(/^\[[ xX]\]\s+/, '') : raw;
|
|
394
|
+
items.push({ text, isCB, checked, sub: '' });
|
|
395
|
+
i++;
|
|
396
|
+
} else {
|
|
397
|
+
if (!items.length) { i++; continue; }
|
|
398
|
+
if (/^\s*(?:[-*+]|\d+\.) /.test(line)) {
|
|
399
|
+
const nested = _parseListBlock(lines, i);
|
|
400
|
+
items[items.length - 1].sub += nested.html;
|
|
401
|
+
i = nested.endIdx;
|
|
402
|
+
} else {
|
|
403
|
+
items[items.length - 1].text += ' ' + line.trim();
|
|
404
|
+
i++;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const hasCB = !isOL && (firstIsCB === true);
|
|
410
|
+
const open = isOL ? '<ol>' : hasCB ? '<ul class="an-checklist">' : '<ul>';
|
|
411
|
+
const close = isOL ? '</ol>' : '</ul>';
|
|
412
|
+
const liHTML = items.map(({ text, isCB, checked, sub }) => {
|
|
413
|
+
const cbHTML = isCB
|
|
414
|
+
? `<input type="checkbox" contenteditable="false"${checked ? ' checked' : ''}>`
|
|
415
|
+
: '';
|
|
416
|
+
return `<li>${cbHTML}${_inline(text)}${sub}</li>`;
|
|
417
|
+
}).join('');
|
|
418
|
+
return { html: `${open}${liHTML}${close}`, endIdx: i };
|
|
419
|
+
}
|
|
420
|
+
|
|
302
421
|
function _inline(text) {
|
|
303
422
|
// Images before links (they share [] syntax)
|
|
304
423
|
text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) =>
|
|
@@ -306,6 +425,22 @@ function _inline(text) {
|
|
|
306
425
|
// Links
|
|
307
426
|
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) =>
|
|
308
427
|
`<a href="${_escAttr(href)}">${_esc(label)}</a>`);
|
|
428
|
+
// Reference-style links [text][ref] and shortcut [text][]
|
|
429
|
+
text = text.replace(/\[([^\]]+)\]\[([^\]]*)\]/g, (m, label, ref) => {
|
|
430
|
+
const def = _linkDefs.get((ref || label).trim().toLowerCase());
|
|
431
|
+
if (!def) return m;
|
|
432
|
+
const titleAttr = def.title ? ` title="${_escAttr(def.title)}"` : '';
|
|
433
|
+
return `<a href="${_escAttr(def.href)}"${titleAttr}>${_esc(label)}</a>`;
|
|
434
|
+
});
|
|
435
|
+
// Bare/implicit reference link [text] — only when a definition exists
|
|
436
|
+
text = text.replace(/\[([^\]]+)\]/g, (m, label) => {
|
|
437
|
+
const def = _linkDefs.get(label.trim().toLowerCase());
|
|
438
|
+
if (!def) return m;
|
|
439
|
+
const titleAttr = def.title ? ` title="${_escAttr(def.title)}"` : '';
|
|
440
|
+
return `<a href="${_escAttr(def.href)}"${titleAttr}>${_esc(label)}</a>`;
|
|
441
|
+
});
|
|
442
|
+
// Footnote reference marker [^id] — run last
|
|
443
|
+
text = text.replace(/\[\^([^\]]+)\]/g, (m, id) => (_footnoteIds.has(id) ? `<sup>[${_esc(id)}]</sup>` : m));
|
|
309
444
|
// Bold + italic ***text***
|
|
310
445
|
text = text.replace(/\*{3}([^*\n]+?)\*{3}/g, (_, c) => `<strong><em>${_esc(c)}</em></strong>`);
|
|
311
446
|
text = text.replace(/_{3}([^_\n]+?)_{3}/g, (_, c) => `<strong><em>${_esc(c)}</em></strong>`);
|
package/src/js/index.js
CHANGED
|
@@ -170,6 +170,30 @@ export class Clipboard {
|
|
|
170
170
|
return doc.body.innerHTML;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Normalizes task lists from external sources (GitHub, GitLab, etc.) so they
|
|
175
|
+
* pass the sanitiser's `ul.an-checklist` guard. Runs before sanitiseHTML().
|
|
176
|
+
* @param {string} html
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
_normalizeExternalTaskLists(html) {
|
|
180
|
+
const doc = new DOMParser().parseFromString(`<body>${html}</body>`, 'text/html');
|
|
181
|
+
for (const cb of doc.querySelectorAll('input[type="checkbox"]')) {
|
|
182
|
+
const li = cb.closest('li');
|
|
183
|
+
const ul = li?.closest('ul');
|
|
184
|
+
if (!li || !ul || ul.classList.contains('an-checklist')) continue;
|
|
185
|
+
ul.classList.add('an-checklist');
|
|
186
|
+
cb.removeAttribute('disabled');
|
|
187
|
+
cb.setAttribute('contenteditable', 'false');
|
|
188
|
+
for (const attr of Array.from(cb.attributes)) {
|
|
189
|
+
if (!['type', 'checked', 'contenteditable'].includes(attr.name)) {
|
|
190
|
+
cb.removeAttribute(attr.name);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return doc.body.innerHTML;
|
|
195
|
+
}
|
|
196
|
+
|
|
173
197
|
/**
|
|
174
198
|
* Forces the next paste operation to strip all HTML formatting.
|
|
175
199
|
* Called by Editor when Ctrl+Shift+V is pressed.
|
|
@@ -255,6 +279,7 @@ export class Clipboard {
|
|
|
255
279
|
let html = raw;
|
|
256
280
|
if (isWordContent) html = this._cleanWordHtml(html);
|
|
257
281
|
else if (isSocialContent) html = this._cleanSocialHtml(html);
|
|
282
|
+
html = this._normalizeExternalTaskLists(html);
|
|
258
283
|
html = sanitiseHTML(html);
|
|
259
284
|
if (this.options.pasteStripAttributes) html = this._stripAttributes(html);
|
|
260
285
|
execCommand('insertHTML', html);
|