docsify-emmet-playground 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ZG
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # docsify-emmet-playground
2
+
3
+ 一个轻量的 Docsify 插件,用于在 Markdown 文档里嵌入 Emmet 实时演示器。读者可以输入 Emmet 缩写,页面会自动展开为 HTML,并在 iframe 中实时预览效果。
4
+
5
+ ## 特性
6
+
7
+ - 支持 Emmet HTML/markup 缩写实时展开
8
+ - 展示 Emmet 输入、展开后的 HTML、实时预览三块区域
9
+ - 通过 Docsify `doneEach` 自动初始化,支持路由切换后重新渲染
10
+ - iframe 使用 `sandbox`,预览内容和页面主体隔离
11
+ - 支持响应式布局和暗色主题基础适配
12
+ - 通过 CDN 动态加载 `emmet@2.4.11`
13
+
14
+ ## 安装
15
+
16
+ ### GitHub + jsDelivr
17
+
18
+ 把仓库上传到 GitHub 后,在 Docsify 的 `index.html` 中加入:
19
+
20
+ ```html
21
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/your-name/docsify-emmet-playground/dist/docsify-emmet-playground.css">
22
+ <script src="https://cdn.jsdelivr.net/gh/your-name/docsify-emmet-playground/dist/docsify-emmet-playground.js"></script>
23
+ ```
24
+
25
+ ### npm + jsDelivr
26
+
27
+ 如果发布到 npm,可以这样引用:
28
+
29
+ ```html
30
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsify-emmet-playground/dist/docsify-emmet-playground.css">
31
+ <script src="https://cdn.jsdelivr.net/npm/docsify-emmet-playground/dist/docsify-emmet-playground.js"></script>
32
+ ```
33
+
34
+ ## 使用
35
+
36
+ 在任意 Docsify Markdown 文件中插入:
37
+
38
+ ```html
39
+ <div class="emmet-playground" data-emmet="ul>li.item$*3>a{第 $ 项}"></div>
40
+ ```
41
+
42
+ 也可以自定义标题:
43
+
44
+ ```html
45
+ <div
46
+ class="emmet-playground"
47
+ data-title="列表语法演示"
48
+ data-emmet="ul>li.item$*3>a{第 $ 项}"
49
+ ></div>
50
+ ```
51
+
52
+ ## 示例
53
+
54
+ 可以尝试这些缩写:
55
+
56
+ ```text
57
+ !
58
+ ul>li*3
59
+ .box>h2{标题}+p{内容}
60
+ table>tr*2>td*3
61
+ ```
62
+
63
+ ## 本地预览
64
+
65
+ ```bash
66
+ npm install -g docsify-cli
67
+ npm run demo
68
+ ```
69
+
70
+ 然后打开:
71
+
72
+ ```text
73
+ http://localhost:3000
74
+ ```
75
+
76
+ ## 注意事项
77
+
78
+ - 插件需要浏览器能访问 Emmet CDN;离线或网络受限时会显示加载失败提示。
79
+ - 当前版本只处理 HTML/markup 类型的 Emmet 展开,不处理 CSS 缩写展开。
80
+ - 如果你的站点设置了严格 CSP,需要允许插件中使用的 CDN 域名和动态 `import()`。
81
+
82
+ ## License
83
+
84
+ MIT
@@ -0,0 +1,163 @@
1
+ .emmet-playground {
2
+ margin: 24px 0;
3
+ border: 1px solid rgba(86, 116, 168, 0.28);
4
+ border-radius: 8px;
5
+ overflow: hidden;
6
+ background: var(--background, #ffffff);
7
+ box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
8
+ }
9
+
10
+ .emmet-playground__header {
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: space-between;
14
+ gap: 12px;
15
+ padding: 12px 14px;
16
+ border-bottom: 1px solid rgba(86, 116, 168, 0.18);
17
+ background: rgba(86, 116, 168, 0.08);
18
+ }
19
+
20
+ .emmet-playground__title {
21
+ color: var(--heading-color, #1f2937);
22
+ font-size: 15px;
23
+ font-weight: 700;
24
+ }
25
+
26
+ .emmet-playground__status {
27
+ flex: 0 0 auto;
28
+ color: #2563eb;
29
+ font-size: 12px;
30
+ }
31
+
32
+ .emmet-playground__status.is-error {
33
+ color: #b91c1c;
34
+ }
35
+
36
+ .emmet-playground__grid {
37
+ display: grid;
38
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
39
+ gap: 0;
40
+ }
41
+
42
+ .emmet-playground__panel {
43
+ display: flex;
44
+ min-width: 0;
45
+ min-height: 260px;
46
+ flex-direction: column;
47
+ margin: 0;
48
+ border-right: 1px solid rgba(86, 116, 168, 0.16);
49
+ }
50
+
51
+ .emmet-playground__panel:last-child {
52
+ border-right: 0;
53
+ }
54
+
55
+ .emmet-playground__preview-panel {
56
+ grid-column: 1 / -1;
57
+ border-top: 1px solid rgba(86, 116, 168, 0.16);
58
+ }
59
+
60
+ .emmet-playground__label {
61
+ display: block;
62
+ padding: 10px 12px;
63
+ color: var(--text-color-base, #374151);
64
+ font-size: 13px;
65
+ font-weight: 600;
66
+ background: rgba(148, 163, 184, 0.1);
67
+ }
68
+
69
+ .emmet-playground__input,
70
+ .emmet-playground__output {
71
+ width: 100%;
72
+ min-height: 220px;
73
+ flex: 1;
74
+ padding: 12px;
75
+ border: 0;
76
+ outline: 0;
77
+ resize: vertical;
78
+ color: var(--code-color, #111827);
79
+ background: var(--code-block-background, #f8fafc);
80
+ font: 14px/1.55 Consolas, Monaco, "Courier New", monospace;
81
+ }
82
+
83
+ .emmet-playground__output {
84
+ color: #0f766e;
85
+ }
86
+
87
+ .emmet-playground__preview {
88
+ width: 100%;
89
+ min-height: 240px;
90
+ flex: 1;
91
+ border: 0;
92
+ background: #ffffff;
93
+ }
94
+
95
+ body.dark .emmet-playground,
96
+ [data-theme="dark"] .emmet-playground {
97
+ border-color: rgba(148, 163, 184, 0.28);
98
+ background: #111827;
99
+ box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3);
100
+ }
101
+
102
+ body.dark .emmet-playground__header,
103
+ [data-theme="dark"] .emmet-playground__header {
104
+ background: rgba(148, 163, 184, 0.12);
105
+ border-bottom-color: rgba(148, 163, 184, 0.2);
106
+ }
107
+
108
+ body.dark .emmet-playground__title,
109
+ [data-theme="dark"] .emmet-playground__title,
110
+ body.dark .emmet-playground__label,
111
+ [data-theme="dark"] .emmet-playground__label {
112
+ color: #e5e7eb;
113
+ }
114
+
115
+ body.dark .emmet-playground__input,
116
+ body.dark .emmet-playground__output,
117
+ [data-theme="dark"] .emmet-playground__input,
118
+ [data-theme="dark"] .emmet-playground__output {
119
+ color: #d1d5db;
120
+ background: #0b1120;
121
+ }
122
+
123
+ body.dark .emmet-playground__output,
124
+ [data-theme="dark"] .emmet-playground__output {
125
+ color: #5eead4;
126
+ }
127
+
128
+ @media screen and (min-width: 1180px) {
129
+ .emmet-playground__grid {
130
+ grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr) minmax(0, 1fr);
131
+ }
132
+
133
+ .emmet-playground__preview-panel {
134
+ grid-column: auto;
135
+ border-top: 0;
136
+ }
137
+ }
138
+
139
+ @media screen and (max-width: 760px) {
140
+ .emmet-playground__header {
141
+ align-items: flex-start;
142
+ flex-direction: column;
143
+ }
144
+
145
+ .emmet-playground__grid {
146
+ grid-template-columns: 1fr;
147
+ }
148
+
149
+ .emmet-playground__panel {
150
+ min-height: 220px;
151
+ border-right: 0;
152
+ border-bottom: 1px solid rgba(86, 116, 168, 0.16);
153
+ }
154
+
155
+ .emmet-playground__panel:last-child {
156
+ border-bottom: 0;
157
+ }
158
+
159
+ .emmet-playground__input,
160
+ .emmet-playground__output {
161
+ min-height: 170px;
162
+ }
163
+ }
@@ -0,0 +1,176 @@
1
+ (function () {
2
+ var EMMET_CDNS = [
3
+ "https://cdn.jsdelivr.net/npm/emmet@2.4.11/+esm",
4
+ "https://esm.sh/emmet@2.4.11",
5
+ "https://cdnjs.cloudflare.com/ajax/libs/emmet/2.4.11/emmet.es.min.js",
6
+ "https://cdn.jsdelivr.net/npm/emmet@2.4.11/dist/emmet.es.js",
7
+ ];
8
+ var emmetModulePromise;
9
+
10
+ function loadEmmet() {
11
+ if (!emmetModulePromise) {
12
+ emmetModulePromise = EMMET_CDNS.reduce(function (promise, url) {
13
+ return promise.catch(function () {
14
+ return import(url);
15
+ });
16
+ }, Promise.reject()).then(function (mod) {
17
+ var expand =
18
+ mod.expandAbbreviation ||
19
+ (typeof mod.default === "function" && mod.default) ||
20
+ (mod.default && mod.default.expandAbbreviation);
21
+
22
+ if (!expand) {
23
+ throw new Error("Emmet expandAbbreviation API was not found.");
24
+ }
25
+
26
+ return expand;
27
+ });
28
+ }
29
+
30
+ return emmetModulePromise;
31
+ }
32
+
33
+ function escapeHtml(value) {
34
+ return value
35
+ .replace(/&/g, "&amp;")
36
+ .replace(/</g, "&lt;")
37
+ .replace(/>/g, "&gt;")
38
+ .replace(/"/g, "&quot;");
39
+ }
40
+
41
+ function createPreviewDocument(html) {
42
+ if (/^\s*<!doctype|<html[\s>]/i.test(html)) {
43
+ return html;
44
+ }
45
+
46
+ return [
47
+ "<!doctype html>",
48
+ '<html lang="zh-Hans">',
49
+ "<head>",
50
+ '<meta charset="UTF-8">',
51
+ '<meta name="viewport" content="width=device-width, initial-scale=1.0">',
52
+ "<style>",
53
+ "body{box-sizing:border-box;margin:0;padding:16px;font-family:Arial,'Microsoft YaHei',sans-serif;line-height:1.6;color:#1f2937;background:#fff;}",
54
+ "*{box-sizing:border-box;}",
55
+ "img,video{max-width:100%;height:auto;}",
56
+ "table{border-collapse:collapse;width:100%;}",
57
+ "td,th{border:1px solid #d1d5db;padding:6px 8px;}",
58
+ "a{color:#2563eb;}",
59
+ "</style>",
60
+ "</head>",
61
+ "<body>",
62
+ html,
63
+ "</body>",
64
+ "</html>",
65
+ ].join("");
66
+ }
67
+
68
+ function buildPlayground(container) {
69
+ if (container.dataset.emmetReady === "true") {
70
+ return;
71
+ }
72
+
73
+ container.dataset.emmetReady = "true";
74
+
75
+ var initialEmmet = container.getAttribute("data-emmet") || "ul>li.item$*3>a{第 $ 项}";
76
+ var title = container.getAttribute("data-title") || "Emmet 实时演示";
77
+
78
+ container.innerHTML = [
79
+ '<div class="emmet-playground__header">',
80
+ '<span class="emmet-playground__title">' + escapeHtml(title) + "</span>",
81
+ '<span class="emmet-playground__status" aria-live="polite">加载 Emmet...</span>',
82
+ "</div>",
83
+ '<div class="emmet-playground__grid">',
84
+ '<label class="emmet-playground__panel">',
85
+ '<span class="emmet-playground__label">Emmet 缩写</span>',
86
+ '<textarea class="emmet-playground__input" spellcheck="false"></textarea>',
87
+ "</label>",
88
+ '<label class="emmet-playground__panel">',
89
+ '<span class="emmet-playground__label">展开后的 HTML</span>',
90
+ '<textarea class="emmet-playground__output" spellcheck="false" readonly></textarea>',
91
+ "</label>",
92
+ '<section class="emmet-playground__panel emmet-playground__preview-panel" aria-label="实时预览">',
93
+ '<span class="emmet-playground__label">实时预览</span>',
94
+ '<iframe class="emmet-playground__preview" sandbox="" title="Emmet 预览"></iframe>',
95
+ "</section>",
96
+ "</div>",
97
+ ].join("");
98
+
99
+ var input = container.querySelector(".emmet-playground__input");
100
+ var output = container.querySelector(".emmet-playground__output");
101
+ var preview = container.querySelector(".emmet-playground__preview");
102
+ var status = container.querySelector(".emmet-playground__status");
103
+ var expandAbbreviation;
104
+ var debounceTimer;
105
+
106
+ input.value = initialEmmet;
107
+
108
+ function setError(message) {
109
+ output.value = "";
110
+ preview.srcdoc = createPreviewDocument(
111
+ '<p style="color:#b91c1c;margin:0;">' + escapeHtml(message) + "</p>"
112
+ );
113
+ status.textContent = "展开失败";
114
+ status.classList.add("is-error");
115
+ }
116
+
117
+ function render() {
118
+ if (!expandAbbreviation) {
119
+ return;
120
+ }
121
+
122
+ var abbreviation = input.value.trim();
123
+
124
+ if (!abbreviation) {
125
+ output.value = "";
126
+ preview.srcdoc = createPreviewDocument("");
127
+ status.textContent = "等待输入";
128
+ status.classList.remove("is-error");
129
+ return;
130
+ }
131
+
132
+ try {
133
+ var html = expandAbbreviation(abbreviation, {
134
+ syntax: "html",
135
+ type: "markup",
136
+ options: {
137
+ "output.indent": " ",
138
+ "output.selfClosingStyle": "html",
139
+ },
140
+ });
141
+
142
+ output.value = html;
143
+ preview.srcdoc = createPreviewDocument(html);
144
+ status.textContent = "已实时预览";
145
+ status.classList.remove("is-error");
146
+ } catch (error) {
147
+ setError(error && error.message ? error.message : "当前缩写无法展开。");
148
+ }
149
+ }
150
+
151
+ input.addEventListener("input", function () {
152
+ window.clearTimeout(debounceTimer);
153
+ debounceTimer = window.setTimeout(render, 120);
154
+ });
155
+
156
+ loadEmmet()
157
+ .then(function (expand) {
158
+ expandAbbreviation = expand;
159
+ render();
160
+ })
161
+ .catch(function () {
162
+ setError("Emmet CDN 加载失败,请检查网络连接后刷新页面。");
163
+ });
164
+ }
165
+
166
+ function initPlaygrounds() {
167
+ document.querySelectorAll(".emmet-playground").forEach(buildPlayground);
168
+ }
169
+
170
+ function docsifyPlugin(hook) {
171
+ hook.doneEach(initPlaygrounds);
172
+ }
173
+
174
+ window.$docsify = window.$docsify || {};
175
+ window.$docsify.plugins = [].concat(window.$docsify.plugins || [], docsifyPlugin);
176
+ })();
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "docsify-emmet-playground",
3
+ "version": "0.1.0",
4
+ "description": "A lightweight Docsify plugin for live Emmet abbreviation expansion and HTML preview.",
5
+ "keywords": [
6
+ "docsify",
7
+ "docsify-plugin",
8
+ "emmet",
9
+ "html",
10
+ "playground"
11
+ ],
12
+ "homepage": "https://github.com/your-name/docsify-emmet-playground#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/your-name/docsify-emmet-playground/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/your-name/docsify-emmet-playground.git"
19
+ },
20
+ "license": "MIT",
21
+ "author": "ZG",
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "main": "dist/docsify-emmet-playground.js",
26
+ "style": "dist/docsify-emmet-playground.css",
27
+ "scripts": {
28
+ "demo": "docsify serve demo",
29
+ "check": "node --check dist/docsify-emmet-playground.js"
30
+ }
31
+ }