0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-30 19:42:43 +00:00

Merge pull request #3163 from naturalcrit/BrewRendererToFunctionalComponent

Convert BrewRenderer to function, PPR always on
This commit is contained in:
Trevor Buckner
2023-12-04 17:08:04 -05:00
committed by GitHub
2 changed files with 151 additions and 189 deletions

View File

@@ -1,9 +1,8 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
require('./brewRenderer.less'); require('./brewRenderer.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const { useState, useRef, useEffect } = React;
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js'); const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
const Markdown = require('naturalcrit/markdown.js'); const Markdown = require('naturalcrit/markdown.js');
@@ -13,228 +12,189 @@ const ErrorBar = require('./errorBar/errorBar.jsx');
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx'); const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx'); const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
const Frame = require('react-frame-component').default; const Frame = require('react-frame-component').default;
const dedent = require('dedent-tabs').default;
const Themes = require('themes/themes.json'); const Themes = require('themes/themes.json');
const PAGE_HEIGHT = 1056; const PAGE_HEIGHT = 1056;
const PPR_THRESHOLD = 50;
const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head>
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href='/homebrew/bundle.css' rel='stylesheet' />
<base target=_blank>
</head><body style='overflow: hidden'><div></div></body></html>`;
//v=====----------------------< Brew Page Component >---------------------=====v//
const BrewPage = (props)=>{ const BrewPage = (props)=>{
props = { props = {
contents : '', contents : '',
index : 0, index : 0,
...props ...props
}; };
return <div className='page' id={`p${props.index + 1}`} > return <div className={props.className} id={`p${props.index + 1}`} >
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: props.contents }} /> <div className='columnWrapper' dangerouslySetInnerHTML={{ __html: props.contents }} />
</div>; </div>;
}; };
const BrewRenderer = createClass({
displayName : 'BrewRenderer', //v=====--------------------< Brew Renderer Component >-------------------=====v//
getDefaultProps : function() { const renderedPages = [];
return { let rawPages = [];
const BrewRenderer = (props)=>{
props = {
text : '', text : '',
style : '', style : '',
renderer : 'legacy', renderer : 'legacy',
theme : '5ePHB', theme : '5ePHB',
lang : '', lang : '',
errors : [] errors : [],
...props
}; };
},
getInitialState : function() {
let pages;
if(this.props.renderer == 'legacy') {
pages = this.props.text.split('\\page');
} else {
pages = this.props.text.split(/^\\page$/gm);
}
return { const [state, setState] = useState({
viewablePageNumber : 0, viewablePageNumber : 0,
height : 0, height : PAGE_HEIGHT,
isMounted : false, isMounted : false,
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD,
visibility : 'hidden', visibility : 'hidden',
initialContent : `<!DOCTYPE html><html><head> });
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href='/homebrew/bundle.css' rel='stylesheet' />
<base target=_blank>
</head><body style='overflow: hidden'><div></div></body></html>`
};
},
height : 0,
lastRender : <div></div>,
renderedPages : [],
componentWillUnmount : function() { const mainRef = useRef(null);
window.removeEventListener('resize', this.updateSize);
},
componentDidUpdate : function(prevProps) { if(props.renderer == 'legacy') {
if(prevProps.text !== this.props.text) { rawPages = props.text.split('\\page');
let pages;
if(this.props.renderer == 'legacy') {
pages = this.props.text.split('\\page');
} else { } else {
pages = this.props.text.split(/^\\page$/gm); rawPages = props.text.split(/^\\page$/gm);
} }
this.setState({
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD
});
}
},
updateSize : function() { useEffect(()=>{ // Unmounting steps
this.setState({ return ()=>{window.removeEventListener('resize', updateSize);};
height : this.refs.main.parentNode.clientHeight, }, []);
});
},
handleScroll : function(e){ const updateSize = ()=>{
const target = e.target; setState((prevState)=>({
this.setState((prevState)=>({ ...prevState,
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * prevState.pages.length) height : mainRef.current.parentNode.clientHeight,
})); }));
}, };
shouldRender : function(pageText, index){ const handleScroll = (e)=>{
if(!this.state.isMounted) return false; const target = e.target;
setState((prevState)=>({
...prevState,
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * rawPages.length)
}));
};
const viewIndex = this.state.viewablePageNumber; const shouldRender = (index)=>{
if(index == viewIndex - 3) return true; if(!state.isMounted) return false;
if(index == viewIndex - 2) return true;
if(index == viewIndex - 1) return true;
if(index == viewIndex) return true;
if(index == viewIndex + 1) return true;
if(index == viewIndex + 2) return true;
if(index == viewIndex + 3) return true;
//Check for style tages if(Math.abs(index - state.viewablePageNumber) <= 3)
if(pageText.indexOf('<style>') !== -1) return true; return true;
return false; return false;
}, };
sanitizeScriptTags : function(content) { const sanitizeScriptTags = (content)=>{
return content return content
.replace(/<script/ig, '&lt;script') .replace(/<script/ig, '&lt;script')
.replace(/<\/script>/ig, '&lt;/script&gt;'); .replace(/<\/script>/ig, '&lt;/script&gt;');
}, };
renderPageInfo : function(){ const renderPageInfo = ()=>{
return <div className='pageInfo' ref='main'> return <div className='pageInfo' ref={mainRef}>
<div> <div>
{this.props.renderer} {props.renderer}
</div> </div>
<div> <div>
{this.state.viewablePageNumber + 1} / {this.state.pages.length} {state.viewablePageNumber + 1} / {rawPages.length}
</div> </div>
</div>; </div>;
}, };
renderPPRmsg : function(){ const renderDummyPage = (index)=>{
if(!this.state.usePPR) return;
return <div className='ppr_msg'>
Partial Page Renderer is enabled, because your brew is so large. May affect rendering.
</div>;
},
renderDummyPage : function(index){
return <div className='phb page' id={`p${index + 1}`} key={index}> return <div className='phb page' id={`p${index + 1}`} key={index}>
<i className='fas fa-spinner fa-spin' /> <i className='fas fa-spinner fa-spin' />
</div>; </div>;
}, };
renderStyle : function() { const renderStyle = ()=>{
if(!this.props.style) return; if(!props.style) return;
const cleanStyle = this.sanitizeScriptTags(this.props.style); const cleanStyle = sanitizeScriptTags(props.style);
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.sanitizeScriptTags(this.props.style)}\n} </style>` }} />; //return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${sanitizeScriptTags(props.style)}\n} </style>` }} />;
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />; return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />;
}, };
renderPage : function(pageText, index){ const renderPage = (pageText, index)=>{
let cleanPageText = this.sanitizeScriptTags(pageText); let cleanPageText = sanitizeScriptTags(pageText);
if(this.props.renderer == 'legacy') if(props.renderer == 'legacy') {
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(cleanPageText) }} key={index} />; const html = MarkdownLegacy.render(cleanPageText);
else { return <BrewPage className='page phb' index={index} key={index} contents={html} />;
} else {
cleanPageText += `\n\n&nbsp;\n\\column\n&nbsp;`; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear) cleanPageText += `\n\n&nbsp;\n\\column\n&nbsp;`; //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(cleanPageText); const html = Markdown.render(cleanPageText);
return ( return <BrewPage className='page' index={index} key={index} contents={html} />;
<BrewPage index={index} key={index} contents={html} />
);
} }
}, };
renderPages : function(){ const renderPages = ()=>{
if(this.state.usePPR){ if(props.errors && props.errors.length)
_.forEach(this.state.pages, (page, index)=>{ return renderedPages;
if((this.shouldRender(page, index) || !this.renderedPages[index]) && typeof window !== 'undefined'){
this.renderedPages[index] = this.renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range _.forEach(rawPages, (page, index)=>{
if((shouldRender(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 this.renderedPages; return renderedPages;
} };
if(this.props.errors && this.props.errors.length) return this.lastRender;
this.lastRender = _.map(this.state.pages, (page, index)=>{
if(typeof window !== 'undefined') {
return this.renderPage(page, index);
} else {
return this.renderDummyPage(index);
}
});
return this.lastRender;
},
frameDidMount : function(){ //This triggers when iFrame finishes internal "componentDidMount" 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 setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
this.updateSize(); updateSize();
window.addEventListener('resize', this.updateSize); window.addEventListener('resize', updateSize);
this.renderPages(); //Make sure page is renderable before showing renderPages(); //Make sure page is renderable before showing
this.setState({ setState((prevState)=>({
...prevState,
isMounted : true, isMounted : true,
visibility : 'visible' visibility : 'visible'
}); }));
}, 100); }, 100);
}, };
emitClick : function(){ const emitClick = ()=>{ // Allow clicks inside iFrame to interact with dropdowns, etc. from outside
// console.log('iFrame clicked');
if(!window || !document) return; if(!window || !document) return;
document.dispatchEvent(new MouseEvent('click')); document.dispatchEvent(new MouseEvent('click'));
}, };
render : function(){ const rendererPath = props.renderer == 'V3' ? 'V3' : 'Legacy';
//render in iFrame so broken code doesn't crash the site. const themePath = props.theme ?? '5ePHB';
//Also render dummy page while iframe is mounting.
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
const themePath = this.props.theme ?? '5ePHB';
const baseThemePath = Themes[rendererPath][themePath].baseTheme; const baseThemePath = Themes[rendererPath][themePath].baseTheme;
return ( return (
<React.Fragment> <>
{!this.state.isMounted {/*render dummy page while iFrame is mounting.*/}
? <div className='brewRenderer' onScroll={this.handleScroll}> {!state.isMounted
<div className='pages' ref='pages'> ? <div className='brewRenderer' onScroll={handleScroll}>
{this.renderDummyPage(1)} <div className='pages'>
{renderDummyPage(1)}
</div> </div>
</div> </div>
: null} : null}
<Frame id='BrewRenderer' initialContent={this.state.initialContent} {/*render in iFrame so broken code doesn't crash the site.*/}
style={{ width: '100%', height: '100%', visibility: this.state.visibility }} <Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
contentDidMount={this.frameDidMount} style={{ width: '100%', height: '100%', visibility: state.visibility }}
onClick={()=>{this.emitClick();}} contentDidMount={frameDidMount}
onClick={()=>{emitClick();}}
> >
<div className={'brewRenderer'} <div className={'brewRenderer'}
onScroll={this.handleScroll} onScroll={handleScroll}
style={{ height: this.state.height }}> style={{ height: state.height }}>
<ErrorBar errors={this.props.errors} /> <ErrorBar errors={props.errors} />
<div className='popups'> <div className='popups'>
<RenderWarnings /> <RenderWarnings />
<NotificationPopup /> <NotificationPopup />
@@ -244,23 +204,22 @@ const BrewRenderer = createClass({
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/> <link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
} }
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/> <link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
{/* Apply CSS from Style tab and render pages from Markdown tab */} {/* Apply CSS from Style tab and render pages from Markdown tab */}
{this.state.isMounted {state.isMounted
&& &&
<> <>
{this.renderStyle()} {renderStyle()}
<div className='pages' ref='pages' lang={`${this.props.lang || 'en'}`}> <div className='pages' lang={`${props.lang || 'en'}`}>
{this.renderPages()} {renderPages()}
</div> </div>
</> </>
} }
</div> </div>
</Frame> </Frame>
{this.renderPageInfo()} {renderPageInfo()}
{this.renderPPRmsg()} </>
</React.Fragment>
); );
} };
});
module.exports = BrewRenderer; module.exports = BrewRenderer;

View File

@@ -40,7 +40,7 @@ body {
-webkit-column-gap : 1cm; -webkit-column-gap : 1cm;
-moz-column-gap : 1cm; -moz-column-gap : 1cm;
} }
.phb{ .phb, .page{
.useColumns(); .useColumns();
counter-increment : phb-page-numbers; counter-increment : phb-page-numbers;
position : relative; position : relative;
@@ -59,6 +59,9 @@ body {
page-break-before : always; page-break-before : always;
page-break-after : always; page-break-after : always;
contain : size; contain : size;
}
.phb{
//***************************** //*****************************
// * BASE // * BASE
// *****************************/ // *****************************/