/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ require('./brewRenderer.less'); const React = require('react'); const { useState, useRef, useCallback, useMemo, useEffect } = React; const _ = require('lodash'); const MarkdownLegacy = require('naturalcrit/markdownLegacy.js'); const Markdown = require('naturalcrit/markdown.js'); const ErrorBar = require('./errorBar/errorBar.jsx'); const ToolBar = require('./toolBar/toolBar.jsx'); //TODO: move to the brew renderer const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx'); const NotificationPopup = require('./notificationPopup/notificationPopup.jsx'); const Frame = require('react-frame-component').default; const dedent = require('dedent-tabs').default; const { printCurrentBrew } = require('../../../shared/helpers.js'); const DOMPurify = require('dompurify'); const purifyConfig = { FORCE_BODY: true, SANITIZE_DOM: false }; const PAGE_HEIGHT = 1056; const INITIAL_CONTENT = dedent`
`; //v=====----------------------< Brew Page Component >---------------------=====v// const BrewPage = ({ contents = '', index = 0, onVisibilityChange, onCenterPageChange, ...props })=>{ const pageRef = useRef(null); const cleanText = contents; //DOMPurify.sanitize(props.contents, purifyConfig); useEffect(()=>{ if(!pageRef.current) return; // Observer for tracking pages within the `.pages` div const visibleObserver = new IntersectionObserver( (entries)=>{ entries.forEach((entry)=>{ if(entry.isIntersecting){ onVisibilityChange(index + 1, true); // add page to array of visible pages. } else { onVisibilityChange(index + 1, false); } }); }, { 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) { onCenterPageChange(index + 1); // Set this page as the center page } }); }, { threshold: 0, rootMargin: '-50% 0px -50% 0px' } // Detect when the page is at the center ); // attach observers to each `.page` visibleObserver.observe(pageRef.current); centerObserver.observe(pageRef.current); return ()=>{ visibleObserver.disconnect(); centerObserver.disconnect(); }; }, [index, onVisibilityChange, onCenterPageChange]); return
; }; //v=====--------------------< Brew Renderer Component >-------------------=====v// let renderedPages = []; let rawPages = []; const BrewRenderer = (props)=>{ props = { text : '', style : '', renderer : 'legacy', theme : '5ePHB', lang : '', errors : [], currentEditorCursorPageNum : 1, currentEditorViewPageNum : 1, currentBrewRendererPageNum : 1, themeBundle : {}, onPageChange : ()=>{}, ...props }; const [state, setState] = useState({ isMounted : false, visibility : 'hidden', zoom : 100, visiblePages : [], centerPage : 1 }); const iframeRef = useRef(null); const mainRef = useRef(null); if(props.renderer == 'legacy') { rawPages = props.text.split('\\page'); } else { rawPages = props.text.split(/^\\page$/gm); } // update centerPage (aka "current page") and pass it up to parent components useEffect(()=>{ props.onPageChange(state.centerPage); }, [state.centerPage]); const handlePageVisibilityChange = useCallback((pageNum, isVisible)=>{ setState((prevState)=>{ const updatedVisiblePages = new Set(prevState.visiblePages); if(isVisible){ updatedVisiblePages.add(pageNum); } else { updatedVisiblePages.delete(pageNum); } const pages = Array.from(updatedVisiblePages); return { ...prevState, visiblePages : _.sortBy(pages) }; }); }, []); const handleCenterPageChange = useCallback((pageNum)=>{ setState((prevState)=>({ ...prevState, centerPage : pageNum, })); }, []); const isInView = (index)=>{ if(!state.isMounted) return false; if(index == props.currentEditorCursorPageNum - 1) //Already rendered before this step return false; if(Math.abs(index - props.currentBrewRendererPageNum - 1) <= 3) return true; return false; }; const renderDummyPage = (index)=>{ return
; }; const renderStyle = ()=>{ const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig); const themeStyles = props.themeBundle?.joinedStyles ?? ''; return
${cleanStyle} ` }} />; }; const renderPage = (pageText, index)=>{ if(props.renderer == 'legacy') { const html = MarkdownLegacy.render(pageText); return ; } else { 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); return ; } }; const renderPages = ()=>{ if(props.errors && props.errors.length) return renderedPages; if(rawPages.length != renderedPages.length) // Re-render all pages when page count changes renderedPages.length = 0; // Render currently-edited page first so cross-page effects (variables, links) can propagate out first renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1); _.forEach(rawPages, (page, index)=>{ 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 } }); return renderedPages; }; const handleControlKeys = (e)=>{ if(!(e.ctrlKey || e.metaKey)) return; const P_KEY = 80; if(e.keyCode == P_KEY && props.allowPrint) printCurrentBrew(); if(e.keyCode == P_KEY) { e.stopPropagation(); e.preventDefault(); } }; const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount" 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, visibility : 'visible' })); }, 100); }; const emitClick = ()=>{ // Allow clicks inside iFrame to interact with dropdowns, etc. from outside if(!window || !document) return; document.dispatchEvent(new MouseEvent('click')); }; //Toolbar settings: const handleZoom = (newZoom)=>{ setState((prevState)=>({ ...prevState, zoom : newZoom })); }; const styleObject = {}; if(global.config.deployment) { styleObject.backgroundImage = `url("data:image/svg+xml;utf8,${global.config.deployment}")`; } const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]); renderedPages = useMemo(()=>renderPages(), [props.text]); return ( <> {/*render dummy page while iFrame is mounting.*/} {!state.isMounted ?
{renderDummyPage(1)}
: null}
{/*render in iFrame so broken code doesn't crash the site.*/} {emitClick();}} >
{/* Apply CSS from Style tab and render pages from Markdown tab */} {state.isMounted && <> {renderedStyle}
{renderedPages}
}
); }; module.exports = BrewRenderer;