-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathmarkdown-it-notice.js
More file actions
131 lines (111 loc) · 4.68 KB
/
markdown-it-notice.js
File metadata and controls
131 lines (111 loc) · 4.68 KB
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
/**
* markdown-it plugin that transforms GFM-style blockquote alerts into
* <dt-notice> Vue components.
*
* Syntax:
* > [!WARNING] Optional title
* > Body text with **markdown** and [links](/path).
*
* Supported kinds: BASE, INFO, SUCCESS, WARNING, ERROR
* (uppercase by convention, case-insensitive — maps to DtNotice's `kind` prop)
*
* Always outputs: :show-close="false", class="d-wmx100p d-my-200"
*
* Two-pass design:
* 1. `notice_detect` runs BEFORE inline parsing — matches [!KIND] in raw
* text, strips the marker line, and stores kind/title on the token.
* 2. `notice_render` runs AFTER all other core rules (including
* markdownItClass) — renders the fully-processed body tokens and wraps
* them in a <dt-notice> html_block.
*/
import { encodeForAttr } from './fenced-demo-shared.js';
const ALERT_RE = /^\[!(base|error|info|success|warning)\][ \t]*(.*)/i;
export default function noticePlugin (md) {
// Pass 1: Before inline parsing — detect [!KIND] marker and strip it.
// At this stage inline tokens have `content` (raw text) but no `children`.
md.core.ruler.before('inline', 'notice_detect', function (state) {
const tokens = state.tokens;
for (let i = tokens.length - 1; i >= 0; i--) {
if (tokens[i].type !== 'blockquote_open') continue;
const closeIdx = findMatchingClose(tokens, i, 'blockquote_open', 'blockquote_close');
if (closeIdx === -1) continue;
// Find first inline token inside the blockquote
let firstInlineIdx = -1;
for (let j = i + 1; j < closeIdx; j++) {
if (tokens[j].type === 'inline') {
firstInlineIdx = j;
break;
}
}
if (firstInlineIdx === -1) continue;
const firstInline = tokens[firstInlineIdx];
const lines = firstInline.content.split('\n');
const match = ALERT_RE.exec(lines[0]);
if (!match) continue;
const kind = match[1].toLowerCase();
const title = match[2].trim();
// Mark the blockquote for pass 2
tokens[i].meta = tokens[i].meta || {};
tokens[i].meta.notice = { kind, title: title || null };
// Remove the [!KIND] line from inline content
lines.shift();
if (lines.length > 0 && lines.some(l => l.trim() !== '')) {
// Remaining lines become the new content for inline parsing
firstInline.content = lines.join('\n');
} else {
// First paragraph is now empty — remove paragraph_open + inline + paragraph_close
const pOpenIdx = firstInlineIdx - 1;
if (pOpenIdx > i && tokens[pOpenIdx].type === 'paragraph_open') {
tokens.splice(pOpenIdx, 3);
}
}
}
});
// Pass 2: After all core rules (including markdownItClass) — render marked
// blockquotes as <dt-notice> HTML blocks.
md.core.ruler.push('notice_render', function (state) {
const tokens = state.tokens;
for (let i = tokens.length - 1; i >= 0; i--) {
if (tokens[i].type !== 'blockquote_open') continue;
if (!tokens[i].meta?.notice) continue;
const { kind, title } = tokens[i].meta.notice;
const closeIdx = findMatchingClose(tokens, i, 'blockquote_open', 'blockquote_close');
if (closeIdx === -1) continue;
// Extract body tokens (everything between blockquote_open and blockquote_close)
const bodyTokens = tokens.slice(i + 1, closeIdx);
let bodyHtml = bodyTokens.length > 0
? md.renderer.render(bodyTokens, md.options, state.env)
: '';
// Strip <p> wrappers — DtNotice wraps slot content in its own <p>.
// Keeping them creates invalid nested <p><p>...</p></p>.
// Adjacent paragraphs get <br> separators; remaining <p> tags are
// removed so block elements like <ul> don't cause orphaned </p>.
bodyHtml = bodyHtml
.replace(/<\/p>\s*<p[^>]*>/g, '<br>')
.replace(/<p[^>]*>/g, '')
.replace(/<\/p>/g, '');
// Build <dt-notice> tag
const attrs = [`kind="${kind}"`];
if (title) {
attrs.push(`title="${encodeForAttr(title)}"`);
}
attrs.push(':show-close="false"', 'class="d-wmx100p d-my-200 dialtone-doc-notice"');
const html = `<dt-notice ${attrs.join(' ')}>\n${bodyHtml}</dt-notice>\n`;
const newToken = new state.Token('html_block', '', 0);
newToken.content = html;
tokens.splice(i, closeIdx - i + 1, newToken);
}
});
}
/**
* Find the matching close token for an open token, accounting for nesting.
*/
function findMatchingClose (tokens, openIdx, openType, closeType) {
let depth = 1;
for (let j = openIdx + 1; j < tokens.length; j++) {
if (tokens[j].type === openType) depth++;
if (tokens[j].type === closeType) depth--;
if (depth === 0) return j;
}
return -1;
}