Skip to content

Instantly share code, notes, and snippets.

@dunossauro
Created September 12, 2025 16:19
Show Gist options
  • Select an option

  • Save dunossauro/f0003effa1ed92dc85edb9706165972f to your computer and use it in GitHub Desktop.

Select an option

Save dunossauro/f0003effa1ed92dc85edb9706165972f to your computer and use it in GitHub Desktop.
aceodide
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Editor de Exercícios</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.js"></script>
<script src="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
<script src="test.js"></script>
<link rel="stylesheet" href="test_css.css">
</head>
<body>
<h1>Editor de Exercícios</h1>
<code-exercice source="" test="assert xpto"></code-exercice>
</body>
</html>
hljs.highlightAll();
class CodeExercice extends HTMLElement {
constructor() {
super();
this.source = this.getAttribute('source') || ''
if (this.hasAttribute('testfilepath')) {
this.testPath = this.attributes.testfilepath.value;
this.hasTest = true;
} else {
this.testPath = this.id;
this.hasTest = false;
}
this.darkTheme = 'github_dark'
this.lightTheme = 'github_light_default'
}
// Função para codificar o código em Base64
encodeCode(code) {
return btoa(code); // btoa codifica a string em Base64
}
// Função para decodificar o código da URL
decodeCodeFromUrl() {
const params = new URLSearchParams(window.location.search);
const encodedCode = params.get("code"); // Espera algo como ?code=base64_encoded_code
if (encodedCode) {
return atob(encodedCode); // atob decodifica a string Base64
}
return "";
}
// Função para gerar o link de compartilhamento
generateShareLink(code) {
const encodedCode = this.encodeCode(code);
const currentUrl = window.location.origin + window.location.pathname;
return `${currentUrl}?code=${encodedCode}`;
}
setTheme(currentTheme) {
if (currentTheme === "default") {
this.editor.setTheme("ace/theme/" + this.lightTheme);
} else if (currentTheme === "slate") {
this.editor.setTheme("ace/theme/" + this.darkTheme);
}
}
getTheme() {
var theme = document.body.getAttribute('data-md-color-scheme')
if (theme == 'default') {
return this.lightTheme
}
return this.darkTheme
}
saveCode() {
function dataUrl(data) {
return "data:x-application/text;charset=utf-8," + escape(data);
}
var downloadLink = document.createElement("a");
downloadLink.href = dataUrl(this.editor.getValue());
downloadLink.download = 'exercicio.py';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}
copyCode() {
navigator.clipboard.writeText(this.editor.getValue()).then(function () {
console.log('Async: Copying to clipboard was successful!');
}, function (err) {
console.error('Async: Could not copy text: ', err);
});
}
cleanOutput() {
this.result.innerHTML = '';
}
async initPyodide() {
this.pyodide = await loadPyodide({ packages: ['pytest'] });
this.cleanOutput();
this.pyodide.setStdin(
{ stdin: () => prompt() }
);
this.pyodide.setStdout(
{ batched: (string) => { this.writeOutput(string.toString()) } }
);
this.pyodide.setStderr(
{ batched: (string) => { this.writeOutput(string.toString()) } }
);
}
async runCode() {
let pyodide = await this.pyodideReadyPromise;
this.cleanOutput();
this.pyodide.setStdout(
{
batched: (string) => {
this.writeOutput(string.toString());
}
}
);
if (this.editor.getValue()) {
try {
let output = this.pyodide.runPython(this.editor.getValue());
} catch (err) {
this.writeOutput(err.toString());
}
} else {
this.writeOutput('Não existe código no editor...');
}
}
async writeOutput(value) {
let code = hljs.highlight(value, { language: "python" }).value
this.result.innerHTML += `<pre><code>${code}</code></pre>`;
}
loadAce() {
this.editor = ace.edit(`${this.code}`, {
mode: "ace/mode/python",
minLines: 10,
maxLines: 20,
fontSize: '.8rem',
theme: `ace/theme/${this.getTheme()}`
});
}
connectedCallback() {
this.getTheme()
this.code = `code-${this.testPath}`
this.innerHTML = `
<button id="play-${this.code}" class="editor-btn">Play!</button>
<button id="test-${this.code}" class="editor-btn" disabled>Test!</button>
<button id="clean-${this.code}" class="editor-btn">Clean!</button>
<button id="save-${this.code}" class="editor-btn">Save!</button>
<button id="copy-${this.code}" class="editor-btn">Copy!</button>
<button id="share-${this.code}" class="editor-btn">Share!</button>
<div id="${this.code}" class="editor"></div>
<div id="result-${this.code}" class="result">Iniciando...</div>
`;
this.loadAce();
this.result = document.getElementById(`result-${this.code}`);
const playBtn = document.getElementById(`play-${this.code}`);
const testBtn = document.getElementById(`test-${this.code}`);
const cleanBtn = document.getElementById(`clean-${this.code}`);
const saveBtn = document.getElementById(`save-${this.code}`);
const copyBtn = document.getElementById(`copy-${this.code}`);
const shareBtn = document.getElementById(`share-${this.code}`);
if (this.hasTest) {
testBtn.disabled = false;
}
playBtn.addEventListener('click', () => this.runCode())
testBtn.addEventListener('click', () => this.testCode())
cleanBtn.addEventListener('click', () => this.cleanOutput())
copyBtn.addEventListener('click', () => this.copyCode())
saveBtn.addEventListener('click', () => this.saveCode())
shareBtn.addEventListener('click', () => {
const code = this.editor.getValue();
const shareLink = this.generateShareLink(code);
this.writeOutput(`Link para compartilhar: \n${shareLink}`);
});
this.pyodideReadyPromise = this.initPyodide();
// Preenche o editor com o código da URL (se existir)
const codeFromUrl = this.decodeCodeFromUrl();
if (codeFromUrl) {
this.editor.setValue(codeFromUrl);
}
}
async testCode(exercice) {
let pyodide = await this.pyodideReadyPromise;
this.writeOutput('Testing...');
this.cleanOutput()
this.pyodide.setStdout(
{ batched: (string) => {
console.log(string)
if (string.startsWith('FAILED'))
this.writeOutput(
`${string.split('-')[1]}`
)
}}
);
const globals = this.pyodide.toPy(
{
editor: this.editor,
testPath: this.testPath
}
);
let output = this.pyodide.runPythonAsync(`
import pytest
from js import window
from pyodide.http import pyfetch
from pathlib import Path
async def test(exercice: str) -> None:
with open("sut.py", "w") as f:
f.write(editor.getValue())
# checar se o arquivo .py já não existe
if not Path('{exercice}.py').exists():
response = await pyfetch(
f"{window.location.origin}/tests/{exercice}.py"
)
with open(f"{exercice}.py", "wb") as f:
f.write(await response.bytes())
pytest.main(
[
f'{exercice}.py',
'--tb=no',
'--no-header',
'--capture=no',
'-q',
]
)
test(testPath)
`, { globals })
}
}
customElements.define('code-exercice', CodeExercice);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment