diff --git a/changelog.md b/changelog.md index 136f6346a..d783130f0 100644 --- a/changelog.md +++ b/changelog.md @@ -93,23 +93,77 @@ pre { ## changelog For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). -### Saturday 4/20/2026 - v3.22.0 - +### Monday 9/07/2026 - v3.23.0 {{taskList +##### firstmatekidd (new contributor!) +* [x] Add nav buttons to tab order for accesibility +* [x] Add title to brewRenderer iframe for accessibility +* [x] Add aria labels to toolbars + +Partially fixes issue [#3032](https://github.com/naturalcrit/homebrewery/issues/3032) + +##### ReinaSweet (new contributor!) +* [x] Added security to prevent scripts running in brews + +##### Gazook89 +* [x] Lazy load brew images for faster first load +* [x] Fix snippet menus getting cut off behind divider bar + +Fixes issue [#3840](https://github.com/naturalcrit/homebrewery/issues/3840) + +##### Abquintic +* [x] Fix variables not hoisting in later pages + ##### 5e-Cleric -* [x] Major update to editor framework (Codemirror 6) -Fixes issues [#3511](https://github.com/naturalcrit/homebrewery/issues/3511), [#4590](https://github.com/naturalcrit/homebrewery/issues/4590), [#4563](https://github.com/naturalcrit/homebrewery/issues/4653), [#4655](https://github.com/naturalcrit/homebrewery/issues/4655) -* [x] Fix to Admin page tab names +* [x] Add image preview when hovering over image syntax + +Fixes issue [#2037](https://github.com/naturalcrit/homebrewery/issues/2037) + +* [x] Remember editor theme across tabs + +Fixes issue [#4858](https://github.com/naturalcrit/homebrewery/issues/4858) + +* [x] Add ability to remove co-authors if you are the owner + +Fixes part of issue [#4101](https://github.com/naturalcrit/homebrewery/issues/4101) ##### G-Ambatte -* [x] Fix white page crash on certain browsers +* [x] Fix editor panel shrinking when openingdev tools + +Fixes issue [#4866](https://github.com/naturalcrit/homebrewery/issues/4866) + +* [x] Added security to prevent scripts running in brews + +Fixes issue [#4904](https://github.com/naturalcrit/homebrewery/issues/4904) + +##### Abquintic, G-Ambatte, Gazook89, 5e-Cleric +* [x] Multiple background fixes and code cleanups }} +### Wednesday 5/13/2026 - v3.22.1 +{{taskList +##### Frederlk (new Contributor!) +* [x] Add "share current page" to {{openSans **SHARE :fas_share_nodes:**}} menu + +##### Calculuschild, 5e-Cleric +* [x] Fix brews triggering unneeded save when loaded + +##### G-Ambatte +* [x] Fix brews with `$` in text getting out of sync on save + +##### 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) +}} + +\page + ### Saturday 4/04/2026 - v3.21.0 {{taskList ##### Gazook89 -* [x] Allow custom {{openSans **:fas_table_list: SNIPPETS**}} to be inserted mid-line +* [x] Allow custom {{openSans **:fas_table_list: SNIPPETS**}} dropdown to be inserted mid-line ##### abquintic * [x] Move example snippet images out of imgur (for folks without imgur access) @@ -131,6 +185,7 @@ Fixes issues [#543](https://github.com/naturalcrit/homebrewery/issues/543), [#24 * [x] Multiple other backend fixes and refactors }} + ### Friday 1/11/2026 - v3.20.1 {{taskList @@ -326,7 +381,7 @@ Fixes issue [#1729](https://github.com/naturalcrit/homebrewery/issues/1729) Fixes issue [#4079](https://github.com/naturalcrit/homebrewery/issues/4079) ##### Calculuschild -* [x] `꞉꞉꞉꞉` now produces `
` instead of a `
` +* [x] `꞉꞉꞉꞉` now produces `
` instead of a `<div>` * [x] Fix typos in tables freezing the editor Fixes issue [#4059](https://github.com/naturalcrit/homebrewery/issues/4059) @@ -2396,4 +2451,4 @@ Massive changelog incoming: * Added `phb.standalone.css` plus a build system for creating it * Added page numbers and footer text -* Page accent now flips each page \ No newline at end of file +* Page accent now flips each page diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index 25bf4f88e..5ac254132 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -43,9 +43,9 @@ const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery }; const themeCompartment = new Compartment(); const highlightCompartment = new Compartment(); -import { generalKeymap, markdownKeymap } from './extensions/customKeyMaps.js'; +import { generalKeymap, markdownKeymap, cssKeymap, formatCSS } from './extensions/customKeyMaps.js'; import foldOnPages from './extensions/customFolding.js'; -import { customHighlightPlugin, customHighlightStyle } from './extensions/customHighlight.js'; +import { customHighlightStyle , customHighlightPlugin } from './extensions/customHighlight.js'; import { legacyCustomHighlightStyle } from './extensions/legacyCustomHighlight.js'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; @@ -186,7 +186,7 @@ const CodeEditor = forwardRef( //keyboard shortcut keymap.of([...defaultKeymap, foldKeymap, ...searchKeymap]), generalKeymap, - ...(tab !== 'brewStyles' ? [markdownKeymap] : []), + ...(tab === 'brewStyles' ? [cssKeymap] : [markdownKeymap]), //multiple cursors and selections drawSelection(), @@ -356,6 +356,8 @@ const CodeEditor = forwardRef( }, 400); }, + formatCode : ()=>formatCSS(viewRef.current), + undo : ()=>undo(viewRef.current), redo : ()=>redo(viewRef.current), diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index ca77f29a8..1d1e1ae96 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -15,6 +15,18 @@ } } +@keyframes slideacross { + 0% { + bottom: -200px; + left: -200px; + } + + 100% { + bottom:100%; + left:100%; + } +} + :where(.codeEditor) { width : 100%; height : calc(100% - 25px); @@ -23,6 +35,27 @@ .cm-editor { height : 100%; outline : none !important; + + &.cm-flash { + position:relative; + &::after { + position:absolute; + content:''; + bottom:-200px; + left:-200px; + translate:-50%; + width:200%; + height:100px; + background:linear-gradient(0deg, #89eafc00 0px, #89ebfc8e 50px, #89eafc00 100px, transparent); + display:block; + + rotate:30deg; + + @media screen and (prefers-reduced-motion: no-preference) { + animation: .5s linear 1 slideacross ; + } + } + } } &.brewSnippets .cm-snippetLine, diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index 33c68da7a..cb44d3fd9 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -251,13 +251,13 @@ function tokenizeCustomMarkdown(text) { ); if(match) endCh = match.index + match[0].length; const closingMatch = lineText.match(/ *(}})/d); - + if(closingMatch) { tokens.push({ line: lineNumber, from: closingMatch.indices[1][0], to: closingMatch.indices[1][1], type: customTags.block }); } else { tokens.push({ line: lineNumber, type: customTags.block }); } - + } }); @@ -350,10 +350,9 @@ class ImageWidget extends WidgetType { toDOM() { const img = document.createElement('img'); - img.loading = "lazy"; + img.loading = 'lazy'; img.className = 'cm-preview'; img.src = this.url; - img.onerror = ()=>{ img.src = 'client/icons/broken-image.jpg'; @@ -403,8 +402,8 @@ export function customHighlightPlugin(renderer, tab) { const url = getUrl(node, view.state.doc); const widgetPosition = node.node.lastChild.from; - //this is not exactly standard, but should hold, - //and is the shortest way i could find of positioning + //this is not exactly standard, but should hold, + //and is the shortest way i could find of positioning //the image inside the cm-image node if(!url) return; diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js index 581268222..6fe479348 100644 --- a/client/components/codeEditor/extensions/customKeyMaps.js +++ b/client/components/codeEditor/extensions/customKeyMaps.js @@ -3,7 +3,60 @@ import { keymap } from '@codemirror/view'; import { undo, redo, indentMore, indentLess, deleteLine } from '@codemirror/commands'; import { EditorSelection } from '@codemirror/state'; import { Prec } from '@codemirror/state'; +import * as prettier from 'prettier/standalone'; +import * as postcssPlugin from 'prettier/plugins/postcss'; +export async function formatCSS(view) { + try { + const { from, to, empty } = view.state.selection.main; + const fullDoc = view.state.doc.toString(); + const selection = view.state.doc.sliceString(from, to); + const code = empty ? fullDoc : selection; + + let formatted = await prettier.format(code, { + parser: 'css', + plugins: [postcssPlugin], + + // formatting options + tabWidth: 2, + useTabs: false, + printWidth: 100, + singleQuote: false, + trailingComma: 'all', + bracketSpacing: true, + endOfLine: 'lf' + }); + + //format manually single declaration rules to span one line. + //Prettier can't do it by default, this is crude but it works + formatted = formatted.replace( + /([^{]+)\{\s*\n\s*([^;\n]+:[^;\n]+;)\s*\n\s*\}(\s*)/g, + (_, selector, decl, whitespace) => + `${selector} { ${decl.trim()} }${whitespace}` + ); + if(formatted === code) return true; + + const dom = view.dom; + dom.classList.add('cm-flash'); + + setTimeout(()=>{ + dom.classList.remove('cm-flash'); + + view.dispatch({ + changes : { + from : empty ? 0 : from, + to : empty ? view.state.doc.length : to, + insert : formatted + } + }); + + }, 500); + } catch (err) { + console.error('Error formatting css: ', err); + } + + return true; +} const insertTab = (view)=>{ // If any selection spans multiple lines, delegates to CodeMirror's indentMore // Otherwise inserts two spaces at each cursor/selection @@ -30,7 +83,7 @@ const insertTab = (view)=>{ changes, selection : EditorSelection.create( view.state.selection.ranges.map((range)=>EditorSelection.cursor( - mappedChanges.changes.mapPos(range.from, 1) + 2 + mappedChanges.changes.mapPos(range.from, -1) + 2 ) ) ) @@ -188,6 +241,11 @@ export const generalKeymap = Prec.high(keymap.of([ { key: 'Mod-d', run: deleteLine }, //annoyingly overrides "selectNextOccurrence" because users asked ])); +export const cssKeymap = Prec.highest(keymap.of([ + { key: 'Mod-Shift-f', run: formatCSS }, + { key: 'Alt-Shift-f', run: formatCSS }, +])); + export const markdownKeymap = Prec.highest(keymap.of([ { key: 'Mod-b', run: wrapSelection('**', '**') }, // makeBold diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 05ca73bea..75a07c7d1 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 Markdown from '@shared/markdown.js'; +import { hbfm } from 'hbmarkedwrapper'; import ErrorBar from './errorBar/errorBar.jsx'; import ToolBar from './toolBar/toolBar.jsx'; @@ -23,7 +23,6 @@ import safeHTML from './safeHTML.js'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m; const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m; -const PAGE_HEIGHT = 1056; const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState'; @@ -47,31 +46,25 @@ const BrewPage = (props)=>{ }; const pageRef = useRef(null); const cleanText = safeHTML(props.contents); + const pageNum = props.index + 1; useEffect(()=>{ if(!pageRef.current) return; - // Observer for tracking pages within the `.pages` div + // Observer for tracking which pages are at least 30% visible in the iframe const visibleObserver = new IntersectionObserver( - (entries)=>{ - entries.forEach((entry)=>{ - if(entry.isIntersecting) - props.onVisibilityChange(props.index + 1, true, false); // add page to array of visible pages. - else - props.onVisibilityChange(props.index + 1, false, false); - }); - }, + (entries)=>entries.forEach((entry)=>{ + props.onVisibilityChange(pageNum, entry.isIntersecting, false); // add page to array of visible pages. + }), { threshold: .3, rootMargin: '0px 0px 0px 0px' } // detect when >30% of page is within bounds. ); // Observer for tracking the page at the center of the iframe. const centerObserver = new IntersectionObserver( - (entries)=>{ - entries.forEach((entry)=>{ - if(entry.isIntersecting) - props.onVisibilityChange(props.index + 1, true, true); // Set this page as the center page - }); - }, + (entries)=>entries.forEach((entry)=>{ + if(entry.isIntersecting) + props.onVisibilityChange(pageNum, true, true); // Set this page as the center page + }), { threshold: 0, rootMargin: '-50% 0px -50% 0px' } // Detect when the page is at the center ); @@ -92,7 +85,7 @@ const BrewPage = (props)=>{ //v=====--------------------< Brew Renderer Component >-------------------=====v// let renderedPages = []; -let pageTemplates = []; +const pageTemplates = []; let rawPages = []; const BrewRenderer = (props)=>{ @@ -100,22 +93,21 @@ const BrewRenderer = (props)=>{ text : '', style : '', renderer : 'legacy', - theme : '5ePHB', lang : '', errors : [], currentEditorCursorPageNum : 1, - currentEditorViewPageNum : 1, currentBrewRendererPageNum : 1, themeBundle : {}, onPageChange : ()=>{}, ...props }; + const [visiblePages, setVisiblePages] = useState([]); + const [centerPage , setCenterPage ] = useState(1); + const [state, setState] = useState({ - isMounted : false, - visibility : 'hidden', - visiblePages : [], - centerPage : 1 + isMounted : false, + visibility : 'hidden' }); const [displayOptions, setDisplayOptions] = useState({ @@ -135,9 +127,7 @@ const BrewRenderer = (props)=>{ const [headerState, setHeaderState] = useState(false); - const mainRef = useRef(null); const pagesRef = useRef(null); - const urlRef = useRef(''); if(props.renderer == 'legacy') { rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY); @@ -146,20 +136,16 @@ const BrewRenderer = (props)=>{ } const handlePageVisibilityChange = (pageNum, isVisible, isCenter)=>{ - setState((prevState)=>{ - const updatedVisiblePages = new Set(prevState.visiblePages); - if(!isCenter) - isVisible ? updatedVisiblePages.add(pageNum) : updatedVisiblePages.delete(pageNum); - - return { - ...prevState, - visiblePages : [...updatedVisiblePages].sort((a, b)=>a - b), - centerPage : isCenter ? pageNum : prevState.centerPage - }; + setVisiblePages((prev)=>{ + const updatedVisiblePages = new Set(prev); + isVisible ? updatedVisiblePages.add(pageNum) : updatedVisiblePages.delete(pageNum); + return [...updatedVisiblePages].sort((a, b)=>a - b); }); - if(isCenter) + if(isCenter) { + setCenterPage(pageNum); props.onPageChange(pageNum); + } }; const isInView = (index)=>{ @@ -203,7 +189,7 @@ const BrewRenderer = (props)=>{ return ; } else { if(pageText.startsWith('\\page')) { - const firstLineTokens = Markdown.marked.lexer(pageText.split('\n', 1)[0])[0].tokens; + const firstLineTokens = hbfm.marked.lexer(pageText.split('\n', 1)[0])[0].tokens; const injectedTags = firstLineTokens?.find((obj)=>obj.injectedTags !== undefined)?.injectedTags; if(injectedTags) { styles = { ...styles, ...injectedTags.styles }; @@ -231,7 +217,7 @@ const BrewRenderer = (props)=>{ // DO NOT REMOVE!!! REQUIRED FOR BACKWARDS COMPATIBILITY WITH NON-UPGRADABLE VERSIONS OF CHROME. pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear) - const html = Markdown.render(pageText, index); + const html = hbfm.render(pageText, index); return ; } @@ -341,19 +327,19 @@ const BrewRenderer = (props)=>{ : null} -
+
- 0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/> + 0 ? visiblePages : [centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/> {/*render in iFrame so broken code doesn't crash the site.*/} - {emitClick();}} - sandbox="allow-same-origin allow-modals allow-top-navigation" + sandbox='allow-same-origin allow-modals allow-top-navigation' >
{ return notifications.map((notification)=>(
  • {notification.title}
    -

    +

  • )); }; diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx index 92ce0b78c..70db74b21 100644 --- a/client/homebrew/editor/editor.jsx +++ b/client/homebrew/editor/editor.jsx @@ -111,6 +111,10 @@ const Editor = forwardRef( useEffect(()=>{ if(liveScroll) brewJump(currentEditorViewPageNum, false); }, [currentEditorViewPageNum, liveScroll]); useEffect(()=>{ if(liveScroll) brewJump(currentEditorCursorPageNum, false); }, [currentEditorCursorPageNum, liveScroll]); + const handleFormatCode = () => { + codeEditor.current?.formatCode(); + }; + const handleControlKeys = (e)=>{ if(!(e.ctrlKey && e.metaKey && e.shiftKey)) return; const LEFTARROW_KEY = 37; @@ -319,6 +323,7 @@ const Editor = forwardRef( redo={redo} foldCode={foldCode} unfoldCode={unfoldCode} + formatCode={isStyle() ? handleFormatCode : null} historySize={historySize()} currentEditorTheme={currentEditorTheme} updateEditorTheme={updateEditorTheme} diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index 14ea97305..9377aa41c 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -157,11 +157,11 @@ const MetadataEditor = createReactClass({ renderPublish : function(){ if(this.props.metadata.published){ return