diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 4c77728fa..fa17dfee2 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -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)=>{ ; }; - const renderStyle = ()=>{ - if(!props.style) return; - const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig); - //return
@layer styleTab {\n${sanitizeScriptTags(props.style)}\n} ` }} />; - return
${cleanStyle} ` }} />; - }; - 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 + */ + 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 + */ + 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 }}> - - - {/* Apply CSS from Style tab and render pages from Markdown tab */} {state.isMounted && <> - {renderStyle()}
{renderPages()}
diff --git a/server/app.js b/server/app.js index b2f76f280..9680c64ca 100644 --- a/server/app.js +++ b/server/app.js @@ -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)=>{ diff --git a/server/homebrew.api.js b/server/homebrew.api.js index e39026b84..ac91799dc 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -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`); } diff --git a/shared/helpers.js b/shared/helpers.js index 8ca185046..321791f84 100644 --- a/shared/helpers.js +++ b/shared/helpers.js @@ -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 = ()=>{