Skip to content

Commit

Permalink
💻 Add curly braces around teacher adventures' code (#5253)
Browse files Browse the repository at this point in the history
Adds curly braces automatically around keywords in the teacher's adventure code. 

For that, we iterate over the code snippets inside the contents of the editor, and use Lezer to parse and the code, and eventually iterate over the syntax tree to add curly braces around the keywords. The generated code should be as close as possible to the original one, maintaining indentation levels, spaces, and even errors. 

Also adds a warning for the case when the teacher selects several levels and also have code snippets, letting them know this can lead to trouble.

Fixes #5140

**How to test**
* Automated tests should pass (WIP)
* Create an adventure, and write your code normally.
* Log in as a student, or preview the class where that adventure was added and see that you can change the keywords language.
* You could also see the generated code by going to the local database, searching for the adventure you just created and check that the keywords have curly braces around them.
  • Loading branch information
jpelay committed Mar 19, 2024
1 parent 46a05be commit 5f191df
Show file tree
Hide file tree
Showing 63 changed files with 866 additions and 268 deletions.
12 changes: 8 additions & 4 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,12 @@ def load_customized_adventures(level, customizations, into_adventures):
for a in order_for_this_level:
if a['from_teacher'] and (db_row := teacher_adventure_map.get(a['name'])):
try:
db_row['content'] = safe_format(db_row['content'],
**hedy_content.KEYWORDS.get(g.keyword_lang))
if 'formatted_content' in db_row:
db_row['formatted_content'] = safe_format(db_row['formatted_content'],
**hedy_content.KEYWORDS.get(g.keyword_lang))
else:
db_row['content'] = safe_format(db_row['content'],
**hedy_content.KEYWORDS.get(g.keyword_lang))
except Exception:
# We don't want teacher being able to break the student UI -> pass this adventure
pass
Expand Down Expand Up @@ -932,8 +936,8 @@ def translate_list(args):

if len(translated_args) > 1:
return f"{', '.join(translated_args[0:-1])}" \
f" {gettext('or')} " \
f"{translated_args[-1]}"
f" {gettext('or')} " \
f"{translated_args[-1]}"
return ''.join(translated_args)


Expand Down
3 changes: 3 additions & 0 deletions messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,9 @@ msgstr ""
msgid "more_options"
msgstr ""

msgid "multiple_levels_warning"
msgstr ""

msgid "my_account"
msgstr ""

Expand Down
5 changes: 0 additions & 5 deletions static/css/generated.full.css
Original file line number Diff line number Diff line change
Expand Up @@ -30285,11 +30285,6 @@ code {
border-color: rgb(220 76 100 / var(--tw-border-opacity)) !important;
}

.\!border-gray-200 {
--tw-border-opacity: 1 !important;
border-color: rgb(237 242 247 / var(--tw-border-opacity)) !important;
}

.\!border-gray-400 {
--tw-border-opacity: 1 !important;
border-color: rgb(203 213 224 / var(--tw-border-opacity)) !important;
Expand Down
130 changes: 124 additions & 6 deletions static/js/adventure.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,46 @@
import ClassicEditor from "./ckeditor";
import { CustomWindow } from './custom-window';
import { languagePerLevel, keywords } from "./lezer-parsers/language-packages";
import { SyntaxNode } from "@lezer/common";
import { initializeTranslation } from "./lezer-parsers/tokens";

declare let window: CustomWindow;

export interface InitializeCustomizeAdventurePage {
readonly page: 'customize-adventure';
}

let $editor: ClassicEditor;
const editorContainer = document.querySelector('#adventure-editor') as HTMLElement;
const lang = document.querySelector('html')?.getAttribute('lang') || 'en';
// Initialize the editor with the default language
if (editorContainer) {
initializeEditor(lang);

export async function initializeCustomAdventurePage(_options: InitializeCustomizeAdventurePage) {
const editorContainer = document.querySelector('#adventure-editor') as HTMLElement;
const lang = document.querySelector('html')?.getAttribute('lang') || 'en';
// Initialize the editor with the default language
if (editorContainer) {
initializeEditor(lang, editorContainer);
}

// We wait until Tailwind generates the select
const tailwindSelects = await waitForElm('[data-te-select-option-ref]')
tailwindSelects.forEach((el) => {
el.addEventListener('click', () => {
// After clicking, it takes some time for the checkbox to change state, so if we want to target the checkboxess
// that are checked after clicking we can't do that inmediately after the click
// therofore we wait for 100ms
setTimeout(function(){
const numberOfLevels = document.querySelectorAll('[aria-selected="true"]').length;
const numberOfSnippets = document.querySelectorAll('pre[data-language="Hedy"]').length
if(numberOfLevels > 1 && numberOfSnippets > 0) {
$('#warningbox').show()
} else if(numberOfLevels <= 1 || numberOfSnippets === 0) {
$('#warningbox').hide()
}
}, 100);
})
})
}

function initializeEditor(language: string): Promise<void> {
function initializeEditor(language: string, editorContainer: HTMLElement): Promise<void> {
return new Promise((resolve, reject) => {
if ($editor) {
$editor.destroy();
Expand All @@ -38,3 +66,93 @@ function initializeEditor(language: string): Promise<void> {
});
});
}

export function addCurlyBracesToCode(code: string, level: number, language: string = 'en') {
// If code already has curly braces, we don't do anything about it
if (code.match(/\{(\w|_)+\}/g)) return code

initializeTranslation({keywordLanguage: language, level: level})

let parser = languagePerLevel[level];
let parseResult = parser.parse(code);
let formattedCode = ''
let previous_node: SyntaxNode | undefined = undefined

// First we're going to iterate trhough the parse tree, but we're only interested in the set of node
// that actually have code, meaning the leaves of the tree
parseResult.iterate({
enter: (node) => {
const nodeName = node.node.name;
let number_spaces = 0
let previous_name = ''
if (keywords.includes(nodeName)) {
if (previous_node !== undefined) {
number_spaces = node.from - previous_node.to
previous_name = previous_node.name
}
// In case that we have a case of a keyword that uses spaces, then we don't need
// to include the keyword several times in the translation!
// For example `if x not in list` should be `if x {not_in} list`
if (previous_name !== nodeName) {
formattedCode += ' '.repeat(number_spaces) + '{' + nodeName + '}';
}
previous_node = node.node
} else if (['Number', 'String', 'Text', 'Op', 'Comma', 'Int'].includes(nodeName)) {
if (previous_node !== undefined) {
number_spaces = node.from - previous_node.to
previous_name = previous_node.name
}
formattedCode += ' '.repeat(number_spaces) + code.slice(node.from, node.to)
previous_node = node.node
}
},
leave: (node) => {
// Commads signify start of lines, except for level 7, 8 repeats
// In that case, don't add more than one new line
if (node.node.name === "Command" && formattedCode[formattedCode.length - 1] !== '\n') {
formattedCode += '\n'
previous_node = undefined
}
}
});

let formattedLines = formattedCode.split('\n');
let lines = code.split('\n');
let resultingLines = []

for (let i = 0, j = 0; i < lines.length; i++) {
if (lines[i].trim() === '') {
resultingLines.push(lines[i]);
continue;
}
const indent_number = lines[i].search(/\S/)
if (indent_number > -1) {
resultingLines.push(' '.repeat(indent_number) + formattedLines[j])
}
j += 1;
}
formattedCode = resultingLines.join('\n');

return formattedCode;
}

function waitForElm(selector: string): Promise<NodeListOf<Element>> {
return new Promise(resolve => {
if (document.querySelector(selector)) {
return resolve(document.querySelectorAll(selector));
}

const observer = new MutationObserver(_mutations => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelectorAll(selector));
}
});

// If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
Loading

0 comments on commit 5f191df

Please sign in to comment.