fix(creator-studio): render math/footnote shortcodes, inline code blocks, block deletion
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
7885f42cff
commit
6928127bfa
11 files changed
+235
-202
No files matched your search
@@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-element', 'wp-i18n'), 'version' => '5e199a1baccb9f8673e9');
|
||||
<?php return array('dependencies' => array('react', 'wp-element', 'wp-i18n'), 'version' => 'b411c17006fd7ed53e70');
|
||||
@@ -1,3 +1,3 @@
|
||||
(()=>{"use strict";const e=window.React,t=window.wp.element,a=window.wp.i18n;function n(e,t){const a=e&&(e.message||e.error)||(401===t?"登录状态已失效,请刷新页面重新登录。":"请求失败,请稍后重试。"),n=new Error(String(a));return n.status=t,n.code=e&&e.code?String(e.code):"",n.data=e,n}async function r(e){try{return await e.json()}catch(e){return null}}async function o(e,t,a){const o=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-WP-Nonce":a},body:JSON.stringify(t)}),l=await r(o);if(!o.ok)throw n(l,o.status);return l}async function l(e,t,a,o={}){const l=new FormData;l.append("file",t),Object.entries(o).forEach(([e,t])=>l.append(e,t));const i=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"X-WP-Nonce":a},body:l}),c=await r(i);if(!i.ok)throw n(c,i.status);return c}const i=window.jioCreatorStudio||{},c=i.nonce||"";function s(n){const{draft:r,setDraft:o,capabilities:s,limits:u,taxonomy:d,isPublishedEdit:m,onClose:p,onSaveDraft:_,onSubmit:b,privateStorage:f,privateUploads:g,saving:h,captcha:j}=n,[E,y]=(0,t.useState)(""),[v,k]=(0,t.useState)(""),[N,w]=(0,t.useState)(!1),C=(0,t.useRef)(null),S=(0,t.useRef)(null);(0,t.useEffect)(()=>{const e=S.current,t=document.getElementById("jio-creator-captcha");if(!e||!t)return;const a=t.parentElement;return e.appendChild(t),t.removeAttribute("hidden"),()=>{a&&(a.appendChild(t),t.setAttribute("hidden",""))}},[]);const x=(0,t.useMemo)(()=>(r.title||"").trim().length,[r.title])>=Number(u.titleMin||1),M=()=>{const e=E.trim();if(!e)return;const t=e.split(/[,,\n]+/u).map(e=>e.trim()).filter(Boolean);o({tags:Array.from(new Set([...r.tags,...t]))}),y("")};return(0,e.createElement)("div",{className:"jio-creator-drawer",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-publish-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:p}),(0,e.createElement)("div",{className:"jio-creator-drawer__panel"},(0,e.createElement)("header",{className:"jio-creator-drawer__head"},(0,e.createElement)("h2",{id:"jio-creator-publish-title"},m?(0,a.__)("提交改稿","jio"):(0,a.__)("发布文章","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__close",onClick:p,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-drawer__body"},m&&(0,e.createElement)("p",{className:"jio-creator-drawer__note"},(0,a.__)("审核通过前,读者看到的仍然是当前线上版本。","jio")),(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("分类","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},0===d.categories.length&&(0,e.createElement)("span",{className:"jio-creator-muted"},(0,a.__)("暂无可选分类","jio")),d.categories.map(t=>(0,e.createElement)("label",{key:t.id,className:"jio-creator-chip"+(r.categories.includes(t.id)?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:r.categories.includes(t.id),onChange:()=>(e=>{const t=r.categories.includes(e);o({categories:t?r.categories.filter(t=>t!==e):[...r.categories,e]})})(t.id)}),(0,e.createElement)("span",null,t.name))))),d.tags&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("标签","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},r.tags.map(t=>(0,e.createElement)("span",{key:t,className:"jio-creator-chip is-active"},(0,e.createElement)("span",null,t),(0,e.createElement)("button",{type:"button",onClick:()=>o({tags:r.tags.filter(e=>e!==t)}),"aria-label":(0,a.sprintf)((0,a.__)("移除标签 %s","jio"),t)},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))))),(0,e.createElement)("div",{className:"jio-creator-inline"},(0,e.createElement)("input",{type:"text",value:E,placeholder:(0,a.__)("输入标签,逗号分隔","jio"),onChange:e=>y(e.target.value),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),M())}}),(0,e.createElement)("button",{type:"button",onClick:M},(0,a.__)("添加","jio")))),d.customDefs.map(t=>(0,e.createElement)("section",{className:"jio-creator-field",key:t.key},(0,e.createElement)("h3",null,t.name),(0,e.createElement)("div",{className:"jio-creator-chips"},t.values.map(a=>{const n=r.custom[t.key]||[],l=n.includes(a.key);return(0,e.createElement)("label",{key:a.key,className:"jio-creator-chip"+(l?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:l,onChange:()=>o({custom:{...r.custom,[t.key]:l?n.filter(e=>e!==a.key):[...n,a.key]}})}),(0,e.createElement)("span",null,a.name))})))),s.canPay&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("div",{className:"jio-creator-switch"},(0,e.createElement)("div",null,(0,e.createElement)("h3",null,(0,a.__)("付费内容","jio")),(0,e.Line truncated
|
||||
(0,a.__)("按站点时区(%s)发布,至少需要晚于当前时间 1 分钟。","jio"),String(m.timezone||"UTC")))))}const g={ready:(0,a.__)("尚未保存","jio"),saving:(0,a.__)("保存中…","jio"),saved:(0,a.__)("已保存","jio"),error:(0,a.__)("保存失败","jio"),conflict:(0,a.__)("内容已在别处更新","jio"),submitting:(0,a.__)("提交中…","jio"),submitted:(0,a.__)("已提交","jio")};function h({value:t}){return(0,e.createElement)("span",{className:`jio-creator__save is-${t.state}`,role:"status","aria-live":"polite"},(0,e.createElement)("i",{className:"fa fas fa-circle","aria-hidden":"true"}),(0,e.createElement)("span",null,g[t.state]||g.ready),"saved"===t.state&&t.at?(0,e.createElement)("time",null,t.at):null)}function j(e){const t=(e.downloads||[]).map(e=>{const t="string"==typeof e.assetKey?e.assetKey:"";if(t)return{type:"protected",asset_key:t,label:"string"==typeof e.label?e.label:""};const a="string"==typeof e.url?e.url.trim():"";return a?{type:"direct",url:a,label:"string"==typeof e.label?e.label:"",extract_code:"string"==typeof e.extractCode?e.extractCode:""}:null}).filter(Boolean);return{enabled:!!e.enabled,fields:{pay_type:e.enabled?t.length>0?"download":"reading":"no",pay_modo:"points"===e.modo?"points":"0",price:"points"===e.modo?"0.00":String(e.price||"0.00"),points_price:"points"===e.modo?Math.max(0,Number(e.points)||0):0,vip1_price:"points"===e.modo?"0.00":String(e.vip1Price||"0.00"),vip2_price:"points"===e.modo?"0.00":String(e.vip2Price||"0.00"),pay_limit:Number(e.payLimit)||0,downloads:t}}}const E=e=>e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),y=[{value:"generic",label:(0,a.__)("通用","jio")},{value:"javascript",label:"JavaScript"},{value:"typescript",label:"TypeScript"},{value:"php",label:"PHP"},{value:"python",label:"Python"},{value:"java",label:"Java"},{value:"go",label:"Go"},{value:"rust",label:"Rust"},{value:"csharp",label:"C#"},{value:"cpp",label:"C++"},{value:"sql",label:"SQL"},{value:"json",label:"JSON"},{value:"bash",label:"Shell"},{value:"css",label:"CSS"},{value:"html",label:"HTML"},{value:"markdown",label:"Markdown"}];function v(e="generic",t=""){const n=E(""!==t?t:(0,a.__)("在此输入代码","jio"));return`<pre class="enlighter-pre" data-enlighter-language="${E(e)}"><code class="gl" data-enlighter-language="${E(e)}">`+n+"</code></pre><p></p>"}const k=(e,t,a)=>Math.min(a,Math.max(t,Number.isFinite(e)?Math.round(e):t)),N={table:(0,a.__)("插入表格","jio"),math:(0,a.__)("插入公式","jio"),footnote:(0,a.__)("插入注释","jio"),code:(0,a.__)("插入代码块","jio")};function w({kind:n,initialLanguage:r="generic",initialCode:o="",onSubmit:l,onClose:i}){const[c,s]=(0,t.useState)(3),[u,d]=(0,t.useState)(3),[m,p]=(0,t.useState)(!0),[_,b]=(0,t.useState)(!1),[f,g]=(0,t.useState)("a^2 + b^2 = c^2"),[h,j]=(0,t.useState)(!0),[E,v]=(0,t.useState)(""),[w,C]=(0,t.useState)(r),[S,x]=(0,t.useState)(o),M=(0,t.useRef)(null),q=(0,t.useRef)(null),L=(0,t.useRef)(null),$=(0,t.useRef)(null);(0,t.useEffect)(()=>{(q.current||L.current||$.current)?.focus();const e=e=>{"Escape"===e.key&&(e.preventDefault(),i())};return document.addEventListener("keydown",e),()=>document.removeEventListener("keydown",e)},[i]);const I=(0,t.useMemo)(()=>"footnote"===n?""!==E.trim():"math"!==n||""!==f.trim(),[n,E,f]);return(0,e.createElement)("div",{className:"jio-creator-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-dialog-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:i}),(0,e.createElement)("div",{className:"jio-creator-dialog__panel",ref:M},(0,e.createElement)("header",{className:"jio-creator-dialog__head"},(0,e.createElement)("h2",{id:"jio-creator-dialog-title"},N[n]),(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__close",onClick:i,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-dialog__body"},"table"===n&&(0,e.createElement)(e.Fragment,null,(0,e.createElement)("div",{className:"jio-creator-dialog__row"},(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("行数","jio")),(0,e.createElement)("input",{ref:q,type:"number",min:1,max:20,value:c,onChange:e=>s(Number(e.target.value))})),(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("列数","jio")),(0,e.createElement)("input",{type:"number",min:1,max:10,value:u,onChange:e=>d(Number(e.target.value))}))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:m,onChange:e=>p(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("首行为表头","jio"))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:_,onChange:e=>b(e.target.checked)}),(0,e.createLine truncated
|
||||
(0,a.__)("请输入标题(最多 %d 个字)","jio"),Number(Q.titleMax)||100),spellCheck:!1,maxLength:Number(Q.titleMax)||100})),(0,e.createElement)("div",{className:"jio-creator__editor","data-editor-ready":_?"true":"false"},(0,e.createElement)("textarea",{id:M,defaultValue:n.content,onChange:e=>G(e.target.value),"aria-label":(0,a.__)("文章正文","jio")}))),(0,e.createElement)(f,{featured:n.featured,canEditCover:!!X.canUploadImage,canSchedule:!!X.canSchedule,scheduleEnabled:!!n.schedule.enabled,scheduleAt:n.schedule.at,onFeaturedChange:e=>W({featured:e}),onScheduleChange:(e,t)=>W({schedule:{enabled:e,at:t}})}),U&&(0,e.createElement)(w,{kind:U,onSubmit:fe,onClose:()=>z(null)}),F&&(0,e.createElement)(w,{kind:"code",initialLanguage:F.language,initialCode:F.code,onSubmit:e=>{if(T(null),"code"!==e.kind)return;const t=A.current;if(A.current=null,pe(v(e.language,e.code)),t){const e=i.current;e&&!e.removed&&(e.dom.remove(t),G(e.getContent()))}},onClose:()=>{T(null),A.current=null}}),D&&(0,e.createElement)(s,{draft:n,setDraft:W,capabilities:X,limits:Q,taxonomy:{categories:C.categories&&C.categories.items||[],tags:!!C.tags?.enabled,customDefs:C.customDefs||[]},isPublishedEdit:Y,onClose:()=>P(!1),onSaveDraft:()=>{ae(!1)},onSubmit:e=>{ne(e)},privateStorage:!!C.privateStorage,privateUploads:C.privateUploads||[],captcha:!!C.captcha?.enabled,saving:"saving"===I.state||"submitting"===I.state}))}function R(){const a=document.getElementById("jio-creator-studio");a&&(a.classList.add("is-ready"),(0,t.createRoot)(a).render((0,e.createElement)(I,null)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",R):R()})();
|
||||
(()=>{"use strict";const e=window.React,t=window.wp.element,a=window.wp.i18n;function n(e,t){const a=e&&(e.message||e.error)||(401===t?"登录状态已失效,请刷新页面重新登录。":"请求失败,请稍后重试。"),n=new Error(String(a));return n.status=t,n.code=e&&e.code?String(e.code):"",n.data=e,n}async function o(e){try{return await e.json()}catch(e){return null}}async function r(e,t,a){const r=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-WP-Nonce":a},body:JSON.stringify(t)}),l=await o(r);if(!r.ok)throw n(l,r.status);return l}async function l(e,t,a,r={}){const l=new FormData;l.append("file",t),Object.entries(r).forEach(([e,t])=>l.append(e,t));const i=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"X-WP-Nonce":a},body:l}),c=await o(i);if(!i.ok)throw n(c,i.status);return c}const i=window.jioCreatorStudio||{},c=i.nonce||"";function s(n){const{draft:o,setDraft:r,capabilities:s,limits:u,taxonomy:d,isPublishedEdit:m,onClose:p,onSaveDraft:_,onSubmit:f,privateStorage:b,privateUploads:g,saving:h,captcha:j}=n,[y,E]=(0,t.useState)(""),[v,k]=(0,t.useState)(""),[N,w]=(0,t.useState)(!1),C=(0,t.useRef)(null),S=(0,t.useRef)(null);(0,t.useEffect)(()=>{const e=S.current,t=document.getElementById("jio-creator-captcha");if(!e||!t)return;const a=t.parentElement;return e.appendChild(t),t.removeAttribute("hidden"),()=>{a&&(a.appendChild(t),t.setAttribute("hidden",""))}},[]);const x=(0,t.useMemo)(()=>(o.title||"").trim().length,[o.title])>=Number(u.titleMin||1),q=()=>{const e=y.trim();if(!e)return;const t=e.split(/[,,\n]+/u).map(e=>e.trim()).filter(Boolean);r({tags:Array.from(new Set([...o.tags,...t]))}),E("")};return(0,e.createElement)("div",{className:"jio-creator-drawer",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-publish-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:p}),(0,e.createElement)("div",{className:"jio-creator-drawer__panel"},(0,e.createElement)("header",{className:"jio-creator-drawer__head"},(0,e.createElement)("h2",{id:"jio-creator-publish-title"},m?(0,a.__)("提交改稿","jio"):(0,a.__)("发布文章","jio")),(0,e.createElement)("button",{type:"button",className:"jio-creator-drawer__close",onClick:p,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-drawer__body"},m&&(0,e.createElement)("p",{className:"jio-creator-drawer__note"},(0,a.__)("审核通过前,读者看到的仍然是当前线上版本。","jio")),(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("分类","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},0===d.categories.length&&(0,e.createElement)("span",{className:"jio-creator-muted"},(0,a.__)("暂无可选分类","jio")),d.categories.map(t=>(0,e.createElement)("label",{key:t.id,className:"jio-creator-chip"+(o.categories.includes(t.id)?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:o.categories.includes(t.id),onChange:()=>(e=>{const t=o.categories.includes(e);r({categories:t?o.categories.filter(t=>t!==e):[...o.categories,e]})})(t.id)}),(0,e.createElement)("span",null,t.name))))),d.tags&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("h3",null,(0,a.__)("标签","jio")),(0,e.createElement)("div",{className:"jio-creator-chips"},o.tags.map(t=>(0,e.createElement)("span",{key:t,className:"jio-creator-chip is-active"},(0,e.createElement)("span",null,t),(0,e.createElement)("button",{type:"button",onClick:()=>r({tags:o.tags.filter(e=>e!==t)}),"aria-label":(0,a.sprintf)((0,a.__)("移除标签 %s","jio"),t)},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))))),(0,e.createElement)("div",{className:"jio-creator-inline"},(0,e.createElement)("input",{type:"text",value:y,placeholder:(0,a.__)("输入标签,逗号分隔","jio"),onChange:e=>E(e.target.value),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),q())}}),(0,e.createElement)("button",{type:"button",onClick:q},(0,a.__)("添加","jio")))),d.customDefs.map(t=>(0,e.createElement)("section",{className:"jio-creator-field",key:t.key},(0,e.createElement)("h3",null,t.name),(0,e.createElement)("div",{className:"jio-creator-chips"},t.values.map(a=>{const n=o.custom[t.key]||[],l=n.includes(a.key);return(0,e.createElement)("label",{key:a.key,className:"jio-creator-chip"+(l?" is-active":"")},(0,e.createElement)("input",{type:"checkbox",checked:l,onChange:()=>r({custom:{...o.custom,[t.key]:l?n.filter(e=>e!==a.key):[...n,a.key]}})}),(0,e.createElement)("span",null,a.name))})))),s.canPay&&(0,e.createElement)("section",{className:"jio-creator-field"},(0,e.createElement)("div",{className:"jio-creator-switch"},(0,e.createElement)("div",null,(0,e.createElement)("h3",null,(0,a.__)("付费内容","jio")),(0,e.Line truncated
|
||||
(0,a.__)("按站点时区(%s)发布,至少需要晚于当前时间 1 分钟。","jio"),String(m.timezone||"UTC")))))}const g={ready:(0,a.__)("尚未保存","jio"),saving:(0,a.__)("保存中…","jio"),saved:(0,a.__)("已保存","jio"),error:(0,a.__)("保存失败","jio"),conflict:(0,a.__)("内容已在别处更新","jio"),submitting:(0,a.__)("提交中…","jio"),submitted:(0,a.__)("已提交","jio")};function h({value:t}){return(0,e.createElement)("span",{className:`jio-creator__save is-${t.state}`,role:"status","aria-live":"polite"},(0,e.createElement)("i",{className:"fa fas fa-circle","aria-hidden":"true"}),(0,e.createElement)("span",null,g[t.state]||g.ready),"saved"===t.state&&t.at?(0,e.createElement)("time",null,t.at):null)}function j(e){const t=(e.downloads||[]).map(e=>{const t="string"==typeof e.assetKey?e.assetKey:"";if(t)return{type:"protected",asset_key:t,label:"string"==typeof e.label?e.label:""};const a="string"==typeof e.url?e.url.trim():"";return a?{type:"direct",url:a,label:"string"==typeof e.label?e.label:"",extract_code:"string"==typeof e.extractCode?e.extractCode:""}:null}).filter(Boolean);return{enabled:!!e.enabled,fields:{pay_type:e.enabled?t.length>0?"download":"reading":"no",pay_modo:"points"===e.modo?"points":"0",price:"points"===e.modo?"0.00":String(e.price||"0.00"),points_price:"points"===e.modo?Math.max(0,Number(e.points)||0):0,vip1_price:"points"===e.modo?"0.00":String(e.vip1Price||"0.00"),vip2_price:"points"===e.modo?"0.00":String(e.vip2Price||"0.00"),pay_limit:Number(e.payLimit)||0,downloads:t}}}const y=e=>e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),E=[{value:"generic",label:(0,a.__)("通用","jio")},{value:"javascript",label:"JavaScript"},{value:"typescript",label:"TypeScript"},{value:"php",label:"PHP"},{value:"python",label:"Python"},{value:"java",label:"Java"},{value:"go",label:"Go"},{value:"rust",label:"Rust"},{value:"csharp",label:"C#"},{value:"cpp",label:"C++"},{value:"sql",label:"SQL"},{value:"json",label:"JSON"},{value:"bash",label:"Shell"},{value:"css",label:"CSS"},{value:"html",label:"HTML"},{value:"markdown",label:"Markdown"}];const v=(e,t,a)=>Math.min(a,Math.max(t,Number.isFinite(e)?Math.round(e):t)),k={table:(0,a.__)("插入表格","jio"),math:(0,a.__)("插入公式","jio"),footnote:(0,a.__)("插入注释","jio")};function N({kind:n,onSubmit:o,onClose:r}){const[l,i]=(0,t.useState)(3),[c,s]=(0,t.useState)(3),[u,d]=(0,t.useState)(!0),[m,p]=(0,t.useState)(!1),[_,f]=(0,t.useState)("a^2 + b^2 = c^2"),[b,g]=(0,t.useState)(!0),[h,j]=(0,t.useState)(""),y=(0,t.useRef)(null),E=(0,t.useRef)(null),N=(0,t.useRef)(null),w=(0,t.useRef)(null);(0,t.useEffect)(()=>{(E.current||N.current||w.current)?.focus();const e=e=>{"Escape"===e.key&&(e.preventDefault(),r())};return document.addEventListener("keydown",e),()=>document.removeEventListener("keydown",e)},[r]);const C=(0,t.useMemo)(()=>"footnote"===n?""!==h.trim():"math"!==n||""!==_.trim(),[n,h,_]);return(0,e.createElement)("div",{className:"jio-creator-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"jio-creator-dialog-title"},(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__backdrop","aria-label":(0,a.__)("关闭","jio"),onClick:r}),(0,e.createElement)("div",{className:"jio-creator-dialog__panel",ref:y},(0,e.createElement)("header",{className:"jio-creator-dialog__head"},(0,e.createElement)("h2",{id:"jio-creator-dialog-title"},k[n]),(0,e.createElement)("button",{type:"button",className:"jio-creator-dialog__close",onClick:r,"aria-label":(0,a.__)("关闭","jio")},(0,e.createElement)("i",{className:"fa fas fa-times","aria-hidden":"true"}))),(0,e.createElement)("div",{className:"jio-creator-dialog__body"},"table"===n&&(0,e.createElement)(e.Fragment,null,(0,e.createElement)("div",{className:"jio-creator-dialog__row"},(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("行数","jio")),(0,e.createElement)("input",{ref:E,type:"number",min:1,max:20,value:l,onChange:e=>i(Number(e.target.value))})),(0,e.createElement)("label",null,(0,e.createElement)("span",null,(0,a.__)("列数","jio")),(0,e.createElement)("input",{type:"number",min:1,max:10,value:c,onChange:e=>s(Number(e.target.value))}))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:u,onChange:e=>d(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("首行为表头","jio"))),(0,e.createElement)("label",{className:"jio-creator-dialog__check"},(0,e.createElement)("input",{type:"checkbox",checked:m,onChange:e=>p(e.target.checked)}),(0,e.createElement)("span",null,(0,a.__)("隔行变色","jio")))),"math"===n&&(0,e.createElement)(e.Fragment,null,(0,e.createElement)("label",{className:"jio-creator-dialog__field"},(0,e.createElement)("span",null,(0,a.__)("公式","jio")),(0,e.createElement)("textarea",{ref:N,rows:3,value:_,onChange:e=>f(e.target.value),placeholder:"a^2 + b^2 = c^2"})),(0,e.createElement)Line truncated
|
||||
(0,a.__)("请输入标题(最多 %d 个字)","jio"),Number(H.titleMax)||100),spellCheck:!1,maxLength:Number(H.titleMax)||100})),(0,e.createElement)("div",{className:"jio-creator__editor","data-editor-ready":_?"true":"false"},(0,e.createElement)("textarea",{id:x,defaultValue:n.content,onChange:e=>X(e.target.value),"aria-label":(0,a.__)("文章正文","jio")}))),(0,e.createElement)(b,{featured:n.featured,canEditCover:!!J.canUploadImage,canSchedule:!!J.canSchedule,scheduleEnabled:!!n.schedule.enabled,scheduleAt:n.schedule.at,onFeaturedChange:e=>Q({featured:e}),onScheduleChange:(e,t)=>Q({schedule:{enabled:e,at:t}})}),z&&(0,e.createElement)(N,{kind:z,onSubmit:pe,onClose:()=>U(null)}),D&&(0,e.createElement)(s,{draft:n,setDraft:Q,capabilities:J,limits:H,taxonomy:{categories:w.categories&&w.categories.items||[],tags:!!w.tags?.enabled,customDefs:w.customDefs||[]},isPublishedEdit:V,onClose:()=>P(!1),onSaveDraft:()=>{Z(!1)},onSubmit:e=>{ee(e)},privateStorage:!!w.privateStorage,privateUploads:w.privateUploads||[],captcha:!!w.captcha?.enabled,saving:"saving"===I.state||"submitting"===I.state}))}function I(){const a=document.getElementById("jio-creator-studio");a&&(a.classList.add("is-ready"),(0,t.createRoot)(a).render((0,e.createElement)($,null)))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",I):I()})();
|
||||
@@ -10,20 +10,15 @@
|
||||
import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
import { CODE_LANGUAGES } from '../utils/block-markup';
|
||||
|
||||
export type BlockDialogKind = 'table' | 'math' | 'footnote' | 'code';
|
||||
export type BlockDialogKind = 'table' | 'math' | 'footnote';
|
||||
|
||||
export type BlockDialogResult =
|
||||
| { kind: 'table'; rows: number; cols: number; header: boolean; striped: boolean }
|
||||
| { kind: 'math'; formula: string; display: boolean }
|
||||
| { kind: 'footnote'; note: string }
|
||||
| { kind: 'code'; language: string; code: string };
|
||||
| { kind: 'footnote'; note: string };
|
||||
|
||||
type Props = {
|
||||
kind: BlockDialogKind;
|
||||
initialLanguage?: string;
|
||||
initialCode?: string;
|
||||
onSubmit: (result: BlockDialogResult) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -34,10 +29,9 @@ const TITLES: Record<BlockDialogKind, string> = {
|
||||
table: __('插入表格', 'jio'),
|
||||
math: __('插入公式', 'jio'),
|
||||
footnote: __('插入注释', 'jio'),
|
||||
code: __('插入代码块', 'jio'),
|
||||
};
|
||||
|
||||
export function BlockDialog({ kind, initialLanguage = 'generic', initialCode = '', onSubmit, onClose }: Props) {
|
||||
export function BlockDialog({ kind, onSubmit, onClose }: Props) {
|
||||
const [rows, setRows] = useState(3);
|
||||
const [cols, setCols] = useState(3);
|
||||
const [header, setHeader] = useState(true);
|
||||
@@ -45,8 +39,6 @@ export function BlockDialog({ kind, initialLanguage = 'generic', initialCode = '
|
||||
const [formula, setFormula] = useState('a^2 + b^2 = c^2');
|
||||
const [display, setDisplay] = useState(true);
|
||||
const [note, setNote] = useState('');
|
||||
const [language, setLanguage] = useState(initialLanguage);
|
||||
const [code, setCode] = useState(initialCode);
|
||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||
const firstFieldRef = useRef<HTMLInputElement | null>(null);
|
||||
const formulaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
@@ -86,10 +78,6 @@ export function BlockDialog({ kind, initialLanguage = 'generic', initialCode = '
|
||||
onSubmit({ kind, formula: formula.trim(), display });
|
||||
return;
|
||||
}
|
||||
if (kind === 'code') {
|
||||
onSubmit({ kind, language, code });
|
||||
return;
|
||||
}
|
||||
onSubmit({ kind, note: note.trim() });
|
||||
};
|
||||
|
||||
@@ -163,28 +151,6 @@ export function BlockDialog({ kind, initialLanguage = 'generic', initialCode = '
|
||||
</>
|
||||
)}
|
||||
|
||||
{kind === 'code' && (
|
||||
<>
|
||||
<label className="jio-creator-dialog__field">
|
||||
<span>{__('语言', 'jio')}</span>
|
||||
<select value={language} onChange={(event) => setLanguage(event.target.value)}>
|
||||
{CODE_LANGUAGES.map((item) => (
|
||||
<option key={item.value} value={item.value}>{item.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="jio-creator-dialog__field">
|
||||
<span>{__('代码(可留空,稍后在正文里填写)', 'jio')}</span>
|
||||
<textarea
|
||||
rows={6}
|
||||
value={code}
|
||||
onChange={(event) => setCode(event.target.value)}
|
||||
placeholder="console.log('hello');"
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
{kind === 'footnote' && (
|
||||
<label className="jio-creator-dialog__field">
|
||||
<span>{__('注释内容', 'jio')}</span>
|
||||
|
||||
@@ -50,7 +50,7 @@ img { height: auto; }
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(135deg, #3dcabd 10%, #254bdd 100%);
|
||||
color: #fff;
|
||||
content: attr(data-enlighter-language);
|
||||
content: attr(data-jio-code-label);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
pointer-events: none;
|
||||
@@ -117,6 +117,15 @@ img { height: auto; }
|
||||
}
|
||||
.jio-toc-placeholder::after { content: '目录将在发布后自动生成'; }
|
||||
|
||||
/* 注释上标:编辑器里只显示序号,发布后由服务端渲染脚注列表。 */
|
||||
.jio-footnote-ref,
|
||||
.jio-footnote-ref a {
|
||||
color: #267665;
|
||||
font-size: 0.8em;
|
||||
text-decoration: none;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
/* 附件链接。 */
|
||||
.jio-file-link {
|
||||
display: inline-flex;
|
||||
|
||||
@@ -104,6 +104,76 @@ function initialDraft(): Draft {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Backspace / Delete 删除相邻的块级元素。
|
||||
*
|
||||
* 表格、代码块、引用、分割线这类内容不是普通文本,TinyMCE 默认不会在
|
||||
* 空段落里按删除键时主动移除它们,导致「删不掉」。这里在光标处于空段落、
|
||||
* 且紧邻目标块时直接移除该块。
|
||||
*/
|
||||
function removeAdjacentBlock(editor: any, event: any): void {
|
||||
const key = Number(event.keyCode);
|
||||
if (key !== 8 && key !== 46) {
|
||||
return;
|
||||
}
|
||||
const selection = editor.selection;
|
||||
if (!selection || !selection.isCollapsed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = editor.getBody();
|
||||
const block = selection.getNode();
|
||||
const isBlank = (node: any): boolean =>
|
||||
!!node && node.nodeType === 1 && (node.textContent || '').trim() === '' &&
|
||||
!editor.dom.getParent(node, 'table,pre,hr,.quote_q,.jio-math--block,.jio-toc-placeholder', body);
|
||||
const isBlockElement = (node: any): boolean =>
|
||||
!!node && node.nodeType === 1 && !!(node.matches?.('table,pre,hr,.quote_q,.jio-math--block,.jio-toc-placeholder'));
|
||||
|
||||
// 光标可能落在空段落里,也可能落在块级元素的边界(TinyMCE 常见行为);
|
||||
// 两种情况下都优先删除紧邻的块,而不是让按键悄悄失效。
|
||||
const blankContainer = isBlank(block) ? block : null;
|
||||
const direct = blankContainer
|
||||
? (key === 8 ? blankContainer.previousElementSibling : blankContainer.nextElementSibling)
|
||||
: null;
|
||||
|
||||
if (isBlockElement(direct)) {
|
||||
event.preventDefault();
|
||||
editor.dom.remove(direct);
|
||||
editor.nodeChanged();
|
||||
editor.undoManager.add();
|
||||
return;
|
||||
}
|
||||
|
||||
// 光标停在块首/块尾时,删除块本身。
|
||||
const caretBlock = editor.dom.getParent(block, 'table,pre,hr,.quote_q,.jio-math--block,.jio-toc-placeholder', body);
|
||||
if (!caretBlock) {
|
||||
return;
|
||||
}
|
||||
const caretIsAtEdge = (start: boolean): boolean => {
|
||||
const probe = editor.dom.createRng();
|
||||
probe.selectNodeContents(caretBlock);
|
||||
probe.collapse(start);
|
||||
const caret = selection.getRng();
|
||||
return caret && typeof caret.compareBoundaryPoints === 'function'
|
||||
&& caret.compareBoundaryPoints(start ? 1 : 3, probe) === 0;
|
||||
};
|
||||
|
||||
if (!((key === 8 && caretIsAtEdge(true)) || (key === 46 && caretIsAtEdge(false)))) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const parent = caretBlock.parentElement;
|
||||
editor.dom.remove(caretBlock);
|
||||
// 删掉最后一个块后补一个空段落,否则没有可放置光标的位置。
|
||||
if (parent && parent.children.length === 0) {
|
||||
editor.setContent('<p></p>');
|
||||
editor.selection.setCursorLocation(editor.getBody().firstChild, 0);
|
||||
}
|
||||
editor.nodeChanged();
|
||||
editor.undoManager.add();
|
||||
}
|
||||
|
||||
function escapeAttribute(value: string): string {
|
||||
return value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
@@ -145,8 +215,6 @@ function Studio(): JSX.Element {
|
||||
}));
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [blockDialog, setBlockDialog] = useState<BlockDialogKind | null>(null);
|
||||
const [codeDialog, setCodeDialog] = useState<null | { language: string; code: string }>(null);
|
||||
const codeTargetRef = useRef<any>(null);
|
||||
const [notice, setNotice] = useState<{ tone: 'ok' | 'error'; text: string } | null>(null);
|
||||
const [recovery, setRecovery] = useState<null | { savedAt: string }>(null);
|
||||
const dirtyRef = useRef(false);
|
||||
@@ -416,23 +484,6 @@ function Studio(): JSX.Element {
|
||||
});
|
||||
editor.on('NodeChange', () => updateFormatValue(editor));
|
||||
// 代码块右上角的语言标签:点击整块即可换语言,等价于公众号的「高亮」入口。
|
||||
editor.on('click', (event: any) => {
|
||||
const pre = editor.dom.getParent(event.target, 'pre.enlighter-pre');
|
||||
if (!pre) {
|
||||
return;
|
||||
}
|
||||
// 只响应右上角语言标签那一片,光标点进代码正文时保持正常编辑。
|
||||
const rect = pre.getBoundingClientRect();
|
||||
const relativeY = Number(event.clientY) - rect.top;
|
||||
const relativeX = Number(event.clientX) - rect.left;
|
||||
if (relativeY > 40 || relativeX < rect.width - 120) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const language = String(pre.getAttribute('data-enlighter-language') || 'generic');
|
||||
codeTargetRef.current = pre;
|
||||
setCodeDialog({ language, code: String((pre.textContent || '').trim()) });
|
||||
});
|
||||
editor.on('input change Undo Redo AddUndo', () => {
|
||||
updateContent(editor.getContent());
|
||||
updateHistoryAvailability(editor);
|
||||
@@ -441,7 +492,9 @@ function Studio(): JSX.Element {
|
||||
if ((event.metaKey || event.ctrlKey) && event.keyCode === 83) {
|
||||
event.preventDefault();
|
||||
saveActionRef.current();
|
||||
return;
|
||||
}
|
||||
removeAdjacentBlock(editor, event);
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -561,7 +614,8 @@ function Studio(): JSX.Element {
|
||||
return;
|
||||
}
|
||||
if (kind === 'code') {
|
||||
setCodeDialog({ language: 'generic', code: '' });
|
||||
// 直接插入;语言可稍后点代码块右上角的角标更换。
|
||||
insertHtml(codeBlockMarkup('generic'));
|
||||
return;
|
||||
}
|
||||
if (kind === 'list') {
|
||||
@@ -599,10 +653,8 @@ function Studio(): JSX.Element {
|
||||
insertHtml(tableMarkup(result));
|
||||
} else if (result.kind === 'math') {
|
||||
insertHtml(mathMarkup(result));
|
||||
} else if (result.kind === 'footnote') {
|
||||
insertHtml(footnoteMarkup(result.note));
|
||||
} else {
|
||||
insertHtml(codeBlockMarkup(result.language, result.code));
|
||||
insertHtml(footnoteMarkup(result.note));
|
||||
}
|
||||
}, [insertHtml]);
|
||||
|
||||
@@ -759,35 +811,6 @@ function Studio(): JSX.Element {
|
||||
/>
|
||||
)}
|
||||
|
||||
{codeDialog && (
|
||||
<BlockDialog
|
||||
kind="code"
|
||||
initialLanguage={codeDialog.language}
|
||||
initialCode={codeDialog.code}
|
||||
onSubmit={(result) => {
|
||||
setCodeDialog(null);
|
||||
if (result.kind !== 'code') {
|
||||
return;
|
||||
}
|
||||
const previous = codeTargetRef.current;
|
||||
codeTargetRef.current = null;
|
||||
insertHtml(codeBlockMarkup(result.language, result.code));
|
||||
if (previous) {
|
||||
// 换语言时移除旧块,避免留下两份代码。
|
||||
const editor = editorRef.current;
|
||||
if (editor && !editor.removed) {
|
||||
editor.dom.remove(previous);
|
||||
updateContent(editor.getContent());
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClose={() => {
|
||||
setCodeDialog(null);
|
||||
codeTargetRef.current = null;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{drawerOpen && (
|
||||
<PublishDrawer
|
||||
draft={draft}
|
||||
|
||||
@@ -54,8 +54,11 @@ export function codeLabel(language: string): string {
|
||||
/** 代码块:语言写在 data 属性里,前台 enlighter 与编辑器标签共用。 */
|
||||
export function codeBlockMarkup(language = 'generic', code = ''): string {
|
||||
const body = code !== '' ? escapeHtml(code) : escapeHtml(__('在此输入代码', 'jio'));
|
||||
// data-jio-code-label 供编辑器角标直接显示,避免在 CSS 里映射语言标识。
|
||||
const label = escapeHtml(codeLabel(language));
|
||||
return (
|
||||
`<pre class="enlighter-pre" data-enlighter-language="${escapeHtml(language)}"><code class="gl" data-enlighter-language="${escapeHtml(language)}">` +
|
||||
`<pre class="enlighter-pre" data-enlighter-language="${escapeHtml(language)}" data-jio-code-label="${label}">` +
|
||||
`<code class="gl" data-enlighter-language="${escapeHtml(language)}">` +
|
||||
body +
|
||||
'</code></pre><p></p>'
|
||||
);
|
||||
@@ -93,25 +96,27 @@ export function tableMarkup({ rows, cols, header, striped }: TableOptions): stri
|
||||
|
||||
/**
|
||||
* 目录:文章页的目录树由前台 section-navs 依据 [data-nav] 生成,
|
||||
* 因此这里只放一个占位标记,读者端不会看到裸露的标记文本。
|
||||
* 短代码在服务端替换成目录卡片,编辑器里保留可识别的占位。
|
||||
*/
|
||||
export function tocMarkup(): string {
|
||||
return '<div class="jio-toc-placeholder"></div><p></p>';
|
||||
return '<p>[jio_toc]</p><p></p>';
|
||||
}
|
||||
|
||||
/** 注释:Markdown 脚注语法,前台渲染成编号脚注列表。 */
|
||||
/** 注释:用短代码落库,服务端生成上标序号与文末注释列表。 */
|
||||
export function footnoteMarkup(note: string): string {
|
||||
const text = note.trim() || __('注释内容', 'jio');
|
||||
return `<p>[^1]<br>[^1]: ${escapeHtml(text)}</p>`;
|
||||
// 短代码属性里再放 HTML 会被 kses 改写,这里只允许纯文本内容。
|
||||
return `<p>[jio_footnote]${escapeHtml(text)}[/jio_footnote]</p>`;
|
||||
}
|
||||
|
||||
export type MathOptions = { formula: string; display: boolean };
|
||||
|
||||
/** 公式:行内或独占一行,数据保存在 .jio-math 容器里。 */
|
||||
/** 公式:用短代码落库,服务端统一做上标 / 下标排版。 */
|
||||
export function mathMarkup({ formula, display }: MathOptions): string {
|
||||
const className = display ? 'jio-math jio-math--block' : 'jio-math';
|
||||
const body = `<span class="${className}">${escapeHtml(formula.trim())}</span>`;
|
||||
return display ? `<p>${body}</p>` : body;
|
||||
const body = escapeHtml(formula.trim());
|
||||
return display
|
||||
? `<p>[jio_math display="1"]${body}[/jio_math]</p>`
|
||||
: `<p>[jio_math display="0"]${body}[/jio_math]</p>`;
|
||||
}
|
||||
|
||||
export type QuoteColor = '' | 'qe_wzk_c-red' | 'qe_wzk_lan' | 'qe_wzk_lv';
|
||||
|
||||
@@ -124,18 +124,6 @@
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
.enlighter-pre code.gl::before {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(135deg, #3dcabd 10%, #254bdd 100%);
|
||||
color: #fff;
|
||||
content: '高亮代码';
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.file-download-box {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
@@ -190,3 +178,11 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 编辑器内的注释上标(发布后由服务端换成脚注列表) */
|
||||
.mce-content-body sup.jio-footnote-ref,
|
||||
.tox .jio-footnote-ref {
|
||||
color: var(--theme-color, #267665);
|
||||
font-size: 0.8em;
|
||||
vertical-align: super;
|
||||
}
|
||||
@@ -255,7 +255,7 @@ code.gl {
|
||||
}
|
||||
|
||||
code.gl::before {
|
||||
content: '高亮代码';
|
||||
content: attr(data-enlighter-language);
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 8px;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* 正文扩展块的渲染:目录、脚注、公式。
|
||||
* 正文扩展块:目录、脚注、公式。
|
||||
*
|
||||
* 编辑器插入的都是 HTML/文本占位,读者端由这里转换成真正的渲染结果:
|
||||
* - 目录:占位替换成前台 section-navs 识别的 .posts-nav-box 卡片;
|
||||
* - 脚注:[^1] 引用与 [^1]: 定义转换成脚注列表并重新编号;
|
||||
* - 公式:.jio-math 内的 ^ / _ 转成上标下标,给出可读的数学排版。
|
||||
* 三块都以短代码落库,WordPress 保证在 `the_content` 里稳定替换,
|
||||
* 不必依赖编辑器保存后仍然保留的 class / data 属性:
|
||||
* - [jio_toc] → 文章目录卡片(前台 section-navs 填充)
|
||||
* - [jio_footnote]...[/jio_footnote] → 编号脚注 + 文末注释列表
|
||||
* - [jio_math]...[/jio_math] → 公式排版(上标 / 下标)
|
||||
*
|
||||
* @package Jio
|
||||
*/
|
||||
@@ -16,9 +17,12 @@ defined('ABSPATH') || exit;
|
||||
|
||||
class ContentBlocks {
|
||||
|
||||
/** 编辑器插入的目录占位 class。 */
|
||||
/** 目录占位短代码(历史 HTML 占位也用这个 class 识别)。 */
|
||||
public const TOC_CLASS = 'jio-toc-placeholder';
|
||||
|
||||
/** 本次渲染已收集的脚注。 */
|
||||
private $footnotes = [];
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
@@ -29,91 +33,80 @@ class ContentBlocks {
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_filter('the_content', [$this, 'render_blocks'], 9);
|
||||
// 必须晚于 do_shortcode(优先级 11),才能拿到本次渲染收集到的脚注。
|
||||
add_filter('the_content', [$this, 'append_footnotes'], 15);
|
||||
}
|
||||
|
||||
/**
|
||||
* 正文里是否插入了目录占位(用于决定是否加载目录脚本)。
|
||||
* 正文里是否含目录短代码或旧占位。
|
||||
*/
|
||||
public static function has_toc_placeholder(string $content): bool {
|
||||
return strpos($content, self::TOC_CLASS) !== false;
|
||||
}
|
||||
|
||||
public function render_blocks(string $content): string {
|
||||
if ($content === '' || is_admin()) {
|
||||
return $content;
|
||||
}
|
||||
$content = $this->render_toc($content);
|
||||
$content = $this->render_footnotes($content);
|
||||
return $this->render_math($content);
|
||||
return strpos($content, '[jio_toc') !== false || strpos($content, self::TOC_CLASS) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 目录占位 → 文章目录卡片。
|
||||
* [jio_toc]
|
||||
*/
|
||||
private function render_toc(string $content): string {
|
||||
if (!self::has_toc_placeholder($content)) {
|
||||
return $content;
|
||||
}
|
||||
$box = '<div class="widget-container"><div class="posts-nav-box" data-title="'
|
||||
public function shortcode_toc(array $atts = [], string $content = ''): string {
|
||||
return '<div class="widget-container"><div class="posts-nav-box" data-title="'
|
||||
. esc_attr__('本文目录', 'jio')
|
||||
. '"></div></div>';
|
||||
$pattern = '/<div[^>]*class="[^"]*' . preg_quote(self::TOC_CLASS, '/') . '[^"]*"[^>]*>\s*<\/div>\s*(?:<p>\s*<\/p>)?/i';
|
||||
$replaced = preg_replace($pattern, $box, $content, 1);
|
||||
return is_string($replaced) ? $replaced : $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Markdown 脚注:[^1] 引用与 [^1]: 定义。
|
||||
* [jio_math display="1"]公式[/jio_math]
|
||||
*/
|
||||
private function render_footnotes(string $content): string {
|
||||
if (strpos($content, '[^') === false) {
|
||||
return $content;
|
||||
}
|
||||
if (!preg_match_all('/\[\^([^\]]+)\]:[ \t]*([^\n<]+)/u', $content, $defs, PREG_SET_ORDER)) {
|
||||
return $content;
|
||||
public function shortcode_math(array $atts = [], string $content = ''): string {
|
||||
$atts = shortcode_atts(['display' => '1'], $atts, 'jio_math');
|
||||
$raw = trim(wp_strip_all_tags((string) $content));
|
||||
if ($raw === '') {
|
||||
return '';
|
||||
}
|
||||
$body = $this->typeset_math($raw);
|
||||
$display = (string) $atts['display'] !== '0';
|
||||
return $display
|
||||
? '<p><span class="jio-math jio-math--block">' . $body . '</span></p>'
|
||||
: '<span class="jio-math">' . $body . '</span>';
|
||||
}
|
||||
|
||||
$notes = [];
|
||||
foreach ($defs as $def) {
|
||||
$notes[(string) $def[1]] = trim((string) $def[2]);
|
||||
$content = str_replace($def[0], '', $content);
|
||||
}
|
||||
if ($notes === []) {
|
||||
return $content;
|
||||
/**
|
||||
* [jio_footnote]补充说明[/jio_footnote]
|
||||
*
|
||||
* 引用位置插入上标序号,注释正文收集到文末列表。
|
||||
*/
|
||||
public function shortcode_footnote(array $atts = [], string $content = ''): string {
|
||||
$text = trim(wp_strip_all_tags((string) $content));
|
||||
if ($text === '') {
|
||||
return '';
|
||||
}
|
||||
$this->footnotes[] = $text;
|
||||
$index = count($this->footnotes);
|
||||
return '<sup class="jio-footnote-ref"><a href="#jio-fn-' . $index . '" id="jio-fnref-' . $index . '">'
|
||||
. $index
|
||||
. '</a></sup>';
|
||||
}
|
||||
|
||||
// 按首次引用顺序编号,而不是按定义顺序。
|
||||
$order = [];
|
||||
$result = preg_replace_callback('/\[\^([^\]]+)\]/u', static function (array $match) use (&$order): string {
|
||||
$key = (string) $match[1];
|
||||
if (!isset($order[$key])) {
|
||||
$order[$key] = count($order) + 1;
|
||||
}
|
||||
$index = $order[$key];
|
||||
return '<sup class="jio-footnote-ref"><a href="#jio-fn-' . $index . '" id="jio-fnref-' . $index . '">'
|
||||
. $index
|
||||
. '</a></sup>';
|
||||
}, $content);
|
||||
if (!is_string($result)) {
|
||||
/**
|
||||
* 在正文末尾补上注释列表。
|
||||
*/
|
||||
public function append_footnotes(string $content): string {
|
||||
// 同一段正文可能被渲染多次(列表摘要、相关文章等),注释用完后即清空,
|
||||
// 避免把上一篇文章的脚注带到下一篇。
|
||||
$footnotes = $this->footnotes;
|
||||
$this->footnotes = [];
|
||||
if ($footnotes === [] || strpos($content, 'jio-footnotes') !== false) {
|
||||
return $content;
|
||||
}
|
||||
$content = $result;
|
||||
|
||||
$items = '';
|
||||
foreach ($order as $key => $index) {
|
||||
if (!isset($notes[$key])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($footnotes as $offset => $text) {
|
||||
$index = $offset + 1;
|
||||
$items .= '<li id="jio-fn-' . $index . '">'
|
||||
. esc_html($notes[$key])
|
||||
. esc_html($text)
|
||||
. ' <a class="jio-footnote-back" href="#jio-fnref-' . $index . '" aria-label="'
|
||||
. esc_attr__('返回正文', 'jio')
|
||||
. '">↩</a></li>';
|
||||
}
|
||||
if ($items === '') {
|
||||
return $content;
|
||||
}
|
||||
|
||||
return $content
|
||||
. '<section class="jio-footnotes"><h4 class="jio-footnotes__title">'
|
||||
@@ -122,24 +115,11 @@ class ContentBlocks {
|
||||
}
|
||||
|
||||
/**
|
||||
* 公式:把 .jio-math 片段里的 ^ 与 _ 转成真正可见的上标下标。
|
||||
*
|
||||
* 主题没有内置 TeX 引擎,这里只做确定性的公式排版,不猜测其它语法。
|
||||
* 公式排版:把 ^ 与 _ 转成上标下标,其余字符按文本转义。
|
||||
*/
|
||||
private function render_math(string $content): string {
|
||||
if (strpos($content, 'jio-math') === false) {
|
||||
return $content;
|
||||
}
|
||||
$result = preg_replace_callback(
|
||||
'#<span([^>]*\bclass="[^"]*jio-math[^"]*"[^>]*)>(.*?)</span>#is',
|
||||
static function (array $match): string {
|
||||
$inner = esc_html(html_entity_decode($match[2], ENT_QUOTES, 'UTF-8'));
|
||||
$inner = (string) preg_replace('/\^\{([^{}]*)\}|\^(\S)/u', '<sup>$1$2</sup>', $inner);
|
||||
$inner = (string) preg_replace('/_\{([^{}]*)\}|_(\S)/u', '<sub>$1$2</sub>', $inner);
|
||||
return '<span' . $match[1] . '>' . $inner . '</span>';
|
||||
},
|
||||
$content
|
||||
);
|
||||
return is_string($result) ? $result : $content;
|
||||
private function typeset_math(string $raw): string {
|
||||
$escaped = esc_html($raw);
|
||||
$escaped = (string) preg_replace('/\^\{([^{}]*)\}|\^(\S)/u', '<sup>$1$2</sup>', $escaped);
|
||||
return (string) preg_replace('/_\{([^{}]*)\}|_(\S)/u', '<sub>$1$2</sub>', $escaped);
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,40 @@ class Shortcodes {
|
||||
add_shortcode('jio_profile', [$this, 'render_profile']);
|
||||
// 文章列表(统一渲染器)
|
||||
add_shortcode('jio_posts', [$this, 'render_posts']);
|
||||
// 正文扩展块 → \Jio\Content\ContentBlocks
|
||||
add_shortcode('jio_math', [$this, 'render_math']);
|
||||
add_shortcode('jio_footnote', [$this, 'render_footnote']);
|
||||
add_shortcode('jio_toc', [$this, 'render_toc']);
|
||||
}
|
||||
|
||||
/**
|
||||
* [jio_math display="1"]a^2 + b^2 = c^2[/jio_math]
|
||||
*/
|
||||
public function render_math(array $atts = [], string $content = ''): string {
|
||||
if (!class_exists('\Jio\Content\ContentBlocks')) {
|
||||
return esc_html($content);
|
||||
}
|
||||
return \Jio\Content\ContentBlocks::get_instance()->shortcode_math($atts, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* [jio_footnote]补充说明[/jio_footnote]
|
||||
*/
|
||||
public function render_footnote(array $atts = [], string $content = ''): string {
|
||||
if (!class_exists('\Jio\Content\ContentBlocks')) {
|
||||
return esc_html($content);
|
||||
}
|
||||
return \Jio\Content\ContentBlocks::get_instance()->shortcode_footnote($atts, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* [jio_toc]
|
||||
*/
|
||||
public function render_toc(array $atts = [], string $content = ''): string {
|
||||
if (!class_exists('\Jio\Content\ContentBlocks')) {
|
||||
return '';
|
||||
}
|
||||
return \Jio\Content\ContentBlocks::get_instance()->shortcode_toc($atts, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -168,19 +168,39 @@ test('fixed top toolbar has TinyMCE formatting, media, and extension actions', (
|
||||
// 表格 / 公式 / 注释 / 代码都走参数弹窗,不再是固定内容。
|
||||
assert.match(appSource, /<BlockDialog/);
|
||||
assert.match(appSource, /setBlockDialog\(kind as BlockDialogKind\)/);
|
||||
assert.match(appSource, /setCodeDialog\(\{ language: 'generic', code: '' \}\)/);
|
||||
// 代码块直接插入,不弹窗。
|
||||
assert.match(appSource, /insertHtml\(codeBlockMarkup\('generic'\)\)/);
|
||||
assert.doesNotMatch(appSource, /setCodeDialog/);
|
||||
assert.doesNotMatch(read('assets/creator-studio/src/components/block-dialog.tsx'), /kind === 'code'/);
|
||||
assert.match(read('assets/creator-studio/src/components/block-dialog.tsx'), /onSubmit\(\{ kind, rows: clamp\(rows, 1, 20\), cols: clamp\(cols, 1, 10\), header, striped \}\)/);
|
||||
assert.match(read('assets/creator-studio/src/components/block-dialog.tsx'), /kind === 'code'/);
|
||||
assert.match(appSource, /onInsertBlock=\{insertBlock\}/);
|
||||
assert.match(appSource, /onInsertQuote=\{insertQuote\}/);
|
||||
const markup = read('assets/creator-studio/src/utils/block-markup.ts');
|
||||
assert.match(markup, /jio-toc-placeholder/);
|
||||
assert.match(markup, /jio-math/);
|
||||
assert.match(markup, /jio_math/);
|
||||
// 引用沿用主题的 quote_q 结构与配色类。
|
||||
assert.match(markup, /quote_q/);
|
||||
assert.match(markup, /qe_wzk_c-red/);
|
||||
// 公式与注释用短代码落库,由服务端保证渲染。
|
||||
assert.doesNotMatch(markup, /E = mc\^2/);
|
||||
assert.doesNotMatch(markup, /注释内容', 'jio'\)\s*\+\s*'<\/p>'/);
|
||||
assert.match(markup, /\[jio_math display="1"\]/);
|
||||
assert.match(markup, /\[jio_footnote\]/);
|
||||
assert.match(markup, /\[jio_toc\]/);
|
||||
// 代码块角标不再输出固定「高亮代码」文案。
|
||||
assert.doesNotMatch(read('assets/css/frontend/pages/submit-editor.css'), /高亮代码/);
|
||||
assert.doesNotMatch(read('assets/css/tinymce.css'), /content:\s*'高亮代码'/);
|
||||
|
||||
const blocks = read('src/Content/ContentBlocks.php');
|
||||
const shortcodes = read('src/Core/Shortcodes.php');
|
||||
assert.match(shortcodes, /add_shortcode\('jio_math'/);
|
||||
assert.match(shortcodes, /add_shortcode\('jio_footnote'/);
|
||||
assert.match(shortcodes, /add_shortcode\('jio_toc'/);
|
||||
assert.match(blocks, /function shortcode_math/);
|
||||
assert.match(blocks, /function shortcode_footnote/);
|
||||
assert.match(blocks, /function append_footnotes/);
|
||||
assert.match(blocks, /<sup>\$1\$2<\/sup>/);
|
||||
// 删除键必须能移除相邻的块级元素。
|
||||
assert.match(appSource, /function removeAdjacentBlock/);
|
||||
assert.match(appSource, /removeAdjacentBlock\(editor, event\)/);
|
||||
// 工具栏按钮统一为「图标在上、文字在下」。
|
||||
assert.match(editorToolbar, /jio-creator__tool-label/);
|
||||
assert.match(read('assets/creator-studio/src/styles/studio.scss'), /\.jio-creator__tool-btn\s*\{[\s\S]*?flex-direction:\s*column/);
|
||||
|
||||
Reference in new issue
Block a user