md-preview-cli-plus 1.0.6 → 1.1.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/README.md CHANGED
@@ -8,7 +8,7 @@ Markdown 实时预览 + 导出工具
8
8
 
9
9
  CLI + Web + Markdown 三位一体
10
10
 
11
- 由于CLI对系统兼容性要求高,选择 CommonJS 模块化方式
11
+ 由于 CLI 对系统兼容性要求高,服务端选择 CommonJS 模块化方式,浏览器采用 ESM
12
12
 
13
13
  # Getting Started
14
14
 
@@ -36,7 +36,7 @@ npx md-preview-cli-plus <file_path> [options]
36
36
  **Hot Module Replacement (HMR)** Similar to Vite's HMR: wraps `socket.io` for both client and server.
37
37
 
38
38
  - The browser uses `useSocket()` to listen for server messages.
39
- - On notification, the page auto-refreshes.
39
+ - Performance optimization for large files: The page partially updates upon receiving a notification.+ debounce
40
40
  - The server watches file changes and pushes updates to the browser.
41
41
 
42
42
  **HTML Export Feature** Supports `--export` flag to save rendered content as an HTML file.
@@ -47,14 +47,20 @@ npx md-preview-cli-plus <file_path> [options]
47
47
 
48
48
  - Supports custom plugins
49
49
  - Configuration via `.previewrc` file and CLI arguments
50
+ ```json
51
+ //.previewrc
52
+ {
53
+ "theme": "default",
54
+ "pluginsFolder": [//loading all plugins under these folders
55
+ "./plugins"
56
+ ]
57
+ }
58
+ ```
50
59
  - CLI arguments take precedence over config file
60
+ - **Fault Tolerance** If a plugin throws an error, it will be ignored to prevent breaking the core functionality.
51
61
 
52
62
  **Packaging & Distribution** Published as an npm package.
53
63
 
54
- - Supports direct usage via `npx md-preview-cli ...` without installation.
55
-
56
- **Fault Tolerance** If a plugin throws an error, it will be ignored to prevent breaking the core functionality.
57
-
58
64
  # 🔌 Plugin System
59
65
 
60
66
  1. **Multiple Integration Options:**
@@ -65,20 +71,22 @@ npx md-preview-cli-plus <file_path> [options]
65
71
  - `init`
66
72
  - `beforeRender`
67
73
  - `afterRender`
68
-
74
+ <hr>
69
75
  # 功能细化
70
76
 
71
77
  1. 构建CLI 采用commander 解析参数、增加帮助信息--help、版本控制-V --version、错误处理
72
78
  2. 文件监听变化 chokidar 跨平台兼容性好 低耗能
73
79
  3. 打开open 打开各种文件程序 跨平台兼容性好
74
80
  4. 打造彩色终端 🌈chalk,引入代码高亮 highlight.js
75
- 5. 热刷新HMR:(类似 Vite)封装socket.io客户端和服务端,浏览器使用useSocket()监听服务端消息,有通知则刷新页面,服务端监听文件变化并通知浏览器页面更新
81
+ 5. 热刷新HMR:(类似 Vite)封装socket.io客户端和服务端,浏览器使用useSocket()监听服务端消息,有通知则局部更新页面 ~~(借助morphdom做DOM diff算法)~~ ,服务端监听文件变化并通知浏览器页面更新
82
+ 性能优化:
83
+ (1)拆分为逻辑块,手写逻辑块 diff算法对比(按照key比较),将修改块发送给客户端,客户端通过增删逻辑对比替换
84
+ (2)更新逻辑的防抖处理:100ms等待时间
76
85
  6. 导出 HTML 功能:增加 --export 参数保存 HTML 文件
77
86
  7. 多主题支持:通过 --theme=dark 选择不同样式、支持自定义主题(放在styles文件夹下,代码参考default.css)
78
- 8. 插件机制:支持自定义插件及其生命周期钩子、高灵活度配置,支持插件配置文件.previewrc+CLI参数优先级控制
79
- 9. 打包发布:发布为 NPM 包,支持 `npx md-preview-cli ...`无需下载,直接使用
80
- 10. 容错机制:如果某插件有错误,无视该插件;
81
- 11. 后续:结合图床、富文本编辑、插件沙箱机制、vitest单元测试
87
+ 8. 插件机制:支持自定义插件及其生命周期钩子、高灵活度配置,支持插件配置文件.previewrc(一键配置加载文件夹下所有插件)+CLI参数优先级控制;容错机制:如果某插件有错误,无视该插件;
88
+ 9. 打包发布:已发布为 NPM
89
+ 10. 后续:微前端结合文档管理器(传入文件夹)、图床、富文本编辑、插件沙箱机制、vitest单元测试
82
90
 
83
91
  # 插件机制
84
92
 
@@ -95,8 +103,7 @@ md-preview/
95
103
  │ ├── pluginManager.js # 插件加载器
96
104
  │ ├── preview.js # 启动本地服务
97
105
  │ └── renderMarkdown.js # Markdown 渲染函数
98
- │ └── loadConfig.js # 读取配置文件 .previewrc
99
- │ └── loader.js # 读取某文件夹下所有插件
106
+ │ └── utils.js # 多个工具函数,读取配置文件 .previewrc、读取某文件夹下所有插件
100
107
  ├── public/
101
108
  │ └── hmrServer.js #hmr socket服务器
102
109
  │ └── useSocket.js #hmr socket客户端
@@ -105,7 +112,6 @@ md-preview/
105
112
  │ └── dark.css # 暗黑主题样式
106
113
  ├── README.md
107
114
  ├── package.json
108
- └── .gitignore
109
- plugins/
110
- ├── copyright-plugin.js # 示例插件 影响最底部文字 可直接删除
115
+ ├── plugins/
116
+ │ ├── copyright-plugin.js # 示例插件 影响最底部文字 可直接删除
111
117
  ```
package/bin/cli.js CHANGED
@@ -4,8 +4,7 @@ const { program } = require('commander');
4
4
  const path = require('path');
5
5
  const startPreview = require('../lib/preview');
6
6
  const pluginManager = require('../lib/pluginManager');
7
- const loadConfig = require('../lib/loadConfig');
8
- const loadPlugins = require('../lib/loader');
7
+ const { loadConfig, loadPlugins } = require('../lib/utils');
9
8
 
10
9
  //帮助信息自动生成,不需要手动写
11
10
  //默认命令触发条件就是和基本信息分开
@@ -23,7 +22,7 @@ program
23
22
  .option('--theme <theme>', `${theme_d}`, 'default')
24
23
  .option('--export <path>', `${export_d}`, null)
25
24
  //修改CLI支持--plugin参数 可多个
26
- .option('--plugin <path...>', `${plugin_d}`) //...是commander里的rest参数
25
+ .option('--plugin [plugin_path1,plugin_path2,...]', `${plugin_d}`) //...是commander里的rest参数
27
26
  .action((file, options) => {
28
27
  //console.log(options)
29
28
 
package/lib/preview.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const express = require('express');
2
2
  const fs = require('fs');
3
- const markdownToHtml = require('./renderMarkdown');
3
+ const { markdownToHtml } = require('./renderMarkdown');
4
4
  const hmrServer = require('../public/hmrServer');
5
5
  const open = require('open');
6
6
  const chalk = require('chalk');
@@ -10,14 +10,14 @@ function startPreview(filePath, options, pluginManager) {
10
10
  const app = express();
11
11
  const port = options.port || 3000;
12
12
 
13
+ let { htmlBlocks, html } = markdownToHtml(filePath, options.theme, options.port, pluginManager);
13
14
  app.get('/', (req, res) => {
14
- let html = markdownToHtml(filePath, options.theme, options.port, pluginManager);
15
- html = pluginManager.allAfterRender(html, options.theme) ?? html;
16
15
  if (options.export) {
17
16
  const exportPath = path.resolve(process.cwd(), options.export);
18
17
  fs.writeFileSync(exportPath, html);
19
18
  console.log(chalk.green(`✅ HTML exported to: ${exportPath}`));
20
19
  }
20
+ html = pluginManager.allAfterRender(html, options.theme) ?? html;
21
21
  res.send(html);
22
22
  });
23
23
 
@@ -29,7 +29,7 @@ function startPreview(filePath, options, pluginManager) {
29
29
  open(`http://localhost:${port}`);
30
30
  }); //返回http.Server对象
31
31
 
32
- hmrServer(server, filePath);
32
+ hmrServer(server, filePath, options.theme, pluginManager, htmlBlocks);
33
33
  }
34
34
 
35
35
  module.exports = startPreview;
@@ -1,19 +1,173 @@
1
- const fs = require('fs');
2
1
  const markdownIt = require('markdown-it');
2
+ const fs = require('fs');
3
+ const crypto = require('crypto');
4
+ function generateHash(content) {
5
+ return crypto.createHash('md5').update(content).digest('hex');
6
+ }
7
+ function splitBlocks(mdText) {
8
+ //按逻辑块分组 正确渲染多行结构
9
+ /*逻辑块汇总
10
+ 段落(连续非空文本行)
11
+ 标题(# 开头)
12
+ 代码块(``` 包裹)
13
+ 表格(包含 | 且有 |---| 分隔行)
14
+ 引用块(> 开头)
15
+ 列表项(- / * / 1. 开头)
16
+ 水平线(---、*** 等)
17
+ 空行(作为块的分隔)
18
+ */
19
+ const lines = mdText.split('\n');
20
+ const blocks = [];
21
+ let buffer = [];
22
+ let blockType = null;
23
+ let inCodeBlock = false; //是否在代码块中
24
+ let codeBlockFence = '';
25
+ function pushBuffer(index) {
26
+ if (buffer.length) {
27
+ let content = buffer.join('\n');
28
+ blocks.push({
29
+ type: blockType || 'paragraph',
30
+ content,
31
+ index, //逻辑块所在的结束行号
32
+ key: generateHash(`${blockType}${content}`), //生成唯一标识符
33
+ });
34
+ mapKey = [];
35
+ buffer = [];
36
+ blockType = null;
37
+ }
38
+ }
39
+
40
+ for (let i = 0; i < lines.length; i++) {
41
+ const line = lines[i];
42
+
43
+ // --- 处理代码块 ---
44
+ if (/^```/.test(line)) {
45
+ if (!inCodeBlock) {
46
+ pushBuffer(i - 1); // 之前的结束
47
+ inCodeBlock = true;
48
+ codeBlockFence = line.trim();
49
+ blockType = 'code';
50
+ buffer.push(line);
51
+ } else {
52
+ buffer.push(line);
53
+
54
+ pushBuffer(i); // 整个代码块结束
55
+ inCodeBlock = false;
56
+ codeBlockFence = '';
57
+ continue;
58
+ }
59
+ continue;
60
+ }
61
+ if (inCodeBlock) {
62
+ buffer.push(line);
63
+
64
+ continue;
65
+ }
66
+
67
+ // --- 空行:结束当前块 ---
68
+ if (line.trim() === '') {
69
+ pushBuffer(i);
70
+ continue;
71
+ }
72
+
73
+ // --- 处理标题 ---
74
+ if (/^#{1,6} /.test(line)) {
75
+ pushBuffer(i - 1); // 之前的结束
76
+ blockType = 'heading';
77
+ buffer.push(line);
78
+
79
+ pushBuffer(i);
80
+ continue;
81
+ }
82
+ // --- 处理引用 ---
83
+ if (/^\s*>/.test(line)) {
84
+ //引用符号前面可以只有空白符
85
+ if (blockType !== 'blockquote') {
86
+ pushBuffer(i - 1);
87
+ blockType = 'blockquote';
88
+ }
89
+ buffer.push(line);
90
+
91
+ continue;
92
+ }
93
+ // --- 处理列表 ---
94
+ if (/^\s*[-*+] /.test(line) || /^\s*\d+\. /.test(line)) {
95
+ if (blockType !== 'list') {
96
+ pushBuffer(i - 1);
97
+ blockType = 'list';
98
+ }
99
+ buffer.push(line);
100
+
101
+ continue;
102
+ }
103
+ // --- 处理表格 --- \s* 表示可有可无空格
104
+ if (/^\s*\|.*\|\s*$/.test(line)) {
105
+ if (blockType !== 'table') {
106
+ pushBuffer(i - 1);
107
+ blockType = 'table';
108
+ }
109
+ buffer.push(line);
3
110
 
111
+ continue;
112
+ }
113
+ // --- 处理分割线 ---
114
+ if (/^---+$/.test(line) || /^\*\*\*+$/.test(line)) {
115
+ pushBuffer(i - 1);
116
+ blockType = 'hr';
117
+ buffer.push(line);
118
+
119
+ pushBuffer(i);
120
+ continue;
121
+ }
122
+
123
+ // --- 默认处理为段落 ---
124
+ if (blockType !== 'paragraph') {
125
+ pushBuffer(i - 1);
126
+ blockType = 'paragraph';
127
+ }
128
+ buffer.push(line);
129
+ }
130
+
131
+ pushBuffer(lines.length); // 最后剩下的也加入
132
+ return blocks;
133
+ }
134
+ function mergeHtml(htmlBlocks, theme, pluginManager) {
135
+ let html = htmlBlocks.map((block) => block.html).join('');
136
+ // renderHtml = pluginManager.allAfterRender(html,theme) ?? html;
137
+ // return renderHtml;
138
+ return html;
139
+ }
140
+ function preProcessing(filePath, theme, pluginManager) {
141
+ let markdownContent = fs.readFileSync(filePath, 'utf-8');
142
+ markdownContent = pluginManager.allBeforeRender(markdownContent, theme) ?? markdownContent;
143
+ let splitedMd = splitBlocks(markdownContent);
144
+ let htmlBlocks = [];
145
+ for (const idx in splitedMd) {
146
+ const block = splitedMd[idx];
147
+ block.html = md.render(block.content);
148
+ htmlNode = {
149
+ ...block,
150
+ idx,
151
+ html: `<div
152
+ data-key="${block.key}"
153
+ data-idx="${idx}"
154
+ data-index="${block.index}" data-type="${block.type}">${block.html}</div>`,
155
+ };
156
+ htmlBlocks.push(htmlNode); //传入辅助信息
157
+ }
158
+ return htmlBlocks;
159
+ }
4
160
  function markdownToHtml(filePath, theme = 'default', port, pluginManager) {
5
- const md = new markdownIt({
161
+ md = new markdownIt({
6
162
  html: true,
7
163
  linkify: true, //自动识别链接文本
8
164
  typographer: true, //排版符号美化
9
165
  });
10
-
11
- let markdownContent = fs.readFileSync(filePath, 'utf-8');
12
- markdownContent = pluginManager.allBeforeRender(markdownContent, theme) ?? markdownContent;
13
-
14
- let htmlContent = md.render(markdownContent);
15
-
16
- return `
166
+ let htmlBlocks = preProcessing(filePath, theme, pluginManager);
167
+ let renderHtml = mergeHtml(htmlBlocks, theme, pluginManager);
168
+ return {
169
+ htmlBlocks,
170
+ html: `
17
171
  <!DOCTYPE html>
18
172
  <html lang="en">
19
173
  <head>
@@ -23,8 +177,8 @@ function markdownToHtml(filePath, theme = 'default', port, pluginManager) {
23
177
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/styles/github-dark.min.css">
24
178
  </head>
25
179
  <body>
26
- <div class="content">
27
- ${htmlContent}
180
+ <div class="content" id="oldHtml">
181
+ ${renderHtml}
28
182
  </div>
29
183
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
30
184
  <script>
@@ -32,17 +186,11 @@ function markdownToHtml(filePath, theme = 'default', port, pluginManager) {
32
186
  </script>
33
187
  <script type="module">
34
188
  import { useSocket } from '/useSocket.js'
35
-
36
- useSocket({
37
- serverUrl: 'http://localhost:${port}',
38
- onUpdate: (html) => {
39
- //document.querySelector('.content').innerHTML = html
40
- location.reload()
41
- }
42
- })
189
+ useSocket('http://localhost:${port}')
43
190
  </script>
44
191
  </body>
45
- </html>`;
192
+ </html>`,
193
+ };
46
194
  }
47
195
 
48
- module.exports = markdownToHtml;
196
+ module.exports = { preProcessing, mergeHtml, markdownToHtml };
@@ -1,9 +1,8 @@
1
- //loader.js
2
- //把配置的插件都加载放入并返回
3
- const path = require('path');
4
1
  const fs = require('fs');
2
+ const path = require('path');
5
3
 
6
4
  function loadPlugins(folder_path) {
5
+ //加载文件夹下所有插件
7
6
  let plugins = [];
8
7
  //把folder_path目录下的插件注册完
9
8
  if (Array.isArray(folder_path)) {
@@ -27,4 +26,22 @@ function loadPlugins(folder_path) {
27
26
  return plugins;
28
27
  }
29
28
 
30
- module.exports = loadPlugins;
29
+ function loadConfig() {
30
+ //配置读取模块
31
+ const configPath = path.resolve(process.cwd(), '.previewrc');
32
+ if (!fs.existsSync(configPath)) return {};
33
+
34
+ try {
35
+ const raw = fs.readFileSync(configPath, 'utf-8');
36
+ const config = JSON.parse(raw);
37
+ return config;
38
+ } catch (e) {
39
+ console.warn('⚠️ Failed to load .previewrc :', e.message);
40
+ return {};
41
+ }
42
+ }
43
+
44
+ module.exports = {
45
+ loadConfig,
46
+ loadPlugins,
47
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md-preview-cli-plus",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "md real-time preview + export",
5
5
  "keywords": [
6
6
  "markdown",
@@ -38,6 +38,7 @@
38
38
  "chokidar": "^4.0.3",
39
39
  "commander": "^14.0.0",
40
40
  "express": "^5.1.0",
41
+ "lodash.debounce": "^4.0.8",
41
42
  "markdown-it": "^14.1.0",
42
43
  "open": "^8.4.0",
43
44
  "socket.io": "^4.8.1"
@@ -49,5 +50,16 @@
49
50
  "husky": "^9.1.7",
50
51
  "lint-staged": "^16.1.2",
51
52
  "prettier": "^3.6.2"
52
- }
53
+ },
54
+ "files": [
55
+ "bin/",
56
+ "lib/",
57
+ "plugins/",
58
+ "public/",
59
+ "styles/",
60
+ ".editorconfig",
61
+ ".previewrc",
62
+ "README.md",
63
+ "LICENSE"
64
+ ]
53
65
  }
@@ -9,6 +9,12 @@ module.exports = {
9
9
 
10
10
  // 渲染为 HTML 后调用,可修改 HTML 字符串
11
11
  afterRender(html, options) {
12
- return html + `<footer><p style="text-align:center;color:#aaa;">© 2025 </p></footer>`;
12
+ if (html.includes('<footer>')) {
13
+ return html; // 如果已经有 footer,则不添加
14
+ }
15
+ return html.replace(
16
+ '</body>',
17
+ `<footer><p style="text-align:center;color:#aaa;">© 2025 </p></footer></body>`
18
+ );
13
19
  },
14
20
  };
@@ -1,8 +1,41 @@
1
1
  const { Server } = require('socket.io');
2
2
  const chokidar = require('chokidar');
3
3
  const chalk = require('chalk');
4
+ const { preProcessing } = require('../lib/renderMarkdown');
5
+ const debounce = require('lodash.debounce'); // 防抖函数
6
+ function NodeDiff(oldBlocks, newBlocks) {
7
+ //按key比较
8
+ //console.log(JSON.stringify(newBlocks));
9
+ //console.log(Array.isArray(oldBlocks),Array.isArray(newBlocks));
10
+ const oldMap = new Map(oldBlocks.map((b) => [b.key, b]));
11
+ const newMap = new Map(newBlocks.map((b) => [b.key, b]));
4
12
 
5
- function hmrServer(server, filePath) {
13
+ const added = [];
14
+ const updated = [];
15
+ const deleted = [];
16
+
17
+ // 找出新增和更新的
18
+ for (const [key, newBlock] of newMap) {
19
+ //console.log(key,'666',oldMap.get(key))
20
+ const oldBlock = oldMap.get(key);
21
+ if (!oldBlock) {
22
+ added.push(newBlock);
23
+ }
24
+ // else if (JSON.stringify(oldBlock.content) !== JSON.stringify(newBlock.content)) {
25
+ // updated.push(newBlock);
26
+ // }
27
+ }
28
+
29
+ // 找出被删除的
30
+ for (const [key, oldBlock] of oldMap) {
31
+ if (!newMap.has(key)) {
32
+ deleted.push(oldBlock);
33
+ }
34
+ }
35
+ return { added, updated, deleted };
36
+ }
37
+ function hmrServer(server, filePath, theme, pluginManager, oldBlocks) {
38
+ //旧html块
6
39
  const watcher = chokidar.watch(filePath);
7
40
  const io = new Server(server, {
8
41
  cors: { origin: '*' },
@@ -10,9 +43,24 @@ function hmrServer(server, filePath) {
10
43
  io.on('connection', (socket) => {
11
44
  //console.log('connect successfully')
12
45
  });
13
- watcher.on('change', () => {
14
- console.log(chalk.green('📄 Markdown file updated, refreshing page automatically.'));
15
- io.emit('update');
16
- });
46
+ let oldHtmlBlocks = oldBlocks;
47
+ watcher
48
+ .on('ready', () => {
49
+ // console.log('ready!!!!')
50
+ // setTimeout(() => {
51
+ // io.emit('init', pluginManager, theme);
52
+ // }, 1000); // 等客户端连接稳定后再发
53
+ })
54
+ .on(
55
+ 'change',
56
+ debounce(() => {
57
+ console.log(chalk.cyan('📄 Markdown file updated and this page will update partially.'));
58
+ newHtmlBlocks = preProcessing(filePath, theme, pluginManager);
59
+ changedBlocks = NodeDiff(oldHtmlBlocks, newHtmlBlocks);
60
+ //console.log(JSON.stringify(changedBlocks));
61
+ io.emit('update', changedBlocks, pluginManager, theme);
62
+ oldHtmlBlocks = newHtmlBlocks; // 更新旧的逻辑块
63
+ }, 100)
64
+ ); //防抖处理:如果有变化 则需要等待100ms后再执行一次更新逻辑
17
65
  }
18
66
  module.exports = hmrServer;
@@ -1,7 +1,67 @@
1
+ //ESM模块化
1
2
  // useSocket.js(浏览器端模块,支持原生或 Vite 项目)
2
3
  import { io } from 'https://cdn.socket.io/4.7.5/socket.io.esm.min.js';
4
+ //import morphdom from 'https://cdn.jsdelivr.net/npm/morphdom@2.6.1/dist/morphdom-esm.js';
5
+ function htmlStringToNode(htmlStr) {
6
+ const template = document.createElement('template'); //安全解析DOM结构 支持所有浏览器
7
+ template.innerHTML = htmlStr.trim(); // 去除空格,避免空文本节点
8
+ return template.content.firstChild;
9
+ }
10
+ function applyDiff({ added = [], updated = [], deleted = [] }, container) {
11
+ const keyToElement = new Map();
12
+ // 先缓存当前 DOM 中的所有 block 节点
13
+ container.querySelectorAll('[data-key]').forEach((el) => {
14
+ keyToElement.set(el.dataset.key, el);
15
+ });
16
+
17
+ // 1. 删除
18
+ deleted.forEach((block) => {
19
+ const el = keyToElement.get(block.key);
20
+ if (el) el.remove();
21
+ });
3
22
 
4
- export function useSocket({ serverUrl, onUpdate }) {
23
+ // 2. 更新 不可能有完全一样的key,因为key的组成是type+content 如果有变化则content不可能相同
24
+ // updated.forEach(block => {
25
+ // const oldEl = keyToElement.get(block.key);
26
+ // if (oldEl) {
27
+ // const newEl = htmlStringToNode(block.html);
28
+ // morphdom(oldEl, newEl, {//按照key来找到旧节点 无论顺序如何改变
29
+ // getNodeKey: node => node.dataset?.key ?? null
30
+ // });
31
+ // }
32
+ // });
33
+
34
+ // 3. 新增
35
+ added.forEach((block) => {
36
+ const newEl = htmlStringToNode(block.html);
37
+
38
+ // 插入位置:用 idx 升序插入
39
+ const all = Array.from(container.querySelectorAll('[data-key]'));
40
+ const idx = block.idx;
41
+
42
+ let inserted = false;
43
+ for (let i = 0; i < all.length; i++) {
44
+ const elIdx = parseInt(all[i].dataset.idx);
45
+ if (elIdx > idx) {
46
+ container.insertBefore(newEl, all[i]);
47
+ inserted = true;
48
+ break;
49
+ }
50
+ }
51
+
52
+ if (!inserted) {
53
+ container.appendChild(newEl);
54
+ }
55
+ });
56
+ }
57
+
58
+ // function OneParentNode(html) {//morphdom需要一个父节点
59
+ // const div = document.createElement('div');
60
+ // div.id = 'oldHtml';
61
+ // div.innerHTML = html;
62
+ // return div;
63
+ // }
64
+ export function useSocket(serverUrl) {
5
65
  const socket = io(serverUrl);
6
66
 
7
67
  socket.on('connect', () => {
@@ -13,11 +73,18 @@ export function useSocket({ serverUrl, onUpdate }) {
13
73
  });
14
74
 
15
75
  // 接收后端推送的 HTML 内容
16
- socket.on('update', (htmlContent) => {
17
- console.log('📥 收到 update 事件');
18
- if (typeof onUpdate === 'function') {
19
- onUpdate(htmlContent);
20
- }
76
+ // socket.on('init',(pluginManager,theme)=>{
77
+ // console.log(pluginManager,theme)
78
+ // let node=document.getElementById('oldHtml');
79
+ // let html=node.innerHTML;
80
+ // node.innerHTML=pluginManager.allAfterRender(html,theme) ?? html;
81
+ // })
82
+ socket.on('update', (changedBlocks, pluginManager, theme) => {
83
+ console.log('📥 收到 update 事件', changedBlocks);
84
+ applyDiff(changedBlocks, document.getElementById('oldHtml'));
85
+
86
+ //morphdom(document.getElementById('oldHtml'), OneParentNode(newHtml));
87
+ // 自动把 old 中的 DOM 节点,变更为 new 的内容,只更新必要的差异部分。
21
88
  });
22
89
 
23
90
  return socket;
@@ -1,31 +0,0 @@
1
- name: Publish to npm
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'v*' # 仅当发布 tag,如 v1.0.0 时触发
7
-
8
- jobs:
9
- publish:
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - name: Checkout repo
14
- uses: actions/checkout@v3
15
-
16
- - name: Setup Node.js
17
- uses: actions/setup-node@v3
18
- with:
19
- node-version: 18
20
- registry-url: 'https://registry.npmjs.org/'
21
-
22
- - name: Install dependencies
23
- run: npm ci
24
-
25
- - name: Run
26
- run: npm run # 如果你没有 build 步骤可以删掉
27
-
28
- - name: Publish to npm
29
- run: npm publish
30
- env:
31
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/lib/loadConfig.js DELETED
@@ -1,20 +0,0 @@
1
- //配置读取模块 lib/loadConfig.js
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- function loadConfig() {
7
- const configPath = path.resolve(process.cwd(), '.previewrc');
8
- if (!fs.existsSync(configPath)) return {};
9
-
10
- try {
11
- const raw = fs.readFileSync(configPath, 'utf-8');
12
- const config = JSON.parse(raw);
13
- return config;
14
- } catch (e) {
15
- console.warn('⚠️ Failed to load .previewrc :', e.message);
16
- return {};
17
- }
18
- }
19
-
20
- module.exports = loadConfig;