diff --git a/.circleci/config.yml b/.circleci/config.yml index 5effc0bb2..cc6bd8d82 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -82,6 +82,9 @@ jobs: - run: name: Test - HTML sanitization command: npm run test:safehtml + - run: + name: Test - Helpers + command: npm run test:helpers - run: name: Test - Coverage command: npm run test:coverage diff --git a/changelog.md b/changelog.md index d783130f0..1c5a9b714 100644 --- a/changelog.md +++ b/changelog.md @@ -128,7 +128,7 @@ Fixes issue [#4858](https://github.com/naturalcrit/homebrewery/issues/4858) Fixes part of issue [#4101](https://github.com/naturalcrit/homebrewery/issues/4101) ##### G-Ambatte -* [x] Fix editor panel shrinking when openingdev tools +* [x] Fix editor panel shrinking when opening dev tools Fixes issue [#4866](https://github.com/naturalcrit/homebrewery/issues/4866) @@ -154,7 +154,7 @@ Fixes issue [#4904](https://github.com/naturalcrit/homebrewery/issues/4904) ##### 5e-Cleric, Gazook89 * [x] Fix various issues with Codemirror 6 -Fixes issues [#4771](https://github.com/naturalcrit/homebrewery/issues/4771), [#4583](https://github.com/naturalcrit/homebrewery/issues/4783) +Fixes issues [#4771](https://github.com/naturalcrit/homebrewery/issues/4771), [#4783](https://github.com/naturalcrit/homebrewery/issues/4783) }} \page @@ -170,7 +170,6 @@ Fixes issues [#4771](https://github.com/naturalcrit/homebrewery/issues/4771), [# ##### 5e-Cleric * [x] Add auto-suggest to tag entry input box -* [x] Replace all example artwork with * [x] Added tooltips to the {{openSans :fas_circle_info: **Properties**}} menu * [x] Removed {{openSans **SYSTEMS**}} checkboxes from {{openSans :fas_circle_info: **Properties**}} menu; instead {{openSans **TAGS**}} should be used for this purpose * [x] Replace all AI-generated art with public domain art @@ -222,7 +221,7 @@ Fixes issue [#4559](https://github.com/naturalcrit/homebrewery/issues/4559) ##### G-Ambatte * [x] Fix default save location failing on new documents -Fixes issue [#4437](https://github.com/naturalcrit/homebrewery/issues/3175) +Fixes issue [#4437](https://github.com/naturalcrit/homebrewery/issues/4437) * [x] Fix usernames with special symbols unable to open userpage Fixes issue [#807](https://github.com/naturalcrit/homebrewery/issues/807) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index 5ac254132..d488102ed 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -1,4 +1,4 @@ -/* eslint max-lines: ["error", { "max": 405 }] */ +/* eslint max-lines: ["error", { "max": 455 }] */ import './codeEditor.less'; import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'; @@ -42,6 +42,7 @@ import cm5Themes from 'codemirror-5-themes'; const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery }; const themeCompartment = new Compartment(); const highlightCompartment = new Compartment(); +const settingsCompartment = new Compartment(); import { generalKeymap, markdownKeymap, cssKeymap, formatCSS } from './extensions/customKeyMaps.js'; import foldOnPages from './extensions/customFolding.js'; @@ -78,6 +79,20 @@ const programmaticCursorLineField = StateField.define({ provide : (decorationSet)=>EditorView.decorations.from(decorationSet) }); +const createSettingsExtensions = (settings)=>[ + ...(settings.autoCloseBrackets ? [autoCloseBrackets] : []), + ...(settings.lineNumbers ? [lineNumbers()] : []), + ...(settings.activeLineShading ? [highlightActiveLine(), + highlightActiveLineGutter()] : []), + ...(settings.fontSize + ? [EditorView.theme({ + '&, .cm-content' : { + fontSize : `${settings.fontSize || 1}em`, + }, + })] + : []), +]; + const CodeEditor = forwardRef( ( { @@ -88,9 +103,11 @@ const CodeEditor = forwardRef( onChange = ()=>{}, onCursorChange = ()=>{}, onViewChange = ()=>{}, + onThemeChange = ()=>{}, editorTheme = 'default', style, renderer, + settings = {}, ...props }, ref, @@ -163,8 +180,7 @@ const CodeEditor = forwardRef( EditorView.lineWrapping, setEventListeners, languageExtension, - autoCloseBrackets, - lineNumbers(), + settingsCompartment.of(createSettingsExtensions(settings)), scrollPastEnd(), search(), history(), //allows for undo and redo @@ -178,10 +194,8 @@ const CodeEditor = forwardRef( }), //highlights - highlightCompartment.of([customHighlightPlugin(renderer, tab), highlightExtension]), + highlightCompartment.of([customHighlightPlugin(renderer, tab, settings), highlightExtension]), themeCompartment.of(themeExtension), - highlightActiveLine(), - highlightActiveLineGutter(), //keyboard shortcut keymap.of([...defaultKeymap, foldKeymap, ...searchKeymap]), @@ -271,6 +285,12 @@ const CodeEditor = forwardRef( } view.setState(nextState); + view.dispatch({ + effects : settingsCompartment.reconfigure( + createSettingsExtensions(settings) + ), + }); + restoreFolds(view, foldsRef.current[tab]); const savedScroll = scrollRef.current[tab]; @@ -308,6 +328,9 @@ const CodeEditor = forwardRef( view.dispatch({ effects : themeCompartment.reconfigure(themeExtension), }); + + const isDark = view.state.facet(EditorView.darkTheme); + onThemeChange(isDark); }, [editorTheme, tab]); useEffect(()=>{ @@ -320,10 +343,21 @@ const CodeEditor = forwardRef( : syntaxHighlighting(legacyCustomHighlightStyle); view.dispatch({ - effects : highlightCompartment.reconfigure([customHighlightPlugin(renderer, tab), highlightExtension]), + effects : highlightCompartment.reconfigure([customHighlightPlugin(renderer, tab, settings), highlightExtension]) }); }, [renderer, tab]); + useEffect(()=>{ + const view = viewRef.current; + if(!view) return; + + view.dispatch({ + effects : settingsCompartment.reconfigure( + createSettingsExtensions(settings) + ), + }); + }, [settings]); + useImperativeHandle(ref, ()=>({ injectText : (text)=>{ diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index cb44d3fd9..b245694ff 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -366,7 +366,7 @@ class ImageWidget extends WidgetType { } } -export function customHighlightPlugin(renderer, tab) { +export function customHighlightPlugin(renderer, tab, settings) { //this function takes the custom tokens created in the tokenize function in customhighlight files //takes the tokens defined by that function and assigns classes to them //it also creates page number and snippet number widgets @@ -398,7 +398,7 @@ export function customHighlightPlugin(renderer, tab) { const tree = ensureSyntaxTree(view.state, view.state.doc.length, 50) || syntaxTree(view.state); tree.iterate({ enter : (node)=>{ - if(node.name === 'Image') { + if(node.name === 'Image' && settings.showImagePreviews) { const url = getUrl(node, view.state.doc); const widgetPosition = node.node.lastChild.from; @@ -431,7 +431,7 @@ export function customHighlightPlugin(renderer, tab) { const to = line.from + token.to; const attrs = {}; - if(token.type === 'Image' && token.url) { + if(token.type === 'Image' && token.url && settings.showImagePreviews) { attrs['data-url'] = token.url; } diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 7c015d3d6..d769e5e58 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -7,6 +7,7 @@ const Combobox = createReactClass({ displayName : 'Combobox', getDefaultProps : function() { return { + id : '', className : '', trigger : 'hover', default : '', @@ -75,6 +76,7 @@ const Combobox = createReactClass({ onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined} {...(this.props.tooltip ? { 'data-tooltip-right': this.props.tooltip } : {})}> this.handleInput(e)} value={this.state.value || ''} diff --git a/client/components/combobox.less b/client/components/combobox.less index a3c7527f2..920723b48 100644 --- a/client/components/combobox.less +++ b/client/components/combobox.less @@ -4,7 +4,7 @@ .item i { position : absolute; right : 10px; - color : black; + color : inherit; } .dropdown-options { position : absolute; @@ -32,14 +32,13 @@ font-size : 11px; cursor : default; &:hover { - background-color : rgb(163, 163, 163); - filter : brightness(120%); + background-color : #ddd; } .detail { width : 100%; font-size : 9px; font-style : italic; - color : rgb(124, 124, 124); + color : #7c7c7c; text-align : left; } } diff --git a/client/components/dropdown/dropdown.less b/client/components/dropdown/dropdown.less index 8148a7c85..7c4a33838 100644 --- a/client/components/dropdown/dropdown.less +++ b/client/components/dropdown/dropdown.less @@ -7,7 +7,7 @@ @property --activeTriggerColor { syntax: ''; inherits: true; - initial-value: #DDD; + initial-value: #999; } :root{ @@ -30,4 +30,8 @@ } .menu-wrapper:has(:popover-open) > button { // if menu is open... background-color: var(--activeTriggerColor, hsl(from var(--menuColor) h s calc(l * .85))); // tint menu triggers based on menu color +} + +.darkMode .menu-wrapper:has(:popover-open) > button { // if menu is open... + --activeTriggerColor : #444; // tint menu triggers based on menu color } \ No newline at end of file diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 75a07c7d1..945d012cb 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -6,7 +6,7 @@ import React, { useState, useRef, useMemo, useEffect } from 'react'; import _ from 'lodash'; import MarkdownLegacy from '@shared/markdownLegacy.js'; -import { hbfm } from 'hbmarkedwrapper'; +import { hbfm } from 'marked-hbfm'; import ErrorBar from './errorBar/errorBar.jsx'; import ToolBar from './toolBar/toolBar.jsx'; @@ -41,7 +41,6 @@ const BrewPage = (props)=>{ props = { contents : '', index : 0, - hoisted : false, ...props }; const pageRef = useRef(null); @@ -54,7 +53,7 @@ const BrewPage = (props)=>{ // Observer for tracking which pages are at least 30% visible in the iframe const visibleObserver = new IntersectionObserver( (entries)=>entries.forEach((entry)=>{ - props.onVisibilityChange(pageNum, entry.isIntersecting, false); // add page to array of visible pages. + props.onVisibilityChange(pageNum, entry.isIntersecting, false); // add/remove page from array of visible pages. }), { threshold: .3, rootMargin: '0px 0px 0px 0px' } // detect when >30% of page is within bounds. ); @@ -96,14 +95,16 @@ const BrewRenderer = (props)=>{ lang : '', errors : [], currentEditorCursorPageNum : 1, - currentBrewRendererPageNum : 1, themeBundle : {}, onPageChange : ()=>{}, ...props }; + const pagesRef = useRef(null); + const [visiblePages, setVisiblePages] = useState([]); const [centerPage , setCenterPage ] = useState(1); + const [headerState , setHeaderState ] = useState(false); const [state, setState] = useState({ isMounted : false, @@ -125,10 +126,6 @@ const BrewRenderer = (props)=>{ toolbarState && setDisplayOptions(toolbarState); }, []); - const [headerState, setHeaderState] = useState(false); - - const pagesRef = useRef(null); - if(props.renderer == 'legacy') { rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY); } else { @@ -155,17 +152,16 @@ const BrewRenderer = (props)=>{ if(index == props.currentEditorCursorPageNum - 1) //Already rendered before this step return false; - if(Math.abs(index - props.currentBrewRendererPageNum - 1) <= 3) + if(Math.abs(index - centerPage - 1) <= 3) return true; return false; }; - const renderDummyPage = (index)=>{ - return
+ const renderDummyPage = (index)=> +
; - }; const renderStyle = ()=>{ const themeStyles = props.themeBundle?.joinedStyles ?? ''; @@ -223,9 +219,8 @@ const BrewRenderer = (props)=>{ } }; - const renderPages = (checkHoists = false)=>{ - - if(props.errors && props.errors.length) + const renderPages = ()=>{ + if(props.errors?.length) return renderedPages; if(rawPages.length != renderedPages.length) { // Re-render all pages when page count changes @@ -238,16 +233,10 @@ const BrewRenderer = (props)=>{ renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1); _.forEach(rawPages, (page, index)=>{ - const varsOnPageRegex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g; // Find out if there are any vars on the page. - const forceRender = checkHoists && - !props.hoisted && - (page.match(varsOnPageRegex)); // forceRender forces pages outside of the PPR range to render if true. - // This is necessary on the first load to fully populate the variable table. - if((isInView(index) || !renderedPages[index] || forceRender) && typeof window !== 'undefined'){ + if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){ renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range } }); - if(!props.hoisted) { props.hoisted = true; } // Only fully hoist once. return renderedPages; }; @@ -286,8 +275,8 @@ const BrewRenderer = (props)=>{ window.addEventListener('hashchange', ()=>scrollToHash(window.location.hash)); - setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame - renderPages(true); //Make sure page is renderable before showing + setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame + renderPages(); //Make sure page is renderable before showing setState((prevState)=>({ ...prevState, isMounted : true, @@ -313,7 +302,7 @@ const BrewRenderer = (props)=>{ }; const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]); - renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]); + renderedPages = useMemo(()=>renderPages(), [props.text, centerPage, displayOptions]); return ( <> @@ -338,7 +327,7 @@ const BrewRenderer = (props)=>{ {emitClick();}} + onClick={emitClick} sandbox='allow-same-origin allow-modals allow-top-navigation' >
Array.isArray(value) && !name.endsWith('Init') && !name.endsWith('Style')) .map(([name])=>name); +const themeNames = Object.entries(themes) + .filter(([name, value])=>Array.isArray(value) && + !name.endsWith('Init') && + !name.endsWith('Style') + ) + .map(([name])=>name); + +const EditorThemeNameList = [ + 'default', + ...themeNames + .filter((name)=>name !== 'default') + .sort((a, b)=>a.localeCompare(b)) +]; + //const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; //const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/; const DEFAULT_STYLE_TEXT = dedent` @@ -50,7 +67,6 @@ const Editor = forwardRef( onCursorPageChange = ()=>{}, onViewPageChange = ()=>{}, - editorTheme = 'default', renderer = 'legacy', moveBrew, @@ -69,9 +85,17 @@ const Editor = forwardRef( }, ref, )=>{ - const [currentEditorTheme, setEditorTheme] = useState(editorTheme); const [view, setView] = useState('text'); // 'text', 'style', 'meta', 'snippet' const [snippetBarHeight, setSnippetBarHeight] = useState(26); + const [isDark, setIsDark] = useState(false); + const [editorSettings, setEditorSettings] = useState({ + autoCloseBrackets : true, + showImagePreviews : true, + activeLineShading : true, + lineNumbers : true, + fontSize : 1, + editorTheme : 'default', + }); const editor = useRef(null); const codeEditor = useRef(null); @@ -81,6 +105,7 @@ const Editor = forwardRef( const isStyle = ()=>isView('style'); const isMeta = ()=>isView('meta'); const isSnip = ()=>isView('snippet'); + const isSettings = ()=>isView('settings'); const isView = (name)=>view === name; @@ -89,8 +114,12 @@ const Editor = forwardRef( brewRenderer.onload = ()=>brewRenderer.contentDocument?.addEventListener('keydown', handleControlKeys); document.addEventListener('keydown', handleControlKeys); - const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); - if(editorTheme && EditorThemes.includes(editorTheme)) setEditorTheme(editorTheme); else setEditorTheme('default'); + const localEditorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); + if(localEditorTheme && EditorThemes.includes(localEditorTheme)) { + setEditorSettings({ ...editorSettings, editorTheme: localEditorTheme }); + } else setEditorSettings({ ...editorSettings, editorTheme: 'default' }); + const localEditorSettings = window.localStorage.getItem(EDITOR_SETTINGS_KEY); + if(localEditorSettings) setEditorSettings(JSON.parse(localEditorSettings)); const snippetBar = document.querySelector('.editor > .snippetBar'); if(!snippetBar) return; @@ -111,7 +140,7 @@ const Editor = forwardRef( useEffect(()=>{ if(liveScroll) brewJump(currentEditorViewPageNum, false); }, [currentEditorViewPageNum, liveScroll]); useEffect(()=>{ if(liveScroll) brewJump(currentEditorCursorPageNum, false); }, [currentEditorCursorPageNum, liveScroll]); - const handleFormatCode = () => { + const handleFormatCode = ()=>{ codeEditor.current?.formatCode(); }; @@ -211,7 +240,12 @@ const Editor = forwardRef( const updateEditorTheme = (newTheme)=>{ window.localStorage.setItem(EDITOR_THEME_KEY, newTheme); - setEditorTheme(newTheme); + setEditorSettings({ ...editorSettings, editorTheme: newTheme }); + }; + + const updateEditorSettings = (newEditorSettings)=>{ + window.localStorage.setItem(EDITOR_SETTINGS_KEY, JSON.stringify(newEditorSettings)); + setEditorSettings(newEditorSettings); }; const renderEditor = ()=>{ @@ -228,9 +262,11 @@ const Editor = forwardRef( onChange={onBrewChange('text')} onCursorChange={(page)=>updateCurrentCursorPage(page)} onViewChange={(page)=>updateCurrentViewPage(page)} - editorTheme={currentEditorTheme} + editorTheme={editorSettings.editorTheme} + onThemeChange={setIsDark} renderer={brew.renderer} style={{ height: `calc(100% - ${snippetBarHeight}px)` }} + settings={editorSettings} /> ); @@ -246,9 +282,11 @@ const Editor = forwardRef( view={view} value={brew.style ?? DEFAULT_STYLE_TEXT} onChange={onBrewChange('style')} - editorTheme={currentEditorTheme} + editorTheme={editorSettings.editorTheme} + onThemeChange={setIsDark} renderer={brew.renderer} style={{ height: `calc(100% - ${snippetBarHeight}px)` }} + settings={editorSettings} /> ); @@ -268,9 +306,11 @@ const Editor = forwardRef( value={brew.snippets} onChange={onBrewChange('snippets')} enableFolding={true} - editorTheme={currentEditorTheme} + editorTheme={editorSettings.editorTheme} + onThemeChange={setIsDark} renderer={brew.renderer} style={{ height: `calc(100% - 25px)` }} + settings={editorSettings} /> ); @@ -278,7 +318,7 @@ const Editor = forwardRef( if(isMeta()) { return ( <> - + ); } + if(isSettings()){ + return ( + <> + + + + ); + } }; const redo = ()=>codeEditor.current?.redo(); @@ -308,9 +368,8 @@ const Editor = forwardRef( unfoldCode, historySize, })); - return ( -
+
this.handlePublish(false)}> + return