feat(lessons): SVG-рисовалка как блок урока (svg-draw)
Лёгкий векторный редактор frontend/js/svg-draw.js (перо со сглаживанием, линия, прямоугольник, эллипс, стрелка, текст, цвет/толщина/заливка, выбор/перемещение/удаление, undo/redo, очистка) → выдаёт чистый <svg>. Хранится inline в данных блока, переоткрывается для дорисовки. - Новый тип блока svg-draw: палитра «Рисунок», редактор (монтирование виджета + подпись), превью и студенческий рендер (lesson.html) — санитизированный inline-SVG, адаптивный. - Санитайзер frontend/js/svg-sanitize.js (UMD, общий клиент/сервер): whitelist тегов/атрибутов, вырезает script/foreignObject/style/image/a, on*=, href, javascript:. Без зависимостей. - Сервер (lessonController): svg-draw в VALID_TYPES + очистка data.svg при сохранении. - Переиспользуемо: тот же виджет пригоден для флешкарт и фигур генератора задач. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
const db = require('../db/db');
|
||||
const { onLessonComplete } = require('./gamificationController');
|
||||
const { stripTags } = require('../utils/sanitize');
|
||||
// Shared whitelist SVG sanitizer (UMD module, node-safe) — single source of
|
||||
// truth with the client. In node it uses the conservative regex path.
|
||||
const { clean: cleanSvg } = require('../../../frontend/js/svg-sanitize.js');
|
||||
|
||||
/* ── helpers ──────────────────────────────────────────────────────────── */
|
||||
function parseBlock(b) {
|
||||
@@ -151,7 +154,7 @@ function saveBlocks(req, res) {
|
||||
if (!Array.isArray(blocks))
|
||||
return res.status(400).json({ error: 'blocks must be an array' });
|
||||
|
||||
const VALID_TYPES = ['heading','text','formula','image','quiz','sim','table','code','divider','callout','video','flashcard','matching','fill-blank','ordering','accordion','timeline','diagram','geogebra','audio','columns','alert'];
|
||||
const VALID_TYPES = ['heading','text','formula','image','svg-draw','quiz','sim','table','code','divider','callout','video','flashcard','matching','fill-blank','ordering','accordion','timeline','diagram','geogebra','audio','columns','alert'];
|
||||
|
||||
db.transaction(() => {
|
||||
db.prepare('DELETE FROM lesson_blocks WHERE lesson_id = ?').run(lesson.id);
|
||||
@@ -160,7 +163,12 @@ function saveBlocks(req, res) {
|
||||
);
|
||||
blocks.forEach((b, i) => {
|
||||
const type = VALID_TYPES.includes(b.type) ? b.type : 'text';
|
||||
ins.run(lesson.id, type, b.orderIndex ?? i, JSON.stringify(b.data || {}));
|
||||
let data = b.data || {};
|
||||
// Sanitize inline SVG drawings server-side (defense-in-depth).
|
||||
if (type === 'svg-draw' && data && typeof data.svg === 'string') {
|
||||
data = Object.assign({}, data, { svg: cleanSvg(data.svg) });
|
||||
}
|
||||
ins.run(lesson.id, type, b.orderIndex ?? i, JSON.stringify(data));
|
||||
});
|
||||
|
||||
// recalculate read time — pass already-parsed data objects, no double stringify/parse
|
||||
|
||||
Reference in New Issue
Block a user