diff --git a/apps/backend/package.json b/apps/backend/package.json index 65828b1f..154c5848 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -30,6 +30,7 @@ "express": "^4.17.1", "express-rate-limit": "^6.9.0", "form-data": "^4.0.0", + "katex": "^0.16.10", "marked": "^7.0.3", "multer": "^1.4.3", "nodemon": "^3.0.1", diff --git a/apps/backend/server.js b/apps/backend/server.js index 09142fe6..90670aff 100644 --- a/apps/backend/server.js +++ b/apps/backend/server.js @@ -9,6 +9,7 @@ const uploadVideo = require('./src/routes/uploadVideo'); const downloadCertificate = require('./src/routes/downloadCertificate'); const downloadLesson = require('./src/routes/downloadLesson'); const downloadCourse = require('./src/routes/downloadCourse'); +const katex = require('./src/routes/katex'); // Express server const app = express(); @@ -32,6 +33,7 @@ app.use('/downloadCertificate', downloadCertificate); app.use('/downloadLesson', downloadLesson); app.use('/downloadCourse', downloadCourse); app.use('/uploadVideo', uploadVideo); +app.use('/katex', katex); app.get('/', (req, res) => { res.send('Welcome to ClassroomIO'); diff --git a/apps/backend/src/routes/katex.js b/apps/backend/src/routes/katex.js new file mode 100644 index 00000000..fde58017 --- /dev/null +++ b/apps/backend/src/routes/katex.js @@ -0,0 +1,24 @@ +const express = require('express'); +// https://ben.page/node-latex-to-html +const katex = require('katex'); + +const router = express.Router(); + +// localhost:3002/katex +router.get('/', async (req, res) => { + const original = req._parsedUrl.search; + + const latexString = original.replace('?', '').replaceAll('+', '+').replaceAll('&space;', ''); + console.log({ latexString }); + + const html = katex.renderToString(latexString, { + throwOnError: false, + output: 'mathml' + }); + + console.log({ html }); + + res.send(html); +}); + +module.exports = router; diff --git a/apps/dashboard/src/lib/components/TextEditor/index.svelte b/apps/dashboard/src/lib/components/TextEditor/index.svelte index 1cef0a8b..1485f9fc 100644 --- a/apps/dashboard/src/lib/components/TextEditor/index.svelte +++ b/apps/dashboard/src/lib/components/TextEditor/index.svelte @@ -2,6 +2,7 @@ import { PUBLIC_TINYMCE_API_KEY } from '$env/static/public'; import Editor from '@tinymce/tinymce-svelte'; import { globalStore } from '$lib/utils/store/app'; + import { addMathPlugin } from '$lib/utils/functions/tinymce/plugins'; export let id = ''; export let value: string | undefined = ''; @@ -13,12 +14,19 @@ const apiKey = PUBLIC_TINYMCE_API_KEY; - let tinmycEditor: any; let unmount = false; + function getTinymce() { + const getSink = () => { + return typeof window !== 'undefined' ? window : global; + }; + const sink = getSink(); + return sink && sink.tinymce ? sink.tinymce : null; + } + // editor configuration let conf = { - plugins: 'lists, link, emoticons, code, media', + plugins: 'lists, link, emoticons, code, media, eqneditor', toolbar: [ { name: 'history', items: ['undo', 'redo'] }, { name: 'styles', items: ['styles'] }, @@ -29,7 +37,8 @@ { name: 'link', items: ['link'] }, { name: 'insert', items: ['emoticons'] }, { name: 'code', items: ['code'] }, - { name: 'media', items: ['media'] } + { name: 'media', items: ['media'] }, + { name: 'eqneditor', items: ['eqneditor'] } ], lists_indent_on_tab: false, min_height: height, @@ -37,10 +46,13 @@ placeholder: placeholder, skin: 'oxide-dark', content_css: 'dark', + extended_valid_elements: '*[.*]', + setup: () => { + addMathPlugin(getTinymce()); + }, init_instance_callback: function (editor: any) { editorWindowRef = editor.iframeElement?.contentWindow; - tinmycEditor = editor; editor.on('Change', function () { const html = editor.getContent(); if (onChange) { diff --git a/apps/dashboard/src/lib/utils/functions/tinymce/plugins.ts b/apps/dashboard/src/lib/utils/functions/tinymce/plugins.ts new file mode 100644 index 00000000..1199b570 --- /dev/null +++ b/apps/dashboard/src/lib/utils/functions/tinymce/plugins.ts @@ -0,0 +1,169 @@ +import { PUBLIC_SERVER_URL } from '$env/static/public'; + + +var win, output, textarea; + +// Follow this guide to get source code: https://editor.codecogs.com/docs/3-Tiny_MCE_v6.php +export const addMathPlugin = (tinymce: any) => { + tinymce.PluginManager.add('eqneditor', function (editor, url) { + // Load necessary javascript for editor from CodeCogs + var sl = new tinymce.dom.ScriptLoader(); + sl.add('https://pub-19b235c5098f4ff1a6b1099331c8d00e.r2.dev/eqneditor.api.min.js'); + var loaded = sl.loadQueue(); + + // Load custom CSS + tinymce.DOM.loadCSS('https://pub-19b235c5098f4ff1a6b1099331c8d00e.r2.dev/eqneditor_1.css'); + + editor.ui.registry.addIcon( + 'logo', + `` + ); + + editor.ui.registry.addIcon( + 'menu', + `` + ); + + function showDialog(inp = '') { + var http = 'https:' == document.location.protocol ? 'https://' : 'http://'; + + win = editor.windowManager.open({ + title: 'Equation Editor', + size: 'medium', + body: { + type: 'panel', + items: [ + { + type: 'htmlpanel', + html: + '
${html}
`; + } else { + console.error("No src attribute found in the img tag."); + } + + tinymce.activeEditor.execCommand('mceInsertContent', false, content); + textarea.pushToHistory(); + w.close(); + }, + onCancel: function (w) { + w.close(); + }, + onAction: function (w, dets) { + if (dets.name == 'logo') { + window.open(http + 'editor.codecogs.com', '_blank')?.focus(); + } + } + }); + + // Ensure EqnEditor js is loaded before continuing + loaded.then(() => { + output = new EqEditor.Output('output'); + textarea = EqEditor.TextArea.link('latexInput') + .addOutput(output) + .addHistoryMenu(new EqEditor.History('history')); + EqEditor.Toolbar.link('toolbar').addTextArea(textarea); + + var nodes = document.getElementById('history')?.childNodes || []; + for (var i = 0; i < nodes.length; i++) { + nodes[i].style.padding = 'revert'; + } + + textarea.clear(); + textarea.insert(inp.replace(/&space;/g, ' '), inp.length); + }); + } + + editor.ui.registry.addButton('eqneditor', { + title: 'Equation', + icon: 'menu', + tooltip: 'Insert Equation', + onAction: () => { + showDialog(); + } + // stateSelector: 'img[src*="latex"]' + }); + + editor.on('dblclick', function (e) { + if (e.target.nodeName.toLowerCase() == 'img') { + var sName = e.target.src.match(/(gif|svg)\.image\?(.*)/); + if (sName != null) showDialog(sName[2]); + } + }); + + return { + getMetadata: () => ({ + name: 'EqnEditor', + url: 'https://editor.codecogs.com/docs/3-Tiny_MCE_v4x.php' + }) + }; + }); +}; diff --git a/apps/dashboard/src/routes/courses/[id]/lessons/+layout.svelte b/apps/dashboard/src/routes/courses/[id]/lessons/+layout.svelte new file mode 100644 index 00000000..0dfdd249 --- /dev/null +++ b/apps/dashboard/src/routes/courses/[id]/lessons/+layout.svelte @@ -0,0 +1,5 @@ +