diff --git a/server/homebrew.api.js b/server/homebrew.api.js index e99b1b1d9..f77c429e2 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -11,6 +11,19 @@ const { nanoid } = require('nanoid'); const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js'); +const themes = require('../themes/themes.json'); + +const isStaticTheme = (engine, themeName)=>{ + if(!themes.hasOwnProperty(engine)) { + return undefined; + } + if(themes[engine].hasOwnProperty(themeName)) { + return themes[engine][themeName].baseTheme; + } else { + return undefined; + } +}; + // const getTopBrews = (cb) => { // HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) { // cb(brews); @@ -19,6 +32,22 @@ const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js'); const MAX_TITLE_LENGTH = 100; +const splitTextStyleAndMetadata = (brew)=>{ + brew.text = brew.text.replaceAll('\r\n', '\n'); + if(brew.text.startsWith('```metadata')) { + const index = brew.text.indexOf('```\n\n'); + const metadataSection = brew.text.slice(12, index - 1); + const metadata = yaml.load(metadataSection); + Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])); + brew.text = brew.text.slice(index + 5); + } + if(brew.text.startsWith('```css')) { + const index = brew.text.indexOf('```\n\n'); + brew.style = brew.text.slice(7, index - 1); + brew.text = brew.text.slice(index + 5); + } +}; + const api = { homebrewApi : router, getId : (req)=>{ @@ -40,6 +69,7 @@ const api = { getBrew : (accessType, stubOnly = false)=>{ // Create middleware with the accessType passed in as part of the scope return async (req, res, next)=>{ + // Get relevant IDs for the brew const { id, googleId } = api.getId(req); @@ -205,65 +235,29 @@ const api = { res.status(200).send(saved); }, - getBrewTheme : async (req, res)=>{ - req.brew.text = req.brew.text.replaceAll('\r\n', '\n'); - if(req.brew.text.startsWith('```metadata')) { - const index = req.brew.text.indexOf('```\n\n'); - const metadataSection = req.brew.text.slice(12, index - 1); - const metadata = yaml.load(metadataSection); - Object.assign(req.brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])); - req.brew.text = req.brew.text.slice(index + 5); - } - if(req.brew.text.startsWith('```css')) { - const index = req.brew.text.indexOf('```\n\n'); - req.brew.style = req.brew.text.slice(7, index - 1); - req.brew.text = req.brew.text.slice(index + 5); - } - return res.status(200).send({ - parent : req.brew.theme, - theme : req.brew.style, - themeName : req.brew.title - }); - }, - getBrewThemeAsCSS : async (req, res)=>{ - req.brew.text = req.brew.text.replaceAll('\r\n', '\n'); - if(req.brew.text.startsWith('```metadata')) { - const index = req.brew.text.indexOf('```\n\n'); - const metadataSection = req.brew.text.slice(12, index - 1); - const metadata = yaml.load(metadataSection); - Object.assign(req.brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])); - req.brew.text = req.brew.text.slice(index + 5); - } - if(req.brew.text.startsWith('```css')) { - const index = req.brew.text.indexOf('```\n\n'); - req.brew.style = req.brew.text.slice(7, index - 1); - req.brew.text = req.brew.text.slice(index + 5); - } - if(res.hasOwnProperty('set')) { - res.set('Content-Type', 'text/css'); - } - return res.status(200).send(`// From Theme: ${req.brew.title}\n\n${req.brew.style}`); - }, getBrewThemeWithCSS : async (req, res)=>{ - req.brew.text = req.brew.text.replaceAll('\r\n', '\n'); - if(req.brew.text.startsWith('```metadata')) { - const index = req.brew.text.indexOf('```\n\n'); - const metadataSection = req.brew.text.slice(12, index - 1); - const metadata = yaml.load(metadataSection); - Object.assign(req.brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])); - req.brew.text = req.brew.text.slice(index + 5); - } - if(req.brew.text.startsWith('```css')) { - const index = req.brew.text.indexOf('```\n\n'); - req.brew.style = req.brew.text.slice(7, index - 1); - req.brew.text = req.brew.text.slice(index + 5); - } + const brew = req.brew; + splitTextStyleAndMetadata(brew); if(res.hasOwnProperty('set')) { res.set('Content-Type', 'text/css'); } - const parentThemeImport = `// From Theme: ${req.brew.title}\n\n@import /themes/${req.brew.renderer}/${req.brew.theme}/styles.css\n\n`; + const staticTheme = `/api/css/${req.brew.renderer}/${req.brew.theme}/styles.css`; + const userTheme = `/api/css/${req.brew.theme.slice(1)}`; + const parentThemeImport = `// From Theme: ${req.brew.title}\n\n@import ${req.brew.theme[0] != '#' ? staticTheme : userTheme}\n\n`; return res.status(200).send(`${req.brew.renderer == 'legacy' ? '' : parentThemeImport}${req.brew.style}`); }, + getStaticTheme : async(req, res)=>{ + const themeParent = isStaticTheme(req.params.engine, req.params.id); + if(themeParent === undefined){ + res.status(404).send(`Invalid Theme - Engine: ${req.params.engine}, Name: ${req.params.id}`); + } else { + if(res.hasOwnProperty('set')) { + res.set('Content-Type', 'text/css'); + } + const parentTheme = themeParent ? `@import /api/css/${req.params.engine}/${themeParent}\n` : ''; + return res.status(200).send(`${parentTheme}@import /themes/${req.params.engine}/${req.params.id}\n`); + } + }, updateBrew : async (req, res)=>{ // Initialize brew from request and body, destructure query params, and set the initial value for the after-save method const brewFromClient = api.excludePropsFromUpdate(req.body); @@ -424,9 +418,7 @@ router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew)); router.delete('/api/:id', asyncHandler(api.deleteBrew)); router.get('/api/remove/:id', asyncHandler(api.deleteBrew)); -router.get('/api/theme/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.getBrewTheme)); -router.get('/api/css/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.getBrewThemeAsCSS)); -router.get('/api/csstheme/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.getBrewThemeWithCSS)); - +router.get('/api/css/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.getBrewThemeWithCSS)); +router.get('/api/css/:engine/:id/', asyncHandler(api.getStaticTheme)); module.exports = api; diff --git a/server/homebrew.api.spec.js b/server/homebrew.api.spec.js index cc8ae8044..760e1ae75 100644 --- a/server/homebrew.api.spec.js +++ b/server/homebrew.api.spec.js @@ -569,64 +569,8 @@ brew`); }); }); - describe('getBrewTheme', ()=>{ - it('should collect parent theme and brew style', async ()=>{ - const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew })); - model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', style: 'I Have a style!' })); - const brewResults = { - authors : [], - createdAt : undefined, - description : '', - editId : undefined, - gDrive : false, - lang : 'en', - pageCount : 1, - published : false, - renderer : 'legacy', - shareId : undefined, - style : 'I Have a style!', - systems : [], - tags : [], - text : '', - theme : '5ePHB', - thumbnail : '', - title : 'test brew', - trashed : false, - updatedAt : undefined, - views : 0 - }; - const fn = api.getBrew('share', true); - const req = { brew: {} }; - const next = jest.fn(); - await fn(req, null, next); - - api.getBrewTheme(req, res); - const sent = res.send.mock.calls[0][0]; - expect(req.brew).toStrictEqual(brewResults); - expect(res.status).toHaveBeenCalledWith(200); - expect(sent.parent).toBe('5ePHB'); - expect(sent.theme).toBe('I Have a style!'); - }); - }); - - describe('getBrewAsThemeCSS', ()=>{ - it('should collect the brew style - returning as css', async ()=>{ - const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew })); - model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', style: 'I Have a style!' })); - const fn = api.getBrew('share', true); - const req = { brew: {} }; - const next = jest.fn(); - await fn(req, null, next); - - api.getBrewThemeAsCSS(req, res); - const sent = res.send.mock.calls[0][0]; - expect(sent).toBe('I Have a style!'); - expect(res.status).toHaveBeenCalledWith(200); - }); - }); - - describe('getBrewThemeWithCSS', ()=>{ - it('should collect parent theme and brew style - returning as css with parent imported.', async ()=>{ + describe('getBrewThemeWithStaticParent', ()=>{ + it('should collect parent theme and brew style - returning as css with static parent imported.', async ()=>{ const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew })); model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', renderer: 'V3', style: 'I Have a style!' })); const fn = api.getBrew('share', true); @@ -636,11 +580,67 @@ brew`); api.getBrewThemeWithCSS(req, res); const sent = res.send.mock.calls[0][0]; - expect(sent).toBe(`@import /themes/V3/5ePHB/styles.css\n\nI Have a style!`); + expect(sent).toBe(`// From Theme: test brew\n\n@import /api/css/V3/5ePHB/styles.css\n\nI Have a style!`); expect(res.status).toHaveBeenCalledWith(200); }); }); + describe('getBrewThemeWithUserParent', ()=>{ + it('should collect parent theme and brew style - returning as css with user-theme parent imported.', async ()=>{ + const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew })); + model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', renderer: 'V3', theme: '#IamATheme', style: 'I Have a style!' })); + const fn = api.getBrew('share', true); + const req = { brew: {} }; + const next = jest.fn(); + await fn(req, null, next); + + api.getBrewThemeWithCSS(req, res); + const sent = res.send.mock.calls[0][0]; + expect(sent).toBe(`// From Theme: test brew\n\n@import /api/css/IamATheme\n\nI Have a style!`); + expect(res.status).toHaveBeenCalledWith(200); + }); + }); + + describe('getStaticTheme', ()=>{ + it('should return an import of the theme without including a parent.', async ()=>{ + const req = { + params : { + engine : 'V3', + id : '5ePHB' + } + }; + api.getStaticTheme(req, res); + const sent = res.send.mock.calls[0][0]; + expect(sent).toBe('@import /themes/V3/5ePHB\n'); + expect(res.status).toHaveBeenCalledWith(200); + }); + it('should return an import of the theme including a parent.', async ()=>{ + const req = { + params : { + engine : 'V3', + id : '5eDMG' + } + }; + api.getStaticTheme(req, res); + const sent = res.send.mock.calls[0][0]; + expect(sent).toBe('@import /api/css/V3/5ePHB\n@import /themes/V3/5eDMG\n'); + expect(res.status).toHaveBeenCalledWith(200); + }); + it('should fail for an invalid static theme.', async()=>{ + const req = { + params : { + engine : 'V3', + id : '5eDMGGGG' + } + }; + api.getStaticTheme(req, res); + const sent = res.send.mock.calls[0][0]; + expect(sent).toBe('Invalid Theme - Engine: V3, Name: 5eDMGGGG'); + expect(res.status).toHaveBeenCalledWith(404); + }); + }); + + describe('deleteBrew', ()=>{ it('should handle case where fetching the brew returns an error', async ()=>{ api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });