0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-24 16:22:44 +00:00

A not so light rework.

This removes the existing endpoints and replaces them with /theme.

/theme/:id - return a theme bundle containing all styling from this USER theme and any parents.
/theme/:engine/:id - return a theme bundle containing all styling from this STATIC theme and any parents

The theme bundle returns a marshalled JSON object containing:
  styles - an array of strings representing the collected styles in loading order
  snippets - an array ( currently empty ) of collected snippets.

The various bits of theme rendering code for <style> an style <link> have been swapped out with an 'onDidMount' call that loads the thendpoint and appends a series of <style> blocks to the brewRender's head.

This loses some caching advantages, but probably won't matter in the long run.
This commit is contained in:
David Bolack
2024-07-13 12:12:05 -05:00
parent 2fa3c0f311
commit ade819c70c
4 changed files with 143 additions and 28 deletions

View File

@@ -20,6 +20,10 @@ const purifyConfig = { FORCE_BODY: true, SANITIZE_DOM: false };
const staticThemes = require('themes/themes.json');
const isStaticTheme = (renderer, themeName)=>{
return staticThemes[renderer]?.[themeName] !== undefined;
};
const PAGE_HEIGHT = 1056;
const INITIAL_CONTENT = dedent`
@@ -124,13 +128,6 @@ const BrewRenderer = (props)=>{
</div>;
};
const renderStyle = ()=>{
if(!props.style) return;
const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig);
//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>` }} />;
};
const renderPage = (pageText, index)=>{
if(props.renderer == 'legacy') {
const html = MarkdownLegacy.render(pageText);
@@ -170,7 +167,44 @@ const BrewRenderer = (props)=>{
}
};
const loadAllBrewStylesAndSnippets = ()=>{
/*
Loads the theme bundle and parses it out.
These functionally replaces the previous renderStyles() function but needs to wait until the window is mounted.
*/
const rendererPath = isStaticTheme(props.renderer, props.theme) ? `/${props.renderer}/` : '/';
// Check for a User or Static Theme to change the endpoint path
fetch(`${window.location.protocol}//${window.location.host}/theme${rendererPath}${props.theme}`).then((response)=>response.json()).then((themeBundle)=>{
// Load the themeBundle from the endpoint as an object.
const documentFrame = document.getElementById('BrewRenderer');
const iframeDocument = documentFrame.contentDocument || documentFrame.contentWindow.document;
// Find the brew frame Document root.
for (let style=0; style < themeBundle.styles.length; style++){
/*
Walk through the styles array on the Theme Bundle.
Create a new style node and add it to the Brew Frame <head>
*/
const newStyles = document.createElement('style');
newStyles.appendChild(document.createTextNode(`${themeBundle.styles[style]}\n`));
iframeDocument.head.appendChild(newStyles);
}
/*
Add the local brew styling to the Brew Frame <head>
*/
const newStyles = document.createElement('style');
const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig);
newStyles.appendChild(document.createTextNode(`/* Local Brew Styling */\n\n${cleanStyle}`));
iframeDocument.head.appendChild(newStyles);
// TO-DO - Walk the snippets returns and add them to the appropriate menu.
});
};
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
loadAllBrewStylesAndSnippets(); // Load the brew's inherited and local styles.
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
updateSize();
window.addEventListener('resize', updateSize);
@@ -189,10 +223,10 @@ const BrewRenderer = (props)=>{
};
let rendererPath = '';
let themePath = props.theme;
const themePath = props.theme;
if (staticThemes[_.upperFirst(props.renderer)]?.[props.theme] !== undefined) //Change CSS path if is staticTheme
rendererPath = _.upperFirst(props.renderer) + '/';
if(staticThemes[_.upperFirst(props.renderer)]?.[props.theme] !== undefined) //Change CSS path if is staticTheme
rendererPath = `${_.upperFirst(props.renderer)}/`;
return (
<>
@@ -222,14 +256,10 @@ const BrewRenderer = (props)=>{
onKeyDown={handleControlKeys}
tabIndex={-1}
style={{ height: state.height }}>
<link href={`/css/${rendererPath}${themePath}`} type='text/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>

View File

@@ -9,7 +9,7 @@ const yaml = require('js-yaml');
const app = express();
const config = require('./config.js');
const { homebrewApi, getBrew, getBrewThemeCSS, getStaticThemeCSS } = require('./homebrew.api.js');
const { homebrewApi, getBrew, getThemeBundle } = require('./homebrew.api.js');
const GoogleActions = require('./googleActions.js');
const serveCompressedStaticAssets = require('./static-assets.mv.js');
const sanitizeFilename = require('sanitize-filename');
@@ -78,10 +78,10 @@ app.get('/robots.txt', (req, res)=>{
});
// Theme
app.get('/css/:id', asyncHandler(getBrew('theme', false)), asyncHandler(getBrewThemeCSS));
app.get('/css/:engine/:id/', asyncHandler(getStaticThemeCSS));
// Path for User Themes
app.get('/theme/:id', asyncHandler(getBrew('theme', false)), asyncHandler(getThemeBundle));
// Path for Static Themes
app.get('/theme/:engine/:id', asyncHandler(getThemeBundle));
//Home page
app.get('/', (req, res, next)=>{

View File

@@ -8,8 +8,8 @@ const Markdown = require('../shared/naturalcrit/markdown.js');
const yaml = require('js-yaml');
const asyncHandler = require('express-async-handler');
const { nanoid } = require('nanoid');
var url = require('url');
const path = require('path');
const fs = require('fs');
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
@@ -274,31 +274,111 @@ const api = {
res.status(200).send(saved);
},
getThemeBundle : async(req, res)=>{
/*
getThemeBundle: Collects the theme and all parent themes
returns an object containing an array of css, in render order, and an array
of snippets ( currently empty )
Important parameter members:
req.params.id: This is the shareId ( User theme ) or name ( static theme )
loaded first.
req.params.engine: This is the Markdown+ version for the static theme. If a
User theme the value will come from the User Theme metadata.
*/
let parentReq = {};
const completeStyles = [];
const completeSnippets = [];
if(!req.params.engine) {
// If this is not set, our *first* theme is a User theme.
const finalChildBrew = req.brew;
// Break up the frontmatter
splitTextStyleAndMetadata(finalChildBrew);
// If there is anything in the snippets member, append it to the snippets array.
if(finalChildBrew?.snippets) completeSnippets.push(JSON.parse(finalChildBrew.snippets));
// If there is anything in the styles member, append it to the styles array with labelling.
if(finalChildBrew?.style) completeStyles.push(`/* From Brew: ${req.protocol}://${req.get('host')}/share/${req.params.id} */\n\n${finalChildBrew.style}`);
// Set up the simulated request we are using for the parent-walking.
// This is our loop control.
parentReq = {
params : {
id : finalChildBrew.theme,
// This is the only value needed for the User themes. This is the shareId of the theme.
},
renderer : finalChildBrew.renderer
// We set this for use later when checking for Static theme inheritance.
};
while ((parentReq.params.id) && (!isStaticTheme(finalChildBrew.renderer, parentReq.params.id))) {
await api.getBrew('share')(parentReq, res, ()=>{});
// Load the referenced Brew
splitTextStyleAndMetadata(parentReq);
// break up the meta data
if(parentReq?.snippets) completeSnippets.push(JSON.parse(parentReq.snippets));
// If there is anything in the snippets member, append it to the snippets array.
if(parentReq?.style) {
completeStyles.push(`/* From Brew: ${req.protocol}://${req.get('host')}/share/${parentReq.params.id} */\n\n${parentReq.style}`);
// If there is anything in the styles member, append it to the styles array with labelling.
}
// Update the loop object to point to this theme's parent
parentReq.params.id = parentReq?.theme;
}
} else {
// If the first theme wasn't a User theme, set up the loop control object
// This is done the same as above for consistant logic.
parentReq = {
params : {
id : req.params.id,
// This is the name of the theme
},
renderer : req.params.engine
// The renderer is needed for the static pathing.
};
}
while ((parentReq.params.id) && (isStaticTheme(parentReq.renderer, parentReq.params.id))) {
// If we have a static path
const localStyle = fs.readFileSync(path.join(__dirname, '../build/themes/', parentReq.renderer, parentReq.params.id, 'style.css')).toString();
// Read the Theme's style.css from the filesystem
completeStyles.push(`/* From Theme ${parentReq.params.id} */\n\n${localStyle}`);
// Label and append the themes style to the styles array.
parentReq.params.id = Themes[parentReq.renderer][parentReq.params.id].baseTheme;
// NOTE: This currently makes NO attempt to do anything with Static theme Snippets. Loading of static snippets remains unchanged.
}
const returnObj = {
styles : completeStyles.reverse(),
// Reverse the order of the styles array so they are rendered oldest aprent to youngest child.
snippets : completeSnippets
};
res.setHeader('Content-Type', 'text/json');
return res.status(200).send(JSON.stringify(returnObj));
},
//Return CSS for a brew theme, with @include endpoint for its parent theme if any
getBrewThemeCSS : async (req, res)=>{
const brew = req.brew;
splitTextStyleAndMetadata(brew);
res.setHeader('Content-Type', 'text/css');
let rendererPath = '';
if(isStaticTheme(req.brew.renderer,req.brew.theme)) //Check if parent is staticBrew
rendererPath = _.upperFirst(req.brew.renderer) + '/';
if(isStaticTheme(req.brew.renderer, req.brew.theme)) //Check if parent is staticBrew
rendererPath = `${_.upperFirst(req.brew.renderer)}/`;
console.log(`getBrewThemeCSS for ${brew.shareId}`)
console.log(`and parentThemeImport for ${brew.theme}`)
console.log(`getBrewThemeCSS for ${brew.shareId}`);
console.log(`and parentThemeImport for ${brew.theme}`);
const parentThemeImport = `@import url(\"/css/${rendererPath}${req.brew.theme}\");\n\n`;
const themeLocationComment = `/* From Brew: ${req.protocol}://${req.get('host')}/share/${req.brew.shareId} */\n\n`;
return res.status(200).send(`${parentThemeImport}${themeLocationComment}${req.brew.style}`);
},
//Return CSS for a static theme, with @include endpoint for its parent theme if any
getStaticThemeCSS : async(req, res)=>{
if (!isStaticTheme(req.params.engine, req.params.id))
if(!isStaticTheme(req.params.engine, req.params.id))
res.status(404).send(`Invalid Theme - Renderer: ${req.params.engine}, Name: ${req.params.id}`);
else {
res.setHeader('Content-Type', 'text/css');
res.setHeader('Cache-Control', 'public, max-age: 43200, must-revalidate');
const themeParent = Themes[req.params.engine][req.params.id].baseTheme;
console.log(`getStaticThemeCSS for ${req.params.id}`)
console.log(`and parentThemeImport for ${themeParent}`)
console.log(`getStaticThemeCSS for ${req.params.id}`);
console.log(`and parentThemeImport for ${themeParent}`);
const parentThemeImport = themeParent ? `@import url(\"/css/${req.params.engine}/${themeParent}\");\n/* Static Theme ${Themes[req.params.engine][themeParent].name} */\n` : '';
return res.status(200).send(`${parentThemeImport}@import url(\"/themes/${req.params.engine}/${req.params.id}/style.css\");\n/* Static Theme ${Themes[req.params.engine][req.params.id].name} */\n`);
}

View File

@@ -15,6 +15,11 @@ const splitTextStyleAndMetadata = (brew)=>{
brew.style = brew.text.slice(7, index - 1);
brew.text = brew.text.slice(index + 5);
}
if(brew.text.startsWith('```snippets')) {
const index = brew.text.indexOf('```\n\n');
brew.snippets = brew.text.slice(11, index - 1);
brew.text = brew.text.slice(index + 5);
}
};
const printCurrentBrew = ()=>{