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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdv-live",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Markdown Viewer - File tree + Live preview + Marp support + Hot reload",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -88,7 +88,7 @@ md.enable('table');
88
88
  md.enable('strikethrough');
89
89
 
90
90
  // Enable task lists (checkboxes)
91
- md.use(taskLists, { enabled: true, label: true, labelAfter: true });
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 `<!--MERMAID_PLACEHOLDER_${blocks.length - 1}-->`;
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
- // Replace both paragraph-wrapped and bare placeholders
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
- .replace(`<p><!--MERMAID_PLACEHOLDER_${i}--></p>`, mermaidHtml)
168
- .replace(`<!--MERMAID_PLACEHOLDER_${i}-->`, mermaidHtml);
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 };
@@ -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 };