feat: txt/tsx/css等全部文本开编辑模式 + 代码语法着色(highlight.js GitHub Dark配色)
This commit is contained in:
Generated
+11
@@ -10,6 +10,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
|
"highlight.js": "^11.12.0",
|
||||||
"markdown-it": "^15.0.0",
|
"markdown-it": "^15.0.0",
|
||||||
"xterm": "^5.3.0"
|
"xterm": "^5.3.0"
|
||||||
},
|
},
|
||||||
@@ -544,6 +545,16 @@
|
|||||||
"@esbuild/win32-x64": "0.25.12"
|
"@esbuild/win32-x64": "0.25.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/highlight.js": {
|
||||||
|
"version": "11.12.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.12.0.tgz",
|
||||||
|
"integrity": "sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/linkify-it": {
|
"node_modules/linkify-it": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-6.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-6.1.0.tgz",
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
|
"highlight.js": "^11.12.0",
|
||||||
"markdown-it": "^15.0.0",
|
"markdown-it": "^15.0.0",
|
||||||
"xterm": "^5.3.0"
|
"xterm": "^5.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,16 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
|||||||
import { Terminal } from 'xterm'
|
import { Terminal } from 'xterm'
|
||||||
import 'xterm/css/xterm.css'
|
import 'xterm/css/xterm.css'
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import hljs from 'highlight.js/lib/core'
|
||||||
|
import hljsTypescript from 'highlight.js/lib/languages/typescript'
|
||||||
|
import hljsJavascript from 'highlight.js/lib/languages/javascript'
|
||||||
|
import hljsCss from 'highlight.js/lib/languages/css'
|
||||||
|
import hljsJson from 'highlight.js/lib/languages/json'
|
||||||
|
|
||||||
|
hljs.registerLanguage('typescript', hljsTypescript)
|
||||||
|
hljs.registerLanguage('javascript', hljsJavascript)
|
||||||
|
hljs.registerLanguage('css', hljsCss)
|
||||||
|
hljs.registerLanguage('json', hljsJson)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* dsh-worktable 乐高式工作区 M1:通用分栏引擎(PRD §13)。
|
* dsh-worktable 乐高式工作区 M1:通用分栏引擎(PRD §13)。
|
||||||
@@ -1121,7 +1131,23 @@ function FileViewer(props: { path: string }) {
|
|||||||
return <TextViewer path={props.path} fileUrl={fileUrl} isMd={MD_EXTS.test('.' + ext)} />
|
return <TextViewer path={props.path} fileUrl={fileUrl} isMd={MD_EXTS.test('.' + ext)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
/** MD/TXT 文本预览(fetch 原文 → MD 渲染或 <pre> 等宽展示);MD 支持编辑/预览自由切换并可保存回磁盘 */
|
/** 代码文件语言映射(预览语法着色) */
|
||||||
|
const CODE_LANGS: Record<string, string> = { ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript', css: 'css', json: 'json' }
|
||||||
|
const CODE_EXTS = /[.](tsx|ts|jsx|js|css|json)$/i
|
||||||
|
|
||||||
|
function escapeHtml(s: string): string {
|
||||||
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
}
|
||||||
|
|
||||||
|
function codeHtml(text: string, ext: string): string {
|
||||||
|
const lang = CODE_LANGS[ext] ?? ''
|
||||||
|
if (lang) {
|
||||||
|
try { return hljs.highlight(text, { language: lang }).value } catch { return escapeHtml(text) }
|
||||||
|
}
|
||||||
|
return escapeHtml(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 文本预览(fetch 原文 → MD 渲染 / 代码高亮 / <pre> 等宽展示);全部文本类型支持编辑/预览切换并可保存回磁盘 */
|
||||||
function TextViewer(props: { path: string; fileUrl: string; isMd: boolean }) {
|
function TextViewer(props: { path: string; fileUrl: string; isMd: boolean }) {
|
||||||
const [text, setText] = useState<string | null>(null)
|
const [text, setText] = useState<string | null>(null)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
@@ -1167,20 +1193,22 @@ function TextViewer(props: { path: string; fileUrl: string; isMd: boolean }) {
|
|||||||
if (text == null) {
|
if (text == null) {
|
||||||
return <div className="dsh-wt_paneWip"><span className="dsh-wt_paneWipText">{T('file.loading')}</span></div>
|
return <div className="dsh-wt_paneWip"><span className="dsh-wt_paneWipText">{T('file.loading')}</span></div>
|
||||||
}
|
}
|
||||||
if (props.isMd) {
|
const ext = (props.path.split('.').pop() || '').toLowerCase()
|
||||||
return (
|
const isCode = CODE_EXTS.test('.' + ext)
|
||||||
<>
|
return (
|
||||||
<div className="dsh-wt_mdBar">
|
<>
|
||||||
<button type="button" className={'dsh-wt_mdBtn' + (mode === 'preview' ? ' dsh-wt_mdBtnOn' : '')} onClick={() => setMode('preview')}>{T('file.preview')}</button>
|
<div className="dsh-wt_mdBar">
|
||||||
<button type="button" className={'dsh-wt_mdBtn' + (mode === 'edit' ? ' dsh-wt_mdBtnOn' : '')} onClick={enterEdit}>{T('file.edit')}</button>
|
<button type="button" className={'dsh-wt_mdBtn' + (mode === 'preview' ? ' dsh-wt_mdBtnOn' : '')} onClick={() => setMode('preview')}>{T('file.preview')}</button>
|
||||||
{mode === 'edit' && (
|
<button type="button" className={'dsh-wt_mdBtn' + (mode === 'edit' ? ' dsh-wt_mdBtnOn' : '')} onClick={enterEdit}>{T('file.edit')}</button>
|
||||||
<button type="button" className="dsh-wt_mdSave" disabled={saving} onClick={save}>{saving ? '…' : T('file.save')}</button>
|
{mode === 'edit' && (
|
||||||
)}
|
<button type="button" className="dsh-wt_mdSave" disabled={saving} onClick={save}>{saving ? '…' : T('file.save')}</button>
|
||||||
{saveFail && <span className="dsh-wt_mdMsg">{T('file.saveFail')}</span>}
|
)}
|
||||||
</div>
|
{saveFail && <span className="dsh-wt_mdMsg">{T('file.saveFail')}</span>}
|
||||||
{mode === 'edit'
|
</div>
|
||||||
? <textarea className="dsh-wt_mdEdit" value={draft} spellCheck={false} onChange={(e) => setDraft(e.target.value)} />
|
{mode === 'edit'
|
||||||
: (
|
? <textarea className="dsh-wt_mdEdit" value={draft} spellCheck={false} onChange={(e) => setDraft(e.target.value)} />
|
||||||
|
: props.isMd
|
||||||
|
? (
|
||||||
<div className="dsh-wt_fileView">
|
<div className="dsh-wt_fileView">
|
||||||
<div
|
<div
|
||||||
className="dsh-wt_md"
|
className="dsh-wt_md"
|
||||||
@@ -1194,11 +1222,16 @@ function TextViewer(props: { path: string; fileUrl: string; isMd: boolean }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
</>
|
: isCode
|
||||||
)
|
? (
|
||||||
}
|
<div className="dsh-wt_fileView">
|
||||||
return <div className="dsh-wt_fileView"><pre className="dsh-wt_txt">{text}</pre></div>
|
<pre className="dsh-wt_code"><code dangerouslySetInnerHTML={{ __html: codeHtml(text, ext) }} /></pre>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: <div className="dsh-wt_fileView"><pre className="dsh-wt_txt">{text}</pre></div>}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单个标签页的内容渲染 */
|
/** 单个标签页的内容渲染 */
|
||||||
|
|||||||
@@ -93,6 +93,20 @@ export const css = [
|
|||||||
'.dsh-wt_mdSave:disabled{opacity:.6;cursor:default}',
|
'.dsh-wt_mdSave:disabled{opacity:.6;cursor:default}',
|
||||||
'.dsh-wt_mdMsg{font-size:11px;color:var(--dsw-alias-state-danger,#e5484d)}',
|
'.dsh-wt_mdMsg{font-size:11px;color:var(--dsw-alias-state-danger,#e5484d)}',
|
||||||
'.dsh-wt_mdEdit{flex:1;min-height:0;width:100%;box-sizing:border-box;padding:12px 14px;background:var(--dsw-alias-bg-base,#0b0e14);color:var(--dsw-alias-label-primary,#e6e8eb);border:none;outline:none;resize:none;font-family:var(--dsw-font-mono,Consolas,Menlo,monospace);font-size:12px;line-height:1.7}',
|
'.dsh-wt_mdEdit{flex:1;min-height:0;width:100%;box-sizing:border-box;padding:12px 14px;background:var(--dsw-alias-bg-base,#0b0e14);color:var(--dsw-alias-label-primary,#e6e8eb);border:none;outline:none;resize:none;font-family:var(--dsw-font-mono,Consolas,Menlo,monospace);font-size:12px;line-height:1.7}',
|
||||||
|
// 代码语法着色(GitHub Dark 风格 token 配色)
|
||||||
|
'.dsh-wt_code{margin:0;font-family:var(--dsw-font-mono,Consolas,Menlo,monospace);font-size:12px;line-height:1.65;white-space:pre;word-break:normal}',
|
||||||
|
'.dsh-wt_code .hljs-keyword,.dsh-wt_code .hljs-selector-tag,.dsh-wt_code .hljs-literal,.dsh-wt_code .hljs-doctag{color:#ff7b72}',
|
||||||
|
'.dsh-wt_code .hljs-string,.dsh-wt_code .hljs-attr,.dsh-wt_code .hljs-regexp{color:#a5d6ff}',
|
||||||
|
'.dsh-wt_code .hljs-comment,.dsh-wt_code .hljs-quote{color:#8b949e;font-style:italic}',
|
||||||
|
'.dsh-wt_code .hljs-number,.dsh-wt_code .hljs-symbol,.dsh-wt_code .hljs-bullet{color:#79c0ff}',
|
||||||
|
'.dsh-wt_code .hljs-title,.dsh-wt_code .hljs-function .hljs-title,.dsh-wt_code .hljs-section{color:#d2a8ff}',
|
||||||
|
'.dsh-wt_code .hljs-built_in,.dsh-wt_code .hljs-type,.dsh-wt_code .hljs-class .hljs-title{color:#ffa657}',
|
||||||
|
'.dsh-wt_code .hljs-variable,.dsh-wt_code .hljs-params,.dsh-wt_code .hljs-meta{color:#e6edf3}',
|
||||||
|
'.dsh-wt_code .hljs-property,.dsh-wt_code .hljs-attribute{color:#79c0ff}',
|
||||||
|
'.dsh-wt_code .hljs-tag,.dsh-wt_code .hljs-name{color:#7ee787}',
|
||||||
|
'.dsh-wt_code .hljs-selector-class,.dsh-wt_code .hljs-selector-id{color:#d2a8ff}',
|
||||||
|
'.dsh-wt_code .hljs-addition{color:#7ee787}',
|
||||||
|
'.dsh-wt_code .hljs-deletion{color:#ff7b72}',
|
||||||
'.dsh-wt_md h1,.dsh-wt_md h2,.dsh-wt_md h3,.dsh-wt_md h4{margin:14px 0 8px;line-height:1.3;font-weight:600;color:var(--dsw-alias-label-primary,#e6e8eb)}',
|
'.dsh-wt_md h1,.dsh-wt_md h2,.dsh-wt_md h3,.dsh-wt_md h4{margin:14px 0 8px;line-height:1.3;font-weight:600;color:var(--dsw-alias-label-primary,#e6e8eb)}',
|
||||||
'.dsh-wt_md h1{font-size:20px;border-bottom:1px solid var(--dsw-alias-border-l1,#262b36);padding-bottom:6px}',
|
'.dsh-wt_md h1{font-size:20px;border-bottom:1px solid var(--dsw-alias-border-l1,#262b36);padding-bottom:6px}',
|
||||||
'.dsh-wt_md h2{font-size:16.5px;border-bottom:1px solid var(--dsw-alias-border-l1,#262b36);padding-bottom:5px}',
|
'.dsh-wt_md h2{font-size:16.5px;border-bottom:1px solid var(--dsw-alias-border-l1,#262b36);padding-bottom:5px}',
|
||||||
|
|||||||
@@ -639,17 +639,16 @@
|
|||||||
原生视图内部是浏览器扩展页,插件注入不了按钮、拦不到鼠标,也驱动不了内部滚动——
|
原生视图内部是浏览器扩展页,插件注入不了按钮、拦不到鼠标,也驱动不了内部滚动——
|
||||||
向用户如实说明后按指示回退);② 视图选项改名「设置」,点开直接内嵌「排序方式 + 管理项目
|
向用户如实说明后按指示回退);② 视图选项改名「设置」,点开直接内嵌「排序方式 + 管理项目
|
||||||
展开列表」,不再二次点击;③ 管理项目里给现有项目加「变更视图」:换预设拓扑,
|
展开列表」,不再二次点击;③ 管理项目里给现有项目加「变更视图」:换预设拓扑,
|
||||||
## 补记(MD 编辑模式:预览/编辑自由切换 + 保存回磁盘)
|
## 补记(txt/tsx/css 编辑开启 + 代码语法着色)
|
||||||
|
|
||||||
- 用户需求:MD 文件加编辑模式,可在编辑与预览之间自由切换。
|
- 用户需求:① txt/tsx/css 也开编辑模式;② 代码文件要有颜色(考虑开源后程序员使用)。
|
||||||
- 处理:
|
- 处理:
|
||||||
① 服务端新增 POST /api/worktable/write({path,content},20MB 上限,writeFile utf8);
|
① TextViewer 编辑/预览工具栏对所有文本类型通用(md/txt/log/tsx/ts/jsx/js/css/json),
|
||||||
② 客户端 TextViewer(MD 分支)加工具栏:预览 | 编辑 切换按钮(当前态描边高亮),
|
保存走既有 /api/worktable/write 路由;
|
||||||
编辑态出现 保存 按钮(保存成功切回预览,失败红字提示);编辑区等宽 textarea 暗色;
|
② 语法着色:highlight.js(只打包 core + typescript/javascript/css/json 四种语言),
|
||||||
保存内容即时成为新预览内容(无需重新 fetch);txt/tsx/css 等保持只读不变;
|
代码预览渲染成 .dsh-wt_code(token class 配色采用 GitHub Dark 风格自写 CSS,无主题依赖);
|
||||||
③ locale file.preview/edit/save/saveFail(zh/en)。
|
无语言映射时 HTML 转义兜底;bundle 859KB。
|
||||||
- 验证(functional-diag STEP16 + 临时夹具 wt-edit-test.md):探针检测写路由;
|
- 验证(functional-diag):STEP15 tsx 预览 hljs-keyword 493 个 token + 编辑区出现/含原文/切回预览;
|
||||||
就绪后:编辑按钮 → textarea 出现且含原文 → 原生 setter 写入 '# EDITED_OK' → 保存 →
|
STEP11 package.json 改断言 json 高亮 31 个 token;其余全绿 ERROR_COUNT: 0;
|
||||||
预览回归 + file 路由回读含 EDITED_OK;跑完还原夹具。当前运行中服务器无写路由 →
|
STEP16 写路由仍 writeRouteReady=false(等用户重启后验证保存闭环)。
|
||||||
writeRouteReady=false(404 已过滤),其余 15 STEP 全绿。
|
- 备注:客户端 F5 生效;保存写盘需重启一次 dsh web(上次已提示)。
|
||||||
- 备注:服务端新增写路由,**需重启一次 dsh web** 后编辑保存生效。
|
|
||||||
|
|||||||
@@ -282,12 +282,24 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|||||||
st.openTab('main',0,{kind:'file',path:'E:\\\\AI_Workspace\\\\DeepseekHarness\\\\Projects\\\\dsh-worktable\\\\01_content\\\\src\\\\client\\\\index.tsx'});
|
st.openTab('main',0,{kind:'file',path:'E:\\\\AI_Workspace\\\\DeepseekHarness\\\\Projects\\\\dsh-worktable\\\\01_content\\\\src\\\\client\\\\index.tsx'});
|
||||||
await new Promise(function(r){setTimeout(r,1200)});
|
await new Promise(function(r){setTimeout(r,1200)});
|
||||||
out.tsxTab=st.spec.main[0].tabs[0].title;
|
out.tsxTab=st.spec.main[0].tabs[0].title;
|
||||||
out.tsxView=!!document.querySelector('.dsh-wt_txt');
|
out.tsxView=!!document.querySelector('.dsh-wt_code');
|
||||||
out.tsxHasText=document.querySelector('.dsh-wt_txt')?String(document.querySelector('.dsh-wt_txt').textContent).indexOf('WorktableSection')>=0:false;
|
out.tsxHljs=document.querySelectorAll('.dsh-wt_code .hljs-keyword').length;
|
||||||
|
out.tsxHasText=document.querySelector('.dsh-wt_code')?String(document.querySelector('.dsh-wt_code').textContent).indexOf('WorktableSection')>=0:false;
|
||||||
st.openTab('main',0,{kind:'file',path:'E:\\\\AI_Workspace\\\\DeepseekHarness\\\\Projects\\\\dsh-worktable\\\\01_content\\\\src\\\\client\\\\styles.ts'});
|
st.openTab('main',0,{kind:'file',path:'E:\\\\AI_Workspace\\\\DeepseekHarness\\\\Projects\\\\dsh-worktable\\\\01_content\\\\src\\\\client\\\\styles.ts'});
|
||||||
await new Promise(function(r){setTimeout(r,900)});
|
await new Promise(function(r){setTimeout(r,900)});
|
||||||
out.cssTab=st.spec.main[0].tabs[1].title;
|
out.cssTab=st.spec.main[0].tabs[1].title;
|
||||||
out.tabsCount=st.spec.main[0].tabs.length;
|
out.tabsCount=st.spec.main[0].tabs.length;
|
||||||
|
// 代码文件编辑模式(tsx/css/txt 通用)
|
||||||
|
var ebtn=document.querySelectorAll('.dsh-wt_mdBtn')[1];
|
||||||
|
if(ebtn){ ebtn.click(); }
|
||||||
|
await new Promise(function(r){setTimeout(r,250)});
|
||||||
|
var ta=document.querySelector('.dsh-wt_mdEdit');
|
||||||
|
out.editArea=!!ta;
|
||||||
|
out.editHasText=ta?ta.value.indexOf('dsh-wt_')>=0:false;
|
||||||
|
var pbtn=document.querySelectorAll('.dsh-wt_mdBtn')[0];
|
||||||
|
if(pbtn){ pbtn.click(); }
|
||||||
|
await new Promise(function(r){setTimeout(r,200)});
|
||||||
|
out.previewBack=!!document.querySelector('.dsh-wt_code');
|
||||||
st.close();
|
st.close();
|
||||||
}catch(err){ out.err=String(err) }
|
}catch(err){ out.err=String(err) }
|
||||||
return JSON.stringify(out);
|
return JSON.stringify(out);
|
||||||
@@ -377,7 +389,8 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|||||||
out.tabsAfterDup=st.spec.main[0].tabs.length;
|
out.tabsAfterDup=st.spec.main[0].tabs.length;
|
||||||
st.openTab('main',0,{kind:'file',path:'E:\\\\AI_Workspace\\\\DeepseekHarness\\\\Projects\\\\dsh-worktable\\\\01_content\\\\package.json'});
|
st.openTab('main',0,{kind:'file',path:'E:\\\\AI_Workspace\\\\DeepseekHarness\\\\Projects\\\\dsh-worktable\\\\01_content\\\\package.json'});
|
||||||
await new Promise(function(r){setTimeout(r,900)});
|
await new Promise(function(r){setTimeout(r,900)});
|
||||||
out.txtView=!!document.querySelector('.dsh-wt_txt');
|
out.jsonView=!!document.querySelector('.dsh-wt_code');
|
||||||
|
out.jsonHljs=document.querySelectorAll('.dsh-wt_code .hljs-keyword, .dsh-wt_code .hljs-string').length;
|
||||||
out.tabsNow=st.spec.main[0].tabs.length;
|
out.tabsNow=st.spec.main[0].tabs.length;
|
||||||
st.close();
|
st.close();
|
||||||
return JSON.stringify(out);
|
return JSON.stringify(out);
|
||||||
|
|||||||
Reference in New Issue
Block a user