0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-24 01:13:15 +00:00

Compare commits

..

147 Commits

Author SHA1 Message Date
Trevor Buckner
8609925da8 Merge branch 'master' into v3.14.0 2024-07-29 21:54:38 -04:00
Trevor Buckner
607244d6e1 Merge pull request #3321 from dbolack-ab/brew_themes_user_selection
Enable User Brew theme selection
2024-07-29 21:54:28 -04:00
Trevor Buckner
9cc81d2ff9 Up to v3.14.0 2024-07-29 21:52:09 -04:00
Trevor Buckner
32fa50d608 Fallback to showing "Blank" theme if themes fail to load. 2024-07-29 12:30:13 -04:00
Trevor Buckner
8221579b6a Linting 2024-07-28 18:03:25 -04:00
Trevor Buckner
88eaebfd49 Raise test coverage threshold
This PR adds tests which means we are now covering a larger % of the codebase. Raise the coverage thresholds to match.
2024-07-28 18:00:33 -04:00
Trevor Buckner
ee9f2c8c83 Remove unused CSS endpoints in favor of #3075
Now that we have a dedicated /theme/ route for the recursive theming, the CSS endpoint can be simpler for only getting the `style` of a single brew. #3075 already has this simpler version, but no testing, so I have copied this into a comment there for implementation when it is ready.
2024-07-28 17:53:25 -04:00
Trevor Buckner
2870caaae6 Clean up metadataEditor theme dropdown 2024-07-28 17:18:30 -04:00
Trevor Buckner
e0425ec6c0 Simplify API call url 2024-07-28 16:47:16 -04:00
Trevor Buckner
8aa88a2e45 Add proper error popup when theme fails to load 2024-07-28 16:45:01 -04:00
Trevor Buckner
edec9369ec Finish adding test cases 2024-07-27 19:17:19 -04:00
Trevor Buckner
f2d933410e Add error handling for missing themes 2024-07-27 19:17:05 -04:00
Trevor Buckner
b64a0c5200 Start adding tests for /theme/ endpoint 2024-07-27 03:30:51 -04:00
Trevor Buckner
113f9b3fe3 No need to stringify Theme Bundle object 2024-07-27 02:00:38 -04:00
David Bolack
d2afa7adea Move fetchThemeBundle into /shared/helpers
This might not be the best rework - I was unsure if the *this* that would be available when called would see the appropriate object so I assumed not and pass it as a parameter.
Works, but may be bad form.
2024-07-23 22:17:52 -05:00
Trevor Buckner
8e7baca47d Fix tests 2024-07-23 17:40:32 -04:00
Trevor Buckner
ddc5693778 revert package-lock 2024-07-23 17:31:07 -04:00
Trevor Buckner
82f73fb21d cleanup 2024-07-23 17:24:50 -04:00
Trevor Buckner
27c52fc244 Fix loading CSS for Legacy 2024-07-23 17:11:48 -04:00
Trevor Buckner
ac82e3ecb2 Add to home page 2024-07-23 16:50:29 -04:00
Trevor Buckner
22b6aa14f0 Add to /new page 2024-07-23 16:43:23 -04:00
Trevor Buckner
24ab3d3392 Merge branch 'brew_themes_user_selection' of https://github.com/dbolack-ab/homebrewery into pr/3321 2024-07-23 16:26:35 -04:00
Trevor Buckner
0b01f27d11 Load theme bundles on /share page 2024-07-23 16:26:33 -04:00
Víctor Losada Hernández
270aa9e0f9 Merge branch 'master' into brew_themes_user_selection 2024-07-22 22:46:12 +02:00
Trevor Buckner
6ae249a527 Lint 2024-07-22 02:46:26 -04:00
Trevor Buckner
c0123b96eb Support snippet compilation
Original handling of snippets only worked if the current selected theme was a staticTheme. This now fully merges all snippets through the theme chain no matter what the top-level theme is. So user themes built on 5ePHB can benefit from 5ePHB snippets too.

User input of user snippets will be a later PR, but merging them into static snippets is now supported.
2024-07-22 02:44:41 -04:00
Trevor Buckner
45f7080afd Move loadAllBrewStylesAndSnippets to the parent page component
Themes contain both CSS and Snippets. The brewRenderer only cares about the CSS, but other components need the Snippets. Better to have the parent "editPage", etc. load the theme bundles and pass them down to each child that needs it, rather than trying to pass from the child up.

This also fixes the `metadataEditor.jsx` not being able to change themes live; A new theme bundle is now loaded when a new theme is selected, instead of only the first time the BrewRenderer mounts.

Also renamed to "fetchThemeBundle"
2024-07-21 16:25:24 -04:00
Trevor Buckner
0a5ff213de use same theme endpoint for user and static themes
`getThemeBundle()` rework no longer needs two separate endpoints
2024-07-20 11:39:23 -04:00
Trevor Buckner
f364f054f8 restore renderStyle
`renderStyle` is still necessary; it allows us to update the style live in the component render step as the user types into the style tab. Otherwise the style is only rendered once and never updates.

React also discourages directly editing the DOM ourselves, because it makes changes to the DOM that react cannot track; we should aim to provide all DOM writes inside of the component render function instead of using `document.createElement`, etc.

Too that end, this commit reduces the `loadAllStylesAndSnippets` function to just fetch and parse the data; actual rendering is moved back to `renderStyle()`
2024-07-19 01:33:56 -04:00
Trevor Buckner
460358ce1f Simplify some logic 2024-07-19 00:09:21 -04:00
Trevor Buckner
0448f15322 Classify user brews as V3 if they use V3
Each theme in the theme chain, including user brews, must use the same renderer. When moving to V4 or future versions, it will be important to distinguish which themes are compatible with each other
2024-07-19 00:05:45 -04:00
Trevor Buckner
d741878f78 Also remove userthemes from Brew object in sharePage 2024-07-19 00:00:06 -04:00
Trevor Buckner
d22cd88446 fix crash in metadataeditor 2024-07-15 23:47:19 -04:00
Trevor Buckner
1444581c86 pass userThemes prop to Editor -> MetadataEditor 2024-07-15 23:44:07 -04:00
Trevor Buckner
dfbd85a8ce pass userThemes as a new prop, rather than inside of the brew 2024-07-15 23:29:16 -04:00
Trevor Buckner
af5434c9b7 cleanup 2024-07-15 16:45:55 -04:00
Trevor Buckner
484b0a6dff simplify getThemeBundle() by using just one loop
Also, removes need for special handling of the "first" theme.
2024-07-15 16:38:19 -04:00
Trevor Buckner
4951b9bf1a Add async error handler to /edit and /new
Since /edit and /new endpoints now have an `await` inside that could return an error (`getUsersBrewThemes()`), asyncHandler must be added to pass errors along instead of just crashing
2024-07-13 19:46:12 -04:00
Trevor Buckner
62c619de24 userThemes need not be nested inside a Brew object 2024-07-13 19:38:51 -04:00
Trevor Buckner
44c96aad04 spacing 2024-07-13 18:11:04 -04:00
Trevor Buckner
f392216ff4 Spacing 2024-07-13 18:08:29 -04:00
Trevor Buckner
591cae0e8f more renaming engine to renderer 2024-07-13 18:08:00 -04:00
Trevor Buckner
e222811d03 Rename engine to renderer to unify naming
This value is named `renderer` everywhere else. Relabeling to a consistent name.
2024-07-13 18:06:46 -04:00
Trevor Buckner
c9b885f868 include theme as baseTheme when getting user brew themes
`baseTheme` for a user brew theme is just the `theme` value of that brew.
2024-07-13 18:01:50 -04:00
Trevor Buckner
47f912750b Extract getting userThemes from getBrew()
`getBrew()` should do one thing only; retrieve a brew. UI elements like the list of themes available to the user are not part of a brew.

Moved into the handers for the `/edit/` and `/new/` endpoints
2024-07-13 17:44:23 -04:00
Trevor Buckner
f29a5e346e Remove id parameter from getUsersBrewThemes
Filtering out the current brew can be done later as needed; certain situations may call for retrieving the whole list.
2024-07-13 17:35:19 -04:00
Trevor Buckner
ee381c91fe Simplify getUserBrewThemes function a bit 2024-07-13 17:26:38 -04:00
Trevor Buckner
5f8d46f1b6 Reuse splitTextStyleAndMetadata from helpers.js 2024-07-13 17:09:45 -04:00
David Bolack
ade819c70c 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.
2024-07-13 12:12:05 -05:00
Trevor Buckner
2fa3c0f311 themeClass is never used 2024-07-11 00:26:50 -04:00
Trevor Buckner
5c0a072115 userThemes passed to SnippetBar.jsx is never used 2024-07-11 00:25:11 -04:00
Trevor Buckner
29c2274a19 Unify some variable naming 2024-07-10 18:54:45 -04:00
Trevor Buckner
a6f787ea8f Remove getBrewThemeParentCSS 2024-07-10 17:56:39 -04:00
Trevor Buckner
24c86dd199 Remove unused test 2024-07-10 17:49:57 -04:00
Trevor Buckner
7eb96ee6be Simplify brewRenderer output to only emit current theme
Instead of Blank, Parent, and Theme, just make use of the @include chaining, to handle all parent themes down to and including Blank
2024-07-10 17:46:51 -04:00
Trevor Buckner
27aebf0e3b Give 5ePHB and Journal themes a baseTheme of "Blank" 2024-07-10 17:15:45 -04:00
Trevor Buckner
88578a3d16 Fix failing test 2024-07-10 14:22:42 -04:00
Trevor Buckner
28446d3ae2 Comments for theme CSS endpoints 2024-07-10 14:21:23 -04:00
Trevor Buckner
a247e50c9f renaming "get" functions
rename `getStaticTheme` to `getStaticThemeCSS`
rename `getBrewThemeWithCSS` to `getBrewThemeCSS`
rename `getBrewThemeParent` to `getBrewThemeParentCSS`

to avoid confusion with other "get" endpoints like `getBrew`, and unify naming for endpoint functions that return CSS.

Simplify `isStaticTheme` function (getting the parent theme is handled elsewhere)
2024-07-10 14:15:03 -04:00
David Bolack
656edb07ea Rework detection of user brews to look up themeid in static themes list before assuming is a user brew.
Ended up being a fairly straightforward change. A few ternaries got smooshed or inverted. Passes builtin and local tests. Need to compare on the test instance.
2024-07-08 18:12:58 -05:00
David Bolack
ea6595d4d6 Merge branch 'master' into brew_themes_user_selection
Fixes a regression for legacy brews.
2024-07-07 12:03:15 -05:00
David Bolack
16ca52756d Merge branch 'master' into brew_themes_user_selection 2024-07-05 16:55:14 -05:00
David Bolack
645da7ae5f Merge branch 'brew_themes_user_selection' of github.com:dbolack-ab/homebrewery into brew_themes_user_selection 2024-07-05 16:54:11 -05:00
David Bolack
8570335d79 Consolidate variable redundancy. 2024-07-05 16:53:21 -05:00
Trevor Buckner
e4bde91f6a Merge branch 'master' into brew_themes_user_selection 2024-07-02 12:04:17 -04:00
David Bolack
ba76c51da7 Merge branch 'master' into brew_themes_user_selection 2024-06-19 19:32:04 -05:00
David Bolack
7a349ae26d Remove weirdly redundant error box. 2024-06-13 18:13:32 -05:00
David Bolack
0945a5e47e Merge branch 'master' into brew_themes_user_selection 2024-06-13 15:15:30 -05:00
David Bolack
6464f35ce9 Forgot to run npm install after merge 2024-05-31 22:40:22 -05:00
David Bolack
5442f232d5 Merge branch 'master' into brew_themes_user_selection 2024-05-31 22:32:14 -05:00
David Bolack
54d2709d6a Merge 2024-05-20 17:58:22 -05:00
David Bolack
916bd5f4d6 Merge branch 'master' into brew_themes_user_selection 2024-05-20 17:56:21 -05:00
David Bolack
c6f62142e1 Change the ID used for User Brews to the shareId for future-proofing. 2024-05-17 20:53:06 -05:00
David Bolack
69f01b282a CSS Tweaks for Theme Selector
Add 5e-Cleric's suggestsions to acvoid the title overflowing over the preview.
2024-05-13 22:33:58 -05:00
David Bolack
66e39d9c65 Update Theme Selector display
For User/Brew Themes, display the first author instead of Brew/V3 in the first column.
2024-05-13 22:24:41 -05:00
David Bolack
8c5f4e0605 Brew Theme Fixes.
This adds the User Brew themes, where applicible, to the /new path.

This adds a semi-graceful failure to the metadata panel when a Brew Theme is declared as used but is not present.

More gracefully handles loading with themes not present.
2024-05-13 11:14:35 -05:00
David Bolack
ed210da4af Merge branch 'master' into brew_themes_user_selection 2024-05-12 12:08:16 -05:00
David Bolack
b6c2f96b82 Change tag filtering for theme detection to require meta prefix 2024-05-10 01:40:01 -04:00
David Bolack
6f7a657b59 Merge branch 'brew_themes_user_selection' of github.com:dbolack-ab/homebrewery into brew_themes_user_selection 2024-05-08 12:52:01 -05:00
David Bolack
65495b4e7c Prevent Legacy renderer brews from being listed as themes. 2024-05-08 12:51:10 -05:00
David Bolack
07ca134d98 Merge pull request #1 from Gazook89/dropdownTextures-User-Themes
Add dropdownTexture for user theme options
2024-05-07 16:39:13 -05:00
David Bolack
dde8e28d07 Merge branch 'brew_themes_user_selection' into dropdownTextures-User-Themes 2024-05-07 16:38:56 -05:00
David Bolack
872ee339da Clean up console logs
Eliminate erroronous theme pulldown texture load.
2024-05-07 12:13:57 -05:00
Gazook89
295fea7581 Add dropdownTexture for user theme options
If a user theme document has a thumbnail, this will include that thumbnail as a dropdown texture in the options.
2024-05-07 11:00:20 -05:00
David Bolack
f936b8b12b Update User brew endpoint tests 2024-05-06 20:28:46 -05:00
David Bolack
9f04c34b06 Missed $ 2024-05-06 20:07:33 -05:00
David Bolack
c9d416fec0 Small User Brew theme changes.
Move the Static Theme shortcut to getBrewThemeWithCSS to drop an unneeded URL load.

Change the comment in the CSS to refer to the shareURL for the theme
instead of its name if it is a user theme.
2024-05-06 19:39:27 -05:00
David Bolack
3dde6a098c Test code to reduce duplicate theme loading
This shorts out loading of 5ePHB and/or blank from getBrewThemeParent
2024-05-06 12:12:46 -05:00
David Bolack
ef25139ffe Merge branch 'master' into brew_themes_user_selection 2024-05-06 12:04:38 -05:00
David Bolack
88ccb955ce Merge branch 'master' into brew_themes_user_selection 2024-04-30 20:11:32 -05:00
David Bolack
980ed8e265 Merge branch 'master' into brew_themes_user_selection 2024-04-20 21:55:59 -05:00
David Bolack
1292d9ad9b Merge branch 'master' into brew_themes_user_selection 2024-04-18 21:13:01 -05:00
David Bolack
57f0aefbc8 Merge branch 'master' into brew_themes_user_selection 2024-03-25 20:38:28 -05:00
David Bolack
f2f32c35ea Ensure shared pages work with user themes. 2024-03-10 11:54:48 -05:00
David Bolack
d4770f16e3 Merge branch 'master' into brew_themes_user_selection 2024-03-09 20:25:24 -06:00
David Bolack
e487f9a951 Fix remaining jest issues 2024-03-06 23:27:43 -06:00
David Bolack
eb4ecf853b Fix Jest issues I was able to understand 2024-03-06 22:50:24 -06:00
David Bolack
54e2deaddc Merge branch 'master' into brew_themes_user_selection 2024-03-06 19:28:37 -06:00
David Bolack
1ac510af3d Heroku debug 2024-03-06 19:24:16 -06:00
David Bolack
a666c8def3 Heroku debug 2024-03-06 19:20:16 -06:00
David Bolack
33933ef212 Attempted fix on access 2024-03-06 19:14:16 -06:00
David Bolack
d9dade7181 Fix User Brew display label in metadata editor 2024-03-06 19:04:12 -06:00
David Bolack
87502f4249 Heavy rework for usertheme parents. 2024-03-06 18:55:12 -06:00
David Bolack
9adafbd473 more heroku debug 2024-03-06 14:12:13 -06:00
David Bolack
47ea2f6ed7 more heroku debug 2024-03-06 14:04:16 -06:00
David Bolack
e2ba0ec059 more heroku debug 2024-03-06 13:59:36 -06:00
David Bolack
870cbc103d more heroku debug 2024-03-06 13:57:32 -06:00
David Bolack
dfca664f6e more heroku debug 2024-03-06 13:53:54 -06:00
David Bolack
00cfd427b1 more heroku debug 2024-03-06 13:47:40 -06:00
David Bolack
e639a32822 more heroku debug 2024-03-06 13:41:58 -06:00
David Bolack
8765bc800d more heroku debug 2024-03-06 13:38:48 -06:00
David Bolack
1dc73a951e more heroku debug 2024-03-06 13:35:08 -06:00
David Bolack
317b80bf4d more heroku debug 2024-03-06 13:20:57 -06:00
David Bolack
2aaae95e89 more heroku debug 2024-03-06 13:16:57 -06:00
David Bolack
0580e45af9 more heroku debug 2024-03-06 13:11:49 -06:00
David Bolack
0dbf6453ac more heroku debug 2024-03-06 13:04:03 -06:00
David Bolack
695324832c more heroku debug 2024-03-06 12:56:33 -06:00
David Bolack
ac4c84e7a4 config file fix 2024-03-06 12:44:20 -06:00
David Bolack
18aa453bb0 Rearrange and leverage getBrew 2024-03-06 12:38:00 -06:00
David Bolack
17f78169f2 More debug 2024-03-06 12:03:27 -06:00
David Bolack
6f6a06c8c3 More debug 2024-03-06 11:00:43 -06:00
David Bolack
79a4291153 More debug 2024-03-06 10:37:10 -06:00
David Bolack
a54fb98d4e Additional debugging 2024-03-06 10:28:09 -06:00
David Bolack
a3549ae694 Merge branch 'master' into brew_themes_user_selection 2024-03-06 09:35:10 -06:00
David Bolack
42c441f534 Merge branch 'master' into brew_themes_user_selection 2024-03-04 16:54:13 -06:00
David Bolack
a924f53320 Merge branch 'master' into brew_themes_user_selection 2024-03-03 22:11:35 -06:00
David Bolack
4f90f92b38 Additional theme based error checking. 2024-02-28 15:08:00 -06:00
David Bolack
753b3befad Fix issue with empty theme ( /faq ) 2024-02-28 14:53:40 -06:00
David Bolack
544bc9bd01 Catch bad assumption in unlogged saves 2024-02-28 08:32:15 -06:00
David Bolack
1a467565c1 Merge branch 'master' into brew_themes_user_selection 2024-02-27 21:32:33 -06:00
David Bolack
562daf9b04 Handle some statics 2024-02-27 21:13:22 -06:00
David Bolack
8f15887c03 Cleanup of console logging 2024-02-27 20:51:59 -06:00
David Bolack
7384cdc241 My god it works 2024-02-27 20:20:43 -06:00
David Bolack
56851f2c2d Edit working - with noise. 2024-02-27 19:33:33 -06:00
David Bolack
50c9d95ce0 WIP trying to debug theme selection. 2024-02-27 17:30:14 -06:00
David Bolack
4f4659b0e2 Cleaned up noise in homebrew.api.js 2024-02-27 13:57:58 -06:00
David Bolack
7b3a1eb4ff Functional user theme loading though noising console 2024-02-27 13:41:51 -06:00
David Bolack
2456432844 Exclude self from brew themes list to prevent circular ref. 2024-02-23 17:12:54 -06:00
David Bolack
3e66647f9f Fix @import loading on Chrome. 2024-02-23 14:43:29 -06:00
David Bolack
6d6571be0b Merge branch 'master' into brew_themes_user_selection 2024-02-23 13:46:56 -06:00
David Bolack
f9307986cd WIP
@import statements are just not working. Uploaded for other eyes.
2024-02-22 23:06:40 -06:00
David Bolack
f60090e5fa Update Theme Picker to use Brews tagged as a theme
This updates the theme picker to include brews tagged as themes owned by
the user.

Some supporting functions were updated. User themes are loaded on /edit
and added to the request.
2024-02-22 21:12:56 -06:00
David Bolack
ae2bb3a028 Add missing style.css 2024-02-20 23:26:09 -06:00
David Bolack
c319d6bcfa Consolidate and add theme parent walking
This consolidates the style/theme endpoint to a singular method, adds
interpretation of static themes, and allow parent theme recursion.

I am not 100% sure this will order styles correctly.
2024-02-20 23:15:37 -06:00
David Bolack
e2ef9b8122 Report Theme title with CSS
This adds a comment/field ( depending on endpoint ) that reports the
name of the Brew being used as a theming source.
2024-02-20 16:44:17 -06:00
David Bolack
8e48df5de1 Partial Code coverage for new endpoints 2024-02-18 12:45:14 -06:00
David Bolack
a3b1d7fb7c Use a brew as a theme, three ways.
This has been implemented three different ways to allow for comparison
and discussion

- /api/css/:id : This returns the style frontmatter of the referenced
  document as a text/css document.
/api/theme/:id : This returns an object with the reference'd object's
theme and style frontmatter.
/api/csstheme/:id : This returns the stylye frontmatter of the
referenced document as a text/css document and adds the theme as an
@import ( if not using the legacy renderer )
2024-02-17 11:01:21 -06:00
28 changed files with 466 additions and 172 deletions

View File

@@ -84,6 +84,34 @@ pre {
## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Monday 7/29/2024 - v3.14.0
{{taskList
##### abquintic, calculuschild
* [x] Alternative Brew Themes, including importing other brews as a base theme.
- In the :fas_circle_info: **Properties** menu, find the new {{openSans **THEME**}} dropdown. It lists Brew Themes, including a new **Blank** theme as a simpler basis for custom styling.
- Brews tagged with `meta:theme` will appear in the Brew Themes list. Selecting one loads its :fas_paintbrush: **Style** tab contents as the CSS basis for the current brew, allowing one brew to style multiple documents.
- Brews with `meta:theme` can also select their own Theme, i.e. layering Themes on top of each other.
- The next goal is to make **Published** Themes shareable between users.
Fixes issues [#1899](https://github.com/naturalcrit/homebrewery/issues/1899), [#3085](https://github.com/naturalcrit/homebrewery/issues/3085)
##### G-Ambatte
* [x] Fix Drop-cap font becoming corrupted when Bold
Fixes issues [#3551](https://github.com/naturalcrit/homebrewery/issues/3551)
* [x] Fixes to UI styling
Fixes issues [#3568](https://github.com/naturalcrit/homebrewery/issues/3568)
}}
### Saturday 6/7/2024 - v3.13.1
{{taskList
@@ -131,8 +159,6 @@ Fixes issue [#3298](https://github.com/naturalcrit/homebrewery/issues/3298)
Fixes issue [#3397](https://github.com/naturalcrit/homebrewery/issues/3397)
}}
\column
### Monday 18/3/2024 - v3.12.0
{{taskList

View File

@@ -18,8 +18,6 @@ const { printCurrentBrew } = require('../../../shared/helpers.js');
const DOMPurify = require('dompurify');
const purifyConfig = { FORCE_BODY: true, SANITIZE_DOM: false };
const Themes = require('themes/themes.json');
const PAGE_HEIGHT = 1056;
const INITIAL_CONTENT = dedent`
@@ -57,6 +55,7 @@ const BrewRenderer = (props)=>{
lang : '',
errors : [],
currentEditorPage : 0,
themeBundle : {},
...props
};
@@ -125,10 +124,9 @@ const BrewRenderer = (props)=>{
};
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 themeStyles = props.themeBundle?.joinedStyles ?? '<style>@import url("/themes/V3/Blank/style.css");</style>';
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `${themeStyles} \n\n <style> ${cleanStyle} </style>` }} />;
};
const renderPage = (pageText, index)=>{
@@ -188,10 +186,6 @@ const BrewRenderer = (props)=>{
document.dispatchEvent(new MouseEvent('click'));
};
const rendererPath = props.renderer == 'V3' ? 'V3' : 'Legacy';
const themePath = props.theme ?? '5ePHB';
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
return (
<>
{/*render dummy page while iFrame is mounting.*/}
@@ -220,13 +214,6 @@ const BrewRenderer = (props)=>{
onKeyDown={handleControlKeys}
tabIndex={-1}
style={{ height: state.height }}>
<link href={`/themes/${rendererPath}/Blank/style.css`} type='text/css' rel='stylesheet'/>
{baseThemePath &&
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type='text/css' rel='stylesheet'/>
}
<link href={`/themes/${rendererPath}/${themePath}/style.css`} type='text/css' rel='stylesheet'/>
{/* Apply CSS from Style tab and render pages from Markdown tab */}
{state.isMounted
&&

View File

@@ -367,7 +367,7 @@ const Editor = createClass({
view={this.state.view}
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onStyleChange}
enableFolding={true}
enableFolding={false}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} />
</>;
@@ -381,7 +381,8 @@ const Editor = createClass({
<MetadataEditor
metadata={this.props.brew}
onChange={this.props.onMetaChange}
reportError={this.props.reportError}/>
reportError={this.props.reportError}
userThemes={this.props.userThemes}/>
</>;
}
},
@@ -424,6 +425,7 @@ const Editor = createClass({
historySize={this.historySize()}
currentEditorTheme={this.state.editorTheme}
updateEditorTheme={this.updateEditorTheme}
snippetBundle={this.props.snippetBundle}
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} />
{this.renderEditor()}

View File

@@ -8,6 +8,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
const Combobox = require('client/components/combobox.jsx');
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
const Themes = require('themes/themes.json');
const validations = require('./validations.js');
@@ -98,7 +99,7 @@ const MetadataEditor = createClass({
if(renderer == 'legacy')
this.props.metadata.theme = '5ePHB';
}
this.props.onChange(this.props.metadata);
this.props.onChange(this.props.metadata, 'renderer');
},
handlePublish : function(val){
this.props.onChange({
@@ -110,7 +111,7 @@ const MetadataEditor = createClass({
handleTheme : function(theme){
this.props.metadata.renderer = theme.renderer;
this.props.metadata.theme = theme.path;
this.props.onChange(this.props.metadata);
this.props.onChange(this.props.metadata, 'theme');
},
handleLanguage : function(languageCode){
@@ -191,37 +192,41 @@ const MetadataEditor = createClass({
renderThemeDropdown : function(){
if(!global.enable_themes) return;
const mergedThemes = _.merge(Themes, this.props.userThemes);
const listThemes = (renderer)=>{
return _.map(_.values(Themes[renderer]), (theme)=>{
return <div className='item' key={''} onClick={()=>this.handleTheme(theme)} title={''}>
{`${theme.renderer} : ${theme.name}`}
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`}/>
return _.map(_.values(mergedThemes[renderer]), (theme)=>{
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`;
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`;
return <div className='item' key={`${renderer}_${theme.name}`} onClick={()=>this.handleTheme(theme)} title={''}>
{theme.author ?? renderer} : {theme.name}
<div className='texture-container'>
<img src={texture}/>
</div>
<div className='preview'>
<h6>{`${theme.name}`} preview</h6>
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`}/>
<h6>{theme.name} preview</h6>
<img src={preview}/>
</div>
</div>;
});
};
const currentTheme = Themes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme];
const currentRenderer = this.props.metadata.renderer;
const currentTheme = mergedThemes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme]
?? { name: `!!! THEME MISSING !!! ID=${this.props.metadata.theme}` };
let dropdown;
if(this.props.metadata.renderer == 'legacy') {
if(currentRenderer == 'legacy') {
dropdown =
<Nav.dropdown className='disabled value' trigger='disabled'>
<div>
{`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i>
</div>
<div> {`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i> </div>
</Nav.dropdown>;
} else {
dropdown =
<Nav.dropdown className='value' trigger='click'>
<div>
{`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`} <i className='fas fa-caret-down'></i>
</div>
{/*listThemes('Legacy')*/}
{listThemes('V3')}
<div> {currentTheme.author ?? _.upperFirst(currentRenderer)} : {currentTheme.name} <i className='fas fa-caret-down'></i> </div>
{listThemes(currentRenderer)}
</Nav.dropdown>;
}

View File

@@ -191,6 +191,13 @@
color : white;
}
}
.navDropdown .item > p {
width: 45%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
height: 1.1em;
}
.navDropdown {
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
position : absolute;
@@ -230,14 +237,23 @@
&:hover > .preview {
opacity: 1;
}
>img {
mask-image : linear-gradient(90deg, transparent, black 20%);
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
position : absolute;
right : 0;
top : 0px;
width : 50%;
height : 100%;
.texture-container {
position: absolute;
width: 100%;
height: 100%;
min-height: 100%;
top: 0;
left: 0;
overflow: hidden;
> img {
mask-image : linear-gradient(90deg, transparent, black 20%);
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
position : absolute;
right : 0;
top : 0px;
width : 50%;
min-height : 100%;
}
}
}
}

View File

@@ -6,9 +6,6 @@ const _ = require('lodash');
const cx = require('classnames');
//Import all themes
const Themes = require('themes/themes.json');
const ThemeSnippets = {};
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
ThemeSnippets['V3_5ePHB'] = require('themes/V3/5ePHB/snippets.js');
@@ -40,7 +37,8 @@ const Snippetbar = createClass({
foldCode : ()=>{},
unfoldCode : ()=>{},
updateEditorTheme : ()=>{},
cursorPos : {}
cursorPos : {},
snippetBundle : []
};
},
@@ -53,21 +51,15 @@ const Snippetbar = createClass({
},
componentDidMount : async function() {
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
const themePath = this.props.theme ?? '5ePHB';
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
snippets = this.compileSnippets(rendererPath, themePath, snippets);
const snippets = this.compileSnippets();
this.setState({
snippets : snippets
});
},
componentDidUpdate : async function(prevProps) {
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme) {
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
const themePath = this.props.theme ?? '5ePHB';
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
snippets = this.compileSnippets(rendererPath, themePath, snippets);
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
const snippets = this.compileSnippets();
this.setState({
snippets : snippets
});
@@ -75,26 +67,26 @@ const Snippetbar = createClass({
},
mergeCustomizer : function(valueA, valueB, key) {
mergeCustomizer : function(oldValue, newValue, key) {
if(key == 'snippets') {
const result = _.reverse(_.unionBy(_.reverse(valueB), _.reverse(valueA), 'name')); // Join snippets together, with preference for the current theme over the base theme
const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme
return _.filter(result, 'gen'); //Only keep snippets with a 'gen' property.
}
},
compileSnippets : function(rendererPath, themePath, snippets) {
let compiledSnippets = snippets;
const baseSnippetsPath = Themes[rendererPath][themePath].baseSnippets;
compileSnippets : function() {
let compiledSnippets = [];
const objB = _.keyBy(compiledSnippets, 'groupName');
let oldSnippets = _.keyBy(compiledSnippets, 'groupName');
if(baseSnippetsPath) {
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_${baseSnippetsPath}`]), 'groupName');
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
compiledSnippets = this.compileSnippets(rendererPath, baseSnippetsPath, _.cloneDeep(compiledSnippets));
} else {
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_Blank`]), 'groupName');
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
for (let snippets of this.props.snippetBundle) {
if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name
snippets = ThemeSnippets[snippets];
const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName');
compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer));
oldSnippets = _.keyBy(compiledSnippets, 'groupName');
}
return compiledSnippets;
},

View File

@@ -66,10 +66,10 @@ const Homebrew = createClass({
<Router location={this.props.url}>
<div className='homebrew'>
<Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} />} />
<Route path='/new' element={<WithRoute el={NewPage}/>} />
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
<Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } />
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />

View File

@@ -104,6 +104,18 @@ const ErrorNavItem = createClass({
</Nav.item>;
}
if(HBErrorCode === '09') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
Looks like there was a problem retreiving
the theme, or a theme that it inherits,
for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
{response.body.brewId}</a> still exists!
</div>
</Nav.item>;
}
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer'>

View File

@@ -21,6 +21,9 @@
font-size : 10px;
font-weight : 800;
text-transform : uppercase;
.lowercase {
text-transform : none;
}
a{
color : @teal;
}

View File

@@ -25,7 +25,7 @@ const LockNotification = require('./lockNotification/lockNotification.jsx');
const Markdown = require('naturalcrit/markdown.js');
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
const { printCurrentBrew } = require('../../../../shared/helpers.js');
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
const googleDriveIcon = require('../../googleDrive.svg');
@@ -55,7 +55,8 @@ const EditPage = createClass({
autoSaveWarning : false,
unsavedTime : new Date(),
currentEditorPage : 0,
displayLockMessage : this.props.brew.lock || false
displayLockMessage : this.props.brew.lock || false,
themeBundle : {}
};
},
@@ -87,6 +88,8 @@ const EditPage = createClass({
htmlErrors : Markdown.validate(prevState.brew.text)
}));
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
document.addEventListener('keydown', this.handleControlKeys);
},
componentWillUnmount : function() {
@@ -130,7 +133,10 @@ const EditPage = createClass({
}), ()=>{if(this.state.autoSave) this.trySave();});
},
handleMetaChange : function(metadata){
handleMetaChange : function(metadata, field=undefined){
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
fetchThemeBundle(this, metadata.renderer, metadata.theme);
this.setState((prevState)=>({
brew : {
...prevState.brew,
@@ -138,7 +144,6 @@ const EditPage = createClass({
},
isPending : true,
}), ()=>{if(this.state.autoSave) this.trySave();});
},
hasChanges : function(){
@@ -406,12 +411,15 @@ const EditPage = createClass({
onMetaChange={this.handleMetaChange}
reportError={this.errorReported}
renderer={this.state.brew.renderer}
userThemes={this.props.userThemes}
snippetBundle={this.state.themeBundle.snippets}
/>
<BrewRenderer
text={this.state.brew.text}
style={this.state.brew.style}
renderer={this.state.brew.renderer}
theme={this.state.brew.theme}
themeBundle={this.state.themeBundle}
errors={this.state.htmlErrors}
lang={this.state.brew.lang}
currentEditorPage={this.state.currentEditorPage}

View File

@@ -136,6 +136,19 @@ const errorIndex = (props)=>{
**Brew ID:** ${props.brew.brewId}`,
// Theme load error
'09' : dedent`
## No Homebrewery theme document could be found.
The server could not locate the Homebrewery document. It was likely deleted by
its owner.
:
**Requested access:** ${props.brew.accessType}
**Brew ID:** ${props.brew.brewId}`,
// Brew locked by Administrators error
'100' : dedent`
## This brew has been locked.

View File

@@ -13,6 +13,7 @@ const HelpNavItem = require('../../navbar/help.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const AccountNavItem = require('../../navbar/account.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const { fetchThemeBundle } = require('../../../../shared/helpers.js');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
@@ -34,12 +35,17 @@ const HomePage = createClass({
brew : this.props.brew,
welcomeText : this.props.brew.text,
error : undefined,
currentEditorPage : 0
currentEditorPage : 0,
themeBundle : {}
};
},
editor : React.createRef(null),
componentDidMount : function() {
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
},
handleSave : function(){
request.post('/api')
.send(this.state.brew)
@@ -95,6 +101,7 @@ const HomePage = createClass({
style={this.state.brew.style}
renderer={this.state.brew.renderer}
currentEditorPage={this.state.currentEditorPage}
themeBundle={this.state.themeBundle}
/>
</SplitPane>
</div>

View File

@@ -19,7 +19,7 @@ const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
const { printCurrentBrew } = require('../../../../shared/helpers.js');
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
@@ -44,7 +44,8 @@ const NewPage = createClass({
saveGoogle : (global.account && global.account.googleId ? true : false),
error : null,
htmlErrors : Markdown.validate(brew.text),
currentEditorPage : 0
currentEditorPage : 0,
themeBundle : {}
};
},
@@ -77,6 +78,8 @@ const NewPage = createClass({
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
});
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
localStorage.setItem(BREWKEY, brew.text);
if(brew.style)
localStorage.setItem(STYLEKEY, brew.style);
@@ -122,7 +125,10 @@ const NewPage = createClass({
localStorage.setItem(STYLEKEY, style);
},
handleMetaChange : function(metadata){
handleMetaChange : function(metadata, field=undefined){
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
fetchThemeBundle(this, metadata.renderer, metadata.theme);
this.setState((prevState)=>({
brew : { ...prevState.brew, ...metadata },
}), ()=>{
@@ -142,8 +148,6 @@ const NewPage = createClass({
isSaving : true
});
console.log('saving new brew');
let brew = this.state.brew;
// Split out CSS to Style if CSS codefence exists
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
@@ -153,12 +157,10 @@ const NewPage = createClass({
}
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
const res = await request
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
.send(brew)
.catch((err)=>{
console.log(err);
this.setState({ isSaving: false, error: err });
});
if(!res) return;
@@ -214,12 +216,14 @@ const NewPage = createClass({
onStyleChange={this.handleStyleChange}
onMetaChange={this.handleMetaChange}
renderer={this.state.brew.renderer}
userThemes={this.props.userThemes}
/>
<BrewRenderer
text={this.state.brew.text}
style={this.state.brew.style}
renderer={this.state.brew.renderer}
theme={this.state.brew.theme}
themeBundle={this.state.themeBundle}
errors={this.state.htmlErrors}
lang={this.state.brew.lang}
currentEditorPage={this.state.currentEditorPage}

View File

@@ -12,18 +12,26 @@ const Account = require('../../navbar/account.navitem.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
const { printCurrentBrew } = require('../../../../shared/helpers.js');
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
const SharePage = createClass({
displayName : 'SharePage',
getDefaultProps : function() {
return {
brew : DEFAULT_BREW_LOAD
brew : DEFAULT_BREW_LOAD,
};
},
getInitialState : function() {
return {
themeBundle : {}
};
},
componentDidMount : function() {
document.addEventListener('keydown', this.handleControlKeys);
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
},
componentWillUnmount : function() {
@@ -99,6 +107,7 @@ const SharePage = createClass({
style={this.props.brew.style}
renderer={this.props.brew.renderer}
theme={this.props.brew.theme}
themeBundle={this.state.themeBundle}
allowPrint={true}
/>
</div>

View File

@@ -4,6 +4,7 @@
"secret" : "secret",
"web_port" : 8000,
"enable_v3" : true,
"enable_themes" : true,
"local_environments" : ["docker", "local"],
"publicUrl" : "https://homebrewery.naturalcrit.com"
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "homebrewery",
"version": "3.13.1",
"version": "3.14.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "homebrewery",
"version": "3.13.1",
"version": "3.14.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.13.1",
"version": "3.14.0",
"engines": {
"npm": "^10.2.x",
"node": "^20.8.x"
@@ -22,7 +22,8 @@
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
"verify": "npm run lint && npm test",
"test": "jest --runInBand",
"test:api-unit": "jest server/*.spec.js --verbose",
"test:api-unit": "jest \"server/.*.spec.js\" --verbose",
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --verbose",
"test:coverage": "jest --coverage --silent --runInBand",
"test:dev": "jest --verbose --watch",
"test:basic": "jest tests/markdown/basic.test.js --verbose",
@@ -56,15 +57,15 @@
],
"coverageThreshold": {
"global": {
"statements": 25,
"branches": 10,
"functions": 22,
"lines": 25
"statements": 50,
"branches": 40,
"functions": 40,
"lines": 50
},
"server/homebrew.api.js": {
"statements": 65,
"statements": 70,
"branches": 50,
"functions": 60,
"functions": 65,
"lines": 70
}
},

View File

@@ -9,7 +9,7 @@ const yaml = require('js-yaml');
const app = express();
const config = require('./config.js');
const { homebrewApi, getBrew } = require('./homebrew.api.js');
const { homebrewApi, getBrew, getUsersBrewThemes } = require('./homebrew.api.js');
const GoogleActions = require('./googleActions.js');
const serveCompressedStaticAssets = require('./static-assets.mv.js');
const sanitizeFilename = require('sanitize-filename');
@@ -81,7 +81,8 @@ app.get('/robots.txt', (req, res)=>{
app.get('/', (req, res, next)=>{
req.brew = {
text : welcomeText,
renderer : 'V3'
renderer : 'V3',
theme : '5ePHB'
},
req.ogMeta = { ...defaultMetaTags,
@@ -97,7 +98,8 @@ app.get('/', (req, res, next)=>{
app.get('/legacy', (req, res, next)=>{
req.brew = {
text : welcomeTextLegacy,
renderer : 'legacy'
renderer : 'legacy',
theme : '5ePHB'
},
req.ogMeta = { ...defaultMetaTags,
@@ -265,9 +267,11 @@ app.get('/user/:username', async (req, res, next)=>{
});
//Edit Page
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
req.userThemes = await(getUsersBrewThemes(req.account?.username));
req.ogMeta = { ...defaultMetaTags,
title : req.brew.title || 'Untitled Brew',
description : req.brew.description || 'No description.',
@@ -279,10 +283,10 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
splitTextStyleAndMetadata(req.brew);
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
return next();
});
}));
//New Page
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
//New Page from ID
app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
sanitizeBrew(req.brew, 'share');
splitTextStyleAndMetadata(req.brew);
const brew = {
@@ -292,17 +296,31 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
style : req.brew.style,
renderer : req.brew.renderer,
theme : req.brew.theme,
tags : req.brew.tags
tags : req.brew.tags,
};
req.brew = _.defaults(brew, DEFAULT_BREW);
req.userThemes = await(getUsersBrewThemes(req.account?.username));
req.ogMeta = { ...defaultMetaTags,
title : 'New',
description : 'Start crafting your homebrew on the Homebrewery!'
};
return next();
});
}));
//New Page
app.get('/new', asyncHandler(async(req, res, next)=>{
req.userThemes = await(getUsersBrewThemes(req.account?.username));
req.ogMeta = { ...defaultMetaTags,
title : 'New',
description : 'Start crafting your homebrew on the Homebrewery!'
};
return next();
}));
//Share Page
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
@@ -418,7 +436,8 @@ const renderPage = async (req, res)=>{
enable_v3 : config.get('enable_v3'),
enable_themes : config.get('enable_themes'),
config : configuration,
ogMeta : req.ogMeta
ogMeta : req.ogMeta,
userThemes : req.userThemes
};
const title = req.brew ? req.brew.title : '';
const page = await templateFn('homebrew', title, props)

View File

@@ -8,9 +8,16 @@ const Markdown = require('../shared/naturalcrit/markdown.js');
const yaml = require('js-yaml');
const asyncHandler = require('express-async-handler');
const { nanoid } = require('nanoid');
const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
const Themes = require('../themes/themes.json');
const isStaticTheme = (renderer, themeName)=>{
return Themes[renderer]?.[themeName] !== undefined;
};
// const getTopBrews = (cb) => {
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
// cb(brews);
@@ -37,6 +44,40 @@ const api = {
}
return { id, googleId };
},
//Get array of any of this user's brews tagged with `meta:theme`
getUsersBrewThemes : async (username)=>{
const fields = [
'title',
'tags',
'shareId',
'thumbnail',
'textBin',
'text',
'authors',
'renderer'
];
const userThemes = {};
const brews = await HomebrewModel.getByUser(username, true, fields, { tags: { $in: ['meta:theme', 'meta:Theme'] } });
if(brews) {
for (const brew of brews) {
userThemes[brew.renderer] ??= {};
userThemes[brew.renderer][brew.shareId] = {
name : brew.title,
renderer : brew.renderer,
baseTheme : brew.theme,
baseSnippets : false,
author : brew.authors[0],
path : brew.shareId,
thumbnail : brew.thumbnail || '/assets/naturalCritLogoWhite.svg'
};
}
}
return userThemes;
},
getBrew : (accessType, stubOnly = false)=>{
// Create middleware with the accessType passed in as part of the scope
return async (req, res, next)=>{
@@ -209,6 +250,62 @@ 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.renderer: This is the Markdown+ version for the static theme. If a
User theme the value will come from the User Theme metadata.
*/
req.params.renderer = _.upperFirst(req.params.renderer);
let currentTheme;
const completeStyles = [];
const completeSnippets = [];
while (req.params.id) {
//=== User Themes ===//
if(!isStaticTheme(req.params.renderer, req.params.id)) {
await api.getBrew('share')(req, res, ()=>{})
.catch((err)=>{
if(err.HBErrorCode == '05')
err = { ...err, name: 'ThemeLoad Error', message: 'Theme Not Found', HBErrorCode: '09' };
throw err;
});
currentTheme = req.brew;
splitTextStyleAndMetadata(currentTheme);
// If there is anything in the snippets or style members, append them to the appropriate array
if(currentTheme?.snippets) completeSnippets.push(JSON.parse(currentTheme.snippets));
if(currentTheme?.style) completeStyles.push(`/* From Brew: ${req.protocol}://${req.get('host')}/share/${req.params.id} */\n\n${currentTheme.style}`);
req.params.id = currentTheme.theme;
req.params.renderer = currentTheme.renderer;
}
//=== Static Themes ===//
else {
const localSnippets = `${req.params.renderer}_${req.params.id}`; // Just log the name for loading on client
const localStyle = `@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");`;
completeSnippets.push(localSnippets);
completeStyles.push(`/* From Theme ${req.params.id} */\n\n${localStyle}`);
req.params.id = Themes[req.params.renderer][req.params.id].baseTheme;
}
}
const returnObj = {
// Reverse the order of the arrays so they are listed oldest parent to youngest child.
styles : completeStyles.reverse(),
snippets : completeSnippets.reverse()
};
res.setHeader('Content-Type', 'application/json');
return res.status(200).send(returnObj);
},
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);
@@ -369,5 +466,6 @@ 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/:renderer/:id', asyncHandler(api.getThemeBundle));
module.exports = api;

View File

@@ -14,6 +14,9 @@ describe('Tests for api', ()=>{
let saved;
beforeEach(()=>{
jest.resetModules();
jest.restoreAllMocks();
saved = undefined;
saveFunc = jest.fn(async function() {
saved = { ...this, _id: '1' };
@@ -45,8 +48,9 @@ describe('Tests for api', ()=>{
model.mockImplementation((brew)=>modelBrew(brew));
res = {
status : jest.fn(()=>res),
send : jest.fn(()=>{})
status : jest.fn(()=>res),
send : jest.fn(()=>{}),
setHeader : jest.fn(()=>{})
};
api = require('./homebrew.api');
@@ -81,10 +85,6 @@ describe('Tests for api', ()=>{
};
});
afterEach(()=>{
jest.restoreAllMocks();
});
describe('getId', ()=>{
it('should return only id if google id is not present', ()=>{
const { id, googleId } = api.getId({
@@ -581,6 +581,121 @@ brew`);
});
});
describe('Theme bundle', ()=>{
it('should return Theme Bundle for a User Theme', async ()=>{
const brews = {
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style' }
};
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
await api.getThemeBundle(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
styles : ['/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'],
snippets : []
});
});
it('should return Theme Bundle for nested User Themes', async ()=>{
const brews = {
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: null, shareId: 'userThemeCID', style: 'User Theme C Style' }
};
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
await api.getThemeBundle(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
styles : [
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
'/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'
],
snippets : []
});
});
it('should return Theme Bundle for a Static Theme', async ()=>{
const req = { params: { renderer: 'V3', id: '5ePHB' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
await api.getThemeBundle(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
styles : [
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`
],
snippets : [
'V3_Blank',
'V3_5ePHB'
]
});
});
it('should return Theme Bundle for nested User and Static Themes together', async ()=>{
const brews = {
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: '5eDMG', shareId: 'userThemeCID', style: 'User Theme C Style' }
};
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
await api.getThemeBundle(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
styles : [
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`,
`/* From Theme 5eDMG */\n\n@import url("/themes/V3/5eDMG/style.css");`,
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
'/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'
],
snippets : [
'V3_Blank',
'V3_5ePHB',
'V3_5eDMG'
]
});
});
it('should fail for an invalid Theme in the chain', async()=>{
const brews = {
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'missingTheme', shareId: 'userThemeAID', style: 'User Theme A Style' },
};
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
let err;
await api.getThemeBundle(req, res)
.catch((e)=>err = e);
expect(err).toEqual({
HBErrorCode : '09',
accessType : 'share',
brewId : 'missingTheme',
message : 'Theme Not Found',
name : 'ThemeLoad Error',
status : 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' }; });

View File

@@ -50,8 +50,8 @@ HomebrewSchema.statics.get = async function(query, fields=null){
return brew;
};
HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null){
const query = { authors: username, published: true };
HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null, filter=null){
const query = { authors: username, published: true, ...filter };
if(allowAccess){
delete query.published;
}

View File

@@ -1,5 +1,6 @@
const _ = require('lodash');
const yaml = require('js-yaml');
const request = require('../client/homebrew/utils/request-middleware.js');
const splitTextStyleAndMetadata = (brew)=>{
brew.text = brew.text.replaceAll('\r\n', '\n');
@@ -15,6 +16,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 = ()=>{
@@ -28,7 +34,24 @@ const printCurrentBrew = ()=>{
}
};
const fetchThemeBundle = async (obj, renderer, theme)=>{
const res = await request
.get(`/api/theme/${renderer}/${theme}`)
.catch((err)=>{
obj.setState({ error: err });
});
if(!res) return;
const themeBundle = res.body;
themeBundle.joinedStyles = themeBundle.styles.map((style)=>`<style>${style}</style>`).join('\n\n');
obj.setState((prevState)=>({
...prevState,
themeBundle : themeBundle
}));
};
module.exports = {
splitTextStyleAndMetadata,
printCurrentBrew
printCurrentBrew,
fetchThemeBundle,
};

View File

@@ -39,10 +39,8 @@ if(typeof window !== 'undefined'){
//Autocompletion
require('codemirror/addon/hint/show-hint.js');
const foldPagesCode = require('./fold-pages');
foldPagesCode.registerHomebreweryHelper(CodeMirror);
const foldCSSCode = require('./fold-css');
foldCSSCode.registerHomebreweryHelper(CodeMirror);
const foldCode = require('./fold-code');
foldCode.registerHomebreweryHelper(CodeMirror);
}
const CodeEditor = createClass({
@@ -413,7 +411,7 @@ const CodeEditor = createClass({
foldOptions : function(cm){
return {
scanUp : true,
rangeFinder : this.props.language === 'css' ? CodeMirror.fold.homebrewerycss : CodeMirror.fold.homebrewery,
rangeFinder : CodeMirror.fold.homebrewery,
widget : (from, to)=>{
let text = '';
let currentLine = from.line;
@@ -452,4 +450,3 @@ const CodeEditor = createClass({
});
module.exports = CodeEditor;

View File

@@ -1,44 +0,0 @@
module.exports = {
registerHomebreweryHelper : function(CodeMirror) {
CodeMirror.registerHelper('fold', 'homebrewerycss', function(cm, start) {
// BRACE FOLDING
const startMatcher = /\{[ \t]*$/;
const endMatcher = /\}[ \t]*$/;
const prevLine = cm.getLine(start.line);
if(prevLine.match(startMatcher)) {
const lastLineNo = cm.lastLine();
let end = start.line + 1;
let braceCount = 1;
while (end < lastLineNo) {
const curLine = cm.getLine(end);
if(curLine.match(startMatcher)) braceCount++;
if(curLine.match(endMatcher)) braceCount--;
if(braceCount == 0) break;
++end;
}
return {
from : CodeMirror.Pos(start.line, 0),
to : CodeMirror.Pos(end, cm.getLine(end).length)
};
}
// IMPORT FOLDING
const importMatcher = /^@import.*?[;]/;
if(prevLine.match(importMatcher)) {
return {
from : CodeMirror.Pos(start.line, 0),
to : CodeMirror.Pos(start.line, cm.getLine(start.line).length)
};
}
return null;
});
}
};

View File

@@ -1,6 +1,6 @@
{
"name" : "5e PHB",
"renderer" : "V3",
"baseTheme" : false,
"baseTheme" : "Blank",
"baseSnippets" : false
}

View File

@@ -1,6 +1,6 @@
{
"name" : "Journal",
"renderer" : "V3",
"baseTheme" : false,
"baseTheme" : "Blank",
"baseSnippets" : "5ePHB"
}

View File

@@ -18,7 +18,7 @@
"5ePHB": {
"name": "5e PHB",
"renderer": "V3",
"baseTheme": false,
"baseTheme": "Blank",
"baseSnippets": false,
"path": "5ePHB"
},
@@ -32,7 +32,7 @@
"Journal": {
"name": "Journal",
"renderer": "V3",
"baseTheme": false,
"baseTheme": "Blank",
"baseSnippets": "5ePHB",
"path": "Journal"
}