/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ require('./brewRenderer.less'); const React = require('react'); const { useState, useRef, useMemo, useEffect } = 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 HeaderNav from './headerNav/headerNav.jsx'; import { safeHTML } from './safeHTML.js'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?: *{[^\n{}]*})?$)/m; const PAGE_HEIGHT = 1056; const INITIAL_CONTENT = dedent`
`; //v=====----------------------< Brew Page Component >---------------------=====v// const BrewPage = (props)=>{ props = { contents : '', index : 0, ...props }; const pageRef = useRef(null); const cleanText = safeHTML(props.contents); useEffect(()=>{ if(!pageRef.current) return; // Observer for tracking pages within the `.pages` div 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); }); }, { 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 }); }, { 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(); }; }, []); 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', visiblePages : [], centerPage : 1 }); const [displayOptions, setDisplayOptions] = useState({ zoomLevel : 100, spread : 'single', startOnRight : true, pageShadows : true }); const [headerState, setHeaderState] = useState(false); const mainRef = useRef(null); const pagesRef = useRef(null); if(props.renderer == 'legacy') { rawPages = props.text.split('\\page'); } else { rawPages = props.text.split(PAGEBREAK_REGEX_V3); } 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 }; }); if(isCenter) props.onPageChange(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 themeStyles = props.themeBundle?.joinedStyles ?? ''; const cleanStyle = safeHTML(`${themeStyles} \n\n `); return
; }; const renderPage = (pageText, index)=>{ let styles = { ...(!displayOptions.pageShadows ? { boxShadow: 'none' } : {}) // Add more conditions as needed }; let classes = 'page'; let attributes = {}; if(props.renderer == 'legacy') { const html = MarkdownLegacy.render(pageText); return ; } else { if(pageText.startsWith('\\page')) { const firstLineTokens = Markdown.marked.lexer(pageText.split('\n', 1)[0])[0].tokens; const injectedTags = firstLineTokens.find((obj)=>obj.injectedTags !== undefined)?.injectedTags; if(injectedTags) { styles = { ...styles, ...injectedTags.styles }; styles = _.mapKeys(styles, (v, k) => k.startsWith('--') ? k : _.camelCase(k)); // Convert CSS to camelCase for React classes = [classes, injectedTags.classes].join(' ').trim(); attributes = injectedTags.attributes; } pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line } 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 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 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 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(), [props.text, displayOptions]); return ( <> {/*render dummy page while iFrame is mounting.*/} {!state.isMounted ?
{renderDummyPage(1)}
: null}
0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/> {/*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}
}
{headerState ? : <>} ); }; module.exports = BrewRenderer;