mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
The goal of this MR is to unobtrusively allow the usage of `<script type="py">`, `<script type="pyscript">` or `<script type="py-script">` tags instead of `<py-script>` for all those case where the layout in custom elements get parsed and breaks users' expectations (including our SVG based tests).
This commit is contained in:
committed by
GitHub
parent
074ca0ef8f
commit
a1281d1331
@@ -10,6 +10,9 @@ Features
|
||||
- The `py-mount` attribute on HTML elements has been deprecated, and will be removed in a future release.
|
||||
|
||||
|
||||
### <script type="py">
|
||||
- Added the ability to optionally use `<script type="py">`, `<script type="pyscript">` or `<script type="py-script">` instead of a `<py-script>` custom element, in order to tackle cases where the content of the `<py-script>` tag, inevitably parsed by browsers, could accidentally contain *HTML* able to break the surrounding page layout. ([#1396](https://github.com/pyscript/pyscript/pull/1396))
|
||||
|
||||
### <py-terminal>
|
||||
- Added a `docked` field and attribute for the `<py-terminal>` custom element, enabled by default when the terminal is in `auto` mode, and able to dock the terminal at the bottom of the page with auto scroll on new code execution.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# <py-script>
|
||||
|
||||
The `<py-script>` element lets you execute multi-line Python scripts both inline and via a src attribute.
|
||||
The `<py-script>` element, also available as `<script type="py-script">`, lets you execute multi-line Python scripts both inline and via a src attribute.
|
||||
|
||||
## Attributes
|
||||
|
||||
@@ -12,13 +12,13 @@ The `<py-script>` element lets you execute multi-line Python scripts both inline
|
||||
|
||||
### output
|
||||
|
||||
If the `output` attribute is provided, any output to [sys.stdout](https://docs.python.org/3/library/sys.html#sys.stdout) or [sys.stderr](https://docs.python.org/3/library/sys.html#sys.stderr) is written to the DOM element with the ID matching the attribute. If no DOM element is found with a matching ID, a warning is shown. The msg is output to the `innerHTML` of the HTML Element, with newlines (`\n'`) converted to breaks (`<br\>`).
|
||||
If the `output` attribute is provided, any output to [sys.stdout](https://docs.python.org/3/library/sys.html#sys.stdout) or [sys.stderr](https://docs.python.org/3/library/sys.html#sys.stderr) is written to the DOM element with the ID matching the attribute. If no DOM element is found with a matching ID, a warning is shown. The msg is output to the `innerHTML` of the HTML Element, with newlines (`\n'`) converted to breaks (`<br/>`).
|
||||
|
||||
This output is in addition to the output being written to the developer console and the `<py-terminal>` if it is being used.
|
||||
|
||||
### stderr
|
||||
|
||||
If the `stderr` attribute is provided, any output to [sys.stderr](https://docs.python.org/3/library/sys.html#sys.stderr) is written to the DOM element with the ID matching the attribute. If no DOM element is found with a matching ID, a warning is shown. The msg is output to the `innerHTML` of the HTML Element, with newlines (`\n'`) converted to breaks (`<br\>`).
|
||||
If the `stderr` attribute is provided, any output to [sys.stderr](https://docs.python.org/3/library/sys.html#sys.stderr) is written to the DOM element with the ID matching the attribute. If no DOM element is found with a matching ID, a warning is shown. The msg is output to the `innerHTML` of the HTML Element, with newlines (`\n'`) converted to breaks (`<br/>`).
|
||||
|
||||
This output is in addition to the output being written to the developer console and the `<py-terminal>` if it is being used.
|
||||
|
||||
|
||||
@@ -37,4 +37,12 @@ module.exports = {
|
||||
'@typescript-eslint/no-empty-function': 'error',
|
||||
'@typescript-eslint/restrict-template-expressions': ['error', { allowBoolean: true }],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['src/components/pyscript.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { htmlDecode, ensureUniqueId, createDeprecationWarning } from '../utils';
|
||||
import { ltrim, htmlDecode, ensureUniqueId, createDeprecationWarning } from '../utils';
|
||||
import { getLogger } from '../logger';
|
||||
import { pyExec, displayPyException } from '../pyexec';
|
||||
import { _createAlertBanner } from '../exceptions';
|
||||
@@ -10,12 +10,10 @@ import { InterpreterClient } from '../interpreter_client';
|
||||
const logger = getLogger('py-script');
|
||||
|
||||
export function make_PyScript(interpreter: InterpreterClient, app: PyScriptApp) {
|
||||
class PyScript extends HTMLElement {
|
||||
srcCode: string;
|
||||
stdout_manager: Stdio | null;
|
||||
stderr_manager: Stdio | null;
|
||||
|
||||
async connectedCallback() {
|
||||
/**
|
||||
* A common <py-script> VS <script type="py"> initializator.
|
||||
*/
|
||||
const init = async (pyScriptTag: PyScript, fallback: () => string) => {
|
||||
/**
|
||||
* Since connectedCallback is async, multiple py-script tags can be executed in
|
||||
* an order which is not particularly sequential. The locking mechanism here ensures
|
||||
@@ -27,44 +25,125 @@ export function make_PyScript(interpreter: InterpreterClient, app: PyScriptApp)
|
||||
let releaseLock: () => void;
|
||||
try {
|
||||
releaseLock = await app.tagExecutionLock();
|
||||
ensureUniqueId(this);
|
||||
// Save innerHTML information in srcCode so we can access it later
|
||||
// once we clean innerHTML (which is required since we don't want
|
||||
// source code to be rendered on the screen)
|
||||
this.srcCode = this.innerHTML;
|
||||
const pySrc = await this.getPySrc();
|
||||
this.innerHTML = '';
|
||||
|
||||
await app.plugins.beforePyScriptExec({ interpreter: interpreter, src: pySrc, pyScriptTag: this });
|
||||
const result = (await pyExec(interpreter, pySrc, this)).result;
|
||||
await app.plugins.afterPyScriptExec({
|
||||
interpreter: interpreter,
|
||||
src: pySrc,
|
||||
pyScriptTag: this,
|
||||
result: result,
|
||||
});
|
||||
ensureUniqueId(pyScriptTag);
|
||||
const src = await fetchSource(pyScriptTag, fallback);
|
||||
await app.plugins.beforePyScriptExec({ interpreter, src, pyScriptTag });
|
||||
const { result } = await pyExec(interpreter, src, pyScriptTag);
|
||||
await app.plugins.afterPyScriptExec({ interpreter, src, pyScriptTag, result });
|
||||
} finally {
|
||||
releaseLock();
|
||||
app.decrementPendingTags();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async getPySrc(): Promise<string> {
|
||||
if (this.hasAttribute('src')) {
|
||||
const url = this.getAttribute('src');
|
||||
/**
|
||||
* Given a generic DOM Element, tries to fetch the 'src' attribute, if present.
|
||||
* It either throws an error if the 'src' can't be fetched or it returns a fallback
|
||||
* content as source.
|
||||
*/
|
||||
const fetchSource = async (tag: Element, fallback: () => string): Promise<string> => {
|
||||
if (tag.hasAttribute('src')) {
|
||||
try {
|
||||
const response = await robustFetch(url);
|
||||
const response = await robustFetch(tag.getAttribute('src'));
|
||||
return await response.text();
|
||||
} catch (err) {
|
||||
const e = err as Error;
|
||||
_createAlertBanner(e.message);
|
||||
this.innerHTML = '';
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
return htmlDecode(this.srcCode);
|
||||
}
|
||||
return fallback();
|
||||
};
|
||||
|
||||
class PyScript extends HTMLElement {
|
||||
srcCode: string;
|
||||
stdout_manager: Stdio | null;
|
||||
stderr_manager: Stdio | null;
|
||||
_fetchSourceFallback = () => htmlDecode(this.srcCode);
|
||||
|
||||
async connectedCallback() {
|
||||
// Save innerHTML information in srcCode so we can access it later
|
||||
// once we clean innerHTML (which is required since we don't want
|
||||
// source code to be rendered on the screen)
|
||||
this.srcCode = this.innerHTML;
|
||||
this.innerHTML = '';
|
||||
await init(this, this._fetchSourceFallback);
|
||||
}
|
||||
|
||||
getPySrc(): Promise<string> {
|
||||
return fetchSource(this, this._fetchSourceFallback);
|
||||
}
|
||||
}
|
||||
|
||||
// bootstrap the <script> tag fallback only if needed (once per definition)
|
||||
if (!customElements.get('py-script')) {
|
||||
// allow any HTMLScriptElement to behave like a PyScript custom-elelement
|
||||
type PyScriptElement = HTMLScriptElement & PyScript;
|
||||
|
||||
// the <script> tags to look for, acting like a <py-script> one
|
||||
// both py, pyscript, and py-script, are valid types to help reducing typo cases
|
||||
const pyScriptCSS = 'script[type="py"],script[type="pyscript"],script[type="py-script"]';
|
||||
|
||||
// bootstrap with the same connectedCallback logic any <script>
|
||||
const bootstrap = (script: PyScriptElement) => {
|
||||
const pyScriptTag = document.createElement('py-script-tag') as PyScript;
|
||||
|
||||
// move attributes to the live resulting pyScriptTag reference
|
||||
for (const name of ['output', 'stderr']) {
|
||||
const value = script.getAttribute(name);
|
||||
if (value) {
|
||||
pyScriptTag.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
// insert pyScriptTag companion right after the original script
|
||||
script.after(pyScriptTag);
|
||||
|
||||
// remove the first empty line to preserve line numbers/counting
|
||||
init(pyScriptTag, () => ltrim(script.textContent.replace(/^[\r\n]+/, ''))).catch(() =>
|
||||
pyScriptTag.remove(),
|
||||
);
|
||||
};
|
||||
|
||||
// callback used to bootstrap already known <script> tags
|
||||
const callback: MutationCallback = records => {
|
||||
for (const { addedNodes } of records) {
|
||||
for (const node of addedNodes) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
if ((node as PyScriptElement).matches(pyScriptCSS)) {
|
||||
bootstrap(node as PyScriptElement);
|
||||
}
|
||||
for (const child of (node as PyScriptElement).querySelectorAll(pyScriptCSS)) {
|
||||
bootstrap(child as PyScriptElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// globally shared MutationObserver for <script> special cases
|
||||
const pyScriptMO = new MutationObserver(callback);
|
||||
|
||||
// simplifies observing any root node (document/shadowRoot)
|
||||
const observe = (root: Document | ShadowRoot) => {
|
||||
pyScriptMO.observe(root, { childList: true, subtree: true });
|
||||
return root;
|
||||
};
|
||||
|
||||
// patch attachShadow once to bootstrap <script> special cases in there too
|
||||
const { attachShadow } = Element.prototype;
|
||||
Object.assign(Element.prototype, {
|
||||
attachShadow(init: ShadowRootInit) {
|
||||
return observe(attachShadow.call(this as Element, init));
|
||||
},
|
||||
});
|
||||
|
||||
// bootstrap all already live py <script> tags
|
||||
callback([{ addedNodes: document.querySelectorAll(pyScriptCSS) } as unknown] as MutationRecord[], null);
|
||||
|
||||
// once all tags have been initialized, observe new possible tags added later on
|
||||
// this is to save a few ticks within the callback as each <script> already adds a companion node
|
||||
observe(document);
|
||||
}
|
||||
|
||||
return PyScript;
|
||||
|
||||
@@ -9,10 +9,10 @@ class TestBasic(PyScriptTest):
|
||||
def test_pyscript_hello(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
<script type="py">
|
||||
import js
|
||||
js.console.log('hello pyscript')
|
||||
</py-script>
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines == ["hello pyscript"]
|
||||
|
||||
Reference in New Issue
Block a user