homebrewery icon indicating copy to clipboard operation
homebrewery copied to clipboard

Add route to get brew styling

Open G-Ambatte opened this issue 1 year ago • 1 comments

This PR resolves #1097.

This PR adds the /css route, which returns the stylesheet for a given brew share ID.

G-Ambatte avatar Oct 01 '23 08:10 G-Ambatte

What is this branch blocked by?

5e-Cleric avatar May 18 '24 11:05 5e-Cleric

The User Themes PR #3321 implemented a recursive form of this (retrieve CSS, and also @include CSS from parent theme, which calls it's parent theme, etc.), but that has since been overridden with a dedicated /api/themes endpoint that gets both CSS and any associated snippets (with potential for retrieving other theme-related properties) in a bundle, including from all theme parents.

However, it has been requested that we still implement a CSS-only endpoint but just for individual brews, not recursively, which leads us back here.

I'm going to copy here the recursive implementation from #3321 along with its test cases, as I think there is still some bits that might be helpful in finishing out this PR:

CSS Retrieval
	//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)}/`;

		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.renderer, req.params.id))
			res.status(404).send(`Invalid Theme - Renderer: ${req.params.renderer}, 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.renderer][req.params.id].baseTheme;
			const parentThemeImport = themeParent ? `@import url(\"/css/${req.params.renderer}/${themeParent}\");\n/* Static Theme ${Themes[req.params.renderer][themeParent].name} */\n` : '';
			return res.status(200).send(`${parentThemeImport}@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");\n/* Static Theme ${Themes[req.params.renderer][req.params.id].name} */\n`);
		}
	},
Testing
	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', theme: '5eDMG', shareId: 'iAmAUserTheme', style: 'I Have a style!' }));
			const fn = api.getBrew('share', true);
			const req = { brew: {}, get: ()=>{return 'localhost';}, protocol: 'https' };
			const next = jest.fn();
			await fn(req, null, next);

			api.getBrewThemeCSS(req, res);
			const sent = res.send.mock.calls[0][0];
			expect(sent).toBe(`@import url("/css/V3/5eDMG");\n\n/* From Brew: https://localhost/share/iAmAUserTheme */\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',  shareId: 'iAmAUserTheme', theme: 'IamATheme', style: 'I Have a style!' }));
			const fn = api.getBrew('share', true);
			const req = { brew: {}, get: ()=>{return 'localhost';}, protocol: 'https' };
			const next = jest.fn();
			await fn(req, null, next);

			api.getBrewThemeCSS(req, res);
			const sent = res.send.mock.calls[0][0];
			expect(sent).toBe(`@import url("/css/IamATheme");\n\n/* From Brew: https://localhost/share/iAmAUserTheme */\n\nI Have a style!`);
			expect(res.status).toHaveBeenCalledWith(200);
		});
	});

	describe('getStaticThemeCSS', ()=>{
		it('should return an import of the theme including a parent.', async ()=>{
			const req = {
				params : {
					renderer : 'V3',
					id       : '5eDMG'
				}
			};
			api.getStaticThemeCSS(req, res);
			const sent = res.send.mock.calls[0][0];
			expect(sent).toBe('@import url("/css/V3/5ePHB");\n/* Static Theme 5e PHB */\n@import url("/themes/V3/5eDMG/style.css");\n/* Static Theme 5e DMG */\n');
			expect(res.status).toHaveBeenCalledWith(200);
		});
		it('should fail for an invalid static theme.', async()=>{
			const req = {
				params : {
					renderer : 'V3',
					id       : '5eDMGGGG'
				}
			};
			api.getStaticThemeCSS(req, res);
			const sent = res.send.mock.calls[0][0];
			expect(sent).toBe('Invalid Theme - Renderer: V3, Name: 5eDMGGGG');
			expect(res.status).toHaveBeenCalledWith(404);
		});
	});

calculuschild avatar Jul 28 '24 21:07 calculuschild

You can functionally get the present theme with the bundle endpoint as they are ordered.

dbolack-ab avatar Jul 28 '24 22:07 dbolack-ab

@G-Ambatte is there something left to do here?

5e-Cleric avatar Aug 23 '24 11:08 5e-Cleric