Better Custom CSS editor (#88)
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Über Readest",
|
||||
"Add your notes here...": "Fügen Sie hier Ihre Notizen hinzu...",
|
||||
"Animation": "Animation",
|
||||
"Apply": "Anwenden",
|
||||
"Are you sure to delete the selected books?": "Möchten Sie die ausgewählten Bücher wirklich löschen?",
|
||||
"Auto Mode": "Automatischer Modus",
|
||||
"Behavior": "Verhalten",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Acerca de Readest",
|
||||
"Add your notes here...": "Añade tus notas aquí...",
|
||||
"Animation": "Animación",
|
||||
"Apply": "Aplicar",
|
||||
"Are you sure to delete the selected books?": "¿Estás seguro de eliminar los libros seleccionados?",
|
||||
"Auto Mode": "Modo automático",
|
||||
"Behavior": "Comportamiento",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "À propos de Readest",
|
||||
"Add your notes here...": "Ajoutez vos notes ici...",
|
||||
"Animation": "Animation",
|
||||
"Apply": "Appliquer",
|
||||
"Are you sure to delete the selected books?": "Êtes-vous sûr de vouloir supprimer les livres sélectionnés ?",
|
||||
"Auto Mode": "Mode automatique",
|
||||
"Behavior": "Comportement",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Tentang Readest",
|
||||
"Add your notes here...": "Tambahkan catatan Anda di sini...",
|
||||
"Animation": "Animasi",
|
||||
"Apply": "Terapkan",
|
||||
"Are you sure to delete the selected books?": "Apakah Anda yakin ingin menghapus buku yang dipilih?",
|
||||
"Auto Mode": "Mode Otomatis",
|
||||
"Behavior": "Perilaku",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Informazioni su Readest",
|
||||
"Add your notes here...": "Aggiungi qui le tue note...",
|
||||
"Animation": "Animazione",
|
||||
"Apply": "Applica",
|
||||
"Are you sure to delete the selected books?": "Sei sicuro di voler eliminare i libri selezionati?",
|
||||
"Auto Mode": "Modalità automatica",
|
||||
"Behavior": "Comportamento",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Readestについて",
|
||||
"Add your notes here...": "ここにメモを追加...",
|
||||
"Animation": "アニメーション",
|
||||
"Apply": "適用",
|
||||
"Are you sure to delete the selected books?": "選択した書籍を削除してもよろしいですか?",
|
||||
"Auto Mode": "自動モード",
|
||||
"Behavior": "動作",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Readest 정보",
|
||||
"Add your notes here...": "여기에 메모를 추가하세요...",
|
||||
"Animation": "애니메이션",
|
||||
"Apply": "적용",
|
||||
"Are you sure to delete the selected books?": "선택한 책을 삭제하시겠습니까?",
|
||||
"Auto Mode": "자동 모드",
|
||||
"Behavior": "동작",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Sobre o Readest",
|
||||
"Add your notes here...": "Adicione suas notas aqui...",
|
||||
"Animation": "Animação",
|
||||
"Apply": "Aplicar",
|
||||
"Are you sure to delete the selected books?": "Tem certeza de que deseja excluir os livros selecionados?",
|
||||
"Auto Mode": "Modo Automático",
|
||||
"Behavior": "Comportamento",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "О Readest",
|
||||
"Add your notes here...": "Добавьте свои заметки здесь...",
|
||||
"Animation": "Анимация",
|
||||
"Apply": "Применить",
|
||||
"Are you sure to delete the selected books?": "Вы уверены, что хотите удалить выбранные книги?",
|
||||
"Auto Mode": "Автоматический режим",
|
||||
"Behavior": "Поведение",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Readest Hakkında",
|
||||
"Add your notes here...": "Notlarınızı buraya ekleyin...",
|
||||
"Animation": "Animasyon",
|
||||
"Apply": "Uygula",
|
||||
"Are you sure to delete the selected books?": "Seçili kitapları silmek istediğinizden emin misiniz?",
|
||||
"Auto Mode": "Otomatik Mod",
|
||||
"Behavior": "Davranış",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Về Readest",
|
||||
"Add your notes here...": "Thêm ghi chú của bạn vào đây...",
|
||||
"Animation": "Hiệu ứng động",
|
||||
"Apply": "Áp dụng",
|
||||
"Are you sure to delete the selected books?": "Bạn có chắc chắn muốn xóa các sách đã chọn?",
|
||||
"Auto Mode": "Chế độ tự động",
|
||||
"Behavior": "Hành vi",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "关于 Readest",
|
||||
"Add your notes here...": "在这里添加您的笔记...",
|
||||
"Animation": "动画",
|
||||
"Apply": "应用",
|
||||
"Are you sure to delete the selected books?": "您确定要删除所选书籍吗?",
|
||||
"Auto Mode": "自动主题",
|
||||
"Behavior": "行为",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "關於 Readest",
|
||||
"Add your notes here...": "在這裡添加您的筆記...",
|
||||
"Animation": "動畫",
|
||||
"Apply": "應用",
|
||||
"Are you sure to delete the selected books?": "您確定要刪除所選書籍嗎?",
|
||||
"Auto Mode": "自動主題",
|
||||
"Behavior": "行為",
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import cssbeautify from 'cssbeautify';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
|
||||
const cssRegex =
|
||||
/((?:\s*)([\w#.@*,:\-.:>+~$$$$\"=(),*\s]+)\s*{(?:[\s]*)((?:[^\}]+[:][^\}]+;?)*)*\s*}(?:\s*))/gim;
|
||||
import cssbeautify from 'cssbeautify';
|
||||
import cssValidate from '@/utils/css';
|
||||
|
||||
const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
@@ -18,44 +17,48 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
const [animated, setAnimated] = useState(viewSettings.animated!);
|
||||
const [isDisableClick, setIsDisableClick] = useState(viewSettings.disableClick!);
|
||||
const [userStylesheet, setUserStylesheet] = useState(viewSettings.userStylesheet!);
|
||||
const [draftStylesheet, setDraftStylesheet] = useState(viewSettings.userStylesheet!);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
let cssInput = userStylesheet;
|
||||
|
||||
const validateCSS = (css: string) => {
|
||||
return cssRegex.test(css);
|
||||
};
|
||||
|
||||
const handleUserStylesheetChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
cssInput = e.target.value;
|
||||
const cssInput = e.target.value;
|
||||
setDraftStylesheet(cssInput);
|
||||
|
||||
try {
|
||||
const formattedCSS = cssbeautify(cssInput, {
|
||||
indent: ' ',
|
||||
openbrace: 'end-of-line',
|
||||
autosemicolon: true,
|
||||
});
|
||||
setUserStylesheet(formattedCSS);
|
||||
|
||||
if (cssInput && !validateCSS(cssInput)) {
|
||||
throw new Error('Invalid CSS');
|
||||
const { isValid, error } = cssValidate(cssInput);
|
||||
if (cssInput && !isValid) {
|
||||
throw new Error(error || 'Invalid CSS');
|
||||
}
|
||||
setError(null);
|
||||
|
||||
viewSettings.userStylesheet = formattedCSS;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
if (isFontLayoutSettingsGlobal) {
|
||||
settings.globalViewSettings.userStylesheet = formattedCSS;
|
||||
setSettings(settings);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
} else {
|
||||
setError('Invalid CSS: Please check your input.');
|
||||
}
|
||||
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
} catch (err) {
|
||||
setError('Invalid CSS: Please check your input.');
|
||||
console.log('CSS Error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const applyStyles = () => {
|
||||
const formattedCSS = cssbeautify(draftStylesheet, {
|
||||
indent: ' ',
|
||||
openbrace: 'end-of-line',
|
||||
autosemicolon: true,
|
||||
});
|
||||
|
||||
setDraftStylesheet(formattedCSS);
|
||||
viewSettings.userStylesheet = formattedCSS;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
|
||||
if (isFontLayoutSettingsGlobal) {
|
||||
settings.globalViewSettings.userStylesheet = formattedCSS;
|
||||
setSettings(settings);
|
||||
}
|
||||
|
||||
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
};
|
||||
|
||||
const handleInput = (e: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
@@ -125,21 +128,31 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Custom CSS')}</h2>
|
||||
<div className={`card bg-base-100 border shadow ${error ? 'border-red-500' : ''}`}>
|
||||
<div className='divide-y'>
|
||||
<div className='css-text-area config-item-top config-item-bottom p-1'>
|
||||
<textarea
|
||||
className='textarea textarea-ghost h-48 w-full border-0 p-3 !outline-none'
|
||||
placeholder={_('Enter your custom CSS here...')}
|
||||
spellCheck='false'
|
||||
value={cssInput}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleInput}
|
||||
onKeyUp={handleInput}
|
||||
onChange={handleUserStylesheetChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='relative p-1'>
|
||||
<textarea
|
||||
className='textarea textarea-ghost h-48 w-full border-0 p-3 !outline-none'
|
||||
placeholder={_('Enter your custom CSS here...')}
|
||||
spellCheck='false'
|
||||
value={draftStylesheet}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleInput}
|
||||
onKeyUp={handleInput}
|
||||
onChange={handleUserStylesheetChange}
|
||||
/>
|
||||
<button
|
||||
className={clsx(
|
||||
'btn btn-ghost bg-base-200 absolute bottom-2 right-4 h-8 min-h-8 px-4 py-2',
|
||||
!draftStylesheet ? 'hidden' : '',
|
||||
error ? 'btn-disabled' : '',
|
||||
)}
|
||||
onClick={applyStyles}
|
||||
disabled={!!error}
|
||||
>
|
||||
{_('Apply')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{error && <p className='mt-1 text-sm text-red-500'>{error}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
const cssValidate = (css: string) => {
|
||||
// Remove comments and normalize whitespace
|
||||
css = css.replace(/\/\*[\s\S]*?\*\//g, '').trim();
|
||||
|
||||
// CSS property pattern (validate both property name and value)
|
||||
const propertyPattern = /^[\s\n]*[-\w]+\s*:\s*[^;]+;?$/;
|
||||
|
||||
// Check if empty
|
||||
if (!css) return { isValid: false, error: 'Empty CSS' };
|
||||
|
||||
// Ensure balanced curly braces
|
||||
const openBraces = (css.match(/{/g) || []).length;
|
||||
const closeBraces = (css.match(/}/g) || []).length;
|
||||
if (openBraces !== closeBraces) {
|
||||
return { isValid: false, error: 'Unbalanced curly braces' };
|
||||
}
|
||||
|
||||
// Split into rule blocks
|
||||
const blocks = css
|
||||
.split('}')
|
||||
.map((block) => block.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const block of blocks) {
|
||||
// Ensure the block has a selector and declarations
|
||||
const parts = block.split('{').map((part) => part.trim());
|
||||
if (parts.length !== 2) {
|
||||
return { isValid: false, error: 'Invalid CSS structure' };
|
||||
}
|
||||
|
||||
const [selector, decls] = parts;
|
||||
|
||||
// Ensure selector is not empty
|
||||
if (!selector) {
|
||||
return { isValid: false, error: 'Missing selector' };
|
||||
}
|
||||
|
||||
// Ensure declarations are not empty
|
||||
if (!decls) {
|
||||
return { isValid: false, error: `Missing declarations for selector: ${selector}` };
|
||||
}
|
||||
|
||||
// Validate declarations
|
||||
const props = decls
|
||||
.split(';')
|
||||
.map((prop) => prop.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (props.length === 0) {
|
||||
return { isValid: false, error: `No valid properties for selector: ${selector}` };
|
||||
}
|
||||
|
||||
for (const prop of props) {
|
||||
// Check if property is missing a name or value
|
||||
if (!prop.includes(':')) {
|
||||
return { isValid: false, error: `Missing property or value: ${prop}` };
|
||||
}
|
||||
|
||||
const [name, value] = prop.split(':').map((part) => part.trim());
|
||||
if (!name) {
|
||||
return { isValid: false, error: `Missing property name: ${prop}` };
|
||||
}
|
||||
if (!value) {
|
||||
return { isValid: false, error: `Missing property value: ${prop}` };
|
||||
}
|
||||
|
||||
// Validate full property format
|
||||
if (!propertyPattern.test(prop.endsWith(';') ? prop : prop + ';')) {
|
||||
return { isValid: false, error: `Invalid property: ${prop}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true, error: null };
|
||||
};
|
||||
|
||||
export default cssValidate;
|
||||
Reference in New Issue
Block a user