Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

💻 Add curly braces around teacher adventures' code #5253

Merged
merged 20 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading