0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-27 20:12:40 +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,254 +12,214 @@ 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',
getDefaultProps : function() {
return {
text : '',
style : '',
renderer : 'legacy',
theme : '5ePHB',
lang : '',
errors : []
};
},
getInitialState : function() {
let pages;
if(this.props.renderer == 'legacy') {
pages = this.props.text.split('\\page');
} else {
pages = this.props.text.split(/^\\page$/gm);
}
return { //v=====--------------------< Brew Renderer Component >-------------------=====v//
viewablePageNumber : 0, const renderedPages = [];
height : 0, let rawPages = [];
isMounted : false,
pages : pages, const BrewRenderer = (props)=>{
usePPR : pages.length >= PPR_THRESHOLD, props = {
visibility : 'hidden', text : '',
initialContent : `<!DOCTYPE html><html><head> style : '',
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" /> renderer : 'legacy',
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" /> theme : '5ePHB',
<link href='/homebrew/bundle.css' rel='stylesheet' /> lang : '',
<base target=_blank> errors : [],
</head><body style='overflow: hidden'><div></div></body></html>` ...props
}; };
},
height : 0,
lastRender : <div></div>,
renderedPages : [],
componentWillUnmount : function() { const [state, setState] = useState({
window.removeEventListener('resize', this.updateSize); viewablePageNumber : 0,
}, height : PAGE_HEIGHT,
isMounted : false,
visibility : 'hidden',
});
componentDidUpdate : function(prevProps) { const mainRef = useRef(null);
if(prevProps.text !== this.props.text) {
let pages;
if(this.props.renderer == 'legacy') {
pages = this.props.text.split('\\page');
} else {
pages = this.props.text.split(/^\\page$/gm);
}
this.setState({
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD
});
}
},
updateSize : function() { if(props.renderer == 'legacy') {
this.setState({ rawPages = props.text.split('\\page');
height : this.refs.main.parentNode.clientHeight, } else {
}); rawPages = props.text.split(/^\\page$/gm);
}, }
handleScroll : function(e){ useEffect(()=>{ // Unmounting steps
const target = e.target; return ()=>{window.removeEventListener('resize', updateSize);};
this.setState((prevState)=>({ }, []);
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * prevState.pages.length)
const updateSize = ()=>{
setState((prevState)=>({
...prevState,
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;
}
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; return renderedPages;
}, };
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 baseThemePath = Themes[rendererPath][themePath].baseTheme;
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
const themePath = this.props.theme ?? '5ePHB'; return (
const baseThemePath = Themes[rendererPath][themePath].baseTheme; <>
return ( {/*render dummy page while iFrame is mounting.*/}
<React.Fragment> {!state.isMounted
{!this.state.isMounted ? <div className='brewRenderer' onScroll={handleScroll}>
? <div className='brewRenderer' onScroll={this.handleScroll}> <div className='pages'>
<div className='pages' ref='pages'> {renderDummyPage(1)}
{this.renderDummyPage(1)}
</div>
</div> </div>
: null} </div>
: 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'} >
onScroll={this.handleScroll} <div className={'brewRenderer'}
style={{ height: this.state.height }}> onScroll={handleScroll}
style={{ height: state.height }}>
<ErrorBar errors={this.props.errors} /> <ErrorBar errors={props.errors} />
<div className='popups'> <div className='popups'>
<RenderWarnings /> <RenderWarnings />
<NotificationPopup /> <NotificationPopup />
</div>
<link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
{baseThemePath &&
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
}
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
{/* Apply CSS from Style tab and render pages from Markdown tab */}
{this.state.isMounted
&&
<>
{this.renderStyle()}
<div className='pages' ref='pages' lang={`${this.props.lang || 'en'}`}>
{this.renderPages()}
</div>
</>
}
</div> </div>
</Frame> <link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
{this.renderPageInfo()} {baseThemePath &&
{this.renderPPRmsg()} <link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
</React.Fragment> }
); <link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
}
}); {/* Apply CSS from Style tab and render pages from Markdown tab */}
{state.isMounted
&&
<>
{renderStyle()}
<div className='pages' lang={`${props.lang || 'en'}`}>
{renderPages()}
</div>
</>
}
</div>
</Frame>
{renderPageInfo()}
</>
);
};
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
// *****************************/ // *****************************/