jsmdcui 0.6.3 → 0.8.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +216 -33
  3. package/clean.sh +9 -0
  4. package/demo.resized.jpg +0 -0
  5. package/{image-processor.md → demos/image-processor.md} +1 -0
  6. package/{image-processor.zh-TW.md → demos/image-processor.zh-TW.md} +1 -0
  7. package/demos/maze.md +144 -0
  8. package/{select.md → demos/select.md} +2 -0
  9. package/demos/todo-zh.md +190 -0
  10. package/demos/todo.md +191 -0
  11. package/edit +9 -0
  12. package/package.json +1 -1
  13. package/runmd.mjs +77 -16
  14. package/runtime/help/help.md +216 -33
  15. package/runtime/jsplugins/cdp/cdp.js +5 -2
  16. package/runtime/jsplugins/chapter/chapter.js +4 -2
  17. package/runtime/jsplugins/example/example.js +10 -7
  18. package/runtime/syntax/markdown.yaml +1 -0
  19. package/single-exe/packAssets.sh +1 -1
  20. package/src/cui/fence-events.mjs +106 -0
  21. package/src/cui/id-collision.mjs +10 -28
  22. package/src/cui/kitty-debug.mjs +25 -0
  23. package/src/cui/kitty-images.mjs +200 -0
  24. package/src/cui/rpc.mjs +169 -10
  25. package/src/cui/server.mjs +14 -3
  26. package/src/cui/task-checkbox.mjs +44 -0
  27. package/src/index.js +615 -101
  28. package/src/platform/terminal.js +5 -0
  29. package/src/plugins/js-bridge.js +354 -21
  30. package/src/screen/screen.js +108 -1
  31. package/tests/cat-markdown.test.js +6 -5
  32. package/tests/demo.test.js +99 -9
  33. package/tests/fence-events.test.js +405 -0
  34. package/tests/heading-list-selector.test.js +346 -0
  35. package/tests/id-collision.test.js +14 -2
  36. package/tests/js-plugin-prompts.test.js +46 -0
  37. package/tests/key-event.md +10 -0
  38. package/tests/kitty-demo.md +184 -0
  39. package/tests/kitty-images.test.js +169 -0
  40. package/tests/platform-terminal.test.js +8 -0
  41. package/tests/task-checkbox.test.js +33 -0
  42. package/tests/textarea.md +8 -0
  43. package/tests/wui-responsive-images.test.js +35 -0
  44. package/tests/wui.test.js +16 -1
  45. package/tui +4 -2
  46. package/wui +6 -2
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env jsmdcui
2
+
3
+ # Todo List
4
+
5
+ ## Todos
6
+
7
+ - [ ] canvas support
8
+ - [x] push/pop/splice/slice list items
9
+ - [x] image by --kitty
10
+ - [ ] clean up codebase
11
+
12
+ ## Actions
13
+
14
+ ```text#todo-input
15
+ Fly to the moon
16
+ ```
17
+
18
+ 在上方輸入 Todo 文字,再選擇新增或移除。移除會刪除第一個文字完全相同的項目。
19
+
20
+ - [新增 Todo](javascript:addTodo())
21
+ - .
22
+ - [移除 Todo](javascript:removeTodo())
23
+ - .
24
+ - [顯示已完成](javascript:showCompleted())
25
+ - .
26
+ - [顯示未完成](javascript:showPending())
27
+
28
+ ## 存檔/讀檔
29
+
30
+ ```text#todo-file
31
+ todo_list.json
32
+ ```
33
+
34
+ 在上方輸入 JSON 檔案路徑。存檔會寫入人類可讀的 JSON;讀檔會在確認後取代目前的 Todo 清單。
35
+
36
+ - [儲存 Todos](javascript:saveTodos())
37
+ - .
38
+ - [讀取 Todos](javascript:loadTodos())
39
+
40
+ ## 操作結果:
41
+
42
+ ```text#todo-status
43
+ 尚未操作
44
+ ```
45
+
46
+ ## Filtered Result
47
+
48
+ ```text#todo-result
49
+ 點按「顯示已完成」或「顯示未完成」查看結果
50
+ ```
51
+
52
+ ```js front
53
+ function todoText() {
54
+ return $('#todo-input').val().trim();
55
+ }
56
+
57
+ function showItems(title, checked) {
58
+ const items = $('#todos')
59
+ .slice()
60
+ .filter(item => item.checked === checked);
61
+
62
+ const output = items.length
63
+ ? items.map(item => `${item.checked ? '✓' : '○'} ${item.value}`).join('\n')
64
+ : '(沒有項目)';
65
+ $('#todo-result').val(`${title}\n${output}`);
66
+ $('#todo-status').val(`找到 ${items.length} 個${title}項目`);
67
+ }
68
+
69
+ function todoFile() {
70
+ return $('#todo-file').val().trim();
71
+ }
72
+
73
+ function describeError(error) {
74
+ return error?.message || String(error);
75
+ }
76
+
77
+ export function addTodo() {
78
+ const value = todoText();
79
+ if (!value) {
80
+ $('#todo-status').val('新增失敗:請先輸入 Todo 文字');
81
+ return;
82
+ }
83
+
84
+ const length = $('#todos').push(value);
85
+ $('#todo-input').val('');
86
+ $('#todo-status').val(`已新增「${value}」,目前共有 ${length} 個 Todo`);
87
+ }
88
+
89
+ export function removeTodo() {
90
+ const value = todoText();
91
+ if (!value) {
92
+ $('#todo-status').val('移除失敗:請輸入要移除的 Todo 文字');
93
+ return;
94
+ }
95
+
96
+ const items = $('#todos').slice();
97
+ const index = items.findIndex(item => item.value === value);
98
+ if (index < 0) {
99
+ $('#todo-status').val(`移除失敗:找不到「${value}」`);
100
+ return;
101
+ }
102
+
103
+ const removed = $('#todos').splice(index, 1);
104
+ $('#todo-input').val('');
105
+ $('#todo-status').val(`已移除「${removed[0]}」`);
106
+ }
107
+
108
+ export function showCompleted() {
109
+ showItems('已完成', true);
110
+ }
111
+
112
+ export function showPending() {
113
+ showItems('未完成', false);
114
+ }
115
+
116
+ export async function saveTodos() {
117
+ const file = todoFile();
118
+ if (!file) {
119
+ $('#todo-status').val('存檔失敗:請先輸入 JSON 檔案路徑');
120
+ return;
121
+ }
122
+
123
+ try {
124
+ const obj = { todos: $('#todos').slice() };
125
+ const result = await rpc.saveTodoList(file, obj);
126
+ $('#todo-status').val(`已將 ${obj.todos.length} 個 Todo 儲存至 ${result.path}`);
127
+ } catch (error) {
128
+ $('#todo-status').val(`存檔失敗:${describeError(error)}`);
129
+ }
130
+ }
131
+
132
+ export async function loadTodos() {
133
+ const file = todoFile();
134
+ if (!file) {
135
+ $('#todo-status').val('讀檔失敗:請先輸入 JSON 檔案路徑');
136
+ return;
137
+ }
138
+ try {
139
+ const result = await rpc.loadTodoList(file);
140
+ const json = JSON.stringify(result.obj, null, 1);
141
+ if (!confirm(`要用「${file}」中的以下 JSON 取代目前的 Todo 清單嗎?\n\n${json}`)) {
142
+ $('#todo-status').val('已取消讀檔,目前的 Todos 未變更');
143
+ return;
144
+ }
145
+ const current = $('#todos').slice();
146
+ $('#todos').splice(0, current.length, ...result.obj.todos);
147
+ $('#todo-status').val(`已從 ${result.path} 讀取 ${result.obj.todos.length} 個 Todo`);
148
+ } catch (error) {
149
+ $('#todo-status').val(`讀檔失敗:${describeError(error)}`);
150
+ }
151
+ }
152
+ ```
153
+
154
+ ```js back
155
+ import { resolve } from 'node:path';
156
+
157
+ function todoPath(input) {
158
+ const value = String(input ?? '').trim();
159
+ if (!value) throw new Error('請先輸入 JSON 檔案路徑');
160
+ return resolve(value);
161
+ }
162
+
163
+ function validateTodoObject(obj) {
164
+ if (!obj || typeof obj !== 'object' || !Array.isArray(obj.todos))
165
+ throw new Error('Todo JSON 必須是包含 todos 陣列的物件');
166
+
167
+ return {
168
+ todos: obj.todos.map((item, index) => {
169
+ if (!item || typeof item !== 'object' || typeof item.value !== 'string')
170
+ throw new Error(`第 ${index + 1} 個 Todo 項目必須包含字串 value`);
171
+ return { value: item.value, checked: Boolean(item.checked) };
172
+ }),
173
+ };
174
+ }
175
+
176
+ export async function saveTodoList(input, obj) {
177
+ const path = todoPath(input);
178
+ const validated = validateTodoObject(obj);
179
+ await Bun.write(path, JSON.stringify(validated, null, 1) + '\n');
180
+ return { path };
181
+ }
182
+
183
+ export async function loadTodoList(input) {
184
+ const path = todoPath(input);
185
+ const file = Bun.file(path);
186
+ if (!await file.exists()) throw new Error(`找不到 Todo 檔案:${path}`);
187
+ const obj = validateTodoObject(JSON.parse(await file.text()));
188
+ return { path, obj };
189
+ }
190
+ ```
package/demos/todo.md ADDED
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env jsmdcui
2
+
3
+ # Todo List
4
+
5
+ ## Todos
6
+
7
+ - [ ] canvas support
8
+ - [x] push/pop/splice/slice list items
9
+ - [x] image by --kitty
10
+ - [ ] clean up codebase
11
+
12
+ ## Actions
13
+
14
+ ```text#todo-input
15
+ Fly to the moon
16
+ ```
17
+
18
+ Enter Todo text above, then choose Add or Remove. Remove deletes the first item whose text matches exactly.
19
+
20
+ - [Add Todo](javascript:addTodo())
21
+ - .
22
+ - [Remove Todo](javascript:removeTodo())
23
+ - .
24
+ - [Show Completed](javascript:showCompleted())
25
+ - .
26
+ - [Show Pending](javascript:showPending())
27
+
28
+ ## Save / Load
29
+
30
+ ```text#todo-file
31
+ todo_list.json
32
+ ```
33
+
34
+ Enter a JSON file path above. Saving writes human-readable JSON; loading replaces the current Todo list after confirmation.
35
+
36
+ - [Save Todos](javascript:saveTodos())
37
+ - .
38
+ - [Load Todos](javascript:loadTodos())
39
+
40
+ ## Operation Result
41
+
42
+ ```text#todo-status
43
+ No action yet
44
+ ```
45
+
46
+ ## Filtered Result
47
+
48
+ ```text#todo-result
49
+ Select “Show Completed” or “Show Pending” to view matching items
50
+ ```
51
+
52
+ ```js front
53
+ function todoText() {
54
+ return $('#todo-input').val().trim();
55
+ }
56
+
57
+ function showItems(title, checked) {
58
+ const items = $('#todos')
59
+ .slice()
60
+ .filter(item => item.checked === checked);
61
+
62
+ const output = items.length
63
+ ? items.map(item => `${item.checked ? '✓' : '○'} ${item.value}`).join('\n')
64
+ : '(No items)';
65
+ const noun = items.length === 1 ? 'item' : 'items';
66
+ $('#todo-result').val(`${title}\n${output}`);
67
+ $('#todo-status').val(`Found ${items.length} ${title.toLowerCase()} ${noun}`);
68
+ }
69
+
70
+ function todoFile() {
71
+ return $('#todo-file').val().trim();
72
+ }
73
+
74
+ function describeError(error) {
75
+ return error?.message || String(error);
76
+ }
77
+
78
+ export function addTodo() {
79
+ const value = todoText();
80
+ if (!value) {
81
+ $('#todo-status').val('Add failed: enter Todo text first');
82
+ return;
83
+ }
84
+
85
+ const length = $('#todos').push(value);
86
+ $('#todo-input').val('');
87
+ $('#todo-status').val(`Added “${value}”; ${length} Todos total`);
88
+ }
89
+
90
+ export function removeTodo() {
91
+ const value = todoText();
92
+ if (!value) {
93
+ $('#todo-status').val('Remove failed: enter the Todo text to remove');
94
+ return;
95
+ }
96
+
97
+ const items = $('#todos').slice();
98
+ const index = items.findIndex(item => item.value === value);
99
+ if (index < 0) {
100
+ $('#todo-status').val(`Remove failed: could not find “${value}”`);
101
+ return;
102
+ }
103
+
104
+ const removed = $('#todos').splice(index, 1);
105
+ $('#todo-input').val('');
106
+ $('#todo-status').val(`Removed “${removed[0]}”`);
107
+ }
108
+
109
+ export function showCompleted() {
110
+ showItems('Completed', true);
111
+ }
112
+
113
+ export function showPending() {
114
+ showItems('Pending', false);
115
+ }
116
+
117
+ export async function saveTodos() {
118
+ const file = todoFile();
119
+ if (!file) {
120
+ $('#todo-status').val('Save failed: enter a JSON file path first');
121
+ return;
122
+ }
123
+
124
+ try {
125
+ const obj = { todos: $('#todos').slice() };
126
+ const result = await rpc.saveTodoList(file, obj);
127
+ $('#todo-status').val(`Saved ${obj.todos.length} Todos to ${result.path}`);
128
+ } catch (error) {
129
+ $('#todo-status').val(`Save failed: ${describeError(error)}`);
130
+ }
131
+ }
132
+
133
+ export async function loadTodos() {
134
+ const file = todoFile();
135
+ if (!file) {
136
+ $('#todo-status').val('Load failed: enter a JSON file path first');
137
+ return;
138
+ }
139
+ try {
140
+ const result = await rpc.loadTodoList(file);
141
+ const json = JSON.stringify(result.obj, null, 1);
142
+ if (!confirm(`Replace the current Todo list with this JSON from “${file}”?\n\n${json}`)) {
143
+ $('#todo-status').val('Load cancelled; current Todos were not changed');
144
+ return;
145
+ }
146
+ const current = $('#todos').slice();
147
+ $('#todos').splice(0, current.length, ...result.obj.todos);
148
+ $('#todo-status').val(`Loaded ${result.obj.todos.length} Todos from ${result.path}`);
149
+ } catch (error) {
150
+ $('#todo-status').val(`Load failed: ${describeError(error)}`);
151
+ }
152
+ }
153
+ ```
154
+
155
+ ```js back
156
+ import { resolve } from 'node:path';
157
+
158
+ function todoPath(input) {
159
+ const value = String(input ?? '').trim();
160
+ if (!value) throw new Error('Enter a JSON file path first');
161
+ return resolve(value);
162
+ }
163
+
164
+ function validateTodoObject(obj) {
165
+ if (!obj || typeof obj !== 'object' || !Array.isArray(obj.todos))
166
+ throw new Error('Todo JSON must be an object containing a todos array');
167
+
168
+ return {
169
+ todos: obj.todos.map((item, index) => {
170
+ if (!item || typeof item !== 'object' || typeof item.value !== 'string')
171
+ throw new Error(`Todo item ${index + 1} must contain a string value`);
172
+ return { value: item.value, checked: Boolean(item.checked) };
173
+ }),
174
+ };
175
+ }
176
+
177
+ export async function saveTodoList(input, obj) {
178
+ const path = todoPath(input);
179
+ const validated = validateTodoObject(obj);
180
+ await Bun.write(path, JSON.stringify(validated, null, 1) + '\n');
181
+ return { path };
182
+ }
183
+
184
+ export async function loadTodoList(input) {
185
+ const path = todoPath(input);
186
+ const file = Bun.file(path);
187
+ if (!await file.exists()) throw new Error(`Todo file not found: ${path}`);
188
+ const obj = validateTodoObject(JSON.parse(await file.text()));
189
+ return { path, obj };
190
+ }
191
+ ```
package/edit ADDED
@@ -0,0 +1,9 @@
1
+ #!/bin/sh
2
+
3
+ ':' //; sd=$(dirname "$(realpath "$0")")
4
+
5
+ ':' //; exec bun "$sd"/src/index.js --edit "$@"
6
+
7
+ process.argv.splice(2,0,'--edit')
8
+
9
+ await import('./src/index.js')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsmdcui",
3
- "version": "0.6.3",
3
+ "version": "0.8.0",
4
4
  "description": "Markdown as a Common UI for Terminals and Web Browsers",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/runmd.mjs CHANGED
@@ -3,9 +3,11 @@
3
3
  import path from 'node:path'
4
4
  import { pathToFileURL } from 'node:url'
5
5
  import { readInternalAssetText } from './src/runtime/assets.js'
6
+ import { fenceEventMap } from './src/cui/fence-events.mjs'
6
7
  import { REPO_ROOT } from './single-exe/compiled.js'
7
8
 
8
9
  const csl=console.log
10
+ const cse=console.error
9
11
  const mda=Bun.markdown.ansi
10
12
  const mdh=Bun.markdown.html
11
13
  const jss=JSON.stringify
@@ -18,7 +20,7 @@ const TEST_COL=5
18
20
  function logWroteFile(label,path)
19
21
  {
20
22
  if(!process.stdin.isRaw)
21
- csl(mda(`- Wrote to ${label} file: ${path}`))
23
+ cse(mda(`- Wrote to ${label} file: ${path}`))
22
24
  }
23
25
 
24
26
  async function readTemplate(pathname)
@@ -62,32 +64,35 @@ export async function main(tuiWidth=30)
62
64
 
63
65
  // 3. Create Terminal UI
64
66
  let tui = createTui(md,tuiWidth)
65
- csl(mda("\n# TUI"))
66
- csl(tui)
67
- csl(mda('## TUI raw'))
68
- csl(jss(tui))
67
+ cse(mda("\n# TUI"))
68
+ cse(tui)
69
+ cse(mda('## TUI raw'))
70
+ cse(jss(tui))
69
71
 
70
72
 
71
73
  // 4. Create Web UI
72
74
  let wui = await createWui(md,mdpath)
73
- csl(mda('\n# HTML'))
74
- csl(wui)
75
+ cse(mda('\n# HTML'))
76
+ cse(wui)
75
77
 
76
78
 
79
+ /*
77
80
  // 5. Get character from point for TUI
78
81
  let ch = charFromPoint(tui,TEST_ROW,TEST_COL)
79
82
 
80
- csl(mda(
83
+ cse(mda(
81
84
  '# Slicing row,col: '+TEST_ROW+','+TEST_COL
82
85
  ))
83
86
 
84
- csl(jss(ch))
87
+ cse(jss(ch))
88
+
89
+ */
85
90
 
86
91
 
87
92
  const serverPath = mdpath + "-server.js"
88
93
  const svmod = await import(pathToFileURL(path.resolve(serverPath)).href)
89
94
 
90
- csl("\n\n"+mda('# Server'))
95
+ cse("\n\n"+mda('# Server'))
91
96
  svmod.main();
92
97
  }
93
98
 
@@ -216,7 +221,7 @@ function escapeHtmlAttribute(value)
216
221
  .replaceAll(">", "&gt;");
217
222
  }
218
223
 
219
- export function convertWuiTextareas(html)
224
+ export function convertWuiTextareas(html, eventsById = new Map())
220
225
  {
221
226
  return String(html).replace(
222
227
  /<pre><code class="language-([^"]+)">([^]*?)<\/code><\/pre>/g,
@@ -226,10 +231,50 @@ export function convertWuiTextareas(html)
226
231
  const value = content.replace(/\n$/, "");
227
232
  const contentLines = value.split("\n");
228
233
  const cols = Math.max(1, ...contentLines.map(line => [...line].length));
234
+ const declaration = identity.id && eventsById.get(identity.id)?.tag === identity.tag
235
+ ? eventsById.get(identity.id)
236
+ : null;
237
+ const keydownHandler = declaration?.events.get("keydown");
238
+ const keydownCode = keydownHandler
239
+ ? [
240
+ "const __mdcuiKeyCode=Number(event.keyCode||event.which||0);",
241
+ "const __mdcuiCodeLetter=/^Key[A-Z]$/.test(event.code||\"\")?event.code.charCodeAt(3):0;",
242
+ "const __mdcuiLetterCode=__mdcuiKeyCode>=65&&__mdcuiKeyCode<=90?__mdcuiKeyCode:__mdcuiCodeLetter;",
243
+ "const __mdcuiAltGraph=!!event.getModifierState&&event.getModifierState(\"AltGraph\");",
244
+ "const __mdcuiLetter=!__mdcuiAltGraph&&(event.ctrlKey||event.altKey||event.metaKey)&&__mdcuiLetterCode>=65&&__mdcuiLetterCode<=90;",
245
+ "if(__mdcuiLetter)Object.defineProperty(event,\"key\",{configurable:true,value:String.fromCharCode(__mdcuiLetterCode+(event.shiftKey?0:32))});",
246
+ "this.__mdcuiIdentifiedKeydown=!!event.key&&event.key!==\"Unidentified\";",
247
+ "this.__mdcuiUnidentifiedKeydown=event.key===\"Unidentified\"?{keyCode:__mdcuiKeyCode,ctrlKey:!!event.ctrlKey,shiftKey:!!event.shiftKey,altKey:!!event.altKey,metaKey:!!event.metaKey,altGraph:__mdcuiAltGraph}:null;",
248
+ "clearTimeout(this.__mdcuiKeydownReset);",
249
+ "this.__mdcuiKeydownReset=setTimeout(()=>{this.__mdcuiIdentifiedKeydown=false;this.__mdcuiUnidentifiedKeydown=null},0);",
250
+ "if(event.key!==\"Unidentified\"){\n",
251
+ "Object.defineProperty(event,\"toJSON\",{configurable:true,value:function(){const t=this.target||{};return{type:String(this.type||\"\"),key:String(this.key||\"\"),code:String(this.code||\"\"),raw:String(this.raw||\"\"),ctrlKey:!!this.ctrlKey,shiftKey:!!this.shiftKey,altKey:!!this.altKey,metaKey:!!this.metaKey,repeat:!!this.repeat,defaultPrevented:!!this.defaultPrevented,target:{id:String(t.id||\"\"),tagName:String(t.tagName||\"\"),className:String(t.className||\"\"),value:String(t.value??\"\")}}}});",
252
+ keydownHandler.modifiers.includes("prevent")
253
+ ? "event.preventDefault();"
254
+ : "",
255
+ keydownHandler.code,
256
+ "\n}",
257
+ ].join("")
258
+ : "";
259
+ const beforeInputCode = keydownHandler
260
+ ? [
261
+ "if(!this.__mdcuiIdentifiedKeydown&&event.data!=null&&event.data!==\"\"){",
262
+ "const m=this.__mdcuiUnidentifiedKeydown||{};",
263
+ "const letter=!m.altGraph&&(m.ctrlKey||m.altKey||m.metaKey)&&m.keyCode>=65&&m.keyCode<=90?String.fromCharCode(m.keyCode+(m.shiftKey?0:32)):String(event.data);",
264
+ "Object.defineProperties(event,{key:{configurable:true,value:letter},ctrlKey:{configurable:true,value:!!m.ctrlKey},shiftKey:{configurable:true,value:!!m.shiftKey},altKey:{configurable:true,value:!!m.altKey},metaKey:{configurable:true,value:!!m.metaKey}});",
265
+ "this.onkeydown(event)",
266
+ "}",
267
+ ].join("")
268
+ : "";
269
+ const inlineEventAttrs = [
270
+ keydownCode ? `onkeydown="${escapeHtmlAttribute(keydownCode)}"` : "",
271
+ beforeInputCode ? `onbeforeinput="${escapeHtmlAttribute(beforeInputCode)}"` : "",
272
+ ];
229
273
  const attrs = [
230
274
  `data-mdcui-tag="${identity.tag}"`,
231
275
  `data-mdcui-language="${escapeHtmlAttribute(info)}"`,
232
276
  identity.id ? `id="${escapeHtmlAttribute(identity.id)}"` : "",
277
+ ...inlineEventAttrs,
233
278
  `class="${escapeHtmlAttribute([
234
279
  `language-${info}`,
235
280
  ...identity.classes,
@@ -302,6 +347,7 @@ export function wrapWuiHeadingSections(html)
302
347
 
303
348
  export async function createWui(md,mdpath) // HTML
304
349
  {
350
+ const eventsById = fenceEventMap(md)
305
351
 
306
352
  const opts = {
307
353
  headings: { ids: true }
@@ -331,19 +377,27 @@ export async function createWui(md,mdpath) // HTML
331
377
  }
332
378
  )
333
379
 
334
- md = convertWuiTextareas(md)
380
+ md = convertWuiTextareas(md, eventsById)
335
381
  md = wrapWuiHeadingSections(md)
336
382
 
337
383
  const mdb = path.basename(mdpath);
338
384
 
385
+ const responsiveImageStyle = `<style>
386
+ img {
387
+ max-width: 100%;
388
+ height: auto;
389
+ }
390
+ </style>`;
339
391
  const moduleScript = `<scr`+`ipt type="module" src="./${mdb}.front.js"></scr`+`ipt>`;
340
- if (!/^\s*<!doctype html>/i.test(md)) {
392
+ const isFullHtmlDocument = /^\s*<!doctype html>/i.test(md);
393
+ if (!isFullHtmlDocument) {
341
394
  md = `<!doctype html>
342
395
  <html lang="zh-TW">
343
396
  <head>
344
397
  <meta charset="utf-8">
345
398
  <meta name="viewport" content="width=device-width, initial-scale=1">
346
399
  <title>${escapeHtmlAttribute(mdb)}</title>
400
+ ${responsiveImageStyle}
347
401
  </head>
348
402
  <body>
349
403
  ${md}
@@ -351,10 +405,17 @@ ${moduleScript}
351
405
  </body>
352
406
  </html>
353
407
  `;
354
- } else if (/<\/body\s*>/i.test(md)) {
355
- md = md.replace(/<\/body\s*>/i, `${moduleScript}\n</body>`);
356
408
  } else {
357
- md += `\n${moduleScript}\n`;
409
+ if (/<\/head\s*>/i.test(md)) {
410
+ md = md.replace(/<\/head\s*>/i, `${responsiveImageStyle}\n</head>`);
411
+ } else {
412
+ md = `${responsiveImageStyle}\n${md}`;
413
+ }
414
+ if (/<\/body\s*>/i.test(md)) {
415
+ md = md.replace(/<\/body\s*>/i, `${moduleScript}\n</body>`);
416
+ } else {
417
+ md += `\n${moduleScript}\n`;
418
+ }
358
419
  }
359
420
 
360
421