mdv-live 0.5.4 → 0.5.5
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/CHANGELOG.md +25 -0
- package/package.json +1 -1
- package/src/rendering/markdown.js +20 -10
- package/src/rendering/slides.js +0 -152
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.5] - 2026-04-05
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- タスクリストのインライン要素(太字・リンク・コード)が二重表示されるバグを修正
|
|
13
|
+
- markdown-it-task-lists の labelAfter オプション誤用が原因
|
|
14
|
+
- Mermaidプレースホルダがユーザーコンテンツと衝突する問題を修正(nonce付与)
|
|
15
|
+
- 空frontmatter(`---\n\n---`)で空のyamlコードブロックが生成される問題を修正
|
|
16
|
+
|
|
17
|
+
### Removed
|
|
18
|
+
|
|
19
|
+
- 未使用の `src/rendering/slides.js` を削除(marp.jsに統合済み)
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- WebSocketテスト7件(接続追跡・watch・broadcast・通知・cleanup・不正入力耐性)
|
|
24
|
+
- レンダリングテスト10件(strikethrough・CJK emphasis・linkify・breaks・mermaid edge cases)
|
|
25
|
+
- テスト総数: 92 → 109
|
|
26
|
+
|
|
27
|
+
## [0.5.4] - 2026-04-04
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- 4件の依存関係脆弱性を修正
|
|
32
|
+
|
|
8
33
|
## [0.5.3] - 2026-03-29
|
|
9
34
|
|
|
10
35
|
### Fixed
|
package/package.json
CHANGED
|
@@ -88,7 +88,7 @@ md.enable('table');
|
|
|
88
88
|
md.enable('strikethrough');
|
|
89
89
|
|
|
90
90
|
// Enable task lists (checkboxes)
|
|
91
|
-
md.use(taskLists
|
|
91
|
+
md.use(taskLists);
|
|
92
92
|
|
|
93
93
|
// Pattern to detect Marp frontmatter (must be at very start of file, not using 'm' flag)
|
|
94
94
|
const MARP_PATTERN = /^---\s*\n[\s\S]*?marp:\s*true[\s\S]*?\n---/;
|
|
@@ -118,6 +118,10 @@ function convertFrontmatter(content) {
|
|
|
118
118
|
const match = content.match(FRONTMATTER_PATTERN);
|
|
119
119
|
if (match) {
|
|
120
120
|
const frontmatter = match[1];
|
|
121
|
+
// Skip empty frontmatter (treat as horizontal rules instead)
|
|
122
|
+
if (!frontmatter.trim()) {
|
|
123
|
+
return content;
|
|
124
|
+
}
|
|
121
125
|
const rest = content.slice(match[0].length);
|
|
122
126
|
return `\`\`\`yaml\n${frontmatter}\n\`\`\`\n${rest}`;
|
|
123
127
|
}
|
|
@@ -125,18 +129,22 @@ function convertFrontmatter(content) {
|
|
|
125
129
|
return content;
|
|
126
130
|
}
|
|
127
131
|
|
|
132
|
+
// Generate a per-render nonce to prevent placeholder collision with user content
|
|
133
|
+
const MERMAID_NONCE = Math.random().toString(36).slice(2, 10);
|
|
134
|
+
|
|
128
135
|
/**
|
|
129
136
|
* Protect Mermaid blocks from markdown processing
|
|
130
137
|
* @param {string} content - Markdown content
|
|
131
|
-
* @returns {{ content: string, blocks: string[] }}
|
|
138
|
+
* @returns {{ content: string, blocks: string[], nonce: string }}
|
|
132
139
|
*/
|
|
133
140
|
function protectMermaidBlocks(content) {
|
|
134
141
|
const blocks = [];
|
|
142
|
+
const nonce = MERMAID_NONCE + '_' + Date.now().toString(36);
|
|
135
143
|
const protectedContent = content.replace(MERMAID_PATTERN, (match, code) => {
|
|
136
144
|
blocks.push(code);
|
|
137
|
-
return `<!--
|
|
145
|
+
return `<!--MDV_MERMAID_${nonce}_${blocks.length - 1}-->`;
|
|
138
146
|
});
|
|
139
|
-
return { content: protectedContent, blocks };
|
|
147
|
+
return { content: protectedContent, blocks, nonce };
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
/**
|
|
@@ -155,17 +163,19 @@ function escapeHtmlEntities(text) {
|
|
|
155
163
|
* Restore Mermaid blocks after markdown processing
|
|
156
164
|
* @param {string} html - Rendered HTML
|
|
157
165
|
* @param {string[]} blocks - Mermaid code blocks
|
|
166
|
+
* @param {string} nonce - Nonce used during protection
|
|
158
167
|
* @returns {string}
|
|
159
168
|
*/
|
|
160
|
-
function restoreMermaidBlocks(html, blocks) {
|
|
169
|
+
function restoreMermaidBlocks(html, blocks, nonce) {
|
|
161
170
|
let result = html;
|
|
162
171
|
for (let i = 0; i < blocks.length; i++) {
|
|
163
172
|
const escaped = escapeHtmlEntities(blocks[i]);
|
|
164
173
|
const mermaidHtml = `<pre><code class="language-mermaid">${escaped}</code></pre>`;
|
|
165
|
-
|
|
174
|
+
const placeholder = `<!--MDV_MERMAID_${nonce}_${i}-->`;
|
|
175
|
+
// Replace both paragraph-wrapped and bare placeholders (use split+join for global replace)
|
|
166
176
|
result = result
|
|
167
|
-
.
|
|
168
|
-
.
|
|
177
|
+
.split(`<p>${placeholder}</p>`).join(mermaidHtml)
|
|
178
|
+
.split(placeholder).join(mermaidHtml);
|
|
169
179
|
}
|
|
170
180
|
return result;
|
|
171
181
|
}
|
|
@@ -177,9 +187,9 @@ function restoreMermaidBlocks(html, blocks) {
|
|
|
177
187
|
*/
|
|
178
188
|
export function renderMarkdown(content) {
|
|
179
189
|
const withFrontmatter = convertFrontmatter(content);
|
|
180
|
-
const { content: protectedContent, blocks } = protectMermaidBlocks(withFrontmatter);
|
|
190
|
+
const { content: protectedContent, blocks, nonce } = protectMermaidBlocks(withFrontmatter);
|
|
181
191
|
const html = md.render(protectedContent);
|
|
182
|
-
return restoreMermaidBlocks(html, blocks);
|
|
192
|
+
return restoreMermaidBlocks(html, blocks, nonce);
|
|
183
193
|
}
|
|
184
194
|
|
|
185
195
|
export default { renderMarkdown, isMarp };
|
package/src/rendering/slides.js
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple slide renderer for Marp-compatible markdown
|
|
3
|
-
*
|
|
4
|
-
* Features:
|
|
5
|
-
* - Split by --- (horizontal rule)
|
|
6
|
-
* - Full HTML support (no escaping)
|
|
7
|
-
* - Tailwind CSS compatible
|
|
8
|
-
* - Frontmatter extraction
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import MarkdownIt from 'markdown-it';
|
|
12
|
-
|
|
13
|
-
// Initialize markdown-it with full HTML support
|
|
14
|
-
const md = new MarkdownIt({
|
|
15
|
-
html: true,
|
|
16
|
-
breaks: false,
|
|
17
|
-
linkify: true,
|
|
18
|
-
typographer: true
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Parse frontmatter from markdown content
|
|
23
|
-
* @param {string} content - Raw markdown
|
|
24
|
-
* @returns {{ frontmatter: object, body: string }}
|
|
25
|
-
*/
|
|
26
|
-
function parseFrontmatter(content) {
|
|
27
|
-
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
|
28
|
-
if (!match) {
|
|
29
|
-
return { frontmatter: {}, body: content };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const frontmatter = {};
|
|
33
|
-
const yaml = match[1];
|
|
34
|
-
|
|
35
|
-
// Simple YAML parsing (key: value)
|
|
36
|
-
yaml.split('\n').forEach(line => {
|
|
37
|
-
const [key, ...rest] = line.split(':');
|
|
38
|
-
if (key && rest.length) {
|
|
39
|
-
frontmatter[key.trim()] = rest.join(':').trim();
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
frontmatter,
|
|
45
|
-
body: content.slice(match[0].length)
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Check if content is a slide presentation (has marp: true or uses ---)
|
|
51
|
-
* @param {string} content - Markdown content
|
|
52
|
-
* @returns {boolean}
|
|
53
|
-
*/
|
|
54
|
-
export function isSlidePresentation(content) {
|
|
55
|
-
// Check for marp: true in frontmatter
|
|
56
|
-
if (/^---\s*\n[\s\S]*?marp:\s*true[\s\S]*?\n---/.test(content)) {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Render slide content (handles both HTML blocks and markdown)
|
|
64
|
-
* @param {string} slideContent - Single slide content
|
|
65
|
-
* @returns {string} Rendered HTML
|
|
66
|
-
*/
|
|
67
|
-
function renderSlideContent(slideContent) {
|
|
68
|
-
const trimmed = slideContent.trim();
|
|
69
|
-
|
|
70
|
-
// If it starts with HTML tag, render as-is (minimal processing)
|
|
71
|
-
if (trimmed.startsWith('<')) {
|
|
72
|
-
// Process any markdown within the HTML
|
|
73
|
-
// But preserve the HTML structure
|
|
74
|
-
return trimmed;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Otherwise, render as markdown
|
|
78
|
-
return md.render(trimmed);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Extract scripts and styles from the first slide (typically config)
|
|
83
|
-
* @param {string} content - First slide content after frontmatter
|
|
84
|
-
* @returns {{ scripts: string, styles: string, content: string }}
|
|
85
|
-
*/
|
|
86
|
-
function extractConfigFromFirstSlide(content) {
|
|
87
|
-
let scripts = '';
|
|
88
|
-
let styles = '';
|
|
89
|
-
let remaining = content;
|
|
90
|
-
|
|
91
|
-
// Extract <script> tags
|
|
92
|
-
const scriptMatches = content.match(/<script[\s\S]*?<\/script>/gi) || [];
|
|
93
|
-
scriptMatches.forEach(script => {
|
|
94
|
-
scripts += script + '\n';
|
|
95
|
-
remaining = remaining.replace(script, '');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Extract <style> tags
|
|
99
|
-
const styleMatches = content.match(/<style[\s\S]*?<\/style>/gi) || [];
|
|
100
|
-
styleMatches.forEach(style => {
|
|
101
|
-
styles += style + '\n';
|
|
102
|
-
remaining = remaining.replace(style, '');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
return { scripts, styles, content: remaining.trim() };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Render markdown slides to HTML
|
|
110
|
-
* @param {string} content - Markdown content with slide separators
|
|
111
|
-
* @returns {{ html: string, slideCount: number, scripts: string, styles: string }}
|
|
112
|
-
*/
|
|
113
|
-
export function renderSlides(content) {
|
|
114
|
-
const { frontmatter, body } = parseFrontmatter(content);
|
|
115
|
-
|
|
116
|
-
// Split by --- (must be on its own line)
|
|
117
|
-
const rawSlides = body.split(/\n---\s*\n/);
|
|
118
|
-
|
|
119
|
-
// Extract scripts/styles from first "slide" (config area)
|
|
120
|
-
const firstSlide = rawSlides[0] || '';
|
|
121
|
-
const { scripts, styles, content: firstContent } = extractConfigFromFirstSlide(firstSlide);
|
|
122
|
-
|
|
123
|
-
// Build slides array (first slide might be empty after extracting config)
|
|
124
|
-
const slidesContent = [firstContent, ...rawSlides.slice(1)].filter(s => s.trim());
|
|
125
|
-
|
|
126
|
-
// Render each slide
|
|
127
|
-
const slides = slidesContent.map((slideContent, index) => {
|
|
128
|
-
const rendered = renderSlideContent(slideContent);
|
|
129
|
-
return `
|
|
130
|
-
<section class="slide" data-slide-index="${index}">
|
|
131
|
-
${rendered}
|
|
132
|
-
</section>
|
|
133
|
-
`;
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// Wrap in container
|
|
137
|
-
const html = `
|
|
138
|
-
<div class="slides-container" data-slide-count="${slides.length}">
|
|
139
|
-
${slides.join('\n')}
|
|
140
|
-
</div>
|
|
141
|
-
`;
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
html,
|
|
145
|
-
slideCount: slides.length,
|
|
146
|
-
scripts,
|
|
147
|
-
styles,
|
|
148
|
-
frontmatter
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export default { renderSlides, isSlidePresentation };
|