diff --git a/lib/html_head.typ b/lib/html_head.typ index 1c5fe31..0cbb3b3 100644 --- a/lib/html_head.typ +++ b/lib/html_head.typ @@ -1,6 +1,6 @@ #import "./custom_html.typ" as chtml -#import "./html_utils.typ": get-css, get-js -#import "./pyscript.typ": state-use-pyscript, state-pyscript-headers, state-pyscript-version +#import "./html_utils.typ": get-css, get-js, additionnal-head-tags +#import "./pyscript.typ": state-use-pyscript, state-pyscript-data-list, state-pyscript-version /// Generate the html element for the page. #let html-head( @@ -95,20 +95,25 @@ } }} + html.style(get-css()) + html.script(get-js()) + context for tag in additionnal-head-tags.final() { + tag + } + context if state-use-pyscript.final() { assert( state-pyscript-version.final() != none, message: "Cannot run python script: pyscript-version is not set" ) assert( - state-pyscript-version.final() in state-pyscript-headers.final(), + state-pyscript-version.final() in state-pyscript-data-list.final(), message: "Cannot run python script: urls for " + state-pyscript-version.final() + " not set in pyscript-urls" ) - state-pyscript-headers.final().at(state-pyscript-version.final()) + let pyscript-data = state-pyscript-data-list.final().at(state-pyscript-version.final()) + html.script(type: "module", src: pyscript-data.core-js-url) + pyscript-data.additionnal-head-tags } - html.style(get-css()) - html.script(get-js()) - }) } diff --git a/lib/html_utils.typ b/lib/html_utils.typ index 6531b25..e90ead5 100644 --- a/lib/html_utils.typ +++ b/lib/html_utils.typ @@ -3,6 +3,7 @@ #let css-list = state("css-list", ()) #let js-list = state("js-list", ()) +#let additionnal-head-tags = state("additionnal-head-tags", ()) /// Add string `css` to `css-list` if not already present #let add-css(css) = context { @@ -14,6 +15,11 @@ js-list.update(x => if js in x { x } else { x + (js,) }) } +/// Add additionnal html tag to insert in +#let add-tag-in-head(tag) = context { + additionnal-head-tags.update(x => if tag in x { x } else { x + (tag, ) }) +} + /// Concatenate all css found in css-list at the end of the document #let get-css() = context { css-list.final().join("\n\n") diff --git a/lib/main.typ b/lib/main.typ index 8d79a72..6de6b54 100644 --- a/lib/main.typ +++ b/lib/main.typ @@ -2,7 +2,7 @@ #import "./html_body.typ": html-body #import "./html_utils.typ": html-show #import "./summary.typ": summary, card-list -#import "./pyscript.typ": state-use-pyscript, state-pyscript-headers, state-pyscript-version, state-pyscript-interpreters, state-pyscript-default-interpreter, pyscript-show +#import "./pyscript.typ": state-use-pyscript, state-pyscript-data-list, state-pyscript-version, state-pyscript-interpreters, state-pyscript-default-interpreter, pyscript-show, pyscript-data #import "./rss.typ": rss #import "./icons.typ" @@ -32,8 +32,9 @@ stylesheets: (), /// List of related sites for metadata me-links: (), - /// Dictionnary of available tags to add in header for each versions of pyscript - pyscript-headers: (:), + /// Dictionnary of available pyscript-data for each versions of pyscript + /// expected in the form of ("": pyscript-data-list("", { html.link(...) }), ...) + pyscript-data-list: (:), /// Dictionnary of available python version to add in header for each pyscript interpreter pyscript-interpreters: (:), /// Pyscript version to use @@ -57,7 +58,7 @@ ) = { assert(type(url) == str, message: "A page must have an url") context { - state-pyscript-headers.update(x => pyscript-headers) + state-pyscript-data-list.update(x => pyscript-data-list) state-pyscript-interpreters.update(x => pyscript-interpreters) state-pyscript-version.update(x => pyscript-version) state-pyscript-default-interpreter.update(x => pyscript-default-interpreter) diff --git a/lib/pyscript.typ b/lib/pyscript.typ index 0788d6e..9c6967b 100644 --- a/lib/pyscript.typ +++ b/lib/pyscript.typ @@ -1,8 +1,22 @@ +#import "./html_utils.typ": add-tag-in-head + #let state-use-pyscript = state("state-use-pyscript", false) #let state-pyscript-version = state("state-pyscript-version", none) -#let state-pyscript-headers = state("state-pyscript-headers", (:)) +#let state-pyscript-data-list = state("state-pyscript-data-list", (:)) #let state-pyscript-interpreters = state("state-pyscript-interpreters", (:)) #let state-pyscript-default-interpreter = state("state-pyscript-default-interpreter", none) +#let state-pyscript-canvas-ids = state("state-pyscript-canvas-ids", ()) + +/// Define data needed to load a version of pyscript +#let pyscript-data( + /// Url to `core.js` + core-js-url, + /// Additionnal tags to add to , like mini-coi of core.css + additionnal-head-tags: {}, +) = ( + core-js-url: core-js-url, + additionnal-head-tags: additionnal-head-tags +) #let get-pep723(script) = { script.find(regex( @@ -31,81 +45,131 @@ metadata = (:) } state-use-pyscript.update(x => true) - context { - let config = metadata.at("tool", default: (:)).at("pyscript", default: (:)) + let config = metadata.at("tool", default: (:)).at("pyscript", default: (:)) - let pyscript-config = (:) - // Package dependencies - if "dependencies" in metadata { - pyscript-config.insert("packages", metadata.at("dependencies")) - } - // Files stetup - if "files" in config { - pyscript-config.insert("files", config.at("files")) - } - // Interpreteur selection - if "interpreter" in config { - pyscript-config.insert("interpreter", config.at("interpreter")) - } else if state-pyscript-default-interpreter.final() != none { - assert( - state-pyscript-default-interpreter.final() in state-pyscript-interpreters.final(), - message: state-pyscript-default-interpreter.final() + " is not in pyscript-interpreters", - ) - pyscript-config.insert("interpreter", state-pyscript-interpreters.final().at(state-pyscript-default-interpreter.final())) - } - - let attrs = ( - type: "py" + let pyscript-config = (:) + // Package dependencies + if "dependencies" in metadata { + pyscript-config.insert("packages", metadata.at("dependencies")) + } + // Files stetup + if "files" in config { + pyscript-config.insert("files", config.at("files")) + } + // Interpreteur selection + if "interpreter" in config { + pyscript-config.insert("interpreter", config.at("interpreter")) + } else if state-pyscript-default-interpreter.final() != none { + assert( + state-pyscript-default-interpreter.final() in state-pyscript-interpreters.final(), + message: state-pyscript-default-interpreter.final() + " is not in pyscript-interpreters", ) - let default_val_terminal = true - let default_val_worker = true - let default_val_canvas = none - // TODO: canvas: can only be used once by page, need inserting canvas html tag and pyscript hook to link it to pyodide - if config.at("pygame", default: false) { - default_val_terminal = false - default_val_worker = false - default_val_canvas = "canvas" - } - if config.at("terminal", default: true) { - attrs.insert("terminal", "") - } - if config.at("worker", default: true) { - attrs.insert("worker", "") - } - if config.at("canvas", default: default_val_canvas) not in (none, false) { - attrs.insert("canvas", config.at("canvas", default: default_val_canvas)) - } + pyscript-config.insert("interpreter", state-pyscript-interpreters.final().at(state-pyscript-default-interpreter.final())) + } - if pyscript-config != (:) { - attrs.insert("config", json.encode(pyscript-config)) - } - - let script = it.text; - if config.at("repl", default: false) { - script = "import code\n" + script + "\ncode.interact(banner='', local=globals())" - } - let displayed-code = it.text; - if config.at("hide-meta", default: false) { - displayed-code = displayed-code.replace(get-pep723(displayed-code), "").trim("\n") - } + let attrs = ( + type: "py" + ) + let default-val-terminal = true + let default-val-worker = true + let default-val-canvas = none + if config.at("pygame", default: false) { + default-val-terminal = false + default-val-worker = false + default-val-canvas = "canvas" + } + if config.at("terminal", default: default-val-terminal) { + attrs.insert("terminal", "") + } + if config.at("worker", default: default-val-worker) { + attrs.insert("worker", "") + } + let canvas-attr = config.at("canvas", default: default-val-canvas) + if canvas-attr == false { + canvas-attr = none + } + if canvas-attr == true { + canvas-attr = "canvas" + } + if canvas-attr != none { + attrs.insert("canvas", canvas-attr) - if not config.at("hide-code", default: false) { - raw( - displayed-code, - block: true, - lang: "python", - align: it.align, - syntaxes: it.syntaxes, - theme: it.theme, - tab-size: it.tab-size, - ) - } + assert( + canvas-attr not in state-pyscript-canvas-ids.at(here()), + message: ( + "Can not have multiple canvas with the same name. Note that SDL2 has issues working with canvas", + "with an id different than 'canvas', meaning that it is unadvised to have more than one script", + "using SDL2 by page.", + "(tool.pyscript.pygame = true set the default value for the SDL2 canvas to 'canvas' instead of none)" + ).join(" ") + ) + state-pyscript-canvas-ids.update(canvas-ids => canvas-ids + (canvas-attr,)) + } - html.elem( - "script", - attrs: attrs, - script + if pyscript-config != (:) { + attrs.insert("config", json.encode(pyscript-config)) + } + + let script = it.text; + if config.at("repl", default: false) { + script = "import code\n" + script + "\ncode.interact(banner='', local=globals())" + } + let displayed-code = it.text; + if config.at("hide-meta", default: false) { + displayed-code = displayed-code.replace(get-pep723(displayed-code), "").trim("\n") + } + + if not config.at("hide-code", default: false) { + raw( + displayed-code, + block: true, + lang: "python", + align: it.align, + syntaxes: it.syntaxes, + theme: it.theme, + tab-size: it.tab-size, + ) + } + + html.elem( + "script", + attrs: attrs, + script + ) + if canvas-attr != none { + html.elem("canvas", attrs: ("id": canvas-attr)) + + let core-js-url = state-pyscript-data-list.final().at(state-pyscript-version.final()).core-js-url + add-tag-in-head( + html.script( + type: "module", + ``` + import { hooks } from ""; + hooks.main.onReady.add((wrap, script) => { + if (script.hasAttribute("canvas")) { + const target = script.getAttribute("canvas"); + const canvas = document.getElementById(target); + wrap.interpreter.canvas.setCanvas2D(canvas); + } + }); + ```.text.replace("", core-js-url) + ) ) } - //raw(block: true, lang: "json", json.encode(metadata)) } + // also need pyscript hook to link canvas to pyodide: + // +// + //raw(block: true, lang: "json", json.encode(metadata)) diff --git a/test_template/isn_s_cube-0.1.0-py3-none-any.whl b/test_template/isn_s_cube-0.1.0-py3-none-any.whl new file mode 100644 index 0000000..53c14cc Binary files /dev/null and b/test_template/isn_s_cube-0.1.0-py3-none-any.whl differ diff --git a/test_template/main.typ b/test_template/main.typ index e42bc0a..6d14821 100644 --- a/test_template/main.typ +++ b/test_template/main.typ @@ -61,12 +61,14 @@ icon: "https://jean-marie.mineau.eu/website_assets/platypus.png", // Pyscript: - pyscript-headers: ( - "remote-2026.3.1": { - html.elem("script", attrs: (src: "./mini-coi.js")) - html.elem("script", attrs: (type: "module", src: "https://pyscript.net/releases/2026.3.1/core.js")) - html.elem("link", attrs: (rel: "stylesheet", href: "https://pyscript.net/releases/2026.3.1/core.css")) - }, + pyscript-data-list: ( + "remote-2026.3.1": pyscript-data( + "https://pyscript.net/releases/2026.3.1/core.js", + additionnal-head-tags: { + html.elem("script", attrs: (src: "./mini-coi.js")) + html.elem("link", attrs: (rel: "stylesheet", href: "https://pyscript.net/releases/2026.3.1/core.css")) + }, + ) ), pyscript-version: "remote-2026.3.1", ) @@ -125,7 +127,7 @@ pprint([(k, v["title"]) for k, v in data.items()][:10]) # [tool.pyscript.files] # "https://peps.python.org/api/peps.json" = "./peps.json" # /// - +# setting tool.pyscript.hide-meta to true will hide the `/// script` section import json from rich.pretty import pprint @@ -136,6 +138,19 @@ pprint([(k, v["title"]) for k, v in data.items()][:10]) # Inline script metadata ``` +```python-run +# /// script +# dependencies = [ +# "pygame-ce", +# "./isn_s_cube-0.1.0-py3-none-any.whl" +# ] +# [tool.pyscript] +# pygame = true +# /// +from isn_s_cube import wasm +await wasm() +``` + #summ.card Test, `this is not a code block`, end test.