/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ require('./brewRenderer.less'); const React = require('react'); const { useState, useRef, useCallback, useMemo } = React; const _ = require('lodash'); const MarkdownLegacy = require('naturalcrit/markdownLegacy.js'); import Markdown from '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'); import { safeHTML } from './safeHTML.js'; const PAGE_HEIGHT = 1056; const INITIAL_CONTENT = dedent`
`; //v=====----------------------< Brew Page Component >---------------------=====v// const BrewPage = (props)=>{ props = { contents : '', index : 0, ...props }; const cleanText = safeHTML(props.contents); 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' }); const [displayOptions, setDisplayOptions] = useState({ zoomLevel : 100, spread : 'single', startOnRight : true, pageShadows : true }); const mainRef = useRef(null); if(props.renderer == 'legacy') { rawPages = props.text.split('\\page'); } else { rawPages = props.text.split(/^\\page$/gm); } const scrollToHash = (hash)=>{ if(!hash) return; const iframeDoc = document.getElementById('BrewRenderer').contentDocument; let anchor = iframeDoc.querySelector(hash); if(anchor) { anchor.scrollIntoView({ behavior: 'smooth' }); } else { // Use MutationObserver to wait for the element if it's not immediately available new MutationObserver((mutations, obs)=>{ anchor = iframeDoc.querySelector(hash); if(anchor) { anchor.scrollIntoView({ behavior: 'smooth' }); obs.disconnect(); } }).observe(iframeDoc, { childList: true, subtree: true }); } }; const updateCurrentPage = useCallback(_.throttle((e)=>{ const { scrollTop, clientHeight, scrollHeight } = e.target; const totalScrollableHeight = scrollHeight - clientHeight; const currentPageNumber = Math.max(Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length), 1); props.onPageChange(currentPageNumber); }, 200), []); 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 themeStyles = props.themeBundle?.joinedStyles ?? ''; const cleanStyle = safeHTML(`${themeStyles} \n\n `); return
; }; 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); const styles = { ...(!displayOptions.pageShadows ? { boxShadow: 'none' } : {}) // Add more conditions as needed }; 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 if(rawPages.length > props.currentEditorCursorPageNum -1) 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" scrollToHash(window.location.hash); 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')); }; const handleDisplayOptionsChange = (newDisplayOptions)=>{ setDisplayOptions(newDisplayOptions); }; const pagesStyle = { zoom : `${displayOptions.zoomLevel}%`, columnGap : `${displayOptions.columnGap}px`, rowGap : `${displayOptions.rowGap}px` }; 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(), [displayOptions.pageShadows, 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;