diff --git a/.changeset/blue-ways-teach.md b/.changeset/blue-ways-teach.md new file mode 100644 index 00000000..86465d5a --- /dev/null +++ b/.changeset/blue-ways-teach.md @@ -0,0 +1,6 @@ +--- +"hyperbook": minor +"@hyperbook/markdown": minor +--- + +Add pyide diff --git a/packages/hyperbook/build.ts b/packages/hyperbook/build.ts index 411a935c..8725461d 100644 --- a/packages/hyperbook/build.ts +++ b/packages/hyperbook/build.ts @@ -119,7 +119,11 @@ async function runBuild( case "archive": return posix.join("/", basePath || "", "archives", ...path); case "assets": - return `${posix.join("/", basePath || "", ASSETS_FOLDER, ...path)}?version=${packageJson.version}`; + if (path.length === 1 && path[0] === "/") { + return `${posix.join("/", basePath || "", ASSETS_FOLDER, ...path)}`; + } else { + return `${posix.join("/", basePath || "", ASSETS_FOLDER, ...path)}?version=${packageJson.version}`; + } } }, project: rootProject, diff --git a/packages/markdown/assets/directive-pyide/client.js b/packages/markdown/assets/directive-pyide/client.js new file mode 100644 index 00000000..4da3c0c0 --- /dev/null +++ b/packages/markdown/assets/directive-pyide/client.js @@ -0,0 +1,89 @@ +hyperbook.python = (function () { + window.codeInput?.registerTemplate( + "pyide-highlighted", + codeInput.templates.prism(window.Prism, [ + new codeInput.plugins.AutoCloseBrackets(), + new codeInput.plugins.Indent(true, 2), + ]) + ); + + const pyodideWorker = new Worker( + `${HYPERBOOK_ASSETS}directive-pyide/webworker.js` + ); + + const callbacks = {}; + let isRunning = false; + + const asyncRun = (id) => { + if (isRunning) return; + + isRunning = true; + updateRunning(); + return (script, context) => { + // the id could be generated more carefully + return new Promise((onSuccess) => { + callbacks[id] = onSuccess; + pyodideWorker.postMessage({ + ...context, + python: script, + id, + }); + }); + }; + }; + + const updateRunning = () => { + for (let elem of elems) { + const run = elem.getElementsByClassName("run")[0]; + if (isRunning) { + run.classList.add("running"); + run.textContent = "Running ..."; + } else { + run.classList.remove("running"); + run.textContent = "Run"; + } + run.disabled = isRunning; + } + }; + + pyodideWorker.onmessage = (event) => { + const { id, ...data } = event.data; + if (data.type === "stdout") { + const output = document + .getElementById(id) + .getElementsByClassName("output")[0]; + output.appendChild(document.createTextNode(data.message + "\n")); + return; + } + const onSuccess = callbacks[id]; + delete callbacks[id]; + isRunning = false; + updateRunning(); + onSuccess(data); + }; + + const elems = document.getElementsByClassName("directive-pyide"); + + for (let elem of elems) { + const editor = elem.getElementsByClassName("editor")[0]; + const run = elem.getElementsByClassName("run")[0]; + const output = elem.getElementsByClassName("output")[0]; + const id = elem.id; + + run?.addEventListener("click", () => { + const script = editor.value; + output.innerHTML = ""; + asyncRun(id)(script, {}) + .then(({ results, error }) => { + if (results) { + output.textContent = results; + } else if (error) { + output.textContent = error; + } + }) + .catch((e) => { + output.textContent = `Error: ${e}`; + }); + }); + } +})(); diff --git a/packages/markdown/assets/directive-pyide/style.css b/packages/markdown/assets/directive-pyide/style.css new file mode 100644 index 00000000..42f24330 --- /dev/null +++ b/packages/markdown/assets/directive-pyide/style.css @@ -0,0 +1,86 @@ +.directive-pyide { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + margin-bottom: 16px; + overflow: hidden; + gap: 8px; +} + +.directive-pyide code-input { + margin: 0; +} + +.directive-pyide .container { + width: 100%; + border: 1px solid var(--color-spacer); + border-radius: 8px; + overflow: hidden; + padding: 10px; +} + +.directive-pyide .output { + height: 200px; + white-space: pre; +} + +.directive-pyide .editor-container { + width: 100%; + display: flex; + flex-direction: column; + height: 400px; +} + +.directive-pyide .editor { + width: 100%; + border: 1px solid var(--color-spacer); + border-radius: 8px; + border-top-left-radius: 0; + border-top-right-radius: 0; + flex: 1; +} + +.directive-pyide button { + padding: 8px 16px; + border: 1px solid var(--color-spacer); + border-radius: 8px; + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + background-color: var(--color--background); + color: var(--color-text); + cursor: pointer; +} + +.directive-pyide button:hover { + background-color: var(--color-spacer); +} + +.directive-pyide button.running { + pointer-events: none; + cursor: not-allowed; + opacity: 0.5; +} + +@media screen and (min-width: 1024px) { + .directive-pyide { + flex-direction: row; + height: calc(100dvh - 128px); + + .output { + height: 100%; + } + + .container { + flex: 1; + height: 100% !important; + } + + .editor-container { + flex: 3; + height: 100%; + overflow: hidden; + } + } +} diff --git a/packages/markdown/assets/directive-pyide/webworker.js b/packages/markdown/assets/directive-pyide/webworker.js new file mode 100644 index 00000000..eddb354c --- /dev/null +++ b/packages/markdown/assets/directive-pyide/webworker.js @@ -0,0 +1,42 @@ +// Setup your project to serve `py-worker.js`. You should also serve +// `pyodide.js`, and all its associated `.asm.js`, `.json`, +// and `.wasm` files as well: +importScripts("https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js"); + +async function loadPyodideAndPackages() { + self.pyodide = await loadPyodide(); + await self.pyodide.loadPackage([]); +} +let pyodideReadyPromise = loadPyodideAndPackages(); + +self.onmessage = async (event) => { + // make sure loading is done + await pyodideReadyPromise; + // Don't bother yet with this line, suppose our API is built in such a way: + const { id, python, ...context } = event.data; + // The worker copies the context in its own "memory" (an object mapping name to values) + for (const key of Object.keys(context)) { + self[key] = context[key]; + } + + self.pyodide.setStdout({ + batched: (msg) => { + self.postMessage({ id, type: "stdout", message: msg }); + }, + }); + + self.pyodide.setStderr({ + batched: (msg) => { + self.postMessage({ id, type: "stderr", message: msg }); + }, + }); + + // Now is the easy part, the one that is similar to working in the main thread: + try { + await self.pyodide.loadPackagesFromImports(python); + let results = await self.pyodide.runPythonAsync(python); + self.postMessage({ results, id }); + } catch (error) { + self.postMessage({ error: error.message, id }); + } +}; diff --git a/packages/markdown/assets/prism/prism.js b/packages/markdown/assets/prism/prism.js index 6adbc139..8a133c56 100644 --- a/packages/markdown/assets/prism/prism.js +++ b/packages/markdown/assets/prism/prism.js @@ -1,7 +1,8 @@ /* PrismJS 1.29.0 -https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ +https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+python */ var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; !function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism); Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; +Prism.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern:/![sra](?=[:}]$)/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},Prism.languages.python["string-interpolation"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python; diff --git a/packages/markdown/dev.md b/packages/markdown/dev.md index 834acbf3..877d16c4 100644 --- a/packages/markdown/dev.md +++ b/packages/markdown/dev.md @@ -1,145 +1,21 @@ # Test Site -:::p5{height=400 editor=true} +:::pyide -```js -let circleX = 200; -let circleY = 150; -let circleRadius = 75; -let graphX = 50; -let graphY = 300; -let graphAmplitude = 50; -let graphPeriod = 300; - -function setup() { - createCanvas(400, 400); - angleMode(DEGREES); - describe( - 'Animated demonstration of a point moving around the unit circle, together with the corresponding sine and cosine values moving along their graphs.' - ); -} - -function draw() { - background(0); - - // Set angle based on frameCount, and display current value - - let angle = frameCount % 360; - - fill(255); - textSize(20); - textAlign(LEFT, CENTER); - text(`angle: ${angle}`, 25, 25); - - // Draw circle and diameters - - noFill(); - stroke(128); - strokeWeight(3); - circle(circleX, circleY, 2 * circleRadius); - line(circleX, circleY - circleRadius, circleX, circleY + circleRadius); - line(circleX - circleRadius, circleY, circleX + circleRadius, circleY); - - // Draw moving points - - let pointX = circleX + circleRadius * cos(angle); - let pointY = circleY - circleRadius * sin(angle); - - line(circleX, circleY, pointX, pointY); - - noStroke(); - - fill('white'); - circle(pointX, pointY, 10); - - fill('orange'); - circle(pointX, circleY, 10); - - fill('red'); - circle(circleX, pointY, 10); - - // Draw graph - - stroke('grey'); - strokeWeight(3); - line(graphX, graphY, graphX + 300, graphY); - line(graphX, graphY - graphAmplitude, graphX, graphY + graphAmplitude); - line( - graphX + graphPeriod, - graphY - graphAmplitude, - graphX + graphPeriod, - graphY + graphAmplitude - ); - - fill('grey'); - strokeWeight(1); - textAlign(CENTER, CENTER); - text('0', graphX, graphY + graphAmplitude + 20); - text('360', graphX + graphPeriod, graphY + graphAmplitude + 20); - text('1', graphX / 2, graphY - graphAmplitude); - text('0', graphX / 2, graphY); - text('-1', graphX / 2, graphY + graphAmplitude); - - fill('orange'); - text('cos', graphX + graphPeriod + graphX / 2, graphY - graphAmplitude); - fill('red'); - text('sin', graphX + graphPeriod + graphX / 2, graphY); - - // Draw cosine curve - - noFill(); - stroke('orange'); - beginShape(); - for (let t = 0; t <= 360; t++) { - let x = map(t, 0, 360, graphX, graphX + graphPeriod); - let y = graphY - graphAmplitude * cos(t); - vertex(x, y); - } - endShape(); - - // Draw sine curve - - noFill(); - stroke('red'); - beginShape(); - for (let t = 0; t <= 360; t++) { - let x = map(t, 0, 360, graphX, graphX + graphPeriod); - let y = graphY - graphAmplitude * sin(t); - vertex(x, y); - } - endShape(); - - // Draw moving line - - let lineX = map(angle, 0, 360, graphX, graphX + graphPeriod); - stroke('grey'); - line(lineX, graphY - graphAmplitude, lineX, graphY + graphAmplitude); - - // Draw moving points on graph +```python +a = 5 + 2 +print(a) +``` - let orangeY = graphY - graphAmplitude * cos(angle); - let redY = graphY - graphAmplitude * sin(angle); +::: - noStroke(); +:::pyide - fill('orange'); - circle(lineX, orangeY, 10); - fill('red'); - circle(lineX, redY, 10); -} +```python +a = 5 + 2 +print(a) ``` -::: - -```abcjs editor -X:1 -T: Cooley's Long -M: 4/4 -L: 1/8\nR: reel\nK: Emin -D2|:"Em"EB{c}BA B2 EB|~B2 AB dBAG|"D"FDAD BDAD|FDAD dAFD| -"Em"EBBA B2 EB|B2 AB defg|"D"afe^c dBAF|1"Em"DEFD E2 D2:|2"Em"DEFD E2 gf|| -|:"Em"eB B2 efge|eB B2 gedB|"D"A2 FA DAFA|A2 FA defg| -"Em"eB B2 eBgB|eB B2 defg|"D"afe^c dBAF|1"Em"DEFD E2 gf:|2"Em"DEFD E4|] -``` \ No newline at end of file +::: \ No newline at end of file diff --git a/packages/markdown/src/process.ts b/packages/markdown/src/process.ts index 28707490..83830b5e 100644 --- a/packages/markdown/src/process.ts +++ b/packages/markdown/src/process.ts @@ -50,6 +50,7 @@ import rehypeDirectiveP5 from "./rehypeDirectiveP5"; import remarkCollectSearchDocuments from "./remarkCollectSearchDocuments"; import remarkDirectiveGeogebra from "./remarkDirectiveGeogebra"; import remarkDirectiveAbcMusic from "./remarkDirectiveAbcMusic"; +import remarkDirectivePyide from "./remarkDirectivePyide"; export const remark = (ctx: HyperbookContext) => { const remarkPlugins: PluggableList = [ @@ -76,6 +77,7 @@ export const remark = (ctx: HyperbookContext) => { remarkDirectiveTiles(ctx), remarkDirectiveTabs(ctx), remarkDirectiveSqlIde(ctx), + remarkDirectivePyide(ctx), remarkDirectiveOnlineIde(ctx), remarkDirectivePlantuml(ctx), remarkDirectiveSlideshow(ctx), diff --git a/packages/markdown/src/remarkDirectivePyide.ts b/packages/markdown/src/remarkDirectivePyide.ts new file mode 100644 index 00000000..202d425d --- /dev/null +++ b/packages/markdown/src/remarkDirectivePyide.ts @@ -0,0 +1,112 @@ +// Register directive nodes in mdast: +/// +// +import { HyperbookContext } from "@hyperbook/types"; +import { Root } from "mdast"; +import fs from "fs"; +import path from "path"; +import { visit } from "unist-util-visit"; +import { VFile } from "vfile"; +import { + expectContainerDirective, + isDirective, + registerDirective, + requestCSS, + requestJS, +} from "./remarkHelper"; +import { toText } from "./mdastUtilToText"; +import hash from "./objectHash"; + +export default (ctx: HyperbookContext) => () => { + const name = "pyide"; + return (tree: Root, file: VFile) => { + visit(tree, function (node) { + if (isDirective(node)) { + if (node.name !== name) return; + + const data = node.data || (node.data = {}); + const { src = "", id } = node.attributes || {}; + + expectContainerDirective(node, file, name); + registerDirective(file, name, ["client.js"], ["style.css"]); + requestJS(file, ["code-input", "code-input.min.js"]); + requestCSS(file, ["code-input", "code-input.min.css"]); + requestJS(file, ["code-input", "auto-close-brackets.min.js"]); + requestJS(file, ["code-input", "indent.min.js"]); + + let srcFile = ""; + + if (src) { + srcFile = fs.readFileSync( + path.join(ctx.root, "public", String(src)), + "utf8" + ); + } else if (node.children?.length > 0) { + srcFile = toText(node.children); + } + + data.hName = "div"; + data.hProperties = { + class: "directive-pyide", + id: id || hash(node) + }; + data.hChildren = [ + { + type: "element", + tagName: "div", + properties: { + class: "container", + }, + children: [ + { + type: "element", + tagName: "pre", + properties: { + class: "output", + }, + children: [], + }, + ], + }, + { + type: "element", + tagName: "div", + properties: { + class: "editor-container", + }, + children: [ + { + type: "element", + tagName: "button", + properties: { + class: "run", + }, + children: [ + { + type: "text", + value: "Run", + }, + ], + }, + { + type: "element", + tagName: "code-input", + properties: { + class: "editor", + language: "python", + template: "pyide-highlighted", + }, + children: [ + { + type: "raw", + value: srcFile, + }, + ], + }, + ], + }, + ]; + } + }); + }; +}; diff --git a/website/de/book/elements/pyide.md b/website/de/book/elements/pyide.md new file mode 100644 index 00000000..9608c07a --- /dev/null +++ b/website/de/book/elements/pyide.md @@ -0,0 +1,60 @@ +--- +name: Pyide +permaid: pyide +lang: de +--- + +Das `pyide`-Element repräsentiert eine Python-Integrated-Development-Environment (IDE)-Komponente. +Es wird verwendet, um eine Python-Coding-Umgebung in die Hyperbook-Website einzubetten. +Dieses Element ermöglicht es Benutzern, Python-Code direkt im Browser zu schreiben, zu bearbeiten und auszuführen. + +````md +:::pyide + + +```python +a = 5 + 2 +print(a) +``` +::: + + +```` + +:::pyide + + +```python +a = 5 + 2 +print(a) +``` + +::: + +Sie können auch jedes Paket verwenden, das hier aufgeführt ist: https://pyodide.org/en/stable/usage/packages-in-pyodide.html + +````md +:::pyide + + +```python +import numpy as np + +a = np.arange(15).reshape(3, 5) +print(a) +``` + +::: +```` + +:::pyide + + +```python +import numpy as np + +a = np.arange(15).reshape(3, 5) +print(a) +``` + +::: \ No newline at end of file diff --git a/website/en/book/changelog.md b/website/en/book/changelog.md index 037d0c73..a60e8259 100644 --- a/website/en/book/changelog.md +++ b/website/en/book/changelog.md @@ -38,6 +38,18 @@ If you need a new feature, open an [issue](https://github.com/openpatch/hyperboo :::: --> +## v0.44.0 + +::::tabs + +:::tab{title="New :rocket:" id="new"} + +- A new element pyide that allows you to run python in your browser. [Learn more](/elements/pyide) + +::: + +:::: + ## v0.43.0 ::::tabs diff --git a/website/en/book/elements/pyide.md b/website/en/book/elements/pyide.md new file mode 100644 index 00000000..c158bfcc --- /dev/null +++ b/website/en/book/elements/pyide.md @@ -0,0 +1,59 @@ +--- +name: Pyide +permaid: pyide +--- + +The `pyide` element represents a Python Integrated Development Environment (IDE) component. +It is used to embed a Python coding environment within the hyperbook website. +This element allows users to write, edit, and execute Python code directly in the browser. + +````md +:::pyide + + +```python +a = 5 + 2 +print(a) +``` +::: + + +```` + +:::pyide + + +```python +a = 5 + 2 +print(a) +``` + +::: + +You can also use any package listed here: https://pyodide.org/en/stable/usage/packages-in-pyodide.html + +````md +:::pyide + + +```python +import numpy as np + +a = np.arange(15).reshape(3, 5) +print(a) +``` + +::: +```` + +:::pyide + + +```python +import numpy as np + +a = np.arange(15).reshape(3, 5) +print(a) +``` + +::: \ No newline at end of file