0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-12 21:52:42 +00:00

Merge branch 'master' of https://github.com/naturalcrit/homebrewery into scroll-to-element

This commit is contained in:
Víctor Losada Hernández
2024-09-07 18:45:03 +02:00
66 changed files with 5519 additions and 3236 deletions

View File

@@ -67,6 +67,9 @@ jobs:
- run: - run:
name: Test - Definition Lists name: Test - Definition Lists
command: npm run test:definition-lists command: npm run test:definition-lists
- run:
name: Test - Hard Breaks
command: npm run test:hard-breaks
- run: - run:
name: Test - Variables name: Test - Variables
command: npm run test:variables command: npm run test:variables

View File

@@ -1,79 +0,0 @@
module.exports = {
root : true,
parserOptions : {
ecmaVersion : 2021,
sourceType : 'module',
ecmaFeatures : {
jsx : true
}
},
env : {
browser : true,
node : true
},
plugins : ['react', 'jest'],
rules : {
/** Errors **/
'camelcase' : ['error', { properties: 'never' }],
//'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
'no-array-constructor' : 'error',
'no-iterator' : 'error',
'no-nested-ternary' : 'error',
'no-new-object' : 'error',
'no-proto' : 'error',
'react/jsx-no-bind' : ['error', { allowArrowFunctions: true }],
'react/jsx-uses-react' : 'error',
'react/prefer-es6-class' : ['error', 'never'],
'jest/valid-expect' : ['error', { maxArgs: 3 }],
/** Warnings **/
'max-lines' : ['warn', {
max : 200,
skipComments : true,
skipBlankLines : true,
}],
'max-depth' : ['warn', { max: 4 }],
'max-params' : ['warn', { max: 5 }],
'no-restricted-syntax' : ['warn', 'ClassDeclaration', 'SwitchStatement'],
'no-unused-vars' : ['warn', {
vars : 'all',
args : 'none',
varsIgnorePattern : 'config|_|cx|createClass'
}],
'react/jsx-uses-vars' : 'warn',
/** Fixable **/
'arrow-parens' : ['warn', 'always'],
'brace-style' : ['warn', '1tbs', { allowSingleLine: true }],
'jsx-quotes' : ['warn', 'prefer-single'],
'no-var' : 'warn',
'prefer-const' : 'warn',
'prefer-template' : 'warn',
'quotes' : ['warn', 'single', { 'allowTemplateLiterals': true }],
'semi' : ['warn', 'always'],
/** Whitespace **/
'array-bracket-spacing' : ['warn', 'never'],
'arrow-spacing' : ['warn', { before: false, after: false }],
'comma-spacing' : ['warn', { before: false, after: true }],
'indent' : ['warn', 'tab', { 'MemberExpression': 'off' }],
'keyword-spacing' : ['warn', {
before : true,
after : true,
overrides : {
if : { 'before': false, 'after': false }
}
}],
'key-spacing' : ['warn', {
multiLine : { beforeColon: true, afterColon: true, align: 'colon' },
singleLine : { beforeColon: false, afterColon: true }
}],
'linebreak-style' : 'off',
'no-trailing-spaces' : 'warn',
'no-whitespace-before-property' : 'warn',
'object-curly-spacing' : ['warn', 'always'],
'react/jsx-indent-props' : ['warn', 'tab'],
'space-in-parens' : ['warn', 'never'],
'template-curly-spacing' : ['warn', 'never'],
}
};

4
.gitattributes vendored
View File

@@ -1 +1,3 @@
package-lock.json binary package-lock.json binary
*.json text eol=lf

36
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,36 @@
<!--
Before submitting a Pull Request, please consider the following to speed up reviews:
- 👷‍♀️ Create small PRs. Large PRs can usually be broken down into incremental PRs.
- 🚩 Do you already have several open PRs? Consider finishing or asking for help with existing PRs first.
- 🔧 Does your PR reference a discussed and approved issue, especially for personal or edge-case requests?
- 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
-->
## Description
## Related Issues or Discussions
- Closes #
## QA Instructions, Screenshots, Recordings
_Please replace this line with instructions on how to test or view your changes, as well as any before/after
images for UI changes._
### Reviewer Checklist
_Please replace the list below with specific features you want reviewers to look at._
*Reviewers, refer to this list when testing features, or suggest new items *
- [ ] Verify new features are functional
- [ ] Feature A does X
- [ ] Feature B does Y
- [ ] Verify old features have not broken
- [ ] Feature Z can still be used
- [ ] Test for edge cases / try to break things
- [ ] Feature A handles negative numbers
- [ ] Identify opportunities for simplification and refactoring
- [ ] Check for code legibility and appropriate comments
<details><summary>Copy this list</summary>

View File

@@ -3,7 +3,7 @@
"stylelint-config-recess-order", "stylelint-config-recess-order",
"stylelint-config-recommended"], "stylelint-config-recommended"],
"plugins": [ "plugins": [
"stylelint-stylistic", "@stylistic/stylelint-plugin",
"./stylelint_plugins/declaration-colon-align.js", "./stylelint_plugins/declaration-colon-align.js",
"./stylelint_plugins/declaration-colon-min-space-before", "./stylelint_plugins/declaration-colon-min-space-before",
"./stylelint_plugins/declaration-block-multi-line-min-declarations" "./stylelint_plugins/declaration-block-multi-line-min-declarations"
@@ -16,32 +16,32 @@
"font-family-no-missing-generic-family-keyword" : null, "font-family-no-missing-generic-family-keyword" : null,
"font-weight-notation" : "named-where-possible", "font-weight-notation" : "named-where-possible",
"font-family-name-quotes" : "always-unless-keyword", "font-family-name-quotes" : "always-unless-keyword",
"stylistic/indentation" : "tab", "@stylistic/indentation" : "tab",
"no-duplicate-selectors" : true, "no-duplicate-selectors" : true,
"stylistic/color-hex-case" : "upper", "@stylistic/color-hex-case" : "upper",
"color-hex-length" : "long", "color-hex-length" : "long",
"stylistic/selector-combinator-space-after" : "always", "@stylistic/selector-combinator-space-after" : "always",
"stylistic/selector-combinator-space-before" : "always", "@stylistic/selector-combinator-space-before" : "always",
"stylistic/selector-attribute-operator-space-before" : "never", "@stylistic/selector-attribute-operator-space-before" : "never",
"stylistic/selector-attribute-operator-space-after" : "never", "@stylistic/selector-attribute-operator-space-after" : "never",
"stylistic/selector-attribute-brackets-space-inside" : "never", "@stylistic/selector-attribute-brackets-space-inside" : "never",
"selector-attribute-quotes" : "always", "selector-attribute-quotes" : "always",
"selector-pseudo-element-colon-notation" : "double", "selector-pseudo-element-colon-notation" : "double",
"stylistic/selector-pseudo-class-parentheses-space-inside" : "never", "@stylistic/selector-pseudo-class-parentheses-space-inside" : "never",
"stylistic/block-opening-brace-space-before" : "always", "@stylistic/block-opening-brace-space-before" : "always",
"naturalcrit/declaration-colon-min-space-before" : 1, "naturalcrit/declaration-colon-min-space-before" : 1,
"stylistic/declaration-block-trailing-semicolon" : "always", "@stylistic/declaration-block-trailing-semicolon" : "always",
"stylistic/declaration-colon-space-after" : "always", "@stylistic/declaration-colon-space-after" : "always",
"stylistic/number-leading-zero" : "always", "@stylistic/number-leading-zero" : "always",
"function-url-quotes" : ["always", { "except": ["empty"] }], "function-url-quotes" : ["always", { "except": ["empty"] }],
"function-url-scheme-disallowed-list" : ["data","http"], "function-url-scheme-disallowed-list" : ["data","http"],
"comment-whitespace-inside" : "always", "comment-whitespace-inside" : "always",
"stylistic/string-quotes" : "single", "@stylistic/string-quotes" : "single",
"stylistic/media-feature-range-operator-space-before" : "always", "@stylistic/media-feature-range-operator-space-before" : "always",
"stylistic/media-feature-range-operator-space-after" : "always", "@stylistic/media-feature-range-operator-space-after" : "always",
"stylistic/media-feature-parentheses-space-inside" : "never", "@stylistic/media-feature-parentheses-space-inside" : "never",
"stylistic/media-feature-colon-space-before" : "always", "@stylistic/media-feature-colon-space-before" : "always",
"stylistic/media-feature-colon-space-after" : "always", "@stylistic/media-feature-colon-space-after" : "always",
"naturalcrit/declaration-colon-align" : true, "naturalcrit/declaration-colon-align" : true,
"naturalcrit/declaration-block-multi-line-min-declarations": 1 "naturalcrit/declaration-block-multi-line-min-declarations": 1
} }

View File

@@ -1,4 +1,4 @@
FROM node:18-alpine FROM node:20-alpine
RUN apk --no-cache add git RUN apk --no-cache add git
ENV NODE_ENV=docker ENV NODE_ENV=docker

View File

@@ -84,6 +84,186 @@ pre {
## changelog ## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Wednesday 9/04/2024 - v3.15.0
{{taskList
##### 5e-Cleric, abquintic, calculuschild, Gazook89, G-Ambatte, Ericsheid, Kaiburr
* [x] New {{openSans **VAULT** {{fas,fa-dungeon}}}} page 🎉🎉🎉
:
All **PUBLISHED** brews ({{openSans :fas_circle_info: **Properties**}} menu) will be searchable, by title or author, and filtered by renderer. More features and adjustments will be coming.
:
Note: If any of your own brews are not showing up in search (particularly if stored on Google Drive), please edit and re-save to ensure our database has the data needed from document to be searchable.
Fixes issue [#697](https://github.com/naturalcrit/homebrewery/issues/697)
##### Gazook89
* [x] Auto-focus on text editor when switching editor tabs
}}
### Wednesday 8/28/2024 - v3.14.3
{{taskList
##### calculuschild, G-Ambatte
* [x] New {{openSans **IMAGES → {{fac,image-wrap-left}} IMAGE WRAP LEFT/RIGHT**}} snippets
Fixes issue [#380](https://github.com/naturalcrit/homebrewery/issues/380)
* [x] Fix v3.14.2 bug with `` failing after tables
##### 5e-Cleric
* [x] Fix Account page crash when not logged in
Fixes issue [#3605](https://github.com/naturalcrit/homebrewery/issues/3605)
##### abquintic
* [x] Fix jump hotkeys conflicting with `CTRL + SHIFT`. Preview and Source movement shortcuts now use `CTRL + SHIFT + META + LEFT\RIGHTARROW`
##### G-Ambatte
* [x] Fix display issue with image wrap icons
}}
### Tuesday 8/27/2024 - v3.14.2
{{taskList
##### calculuschild
* [x] Reroute invalid urls to homepage
Fixes issues [#3269](https://github.com/naturalcrit/homebrewery/issues/3629)
* [x] Background dependency updates
##### G-Ambatte
* [x] Add route to get brew styling via `/css/shareId`
Fixes issues [#1097](https://github.com/naturalcrit/homebrewery/issues/1097)
* [x] Fix `:emojis:` preventing code folding
Fixes issues [#3604](https://github.com/naturalcrit/homebrewery/issues/3604)
* [x] Fix mask image warping when rotated and stretched
Fixes issues [#3636](https://github.com/naturalcrit/homebrewery/issues/3636)
* [x] Fix Table of Contents uppercasing
Fixes issues [#3572](https://github.com/naturalcrit/homebrewery/issues/3572)
##### abquintic
* [x] Create globally unique Header IDs across pages
Fixes issues [#1430](https://github.com/naturalcrit/homebrewery/issues/1430)
* [x] Fix colon `` being parsed in codeblocks
* [x] Prevent crashes when loading undefined renderer or theme bundle
* [x] Add Jump-To hotkeys
* Use `CTRL/META + SHIFT + LEFTARROW` to brewJump
* Use `CTRL/META + SHIFT + RIGHTARROW` to sourceJump
* [x] Prevent reload from clobbering modified fresh clones
##### 5e-Cleric, Gazook89
* [x] Viewer tools for zoom/page navigation
}}
### Tuesday 8/13/2024 - v3.14.1
{{taskList
##### abquintic
* [x] Allow Table of Contents to flow across columns
Fixes issues [#2563](https://github.com/naturalcrit/homebrewery/issues/2563)
* [x] Fix unusual margin spacing for adjacent `.descriptive` and `.wide` blocks
Fixes issues [#2688](https://github.com/naturalcrit/homebrewery/issues/2688)
* [x] Add code folding to :fas_paintbrush: {{openSans **STYLE**}} tab
##### G-Ambatte
* [x] Fix edge case where Table of Contents generator changed capitalization of headings
Fixes issues [#3572](https://github.com/naturalcrit/homebrewery/issues/3572)
* [x] Fix **Ink Friendly** snippet causing unselectable PDF text
Fixes issues [#3563](https://github.com/naturalcrit/homebrewery/issues/3563)
* [x] Prevent brews selecting themselves as a theme
Fixes issues [#3614](https://github.com/naturalcrit/homebrewery/issues/3614)
* [x] Fix info pages (`/faq`, `/migrate`, etc.) showing blank authorship info
Fixes issues [#3568](https://github.com/naturalcrit/homebrewery/issues/3568)
* [x] Add `abs()`, `sign()` and `signed()` functions to variable syntax math handler
Fixes issues [#3537](https://github.com/naturalcrit/homebrewery/issues/3537)
* [x] Fix variable math handler not processing commas (i.e., in `$[max(varA,varB)]`
Fixes issues [#3613](https://github.com/naturalcrit/homebrewery/issues/3613)
* [x] Fix variable math handler scrambling variables with names that are subsets of other variables
Fixes issues [#3622](https://github.com/naturalcrit/homebrewery/issues/3622)
##### calculuschild
* [x] Fix `/migrate` page using an editor context instead of share context
##### 5e-Cleric
* [x] Fix Monster Stat Blocks losing color in Safari
}}
\page
### 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 ### Saturday 6/7/2024 - v3.13.1
{{taskList {{taskList
@@ -131,8 +311,6 @@ Fixes issue [#3298](https://github.com/naturalcrit/homebrewery/issues/3298)
Fixes issue [#3397](https://github.com/naturalcrit/homebrewery/issues/3397) Fixes issue [#3397](https://github.com/naturalcrit/homebrewery/issues/3397)
}} }}
\column
### Monday 18/3/2024 - v3.12.0 ### Monday 18/3/2024 - v3.12.0
{{taskList {{taskList
@@ -424,7 +602,7 @@ Fixes issue [#2729](https://github.com/naturalcrit/homebrewery/issues/2729),
### Thursday 17/08/2023 - v3.9.2 ### Thursday 17/08/2023 - v3.9.2
{{taskList {{taskList
##### Calculuschild ##### calculuschild
* [x] Fix links to certain old Google Drive files * [x] Fix links to certain old Google Drive files
@@ -482,7 +660,7 @@ Fixes issue [#1924](https://github.com/naturalcrit/homebrewery/issues/1924)
### Friday 02/06/2023 - v3.9.0 ### Friday 02/06/2023 - v3.9.0
{{taskList {{taskList
##### Calculuschild ##### calculuschild
* [x] Fix some files not showing up on userpage when user has a large number of brews in Google Drive * [x] Fix some files not showing up on userpage when user has a large number of brews in Google Drive
@@ -579,7 +757,7 @@ Fixes issues [#2731](https://github.com/naturalcrit/homebrewery/issues/2731)
### Monday 13/03/2023 - v3.7.2 ### Monday 13/03/2023 - v3.7.2
{{taskList {{taskList
##### Calculuschild ##### calculuschild
* [x] Fix wide Monster Stat Blocks not spanning columns on Legacy * [x] Fix wide Monster Stat Blocks not spanning columns on Legacy
}} }}
@@ -602,7 +780,7 @@ Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569)
* [x] Updated the Google Drive icon * [x] Updated the Google Drive icon
* [x] Backend fix to unit tests failing intermittently * [x] Backend fix to unit tests failing intermittently
##### Calculuschild ##### calculuschild
* [x] Fix PDF pixelation on CoverPage text outlines * [x] Fix PDF pixelation on CoverPage text outlines
}} }}
@@ -614,7 +792,7 @@ Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569)
**NOTE:** Some new snippets will now show a {{beta BETA}} tag. Feel free to use them, but be aware we may change how they work depending on your feedback. **NOTE:** Some new snippets will now show a {{beta BETA}} tag. Feel free to use them, but be aware we may change how they work depending on your feedback.
}} }}
##### Calculuschild ##### calculuschild
* [x] New {{openSans **IMAGES → WATERCOLOR EDGE** {{fac,mask-edge}} }} and {{openSans **WATERCOLOR CORNER** {{fac,mask-corner}} }} snippets for V3, which adds a stylish watercolor texture to the edge of your images! (Thanks to /u/flamableconcrete on Reddit for providing these image masks!) * [x] New {{openSans **IMAGES → WATERCOLOR EDGE** {{fac,mask-edge}} }} and {{openSans **WATERCOLOR CORNER** {{fac,mask-corner}} }} snippets for V3, which adds a stylish watercolor texture to the edge of your images! (Thanks to /u/flamableconcrete on Reddit for providing these image masks!)
@@ -758,7 +936,7 @@ Fixes issues [#1670](https://github.com/naturalcrit/homebrewery/issues/1670)
### Thursday 28/10/2022 - v3.3.1 ### Thursday 28/10/2022 - v3.3.1
{{taskList {{taskList
##### Calculuschild ##### calculuschild
* [x] Fixes to several broken CSS styles from v3.3.0 * [x] Fixes to several broken CSS styles from v3.3.0
@@ -773,7 +951,7 @@ Fixes issues [#2468](https://github.com/naturalcrit/homebrewery/issues/2468)
### Friday 19/10/2022 - v3.3.0 ### Friday 19/10/2022 - v3.3.0
{{taskList {{taskList
##### Calculuschild ##### calculuschild
* [x] Fix for tables broken by Chrome v106 * [x] Fix for tables broken by Chrome v106
@@ -856,7 +1034,7 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
### Wednesday 31/08/2022 - v3.2.1 ### Wednesday 31/08/2022 - v3.2.1
{{taskList {{taskList
##### Calculuschild ##### calculuschild
* [x] Reference Links should now work inside tables * [x] Reference Links should now work inside tables
@@ -882,7 +1060,7 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
### Saturday 27/08/2022 - v3.2.0 ### Saturday 27/08/2022 - v3.2.0
{{taskList {{taskList
##### Calculuschild ##### calculuschild
* [x] The V3 renderer is now the default for new brews. * [x] The V3 renderer is now the default for new brews.
@@ -909,7 +1087,7 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
### Thursday 09/06/2022 - v3.1.1 ### Thursday 09/06/2022 - v3.1.1
{{taskList {{taskList
##### Calculuschild: ##### calculuschild:
* [x] Fixed class table decorations appearing on top of the table in PDF output. * [x] Fixed class table decorations appearing on top of the table in PDF output.
@@ -1753,4 +1931,4 @@ Massive changelog incoming:
* Added `phb.standalone.css` plus a build system for creating it * Added `phb.standalone.css` plus a build system for creating it
* Added page numbers and footer text * Added page numbers and footer text
* Page accent now flips each page * Page accent now flips each page

View File

@@ -7,6 +7,7 @@ const _ = require('lodash');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js'); const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
const Markdown = require('naturalcrit/markdown.js'); const Markdown = require('naturalcrit/markdown.js');
const ErrorBar = require('./errorBar/errorBar.jsx'); const ErrorBar = require('./errorBar/errorBar.jsx');
const ToolBar = require('./toolBar/toolBar.jsx');
//TODO: move to the brew renderer //TODO: move to the brew renderer
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx'); const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
@@ -18,8 +19,6 @@ const { printCurrentBrew } = require('../../../shared/helpers.js');
const DOMPurify = require('dompurify'); const DOMPurify = require('dompurify');
const purifyConfig = { FORCE_BODY: true, SANITIZE_DOM: false }; const purifyConfig = { FORCE_BODY: true, SANITIZE_DOM: false };
const Themes = require('themes/themes.json');
const PAGE_HEIGHT = 1056; const PAGE_HEIGHT = 1056;
const INITIAL_CONTENT = dedent` const INITIAL_CONTENT = dedent`
@@ -56,14 +55,16 @@ const BrewRenderer = (props)=>{
lang : '', lang : '',
errors : [], errors : [],
currentEditorPage : 0, currentEditorPage : 0,
themeBundle : {},
...props ...props
}; };
const [state, setState] = useState({ const [state, setState] = useState({
viewablePageNumber : 0, height : PAGE_HEIGHT,
height : PAGE_HEIGHT, isMounted : false,
isMounted : false, visibility : 'hidden',
visibility : 'hidden', zoom : 100,
currentPageNumber : 1,
}); });
const mainRef = useRef(null); const mainRef = useRef(null);
@@ -152,11 +153,14 @@ const BrewRenderer = (props)=>{
})); }));
}; };
const handleScroll = (e)=>{ const getCurrentPage = (e)=>{
const target = e.target; const { scrollTop, clientHeight, scrollHeight } = e.target;
const totalScrollableHeight = scrollHeight - clientHeight;
const currentPageNumber = Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length);
setState((prevState)=>({ setState((prevState)=>({
...prevState, ...prevState,
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * rawPages.length) currentPageNumber : currentPageNumber || 1
})); }));
}; };
@@ -167,23 +171,12 @@ const BrewRenderer = (props)=>{
if(index == props.currentEditorPage) //Already rendered before this step if(index == props.currentEditorPage) //Already rendered before this step
return false; return false;
if(Math.abs(index - state.viewablePageNumber) <= 3) if(Math.abs(index - state.currentPageNumber) <= 3)
return true; return true;
return false; return false;
}; };
const renderPageInfo = ()=>{
return <div className='pageInfo' ref={mainRef}>
<div>
{props.renderer}
</div>
<div>
{state.viewablePageNumber + 1} / {rawPages.length}
</div>
</div>;
};
const renderDummyPage = (index)=>{ const renderDummyPage = (index)=>{
return <div className='phb page' id={`p${index + 1}`} key={index}> return <div className='phb page' id={`p${index + 1}`} key={index}>
<i className='fas fa-spinner fa-spin' /> <i className='fas fa-spinner fa-spin' />
@@ -191,10 +184,9 @@ const BrewRenderer = (props)=>{
}; };
const renderStyle = ()=>{ const renderStyle = ()=>{
if(!props.style) return;
const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig); 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>` }} />; const themeStyles = props.themeBundle?.joinedStyles ?? '<style>@import url("/themes/V3/Blank/style.css");</style>';
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />; return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `${themeStyles} \n\n <style> ${cleanStyle} </style>` }} />;
}; };
const renderPage = (pageText, index)=>{ const renderPage = (pageText, index)=>{
@@ -254,15 +246,19 @@ const BrewRenderer = (props)=>{
document.dispatchEvent(new MouseEvent('click')); document.dispatchEvent(new MouseEvent('click'));
}; };
const rendererPath = props.renderer == 'V3' ? 'V3' : 'Legacy'; //Toolbar settings:
const themePath = props.theme ?? '5ePHB'; const handleZoom = (newZoom)=>{
const baseThemePath = Themes[rendererPath][themePath].baseTheme; setState((prevState)=>({
...prevState,
zoom : newZoom
}));
};
return ( return (
<> <>
{/*render dummy page while iFrame is mounting.*/} {/*render dummy page while iFrame is mounting.*/}
{!state.isMounted {!state.isMounted
? <div className='brewRenderer' onScroll={handleScroll}> ? <div className='brewRenderer' onScroll={getCurrentPage}>
<div className='pages'> <div className='pages'>
{renderDummyPage(1)} {renderDummyPage(1)}
</div> </div>
@@ -270,11 +266,13 @@ const BrewRenderer = (props)=>{
: null} : null}
<ErrorBar errors={props.errors} /> <ErrorBar errors={props.errors} />
<div className='popups'> <div className='popups' ref={mainRef}>
<RenderWarnings /> <RenderWarnings />
<NotificationPopup /> <NotificationPopup />
</div> </div>
<ToolBar onZoomChange={handleZoom} currentPage={state.currentPageNumber} totalPages={rawPages.length}/>
{/*render in iFrame so broken code doesn't crash the site.*/} {/*render in iFrame so broken code doesn't crash the site.*/}
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT} <Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
style={{ width: '100%', height: '100%', visibility: state.visibility }} style={{ width: '100%', height: '100%', visibility: state.visibility }}
@@ -282,30 +280,23 @@ const BrewRenderer = (props)=>{
onClick={()=>{emitClick();}} onClick={()=>{emitClick();}}
> >
<div className={'brewRenderer'} <div className={'brewRenderer'}
onScroll={handleScroll} onScroll={getCurrentPage}
onKeyDown={handleControlKeys} onKeyDown={handleControlKeys}
tabIndex={-1} tabIndex={-1}
style={{ height: state.height }}> 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 */} {/* Apply CSS from Style tab and render pages from Markdown tab */}
{state.isMounted {state.isMounted
&& &&
<> <>
{renderStyle()} {renderStyle()}
<div className='pages' lang={`${props.lang || 'en'}`}> <div className='pages' lang={`${props.lang || 'en'}`} style={{ zoom: `${state.zoom}%` }}>
{renderPages()} {renderPages()}
</div> </div>
</> </>
} }
</div> </div>
</Frame> </Frame>
{renderPageInfo()}
</> </>
); );
}; };

View File

@@ -1,8 +1,9 @@
@import (multiple, less) 'shared/naturalcrit/styles/reset.less'; @import (multiple, less) 'shared/naturalcrit/styles/reset.less';
.brewRenderer { .brewRenderer {
will-change : transform; overflow-y : scroll;
overflow-y : scroll; will-change : transform;
padding-top : 30px;
:where(.pages) { :where(.pages) {
margin : 30px 0px; margin : 30px 0px;
& > :where(.page) { & > :where(.page) {
@@ -14,66 +15,31 @@
box-shadow : 1px 4px 14px #000000; box-shadow : 1px 4px 14px #000000;
} }
} }
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 20px; width : 20px;
&:horizontal{ &:horizontal {
height: 20px; width : auto;
width:auto; height : 20px;
} }
&-thumb { &-thumb {
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px); background : linear-gradient(90deg, #D3C1AF 15px, #00000000 15px);
&:horizontal{ &:horizontal { background : linear-gradient(0deg, #D3C1AF 15px, #00000000 15px); }
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
}
}
&-corner {
visibility: hidden;
} }
&-corner { visibility : hidden; }
} }
} }
.pane { position : relative; } .pane { position : relative; }
.pageInfo {
position : absolute;
right : 17px;
bottom : 0;
z-index : 1000;
font-size : 10px;
font-weight : 800;
color : white;
background-color : #333333;
div {
display : inline-block;
padding : 8px 10px;
&:not(:last-child) { border-right : 1px solid #666666; }
}
}
.ppr_msg {
position : absolute;
bottom : 0;
left : 0px;
z-index : 1000;
padding : 8px 10px;
font-size : 10px;
font-weight : 800;
color : white;
background-color : #333333;
}
@media print { @media print {
.toolBar { display : none; }
.brewRenderer { .brewRenderer {
height: 100%; height : 100%;
overflow-y: unset; padding-top : unset;
overflow-y : unset;
.pages { .pages {
margin: 0px; margin : 0px;
&>.page { & > .page { box-shadow : unset; }
box-shadow: unset;
}
} }
} }
} }

View File

@@ -4,7 +4,7 @@ const _ = require('lodash');
import Dialog from '../../../components/dialog.jsx'; import Dialog from '../../../components/dialog.jsx';
const DISMISS_KEY = 'dismiss_notification12-04-23'; const DISMISS_KEY = 'dismiss_notification04-09-24';
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />; const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
const NotificationPopup = ()=>{ const NotificationPopup = ()=>{
@@ -15,11 +15,12 @@ const NotificationPopup = ()=>{
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small> <small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
</div> </div>
<ul> <ul>
<li key='psa'> <li key='Vault'>
<em>Don't store IMAGES in Google Drive</em><br /> <em>Search brews with our new page!</em><br />
Google Drive is not an image service, and will block images from being used We have been working very hard in making this possible, now you can share your work and look at it in the new <a href="/vault">Vault</a> page!
in brews if they get more views than expected. Google has confirmed they won't fix All PUBLISHED brews will be available to anyone searching there, by title or author, and filtering by renderer.
this, so we recommend you look for another image hosting service such as imgur, ImgBB or Google Photos.
More features will be coming.
</li> </li>
<li key='googleDriveFolder'> <li key='googleDriveFolder'>

View File

@@ -1,9 +1,10 @@
.popups { .popups {
position : fixed; position : fixed;
top : @navbarHeight; top : calc(@navbarHeight + @viewerToolsHeight);
right : 24px; right : 24px;
z-index : 10001; z-index : 10001;
width : 450px; width : 450px;
margin-top : 5px;
} }
.notificationPopup { .notificationPopup {

View File

@@ -0,0 +1,162 @@
require('./toolBar.less');
const React = require('react');
const { useState, useEffect } = React;
const _ = require('lodash');
const MAX_ZOOM = 300;
const MIN_ZOOM = 10;
const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
const [zoomLevel, setZoomLevel] = useState(100);
const [pageNum, setPageNum] = useState(currentPage);
useEffect(()=>{
onZoomChange(zoomLevel);
}, [zoomLevel]);
useEffect(()=>{
setPageNum(currentPage);
}, [currentPage]);
const handleZoomButton = (zoom)=>{
setZoomLevel(_.round(_.clamp(zoom, MIN_ZOOM, MAX_ZOOM)));
};
const handlePageInput = (pageInput)=>{
if(/[0-9]/.test(pageInput))
setPageNum(parseInt(pageInput)); // input type is 'text', so `page` comes in as a string, not number.
};
const scrollToPage = (pageNumber)=>{
pageNumber = _.clamp(pageNumber, 1, totalPages);
const iframe = document.getElementById('BrewRenderer');
const brewRenderer = iframe?.contentWindow?.document.querySelector('.brewRenderer');
const page = brewRenderer?.querySelector(`#p${pageNumber}`);
page?.scrollIntoView({ block: 'start' });
setPageNum(pageNumber);
};
const calculateChange = (mode)=>{
const iframe = document.getElementById('BrewRenderer');
const iframeWidth = iframe.getBoundingClientRect().width;
const iframeHeight = iframe.getBoundingClientRect().height;
const pages = iframe.contentWindow.document.getElementsByClassName('page');
let desiredZoom = 0;
if(mode == 'fill'){
// find widest page, in case pages are different widths, so that the zoom is adapted to not cut the widest page off screen.
const widestPage = _.maxBy([...pages], 'offsetWidth').offsetWidth;
desiredZoom = (iframeWidth / widestPage) * 100;
} else if(mode == 'fit'){
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
const minDimRatio = [...pages].reduce((minRatio, page) => Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
desiredZoom = minDimRatio * 100;
}
const margin = 5; // extra space so page isn't edge to edge (not truly "to fill")
const deltaZoom = (desiredZoom - zoomLevel) - margin;
return deltaZoom;
};
return (
<div className='toolBar'>
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
<div className='group'>
<button
id='fill-width'
className='tool'
onClick={()=>handleZoomButton(zoomLevel + calculateChange('fill'))}
>
<i className='fac fit-width' />
</button>
<button
id='zoom-to-fit'
className='tool'
onClick={()=>handleZoomButton(zoomLevel + calculateChange('fit'))}
>
<i className='fac zoom-to-fit' />
</button>
<button
id='zoom-out'
className='tool'
onClick={()=>handleZoomButton(zoomLevel - 20)}
disabled={zoomLevel <= MIN_ZOOM}
>
<i className='fas fa-magnifying-glass-minus' />
</button>
<input
id='zoom-slider'
className='range-input tool'
type='range'
name='zoom'
list='zoomLevels'
min={MIN_ZOOM}
max={MAX_ZOOM}
step='1'
value={zoomLevel}
onChange={(e)=>handleZoomButton(parseInt(e.target.value))}
/>
<datalist id='zoomLevels'>
<option value='100' />
</datalist>
<button
id='zoom-in'
className='tool'
onClick={()=>handleZoomButton(zoomLevel + 20)}
disabled={zoomLevel >= MAX_ZOOM}
>
<i className='fas fa-magnifying-glass-plus' />
</button>
</div>
{/*v=====----------------------< Page Controls >---------------------=====v*/}
<div className='group'>
<button
id='previous-page'
className='previousPage tool'
onClick={()=>scrollToPage(pageNum - 1)}
disabled={pageNum <= 1}
>
<i className='fas fa-arrow-left'></i>
</button>
<div className='tool'>
<input
id='page-input'
className='text-input'
type='text'
name='page'
inputMode='numeric'
pattern='[0-9]'
value={pageNum}
onClick={(e)=>e.target.select()}
onChange={(e)=>handlePageInput(e.target.value)}
onBlur={()=>scrollToPage(pageNum)}
onKeyDown={(e)=>e.key == 'Enter' && scrollToPage(pageNum)}
/>
<span id='page-count'>/ {totalPages}</span>
</div>
<button
id='next-page'
className='tool'
onClick={()=>scrollToPage(pageNum + 1)}
disabled={pageNum >= totalPages}
>
<i className='fas fa-arrow-right'></i>
</button>
</div>
</div>
);
};
module.exports = ToolBar;

View File

@@ -0,0 +1,103 @@
@import (less) './client/icons/customIcons.less';
.toolBar {
position : absolute;
z-index : 1;
box-sizing : border-box;
display : flex;
flex-wrap : wrap;
gap : 8px 30px;
align-items : center;
justify-content : center;
width : 100%;
height : auto;
padding : 2px 0;
font-family : 'Open Sans', sans-serif;
color : #CCCCCC;
background-color : #555555;
.group {
box-sizing : border-box;
display : flex;
gap : 0 3px;
align-items : center;
justify-content : center;
height : 28px;
}
.tool {
display : flex;
align-items : center;
}
input {
position : relative;
height : 1.5em;
padding : 2px 5px;
font-family : 'Open Sans', sans-serif;
color : #000000;
background : #EEEEEE;
border : 1px solid gray;
&:focus { outline : 1px solid #D3D3D3; }
// `.range-input` if generic to all range inputs, or `#zoom-slider` if only for zoom slider
&.range-input {
padding : 2px 0;
color : #D3D3D3;
accent-color : #D3D3D3;
&::-webkit-slider-thumb, &::-moz-slider-thumb {
width : 5px;
height : 5px;
cursor : pointer;
outline : none;
}
&:hover::after {
position : absolute;
bottom : -30px;
left : 50%;
z-index : 1;
display : grid;
place-items : center;
width : 4ch;
height : 1.2lh;
pointer-events : none;
content : attr(value);
background-color : #555555;
border : 1px solid #A1A1A1;
transform : translate(-50%, 50%);
}
}
// `.text-input` if generic to all range inputs, or `#page-input` if only for current page input
&#page-input {
width : 4ch;
margin-right : 1ch;
text-align : center;
}
}
button {
box-sizing : content-box;
display : flex;
align-items : center;
justify-content : center;
width : auto;
min-width : 46px;
height : 100%;
padding : 0 0px;
font-weight : unset;
color : inherit;
background-color : unset;
&:hover { background-color : #444444; }
&:focus { outline : 1px solid #D3D3D3; }
&:disabled {
color : #777777;
background-color : unset !important;
}
i {
font-size:1.2em;
}
}
}

View File

@@ -59,6 +59,8 @@ const Editor = createClass({
this.updateEditorSize(); this.updateEditorSize();
this.highlightCustomMarkdown(); this.highlightCustomMarkdown();
window.addEventListener('resize', this.updateEditorSize); window.addEventListener('resize', this.updateEditorSize);
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
document.addEventListener('keydown', this.handleControlKeys);
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
if(editorTheme) { if(editorTheme) {
@@ -82,6 +84,19 @@ const Editor = createClass({
}; };
}, },
handleControlKeys : function(e){
if(!(e.ctrlKey && e.metaKey)) return;
const LEFTARROW_KEY = 37;
const RIGHTARROW_KEY = 39;
if (e.shiftKey && (e.keyCode == RIGHTARROW_KEY)) this.brewJump();
if (e.shiftKey && (e.keyCode == LEFTARROW_KEY)) this.sourceJump();
if ((e.keyCode == LEFTARROW_KEY) || (e.keyCode == RIGHTARROW_KEY)) {
e.stopPropagation();
e.preventDefault();
}
},
updateEditorSize : function() { updateEditorSize : function() {
if(this.codeEditor.current) { if(this.codeEditor.current) {
let paneHeight = this.editor.current.parentNode.clientHeight; let paneHeight = this.editor.current.parentNode.clientHeight;
@@ -98,7 +113,10 @@ const Editor = createClass({
this.props.setMoveArrows(newView === 'text'); this.props.setMoveArrows(newView === 'text');
this.setState({ this.setState({
view : newView view : newView
}, this.updateEditorSize); //TODO: not sure if updateeditorsize needed }, ()=>{
this.codeEditor.current?.codeMirror.focus();
this.updateEditorSize();
}); //TODO: not sure if updateeditorsize needed
}, },
getCurrentPage : function(){ getCurrentPage : function(){
@@ -119,8 +137,19 @@ const Editor = createClass({
const codeMirror = this.codeEditor.current.codeMirror; const codeMirror = this.codeEditor.current.codeMirror;
codeMirror.operation(()=>{ // Batch CodeMirror styling codeMirror.operation(()=>{ // Batch CodeMirror styling
const foldLines = [];
//reset custom text styles //reset custom text styles
const customHighlights = codeMirror.getAllMarks().filter((mark)=>!mark.__isFold); //Don't undo code folding const customHighlights = codeMirror.getAllMarks().filter((mark)=>{
// Record details of folded sections
if(mark.__isFold) {
const fold = mark.find();
foldLines.push({from: fold.from?.line, to: fold.to?.line});
}
return !mark.__isFold;
}); //Don't undo code folding
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear(); for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
let editorPageCount = 2; // start page count from page 2 let editorPageCount = 2; // start page count from page 2
@@ -132,6 +161,11 @@ const Editor = createClass({
codeMirror.removeLineClass(lineNumber, 'text'); codeMirror.removeLineClass(lineNumber, 'text');
codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash'); codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
// Don't process lines inside folded text
// If the current lineNumber is inside any folded marks, skip line styling
if (foldLines.some(fold => lineNumber >= fold.from && lineNumber <= fold.to))
return;
// Styling for \page breaks // Styling for \page breaks
if((this.props.renderer == 'legacy' && line.includes('\\page')) || if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
(this.props.renderer == 'V3' && line.match(/^\\page$/))) { (this.props.renderer == 'V3' && line.match(/^\\page$/))) {
@@ -244,7 +278,7 @@ const Editor = createClass({
// Iterate over conflicting marks and clear them // Iterate over conflicting marks and clear them
var marks = codeMirror.findMarks(startPos, endPos); var marks = codeMirror.findMarks(startPos, endPos);
marks.forEach(function(marker) { marks.forEach(function(marker) {
marker.clear(); if(!marker.__isFold) marker.clear();
}); });
codeMirror.markText(startPos, endPos, { className: 'emoji' }); codeMirror.markText(startPos, endPos, { className: 'emoji' });
} }
@@ -367,7 +401,7 @@ const Editor = createClass({
view={this.state.view} view={this.state.view}
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT} value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onStyleChange} onChange={this.props.onStyleChange}
enableFolding={false} enableFolding={true}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} /> rerenderParent={this.rerenderParent} />
</>; </>;
@@ -381,7 +415,8 @@ const Editor = createClass({
<MetadataEditor <MetadataEditor
metadata={this.props.brew} metadata={this.props.brew}
onChange={this.props.onMetaChange} onChange={this.props.onMetaChange}
reportError={this.props.reportError}/> reportError={this.props.reportError}
userThemes={this.props.userThemes}/>
</>; </>;
} }
}, },
@@ -424,6 +459,7 @@ const Editor = createClass({
historySize={this.historySize()} historySize={this.historySize()}
currentEditorTheme={this.state.editorTheme} currentEditorTheme={this.state.editorTheme}
updateEditorTheme={this.updateEditorTheme} updateEditorTheme={this.updateEditorTheme}
snippetBundle={this.props.snippetBundle}
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} /> cursorPos={this.codeEditor.current?.getCursorPosition() || {}} />
{this.renderEditor()} {this.renderEditor()}

View File

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

View File

@@ -1,327 +1,309 @@
@import 'naturalcrit/styles/colors.less'; @import 'naturalcrit/styles/colors.less';
.metadataEditor{ .metadataEditor {
position : absolute; position : absolute;
z-index : 5; z-index : 5;
box-sizing : border-box; box-sizing : border-box;
width : 100%; width : 100%;
padding : 25px;
background-color : #999;
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this. height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
padding : 25px;
overflow-y : auto; overflow-y : auto;
background-color : #999999;
.sectionHead { .sectionHead {
font-weight: 1000; margin : 20px 0;
margin: 20px 0; font-weight : 1000;
&:first-of-type { &:first-of-type { margin-top : 0; }
margin-top: 0;
}
} }
& > div { & > div { margin-bottom : 10px; }
margin-bottom: 10px;
}
.field-group { .field-group {
display: flex; display : flex;
width: 100%; flex-wrap : wrap;
flex-wrap: wrap; gap : 10px;
gap: 10px; width : 100%;
} }
.field-column { .field-column {
display: flex; display : flex;
flex-direction: column; flex : 5 0 200px;
flex: 5 0 200px; flex-direction : column;
gap: 10px; gap : 10px;
} }
.field{ .field {
position : relative;
display : flex; display : flex;
flex-wrap : wrap; flex-wrap : wrap;
width : 100%; width : 100%;
min-width : 200px; min-width : 200px;
position : relative; & > label {
&>label{
width : 80px; width : 80px;
font-size : 11px; font-size : 11px;
font-weight : 800; font-weight : 800;
line-height : 1.8em; line-height : 1.8em;
text-transform : uppercase; text-transform : uppercase;
} }
&>.value{ & > .value {
flex : 1 1 auto; flex : 1 1 auto;
width : 50px; width : 50px;
&:invalid { &:invalid { background : #FFB9B9; }
background : #ffb9b9;
}
} }
input[type='text'], textarea { input[type='text'], textarea {
border : 1px solid gray; border : 1px solid gray;
&:focus { &:focus { outline : 1px solid #444444; }
outline: 1px solid #444;
}
} }
&.thumbnail{ &.thumbnail {
height : 1.4em; height : 1.4em;
label{ label { line-height : 2.0em; }
line-height: 2.0em; .value {
overflow : hidden;
text-overflow : ellipsis;
} }
.value{ button {
overflow: hidden; padding : 0px 5px;
text-overflow: ellipsis; color : white;
} background-color : black;
button{ border : 1px solid #999999;
border: 1px solid #999; &:hover { background-color : #777777; }
color: white;
padding: 0px 5px;
background-color: black;
&:hover{
background-color: #777;
}
} }
} }
&.description { &.description {
flex: 1; flex : 1;
textarea.value { textarea.value {
resize : none;
height : auto; height : auto;
font-family : 'Open Sans', sans-serif; font-family : 'Open Sans', sans-serif;
font-size : 0.8em; font-size : 0.8em;
resize : none;
} }
} }
&.language .language-dropdown { &.language .language-dropdown {
max-width : 150px;
z-index : 200; z-index : 200;
max-width : 150px;
} }
small { small {
display : inline-block;
font-size : 0.6em; font-size : 0.6em;
font-style : italic; font-style : italic;
line-height : 1.4em; line-height : 1.4em;
display : inline-block;
} }
} }
.thumbnail-preview { .thumbnail-preview {
position: relative; position : relative;
justify-self: center; flex : 1 1;
width: 80px; justify-self : center;
height: min-content; width : 80px;
flex: 1 1; height : min-content;
max-height: 115px; max-height : 115px;
aspect-ratio: 1 / 1; aspect-ratio : 1 / 1;
object-fit: contain; object-fit : contain;
background-color: #AAA; background-color : #AAAAAA;
} }
.systems.field .value{ .systems.field .value {
label{ label {
vertical-align : middle;
margin-right : 15px;
cursor : pointer;
font-size : 0.7em;
font-weight : 800;
user-select : none;
white-space : nowrap;
display : inline-flex; display : inline-flex;
align-items : center; align-items : center;
} margin-right : 15px;
a { font-size : 0.7em;
font-size : 0.7em; font-weight : 800;
font-weight : 800; white-space : nowrap;
display : inline-flex;
}
input{
vertical-align : middle; vertical-align : middle;
cursor : pointer; cursor : pointer;
user-select : none;
}
a {
display : inline-flex;
font-size : 0.7em;
font-weight : 800;
}
input {
margin : 3px; margin : 3px;
vertical-align : middle;
cursor : pointer;
} }
} }
.publish.field .value{ .publish.field .value {
position : relative; position : relative;
margin-bottom: 15px; margin-bottom : 15px;
button{ button { width : 100%; }
width:100%; button.publish {
}
button.publish{
.button(@blueLight); .button(@blueLight);
} }
button.unpublish{ button.unpublish {
.button(@silver); .button(@silver);
} }
} }
.delete.field .value{ .delete.field .value {
button{ button {
.button(@red); .button(@red);
} }
} }
.authors.field .value{ .authors.field .value {
font-size: 0.8em; font-size : 0.8em;
line-height : 1.5em; line-height : 1.5em;
} }
.themes.field{ .themes.field {
font-size : 13.33px; font-size : 13.33px;
.navDropdownContainer { .navDropdownContainer {
background-color : white; position : relative;
position : relative; z-index : 100;
z-index : 100; background-color : white;
&.disabled { &.disabled {
font-style :italic; font-style : italic;
font-style : italic; color : dimgray;
background-color : darkgray; background-color : darkgray;
color : dimgray;
} }
&>div:first-child { & > div:first-child {
border : 2px solid rgb(118,118,118); padding : 6px 3px;
padding : 6px 3px; background-color : inherit;
background-color : inherit; border : 2px solid rgb(118,118,118);
i { i { float : right; }
float : right;
}
&:hover { &:hover {
background-color : @blue; color : white;
color : white; background-color : @blue;
} }
} }
.navDropdown .item > p {
width : 45%;
height : 1.1em;
overflow : hidden;
text-overflow : ellipsis;
white-space : nowrap;
}
.navDropdown { .navDropdown {
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
position : absolute; position : absolute;
width : 100%; width : 100%;
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
.item { .item {
padding : 3px 3px;
border-top : 1px solid rgb(118, 118, 118);
position : relative; position : relative;
padding : 3px 3px;
overflow : visible; overflow : visible;
background-color : white; background-color : white;
border-top : 1px solid rgb(118, 118, 118);
.preview { .preview {
display : flex; position : absolute;
flex-direction: column; top : 0;
background : #ccc; right : 0;
border-radius : 5px; z-index : 1;
box-shadow : 0 0 5px black; display : flex;
width : 200px; flex-direction : column;
color :black; width : 200px;
position : absolute; overflow : hidden;
top : 0; color : black;
right : 0; background : #CCCCCC;
opacity : 0; border-radius : 5px;
transition : opacity 250ms ease; box-shadow : 0 0 5px black;
z-index : 1; opacity : 0;
overflow :hidden; transition : opacity 250ms ease;
h6 { h6 {
font-weight : 900; padding-block : 0.5em;
padding-inline:1em; padding-inline : 1em;
padding-block :.5em; font-weight : 900;
border-bottom :2px solid hsl(0,0%,40%); border-bottom : 2px solid hsl(0,0%,40%);
} }
} }
&:hover { &:hover {
background-color : @blue; color : white;
color : white; background-color : @blue;
} }
&:hover > .preview { &:hover > .preview { opacity : 1; }
opacity: 1; .texture-container {
} position : absolute;
>img { top : 0;
mask-image : linear-gradient(90deg, transparent, black 20%); left : 0;
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%); width : 100%;
position : absolute; height : 100%;
right : 0; min-height : 100%;
top : 0px; overflow : hidden;
width : 50%; > img {
height : 100%; position : absolute;
top : 0px;
right : 0;
width : 50%;
min-height : 100%;
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
mask-image : linear-gradient(90deg, transparent, black 20%);
}
} }
} }
} }
} }
} }
.field .list { .field .list {
display: flex; display : flex;
flex: 1 0; flex : 1 0;
flex-wrap: wrap; flex-wrap : wrap;
> * { > * { flex : 0 0 auto; }
flex: 0 0 auto;
}
#groupedIcon { #groupedIcon {
#backgroundColors; #backgroundColors;
display: inline-block; position : relative;
height: ~"calc(100% + 0.6em)"; top : -0.3em;
position: relative; right : -0.3em;
top: -0.3em; display : inline-block;
right: -0.3em; min-width : 20px;
cursor: pointer; height : ~'calc(100% + 0.6em)';
min-width: 20px; color : white;
text-align: center; text-align : center;
color: white; cursor : pointer;
i { i {
position: relative; position : relative;
top: 50%; top : 50%;
transform: translateY(-50%); transform : translateY(-50%);
} }
&:not(:last-child) { &:not(:last-child) { border-right : 1px solid black; }
border-right: 1px solid black;
}
&:last-child { &:last-child { border-radius : 0 0.5em 0.5em 0; }
border-radius: 0 0.5em 0.5em 0;
}
} }
.badge { .badge {
background-color: #dddddd; padding : 0.3em;
border-radius: .5em; margin : 2px;
font-size: .9em; font-size : 0.9em;
margin: 2px; background-color : #DDDDDD;
padding: .3em; border-radius : 0.5em;
.icon { .icon {
#groupedIcon #groupedIcon; }
}
} }
.input-group { .input-group {
height: ~"calc(.9em + 4px + .6em)"; height : ~'calc(.9em + 4px + .6em)';
input { input { border-radius : 0.5em 0 0 0.5em; }
border-radius: .5em 0 0 .5em;
}
input:last-child { input:last-child { border-radius : 0.5em; }
border-radius: .5em;
}
.value { .value {
width: 7.5vw; width : 7.5vw;
min-width: 75px; min-width : 75px;
height: 100%; height : 100%;
} }
.invalid:focus { .invalid:focus { background-color : pink; }
background-color: pink;
}
.icon { .icon {
#groupedIcon; #groupedIcon;
height: 97%; top : -0.54em;
font-size: .8em; right : 1px;
right: 1px; height : 97%;
top: -.54em; font-size : 0.8em;
i { i { font-size : 1.125em; }
font-size: 1.125em;
}
} }
} }
} }

View File

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

View File

@@ -10,6 +10,7 @@ const UserPage = require('./pages/userPage/userPage.jsx');
const SharePage = require('./pages/sharePage/sharePage.jsx'); const SharePage = require('./pages/sharePage/sharePage.jsx');
const NewPage = require('./pages/newPage/newPage.jsx'); const NewPage = require('./pages/newPage/newPage.jsx');
const ErrorPage = require('./pages/errorPage/errorPage.jsx'); const ErrorPage = require('./pages/errorPage/errorPage.jsx');
const VaultPage = require('./pages/vaultPage/vaultPage.jsx');
const AccountPage = require('./pages/accountPage/accountPage.jsx'); const AccountPage = require('./pages/accountPage/accountPage.jsx');
const WithRoute = (props)=>{ const WithRoute = (props)=>{
@@ -66,13 +67,15 @@ const Homebrew = createClass({
<Router location={this.props.url}> <Router location={this.props.url}>
<div className='homebrew'> <div className='homebrew'>
<Routes> <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='/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/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
<Route path='/new' element={<WithRoute el={NewPage}/>} /> <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='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} /> <Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} /> <Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
<Route path='/migrate' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} accountDetails={this.props.brew.accountDetails} />} /> <Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} accountDetails={this.props.brew.accountDetails} />} />
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} /> <Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} /> <Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />

View File

@@ -104,6 +104,18 @@ const ErrorNavItem = createClass({
</Nav.item>; </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'> return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops! Oops!
<div className='errorContainer'> <div className='errorContainer'>

View File

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

View File

@@ -1,6 +1,7 @@
@import 'naturalcrit/styles/colors.less'; @import 'naturalcrit/styles/colors.less';
@navbarHeight : 28px; @navbarHeight : 28px;
@viewerToolsHeight : 32px;
@keyframes pinkColoring { @keyframes pinkColoring {
0% { color : pink; } 0% { color : pink; }

View File

@@ -0,0 +1,17 @@
const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function (props) {
return (
<Nav.item
color='purple'
icon='fas fa-dungeon'
href='/vault'
newTab={false}
rel='noopener noreferrer'
>
Vault
</Nav.item>
);
};

View File

@@ -19,7 +19,8 @@ const BrewItem = createClass({
stubbed : true stubbed : true
}, },
updateListFilter : ()=>{}, updateListFilter : ()=>{},
reportError : ()=>{} reportError : ()=>{},
renderStorage : true
}; };
}, },
@@ -95,6 +96,7 @@ const BrewItem = createClass({
}, },
renderStorageIcon : function(){ renderStorageIcon : function(){
if(!this.props.renderStorage) return;
if(this.props.brew.googleId) { if(this.props.brew.googleId) {
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}> return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
<a href={this.props.brew.webViewLink} target='_blank'> <a href={this.props.brew.webViewLink} target='_blank'>
@@ -142,10 +144,14 @@ const BrewItem = createClass({
} }
<span title={`Authors:\n${brew.authors?.join('\n')}`}> <span title={`Authors:\n${brew.authors?.join('\n')}`}>
<i className='fas fa-user'/> {brew.authors?.map((author, index)=>( <i className='fas fa-user'/> {brew.authors?.map((author, index)=>(
<> <React.Fragment key={index}>
<a key={index} href={`/user/${author}`}>{author}</a> {author === 'hidden'
{index < brew.authors.length - 1 && ', '} ? <span title="Username contained an email address; hidden to protect user's privacy">{author}</span>
</>))} : <a href={`/user/${author}`}>{author}</a>
}
{index < brew.authors.length - 1 && ', '}
</React.Fragment>
))}
</span> </span>
<br /> <br />
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}> <span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>

View File

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

View File

@@ -2,6 +2,9 @@ const dedent = require('dedent-tabs').default;
const loginUrl = 'https://www.naturalcrit.com/login'; const loginUrl = 'https://www.naturalcrit.com/login';
//001-050 : Brew errors
//050-100 : Other pages errors
const errorIndex = (props)=>{ const errorIndex = (props)=>{
return { return {
// Default catch all // Default catch all
@@ -136,8 +139,29 @@ const errorIndex = (props)=>{
**Brew ID:** ${props.brew.brewId}`, **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}`,
//account page when account is not defined
'50' : dedent`
## You are not signed in
You are trying to access the account page, but are not signed in to an account.
Please login or signup at our [login page](https://www.naturalcrit.com/login?redirect=https://homebrewery.naturalcrit.com/account).`,
// Brew locked by Administrators error // Brew locked by Administrators error
'100' : dedent` '51' : dedent`
## This brew has been locked. ## This brew has been locked.
Only an author may request that this lock is removed. Only an author may request that this lock is removed.
@@ -147,7 +171,12 @@ const errorIndex = (props)=>{
**Brew ID:** ${props.brew.brewId} **Brew ID:** ${props.brew.brewId}
**Brew Title:** ${props.brew.brewTitle}`, **Brew Title:** ${props.brew.brewTitle}`,
'90' : dedent` An unexpected error occurred while looking for these brews.
Try again in a few minutes.`,
'91' : dedent` An unexpected error occurred while trying to get the total of brews.`,
}; };
}; };
module.exports = errorIndex; module.exports = errorIndex;

View File

@@ -10,10 +10,11 @@ const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx'); const Navbar = require('../../navbar/navbar.jsx');
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx'); const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx'); const HelpNavItem = require('../../navbar/help.navitem.jsx');
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const AccountNavItem = require('../../navbar/account.navitem.jsx'); const AccountNavItem = require('../../navbar/account.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx'); const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const { fetchThemeBundle } = require('../../../../shared/helpers.js');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx'); const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx'); const Editor = require('../../editor/editor.jsx');
@@ -34,12 +35,17 @@ const HomePage = createClass({
brew : this.props.brew, brew : this.props.brew,
welcomeText : this.props.brew.text, welcomeText : this.props.brew.text,
error : undefined, error : undefined,
currentEditorPage : 0 currentEditorPage : 0,
themeBundle : {}
}; };
}, },
editor : React.createRef(null), editor : React.createRef(null),
componentDidMount : function() {
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
},
handleSave : function(){ handleSave : function(){
request.post('/api') request.post('/api')
.send(this.state.brew) .send(this.state.brew)
@@ -70,6 +76,7 @@ const HomePage = createClass({
} }
<NewBrewItem /> <NewBrewItem />
<HelpNavItem /> <HelpNavItem />
<VaultNavItem />
<RecentNavItem /> <RecentNavItem />
<AccountNavItem /> <AccountNavItem />
</Nav.section> </Nav.section>
@@ -89,12 +96,14 @@ const HomePage = createClass({
onTextChange={this.handleTextChange} onTextChange={this.handleTextChange}
renderer={this.state.brew.renderer} renderer={this.state.brew.renderer}
showEditButtons={false} showEditButtons={false}
snippetBundle={this.state.themeBundle.snippets}
/> />
<BrewRenderer <BrewRenderer
text={this.state.brew.text} text={this.state.brew.text}
style={this.state.brew.style} style={this.state.brew.style}
renderer={this.state.brew.renderer} renderer={this.state.brew.renderer}
currentEditorPage={this.state.currentEditorPage} currentEditorPage={this.state.currentEditorPage}
themeBundle={this.state.themeBundle}
/> />
</SplitPane> </SplitPane>
</div> </div>

View File

@@ -19,7 +19,7 @@ const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js'); 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 BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style'; const STYLEKEY = 'homebrewery-new-style';
@@ -44,7 +44,8 @@ const NewPage = createClass({
saveGoogle : (global.account && global.account.googleId ? true : false), saveGoogle : (global.account && global.account.googleId ? true : false),
error : null, error : null,
htmlErrors : Markdown.validate(brew.text), htmlErrors : Markdown.validate(brew.text),
currentEditorPage : 0 currentEditorPage : 0,
themeBundle : {}
}; };
}, },
@@ -77,10 +78,15 @@ const NewPage = createClass({
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle) saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
}); });
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
localStorage.setItem(BREWKEY, brew.text); localStorage.setItem(BREWKEY, brew.text);
if(brew.style) if(brew.style)
localStorage.setItem(STYLEKEY, brew.style); localStorage.setItem(STYLEKEY, brew.style);
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang })); localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
if(window.location.pathname != '/new') {
window.history.replaceState({}, window.location.title, '/new/');
}
}, },
componentWillUnmount : function() { componentWillUnmount : function() {
document.removeEventListener('keydown', this.handleControlKeys); document.removeEventListener('keydown', this.handleControlKeys);
@@ -122,7 +128,10 @@ const NewPage = createClass({
localStorage.setItem(STYLEKEY, style); 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)=>({ this.setState((prevState)=>({
brew : { ...prevState.brew, ...metadata }, brew : { ...prevState.brew, ...metadata },
}), ()=>{ }), ()=>{
@@ -142,8 +151,6 @@ const NewPage = createClass({
isSaving : true isSaving : true
}); });
console.log('saving new brew');
let brew = this.state.brew; let brew = this.state.brew;
// Split out CSS to Style if CSS codefence exists // Split out CSS to Style if CSS codefence exists
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) { if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
@@ -153,12 +160,10 @@ const NewPage = createClass({
} }
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
const res = await request const res = await request
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`) .post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
.send(brew) .send(brew)
.catch((err)=>{ .catch((err)=>{
console.log(err);
this.setState({ isSaving: false, error: err }); this.setState({ isSaving: false, error: err });
}); });
if(!res) return; if(!res) return;
@@ -214,12 +219,15 @@ const NewPage = createClass({
onStyleChange={this.handleStyleChange} onStyleChange={this.handleStyleChange}
onMetaChange={this.handleMetaChange} onMetaChange={this.handleMetaChange}
renderer={this.state.brew.renderer} renderer={this.state.brew.renderer}
userThemes={this.props.userThemes}
snippetBundle={this.state.themeBundle.snippets}
/> />
<BrewRenderer <BrewRenderer
text={this.state.brew.text} text={this.state.brew.text}
style={this.state.brew.style} style={this.state.brew.style}
renderer={this.state.brew.renderer} renderer={this.state.brew.renderer}
theme={this.state.brew.theme} theme={this.state.brew.theme}
themeBundle={this.state.themeBundle}
errors={this.state.htmlErrors} errors={this.state.htmlErrors}
lang={this.state.brew.lang} lang={this.state.brew.lang}
currentEditorPage={this.state.currentEditorPage} currentEditorPage={this.state.currentEditorPage}

View File

@@ -12,18 +12,27 @@ const Account = require('../../navbar/account.navitem.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js'); const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
const { printCurrentBrew } = require('../../../../shared/helpers.js'); const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
const SharePage = createClass({ const SharePage = createClass({
displayName : 'SharePage', displayName : 'SharePage',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
brew : DEFAULT_BREW_LOAD brew : DEFAULT_BREW_LOAD,
disableMeta : false
};
},
getInitialState : function() {
return {
themeBundle : {}
}; };
}, },
componentDidMount : function() { componentDidMount : function() {
document.addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys);
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
}, },
componentWillUnmount : function() { componentWillUnmount : function() {
@@ -60,13 +69,21 @@ const SharePage = createClass({
}, },
render : function(){ render : function(){
const titleStyle = this.props.disableMeta ? { cursor: 'default' } : {};
const titleEl = <Nav.item className='brewTitle' style={titleStyle}>{this.props.brew.title}</Nav.item>;
return <div className='sharePage sitePage'> return <div className='sharePage sitePage'>
<Meta name='robots' content='noindex, nofollow' /> <Meta name='robots' content='noindex, nofollow' />
<Navbar> <Navbar>
<Nav.section className='titleSection'> <Nav.section className='titleSection'>
<MetadataNav brew={this.props.brew}> {
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item> this.props.disableMeta ?
</MetadataNav> titleEl
:
<MetadataNav brew={this.props.brew}>
{titleEl}
</MetadataNav>
}
</Nav.section> </Nav.section>
<Nav.section> <Nav.section>
@@ -99,6 +116,7 @@ const SharePage = createClass({
style={this.props.brew.style} style={this.props.brew.style}
renderer={this.props.brew.renderer} renderer={this.props.brew.renderer}
theme={this.props.brew.theme} theme={this.props.brew.theme}
themeBundle={this.state.themeBundle}
allowPrint={true} allowPrint={true}
/> />
</div> </div>

View File

@@ -12,6 +12,7 @@ const Account = require('../../navbar/account.navitem.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx'); const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx'); const HelpNavItem = require('../../navbar/help.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx'); const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const VaultNavitem = require('../../navbar/vault.navitem.jsx');
const UserPage = createClass({ const UserPage = createClass({
displayName : 'UserPage', displayName : 'UserPage',
@@ -66,6 +67,7 @@ const UserPage = createClass({
} }
<NewBrew /> <NewBrew />
<HelpNavItem /> <HelpNavItem />
<VaultNavitem/>
<RecentNavItem /> <RecentNavItem />
<Account /> <Account />
</Nav.section> </Nav.section>

View File

@@ -0,0 +1,396 @@
require('./vaultPage.less');
const React = require('react');
const { useState, useEffect, useRef } = React;
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const Account = require('../../navbar/account.navitem.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx');
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
const request = require('../../utils/request-middleware.js');
const VaultPage = (props)=>{
const [pageState, setPageState] = useState(parseInt(props.query.page) || 1);
//Response state
const [brewCollection, setBrewCollection] = useState(null);
const [totalBrews, setTotalBrews] = useState(null);
const [searching, setSearching] = useState(false);
const [error, setError] = useState(null);
const titleRef = useRef(null);
const authorRef = useRef(null);
const countRef = useRef(null);
const v3Ref = useRef(null);
const legacyRef = useRef(null);
const submitButtonRef = useRef(null);
useEffect(()=>{
disableSubmitIfFormInvalid();
loadPage(pageState, true);
}, []);
const updateStateWithBrews = (brews, page)=>{
setBrewCollection(brews || null);
setPageState(parseInt(page) || 1);
setSearching(false);
};
const updateUrl = (titleValue, authorValue, countValue, v3Value, legacyValue, page)=>{
const url = new URL(window.location.href);
const urlParams = new URLSearchParams(url.search);
urlParams.set('title', titleValue);
urlParams.set('author', authorValue);
urlParams.set('count', countValue);
urlParams.set('v3', v3Value);
urlParams.set('legacy', legacyValue);
urlParams.set('page', page);
url.search = urlParams.toString();
window.history.replaceState(null, '', url.toString());
};
const performSearch = async (title, author, count, v3, legacy, page)=>{
updateUrl(title, author, count, v3, legacy, page);
const response = await request.get(
`/api/vault?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}&count=${count}&page=${page}`
).catch((error)=>{
console.log('error at loadPage: ', error);
setError(error);
updateStateWithBrews([], 1);
});
if(response.ok)
updateStateWithBrews(response.body.brews, page);
};
const loadTotal = async (title, author, v3, legacy)=>{
setTotalBrews(null);
const response = await request.get(
`/api/vault/total?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}`
).catch((error)=>{
console.log('error at loadTotal: ', error);
setError(error);
updateStateWithBrews([], 1);
});
if(response.ok)
setTotalBrews(response.body.totalBrews);
};
const loadPage = async (page, updateTotal)=>{
if(!validateForm())
return;
setSearching(true);
setError(null);
const title = titleRef.current.value || '';
const author = authorRef.current.value || '';
const count = countRef.current.value || 10;
const v3 = v3Ref.current.checked != false;
const legacy = legacyRef.current.checked != false;
performSearch(title, author, count, v3, legacy, page);
if(updateTotal)
loadTotal(title, author, v3, legacy);
};
const renderNavItems = ()=>(
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>
Vault: Search for brews
</Nav.item>
</Nav.section>
<Nav.section>
<NewBrew />
<HelpNavItem />
<RecentNavItem />
<Account />
</Nav.section>
</Navbar>
);
const validateForm = ()=>{
//form validity: title or author must be written, and at least one renderer set
const isTitleValid = titleRef.current.validity.valid && titleRef.current.value;
const isAuthorValid = authorRef.current.validity.valid && authorRef.current.value;
const isCheckboxChecked = legacyRef.current.checked || v3Ref.current.checked;
const isFormValid = (isTitleValid || isAuthorValid) && isCheckboxChecked;
return isFormValid;
};
const disableSubmitIfFormInvalid = ()=>{
submitButtonRef.current.disabled = !validateForm();
};
const renderForm = ()=>(
<div className='brewLookup'>
<h2 className='formTitle'>Brew Lookup</h2>
<div className='formContents'>
<label>
Title of the brew
<input
ref={titleRef}
type='text'
name='title'
defaultValue={props.query.title || ''}
onKeyUp={disableSubmitIfFormInvalid}
pattern='.{3,}'
title='At least 3 characters'
onKeyDown={(e)=>{
if(e.key === 'Enter' && !submitButtonRef.current.disabled)
loadPage(1, true);
}}
placeholder='v3 Reference Document'
/>
</label>
<label>
Author of the brew
<input
ref={authorRef}
type='text'
name='author'
pattern='.{1,}'
defaultValue={props.query.author || ''}
onKeyUp={disableSubmitIfFormInvalid}
onKeyDown={(e)=>{
if(e.key === 'Enter' && !submitButtonRef.current.disabled)
loadPage(1, true);
}}
placeholder='Username'
/>
</label>
<label>
Results per page
<select ref={countRef} name='count' defaultValue={props.query.count || 20}>
<option value='10'>10</option>
<option value='20'>20</option>
<option value='40'>40</option>
<option value='60'>60</option>
</select>
</label>
<label>
<input
className='renderer'
ref={v3Ref}
type='checkbox'
defaultChecked={props.query.v3 !== 'false'}
onChange={disableSubmitIfFormInvalid}
/>
Search for v3 brews
</label>
<label>
<input
className='renderer'
ref={legacyRef}
type='checkbox'
defaultChecked={props.query.legacy !== 'false'}
onChange={disableSubmitIfFormInvalid}
/>
Search for legacy brews
</label>
<button
id='searchButton'
ref={submitButtonRef}
onClick={()=>{
loadPage(1, true);
}}
>
Search
<i
className={searching ? 'fas fa-spin fa-spinner': 'fas fa-search'}
/>
</button>
</div>
<legend>
<h3>Tips and tricks</h3>
<ul>
<li>
Only <b>published</b> brews are searchable via this tool
</li>
<li>
Usernames are case-sensitive
</li>
<li>
Use <code>"word"</code> to match an exact string,
and <code>-</code> to exclude words (at least one word must not be negated)
</li>
<li>
Some common words like "a", "after", "through", "itself", "here", etc.,
are ignored in searches. The full list can be found &nbsp;
<a href='https://github.com/mongodb/mongo/blob/0e3b3ca8480ddddf5d0105d11a94bd4698335312/src/mongo/db/fts/stop_words_english.txt'>
here
</a>
</li>
</ul>
<small>New features will be coming, such as filters and search by tags.</small>
</legend>
</div>
);
const renderPaginationControls = ()=>{
if(!totalBrews) return null;
const countInt = parseInt(props.query.count || 20);
const totalPages = Math.ceil(totalBrews / countInt);
let startPage, endPage;
if(pageState <= 6) {
startPage = 1;
endPage = Math.min(totalPages, 10);
} else if(pageState + 4 >= totalPages) {
startPage = Math.max(1, totalPages - 9);
endPage = totalPages;
} else {
startPage = pageState - 5;
endPage = pageState + 4;
}
const pagesAroundCurrent = new Array(endPage - startPage + 1)
.fill()
.map((_, index)=>(
<a
key={startPage + index}
className={`pageNumber ${
pageState === startPage + index ? 'currentPage' : ''
}`}
onClick={()=>loadPage(startPage + index, false)}
>
{startPage + index}
</a>
));
return (
<div className='paginationControls'>
<button
className='previousPage'
onClick={()=>loadPage(pageState - 1, false)}
disabled={pageState === startPage}
>
<i className='fa-solid fa-chevron-left'></i>
</button>
<ol className='pages'>
{startPage > 1 && (
<a
className='pageNumber firstPage'
onClick={()=>loadPage(1, false)}
>
1 ...
</a>
)}
{pagesAroundCurrent}
{endPage < totalPages && (
<a
className='pageNumber lastPage'
onClick={()=>loadPage(totalPages, false)}
>
... {totalPages}
</a>
)}
</ol>
<button
className='nextPage'
onClick={()=>loadPage(pageState + 1, false)}
disabled={pageState === totalPages}
>
<i className='fa-solid fa-chevron-right'></i>
</button>
</div>
);
};
const renderFoundBrews = ()=>{
if(searching) {
return (
<div className='foundBrews searching'>
<h3 className='searchAnim'>Searching</h3>
</div>
);
}
if(error) {
const errorText = ErrorIndex()[error.HBErrorCode.toString()] || '';
return (
<div className='foundBrews noBrews'>
<h3>Error: {errorText}</h3>
</div>
);
}
if(!brewCollection) {
return (
<div className='foundBrews noBrews'>
<h3>No search yet</h3>
</div>
);
}
if(brewCollection.length === 0) {
return (
<div className='foundBrews noBrews'>
<h3>No brews found</h3>
</div>
);
}
return (
<div className='foundBrews'>
<span className='totalBrews'>
{`Brews found: `}
<span>{totalBrews}</span>
</span>
{brewCollection.map((brew, index)=>{
return (
<BrewItem
brew={{ ...brew }}
key={index}
reportError={props.reportError}
renderStorage={false}
/>
);
})}
{renderPaginationControls()}
</div>
);
};
return (
<div className='vaultPage'>
<link href='/themes/V3/Blank/style.css' rel='stylesheet' />
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet' />
{renderNavItems()}
<div className='content'>
<SplitPane showDividerButtons={false}>
<div className='form dataGroup'>{renderForm()}</div>
<div className='resultsContainer dataGroup'>
{renderFoundBrews()}
</div>
</SplitPane>
</div>
</div>
);
};
module.exports = VaultPage;

View File

@@ -0,0 +1,362 @@
.vaultPage {
height : 100%;
overflow-y : hidden;
background-color : #2C3E50;
*:not(input) { user-select : none; }
.content {
background : #2C3E50;
height: 100%;
.dataGroup {
width : 100%;
height : 100%;
background : white;
&.form .brewLookup {
position : relative;
padding : 50px clamp(20px, 4vw, 50px);
small {
font-size : 10pt;
color : #555555;
a { color : #333333; }
}
code {
padding-inline : 5px;
background : lightgrey;
border-radius : 5px;
font-family : monospace;
}
h1, h2, h3, h4 {
font-family : 'CodeBold';
letter-spacing : 2px;
}
legend {
h3 {
margin-block : 30px 20px;
font-size : 20px;
text-align : center;
border-bottom : 2px solid;
}
ul {
padding-inline : 30px 10px;
li {
margin-block : 5px;
line-height : calc(1em + 5px);
list-style : disc;
}
}
}
&::after {
position : absolute;
top : 0;
right : 0;
left : 0;
display : block;
padding : 10px;
font-weight : 900;
color : white;
white-space : pre-wrap;
content : 'Error:\A At least one renderer should be enabled to make a search';
background : rgb(255, 60, 60);
opacity : 0;
transition : opacity 0.5s;
}
&:not(:has(input[type='checkbox']:checked))::after { opacity : 1; }
.formTitle {
margin : 20px 0;
font-size : 30px;
color : black;
text-align : center;
border-bottom : 2px solid;
}
.formContents {
position : relative;
display : flex;
flex-direction : column;
label {
display : flex;
align-items : center;
margin : 10px 0;
}
select { margin : 0 10px; }
input {
margin : 0 10px;
&:invalid { background : rgb(255, 188, 181); }
&[type='checkbox'] {
position : relative;
display : inline-block;
width : 50px;
height : 30px;
font-family : 'WalterTurncoat';
font-size : 20px;
font-weight : 800;
color : white;
letter-spacing : 2px;
appearance : none;
background : red;
isolation : isolate;
border-radius : 5px;
&::before,&::after {
position : absolute;
inset : 0;
z-index : 5;
padding-top : 2px;
text-align : center;
}
&::before {
display : block;
content : 'No';
}
&::after {
display : none;
content : 'Yes';
}
&:checked {
background : green;
&::before { display : none; }
&::after { display : block; }
}
}
}
#searchButton {
position : absolute;
right : 20px;
bottom : 0;
i {
margin-left : 10px;
animation-duration : 1000s;
}
}
}
}
&.resultsContainer {
display : flex;
flex-direction : column;
height : 100%;
overflow-y : auto;
font-family : 'BookInsanityRemake';
font-size : 0.34cm;
h3 {
font-family : 'Open Sans';
font-weight : 900;
color : white;
}
.foundBrews {
position : relative;
width : 100%;
height : 100%;
max-height : 100%;
padding : 50px 50px 70px 50px;
overflow-y : scroll;
background-color : #2C3E50;
h3 { font-size : 25px; }
&.noBrews {
display : grid;
place-items : center;
color : white;
}
&.searching {
display : grid;
place-items : center;
color : white;
h3 { position : relative; }
h3.searchAnim::after {
position : absolute;
top : 50%;
right : 0;
width : max-content;
height : 1em;
content : '';
translate : calc(100% + 5px) -50%;
animation : trailingDots 2s ease infinite;
}
}
.totalBrews {
position : fixed;
right : 0;
bottom : 0;
z-index : 1000;
padding : 8px 10px;
font-family : 'Open Sans';
font-size : 11px;
font-weight : 800;
color : white;
background-color : #333333;
.searchAnim {
position : relative;
display : inline-block;
width : 3ch;
height : 1em;
}
.searchAnim::after {
position : absolute;
top : 50%;
right : 0;
width : max-content;
height : 1em;
content : '';
translate : -50% -50%;
animation : trailingDots 2s ease infinite;
}
}
.brewItem {
width : 47%;
margin-right : 40px;
color : black;
isolation:isolate;
&:after {
position:absolute;
inset:0;
display:block;
content:'';
background-image : url('/assets/parchmentBackground.jpg');
z-index:-1;
}
&:nth-child(even of .brewItem) { margin-right : 0; }
h2 {
font-family : 'MrEavesRemake';
font-size : 0.75cm;
font-weight : 800;
line-height : 0.988em;
color : var(--HB_Color_HeaderText);
}
.info {
font-family : 'ScalySansRemake';
font-size : 1.2em;
position:relative;
z-index:2;
>span {
margin-right : 12px;
line-height : 1.5em;
}
}
.links {
z-index:2;
}
hr {
margin: 0px;
visibility: hidden;
}
.thumbnail {
z-index:1;
}
}
.paginationControls {
position : absolute;
left : 50%;
display : grid;
grid-template-areas : 'previousPage currentPage nextPage';
grid-template-columns : 50px 1fr 50px;
place-items : center;
width : auto;
translate : -50%;
.pages {
display : flex;
grid-area : currentPage;
justify-content : space-evenly;
width : 100%;
height : 100%;
padding : 5px 8px;
text-align : center;
.pageNumber {
margin-inline : 1vw;
font-family : 'Open Sans';
font-weight : 900;
color : white;
text-underline-position : under;
text-wrap : nowrap;
cursor : pointer;
&.currentPage {
color : gold;
text-decoration : underline;
pointer-events : none;
}
&.firstPage { margin-right : -5px; }
&.lastPage { margin-left : -5px; }
}
}
button {
width : max-content;
&.previousPage { grid-area : previousPage; }
&.nextPage { grid-area : nextPage; }
}
}
}
}
}
}
}
@keyframes trailingDots {
0%,
32% { content : ' .'; }
33%,
65% { content : ' ..'; }
66%,
100% { content : ' ...'; }
}
// media query for when the page is smaller than 1079 px in width
@media screen and (max-width : 1079px) {
.vaultPage .content {
.dataGroup.form .brewLookup { padding : 1px 20px 20px 10px; }
.dataGroup.resultsContainer .foundBrews .brewItem {
width : 100%;
margin-inline : auto;
}
}
}

View File

@@ -1,57 +1,75 @@
.fac { .fac {
display : inline-block; display : inline-block;
background-color : currentColor;
mask-size : contain;
mask-repeat : no-repeat;
mask-position : center;
width : 1em;
aspect-ratio : 1;
} }
.position-top-left { .position-top-left {
content: url('../icons/position-top-left.svg'); mask-image: url('../icons/position-top-left.svg');
} }
.position-top-right { .position-top-right {
content: url('../icons/position-top-right.svg'); mask-image: url('../icons/position-top-right.svg');
} }
.position-bottom-left { .position-bottom-left {
content: url('../icons/position-bottom-left.svg'); mask-image: url('../icons/position-bottom-left.svg');
} }
.position-bottom-right { .position-bottom-right {
content: url('../icons/position-bottom-right.svg'); mask-image: url('../icons/position-bottom-right.svg');
} }
.position-top { .position-top {
content: url('../icons/position-top.svg'); mask-image: url('../icons/position-top.svg');
} }
.position-right { .position-right {
content: url('../icons/position-right.svg'); mask-image: url('../icons/position-right.svg');
} }
.position-bottom { .position-bottom {
content: url('../icons/position-bottom.svg'); mask-image: url('../icons/position-bottom.svg');
} }
.position-left { .position-left {
content: url('../icons/position-left.svg'); mask-image: url('../icons/position-left.svg');
} }
.mask-edge { .mask-edge {
content: url('../icons/mask-edge.svg'); mask-image: url('../icons/mask-edge.svg');
} }
.mask-corner { .mask-corner {
content: url('../icons/mask-corner.svg'); mask-image: url('../icons/mask-corner.svg');
} }
.mask-center { .mask-center {
content: url('../icons/mask-center.svg'); mask-image: url('../icons/mask-center.svg');
} }
.book-front-cover { .book-front-cover {
content: url('../icons/book-front-cover.svg'); mask-image: url('../icons/book-front-cover.svg');
} }
.book-back-cover { .book-back-cover {
content: url('../icons/book-back-cover.svg'); mask-image: url('../icons/book-back-cover.svg');
} }
.book-inside-cover { .book-inside-cover {
content: url('../icons/book-inside-cover.svg'); mask-image: url('../icons/book-inside-cover.svg');
} }
.book-part-cover { .book-part-cover {
content: url('../icons/book-part-cover.svg'); mask-image: url('../icons/book-part-cover.svg');
}
.image-wrap-left {
mask-image: url('../icons/image-wrap-left.svg');
}
.image-wrap-right {
mask-image: url('../icons/image-wrap-right.svg');
} }
.davek { .davek {
content: url('../icons/Davek.svg'); mask-image: url('../icons/Davek.svg');
} }
.rellanic { .rellanic {
content: url('../icons/Rellanic.svg'); mask-image: url('../icons/Rellanic.svg');
} }
.iokharic { .iokharic {
content: url('../icons/Iokharic.svg'); mask-image: url('../icons/Iokharic.svg');
}
.zoom-to-fit {
mask-image: url('../icons/zoom-to-fit.svg');
}
.fit-width {
mask-image: url('../icons/fit-width.svg');
} }

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.07509,0,0,1.07509,-3.75511,-3.75468)">
<g transform="matrix(0.843549,0,0,0.950644,8.38004,4.39672)">
<path d="M28.455,52.413L28.455,58.581C28.455,59.719 27.684,60.745 26.501,61.181C25.318,61.616 23.956,61.375 23.051,60.571L11.114,49.96C9.878,48.862 9.878,47.08 11.114,45.981L23.051,35.371C23.956,34.566 25.318,34.326 26.501,34.761C27.684,35.197 28.455,36.223 28.455,37.361L28.455,43.528L70.223,43.528L70.223,37.361C70.223,36.223 70.995,35.197 72.177,34.761C73.36,34.326 74.722,34.566 75.627,35.371L87.564,45.981C88.8,47.08 88.8,48.862 87.564,49.96L75.627,60.571C74.722,61.375 73.36,61.616 72.177,61.181C70.995,60.745 70.223,59.719 70.223,58.581L70.223,52.413L28.455,52.413Z"/>
</g>
<g transform="matrix(1.46702,0,0,0.986488,-23.0335,3.50686)">
<path d="M23.967,5.877L23.967,88.383C23.967,90.556 22.781,92.321 21.319,92.321L21.157,92.321C19.695,92.321 18.509,90.556 18.509,88.383L18.509,5.877C18.509,3.703 19.695,1.939 21.157,1.939L21.319,1.939C22.781,1.939 23.967,3.703 23.967,5.877Z"/>
</g>
<g transform="matrix(1.46702,0,0,0.986488,60.7211,3.50686)">
<path d="M23.967,5.877L23.967,88.383C23.967,90.556 22.781,92.321 21.319,92.321L21.157,92.321C19.695,92.321 18.509,90.556 18.509,88.383L18.509,5.877C18.509,3.703 19.695,1.939 21.157,1.939L21.319,1.939C22.781,1.939 23.967,3.703 23.967,5.877Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
x="0px"
y="0px"
viewBox="0 0 512.00006 512"
xml:space="preserve"
id="svg10"
sodipodi:docname="noun-wrap-image-left-212078.svg"
width="512.00006"
height="512"
inkscape:export-filename="image-wrap-right.svg"
inkscape:export-xdpi="300"
inkscape:export-ydpi="300"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs10" /><sodipodi:namedview
id="namedview10"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1" /><path
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 185.80018,144 H 32"
id="path11"
sodipodi:nodetypes="cc"
clip-path="none"
inkscape:export-filename="image-wrap-right.svg"
inkscape:export-xdpi="300"
inkscape:export-ydpi="300" /><path
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 185.80018,368 H 32"
id="path11-8"
sodipodi:nodetypes="cc"
clip-path="none" /><path
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 480.00007,32 H 32"
id="path11-8-2-67"
clip-path="none"
sodipodi:nodetypes="cc" /><path
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 480.00008,480 H 32"
id="path11-8-2-67-2"
clip-path="none"
sodipodi:nodetypes="cc" /><path
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 160.0001,255.98832 32,256.01162"
id="path11-0"
sodipodi:nodetypes="cc"
clip-path="none" /><path
id="path23"
style="opacity:0.922046;fill:#000000;fill-opacity:1;stroke-width:64;stroke-linecap:round;stroke-dasharray:none;paint-order:fill markers stroke"
d="m 416.00008,96 a 160,160 0 0 1 96,32.50977 v 254.98046 a 160,160 0 0 1 -96,32.50977 160,160 0 0 1 -160,-160 160,160 0 0 1 160,-160 z" /></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
x="0px"
y="0px"
viewBox="0 0 512.00006 512"
xml:space="preserve"
id="svg10"
sodipodi:docname="noun-wrap-image-left-212078.svg"
width="512.00006"
height="512"
inkscape:export-filename="image-wrap-right.svg"
inkscape:export-xdpi="300"
inkscape:export-ydpi="300"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs10" /><sodipodi:namedview
id="namedview10"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1" /><path
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 326.1999,144 H 480.00008"
id="path11"
sodipodi:nodetypes="cc"
clip-path="none"
inkscape:export-filename="image-wrap-right.svg"
inkscape:export-xdpi="300"
inkscape:export-ydpi="300" /><path
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 326.1999,368 H 480.00008"
id="path11-8"
sodipodi:nodetypes="cc"
clip-path="none" /><path
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 32.00001,32 H 480.00008"
id="path11-8-2-67"
clip-path="none"
sodipodi:nodetypes="cc" /><path
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 32,480 H 480.00008"
id="path11-8-2-67-2"
clip-path="none"
sodipodi:nodetypes="cc" /><path
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 351.99998,255.98832 128.0001,0.0233"
id="path11-0"
sodipodi:nodetypes="cc"
clip-path="none" /><path
id="path23"
style="opacity:0.922046;fill:#000000;fill-opacity:1;stroke-width:64;stroke-linecap:round;stroke-dasharray:none;paint-order:fill markers stroke"
d="M 96,96 A 160,160 0 0 0 0,128.50977 V 383.49023 A 160,160 0 0 0 96,416 160,160 0 0 0 256,256 160,160 0 0 0 96,96 Z" /></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.0781,0,0,1.0781,-3.90545,-3.90502)">
<g transform="matrix(0.841196,0,0,0.947993,8.49652,4.52391)">
<path d="M44.333,52.413L28.455,52.413L28.455,58.581C28.455,59.719 27.684,60.745 26.501,61.181C25.318,61.616 23.956,61.375 23.051,60.571L11.114,49.96C9.878,48.862 9.878,47.08 11.114,45.981L23.051,35.371C23.956,34.566 25.318,34.326 26.501,34.761C27.684,35.197 28.455,36.223 28.455,37.361L28.455,43.528L44.333,43.528L44.333,29.439L37.382,29.439C36.099,29.439 34.943,28.755 34.452,27.705C33.961,26.656 34.233,25.448 35.14,24.644L47.097,14.052C48.335,12.956 50.343,12.956 51.581,14.052L63.539,24.644C64.446,25.448 64.717,26.656 64.226,27.705C63.735,28.755 62.579,29.439 61.296,29.439L54.346,29.439L54.346,43.528L70.223,43.528L70.223,37.361C70.223,36.223 70.995,35.197 72.177,34.761C73.36,34.326 74.722,34.566 75.627,35.371L87.564,45.981C88.8,47.08 88.8,48.862 87.564,49.96L75.627,60.571C74.722,61.375 73.36,61.616 72.177,61.181C70.995,60.745 70.223,59.719 70.223,58.581L70.223,52.413L54.346,52.413L54.346,66.502L61.296,66.502C62.579,66.502 63.735,67.187 64.226,68.236C64.717,69.286 64.446,70.494 63.539,71.297L51.581,81.889C50.343,82.986 48.335,82.986 47.097,81.889L35.14,71.297C34.233,70.494 33.961,69.286 34.452,68.236C34.943,67.187 36.099,66.502 37.382,66.502L44.333,66.503L44.333,52.413Z"/>
</g>
<g transform="matrix(1.0247,0,0,1.0247,-5.47698,-3.53855)">
<path d="M99.4,14.269L99.4,90.227C99.4,94.245 96.137,97.508 92.119,97.508L16.161,97.508C12.142,97.508 8.88,94.245 8.88,90.227L8.88,14.269C8.88,10.25 12.142,6.988 16.161,6.988L92.119,6.988C96.137,6.988 99.4,10.25 99.4,14.269ZM93.633,14.269C93.633,13.433 92.955,12.755 92.119,12.755L16.161,12.755C15.325,12.755 14.647,13.433 14.647,14.269L14.647,90.227C14.647,91.062 15.325,91.741 16.161,91.741L92.119,91.741C92.955,91.741 93.633,91.062 93.633,90.227L93.633,14.269Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

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

71
eslint.config.mjs Normal file
View File

@@ -0,0 +1,71 @@
import react from "eslint-plugin-react";
import jest from "eslint-plugin-jest";
import globals from "globals";
export default [{
ignores: ["build/"]
},
{
files : ['**/*.js', '**/*.jsx'],
plugins : { react, jest },
languageOptions : {
ecmaVersion : "latest",
sourceType : "module",
parserOptions : { ecmaFeatures: { jsx: true } },
globals : { ...globals.browser, ...globals.node }
},
rules: {
/** Errors **/
"camelcase" : ["error", { properties: "never" }],
"no-array-constructor" : "error",
"no-iterator" : "error",
"no-nested-ternary" : "error",
"no-new-object" : "error",
"no-proto" : "error",
"react/jsx-no-bind" : ["error", { allowArrowFunctions: true }],
"react/jsx-uses-react" : "error",
"react/prefer-es6-class" : ["error", "never"],
"jest/valid-expect" : ["error", { maxArgs: 3 }],
/** Warnings **/
"max-lines" : ["warn", { max: 200, skipComments: true, skipBlankLines: true }],
"max-depth" : ["warn", { max: 4 }],
"max-params" : ["warn", { max: 5 }],
"no-restricted-syntax" : ["warn", "ClassDeclaration", "SwitchStatement"],
"no-unused-vars" : ["warn", { vars: "all", args: "none", varsIgnorePattern: "config|_|cx|createClass" }],
"react/jsx-uses-vars" : "warn",
/** Fixable **/
"arrow-parens" : ["warn", "always"],
"brace-style" : ["warn", "1tbs", { allowSingleLine: true }],
"jsx-quotes" : ["warn", "prefer-single"],
"no-var" : "warn",
"prefer-const" : "warn",
"prefer-template" : "warn",
"quotes" : ["warn", "single", { allowTemplateLiterals: true }],
"semi" : ["warn", "always"],
/** Whitespace **/
"array-bracket-spacing" : ["warn", "never"],
"arrow-spacing" : ["warn", { before: false, after: false }],
"comma-spacing" : ["warn", { before: false, after: true }],
"indent" : ["warn", "tab", { MemberExpression: "off" }],
"linebreak-style" : "off",
"no-trailing-spaces" : "warn",
"no-whitespace-before-property" : "warn",
"object-curly-spacing" : ["warn", "always"],
"react/jsx-indent-props" : ["warn", "tab"],
"space-in-parens" : ["warn", "never"],
"template-curly-spacing" : ["warn", "never"],
"keyword-spacing" : ["warn", {
before : true,
after : true,
overrides : { if: { before: false, after: false } }
}],
"key-spacing" : ["warn", {
multiLine : { beforeColon: true, afterColon: true, align: "colon" },
singleLine : { beforeColon: false, afterColon: true }
}]
}
}
];

5325
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "homebrewery", "name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown", "description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.13.1", "version": "3.15.0",
"engines": { "engines": {
"npm": "^10.2.x", "npm": "^10.2.x",
"node": "^20.8.x" "node": "^20.8.x"
@@ -15,14 +15,16 @@
"quick": "node scripts/quick.js", "quick": "node scripts/quick.js",
"build": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js", "build": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
"builddev": "node scripts/buildHomebrew.js --dev", "builddev": "node scripts/buildHomebrew.js --dev",
"lint": "eslint --fix **/*.{js,jsx}", "lint": "eslint --fix",
"lint:dry": "eslint **/*.{js,jsx}", "lint:dry": "eslint",
"stylelint": "stylelint --fix **/*.{less}", "stylelint": "stylelint --fix **/*.{less}",
"stylelint:dry": "stylelint **/*.less", "stylelint:dry": "stylelint **/*.less",
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0", "circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
"verify": "npm run lint && npm test", "verify": "npm run lint && npm test",
"test": "jest --runInBand", "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:api-unit:css": "jest \"server/.*.spec.js\" -t \"Get CSS\" --verbose",
"test:coverage": "jest --coverage --silent --runInBand", "test:coverage": "jest --coverage --silent --runInBand",
"test:dev": "jest --verbose --watch", "test:dev": "jest --verbose --watch",
"test:basic": "jest tests/markdown/basic.test.js --verbose", "test:basic": "jest tests/markdown/basic.test.js --verbose",
@@ -32,6 +34,7 @@
"test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace", "test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace",
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace", "test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace", "test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
"test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace", "test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
"test:route": "jest tests/routes/static-pages.test.js --verbose", "test:route": "jest tests/routes/static-pages.test.js --verbose",
"phb": "node scripts/phb.js", "phb": "node scripts/phb.js",
@@ -56,15 +59,15 @@
], ],
"coverageThreshold": { "coverageThreshold": {
"global": { "global": {
"statements": 25, "statements": 50,
"branches": 10, "branches": 40,
"functions": 22, "functions": 40,
"lines": 25 "lines": 50
}, },
"server/homebrew.api.js": { "server/homebrew.api.js": {
"statements": 65, "statements": 70,
"branches": 50, "branches": 50,
"functions": 60, "functions": 65,
"lines": 70 "lines": 70
} }
}, },
@@ -82,18 +85,18 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.24.7", "@babel/core": "^7.25.2",
"@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-runtime": "^7.25.4",
"@babel/preset-env": "^7.24.7", "@babel/preset-env": "^7.25.4",
"@babel/preset-react": "^7.24.7", "@babel/preset-react": "^7.24.7",
"@googleapis/drive": "^8.11.0", "@googleapis/drive": "^8.14.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"codemirror": "^5.65.6", "codemirror": "^5.65.6",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3", "dedent-tabs": "^0.10.3",
"dompurify": "^3.1.5", "dompurify": "^3.1.6",
"expr-eval": "^2.0.2", "expr-eval": "^2.0.2",
"express": "^4.19.2", "express": "^4.19.2",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
@@ -104,34 +107,35 @@
"less": "^3.13.1", "less": "^3.13.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "11.2.0", "marked": "11.2.0",
"marked-emoji": "^1.4.1", "marked-emoji": "^1.4.2",
"marked-extended-tables": "^1.0.8", "marked-extended-tables": "^1.0.10",
"marked-gfm-heading-id": "^3.2.0", "marked-gfm-heading-id": "^3.2.0",
"marked-smartypants-lite": "^1.0.2", "marked-smartypants-lite": "^1.0.2",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.4.5", "mongoose": "^8.6.1",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.1", "nconf": "^0.12.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-frame-component": "^4.1.3", "react-frame-component": "^4.1.3",
"react-router-dom": "6.24.1", "react-router-dom": "6.26.1",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^9.0.2", "superagent": "^10.1.0",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.57.0", "@stylistic/stylelint-plugin": "^3.0.1",
"eslint-plugin-jest": "^28.6.0", "eslint": "^9.9.1",
"eslint-plugin-react": "^7.34.3", "eslint-plugin-jest": "^28.8.3",
"eslint-plugin-react": "^7.35.2",
"globals": "^15.9.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"stylelint": "^15.11.0", "stylelint": "^16.9.0",
"stylelint-config-recess-order": "^4.6.0", "stylelint-config-recess-order": "^5.1.0",
"stylelint-config-recommended": "^13.0.0", "stylelint-config-recommended": "^14.0.1",
"stylelint-stylistic": "^0.4.3",
"supertest": "^7.0.0" "supertest": "^7.0.0"
} }
} }

View File

@@ -9,11 +9,12 @@ const yaml = require('js-yaml');
const app = express(); const app = express();
const config = require('./config.js'); const config = require('./config.js');
const { homebrewApi, getBrew } = require('./homebrew.api.js'); const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = require('./homebrew.api.js');
const GoogleActions = require('./googleActions.js'); const GoogleActions = require('./googleActions.js');
const serveCompressedStaticAssets = require('./static-assets.mv.js'); const serveCompressedStaticAssets = require('./static-assets.mv.js');
const sanitizeFilename = require('sanitize-filename'); const sanitizeFilename = require('sanitize-filename');
const asyncHandler = require('express-async-handler'); const asyncHandler = require('express-async-handler');
const templateFn = require('./../client/template.js');
const { DEFAULT_BREW } = require('./brewDefaults.js'); const { DEFAULT_BREW } = require('./brewDefaults.js');
@@ -54,6 +55,7 @@ app.use((req, res, next)=>{
app.use(homebrewApi); app.use(homebrewApi);
app.use(require('./admin.api.js')); app.use(require('./admin.api.js'));
app.use(require('./vault.api.js'));
const HomebrewModel = require('./homebrew.model.js').model; const HomebrewModel = require('./homebrew.model.js').model;
const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8'); const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
@@ -81,7 +83,8 @@ app.get('/robots.txt', (req, res)=>{
app.get('/', (req, res, next)=>{ app.get('/', (req, res, next)=>{
req.brew = { req.brew = {
text : welcomeText, text : welcomeText,
renderer : 'V3' renderer : 'V3',
theme : '5ePHB'
}, },
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
@@ -97,7 +100,8 @@ app.get('/', (req, res, next)=>{
app.get('/legacy', (req, res, next)=>{ app.get('/legacy', (req, res, next)=>{
req.brew = { req.brew = {
text : welcomeTextLegacy, text : welcomeTextLegacy,
renderer : 'legacy' renderer : 'legacy',
theme : '5ePHB'
}, },
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
@@ -113,7 +117,8 @@ app.get('/legacy', (req, res, next)=>{
app.get('/migrate', (req, res, next)=>{ app.get('/migrate', (req, res, next)=>{
req.brew = { req.brew = {
text : migrateText, text : migrateText,
renderer : 'V3' renderer : 'V3',
theme : '5ePHB'
}, },
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
@@ -130,7 +135,8 @@ app.get('/changelog', async (req, res, next)=>{
req.brew = { req.brew = {
title : 'Changelog', title : 'Changelog',
text : changelogText, text : changelogText,
renderer : 'V3' renderer : 'V3',
theme : '5ePHB'
}, },
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
@@ -147,7 +153,8 @@ app.get('/faq', async (req, res, next)=>{
req.brew = { req.brew = {
title : 'FAQ', title : 'FAQ',
text : faqText, text : faqText,
renderer : 'V3' renderer : 'V3',
theme : '5ePHB'
}, },
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
@@ -195,6 +202,9 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
res.status(200).send(brew.text); res.status(200).send(brew.text);
}); });
//Serve brew styling
app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
//User Page //User Page
app.get('/user/:username', async (req, res, next)=>{ app.get('/user/:username', async (req, res, next)=>{
const ownAccount = req.account && (req.account.username == req.params.username); const ownAccount = req.account && (req.account.username == req.params.username);
@@ -265,9 +275,11 @@ app.get('/user/:username', async (req, res, next)=>{
}); });
//Edit Page //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.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
req.userThemes = await(getUsersBrewThemes(req.account?.username));
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
title : req.brew.title || 'Untitled Brew', title : req.brew.title || 'Untitled Brew',
description : req.brew.description || 'No description.', description : req.brew.description || 'No description.',
@@ -279,10 +291,10 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
splitTextStyleAndMetadata(req.brew); 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. 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(); return next();
}); }));
//New Page //New Page from ID
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{ app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
sanitizeBrew(req.brew, 'share'); sanitizeBrew(req.brew, 'share');
splitTextStyleAndMetadata(req.brew); splitTextStyleAndMetadata(req.brew);
const brew = { const brew = {
@@ -292,17 +304,31 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
style : req.brew.style, style : req.brew.style,
renderer : req.brew.renderer, renderer : req.brew.renderer,
theme : req.brew.theme, theme : req.brew.theme,
tags : req.brew.tags tags : req.brew.tags,
}; };
req.brew = _.defaults(brew, DEFAULT_BREW); req.brew = _.defaults(brew, DEFAULT_BREW);
req.userThemes = await(getUsersBrewThemes(req.account?.username));
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
title : 'New', title : 'New',
description : 'Start crafting your homebrew on the Homebrewery!' description : 'Start crafting your homebrew on the Homebrewery!'
}; };
return next(); 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 //Share Page
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
@@ -335,6 +361,15 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
app.get('/account', asyncHandler(async (req, res, next)=>{ app.get('/account', asyncHandler(async (req, res, next)=>{
const data = {}; const data = {};
data.title = 'Account Information Page'; data.title = 'Account Information Page';
if(!req.account) {
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
const error = new Error('No valid account');
error.status = 401;
error.HBErrorCode = '50';
error.page = data.title;
return next(error);
};
let auth; let auth;
let googleCount = []; let googleCount = [];
@@ -399,8 +434,21 @@ if(isLocalEnvironment){
}); });
} }
//Vault Page
app.get('/vault', asyncHandler(async(req, res, next)=>{
return next();
}));
//Send rendered page
app.use(asyncHandler(async (req, res, next)=>{
if (!req.route) return res.redirect('/'); // Catch-all for invalid routes
const page = await renderPage(req, res);
if(!page) return;
res.send(page);
}));
//Render the page //Render the page
const templateFn = require('./../client/template.js');
const renderPage = async (req, res)=>{ const renderPage = async (req, res)=>{
// Create configuration object // Create configuration object
const configuration = { const configuration = {
@@ -418,7 +466,8 @@ const renderPage = async (req, res)=>{
enable_v3 : config.get('enable_v3'), enable_v3 : config.get('enable_v3'),
enable_themes : config.get('enable_themes'), enable_themes : config.get('enable_themes'),
config : configuration, config : configuration,
ogMeta : req.ogMeta ogMeta : req.ogMeta,
userThemes : req.userThemes
}; };
const title = req.brew ? req.brew.title : ''; const title = req.brew ? req.brew.title : '';
const page = await templateFn('homebrew', title, props) const page = await templateFn('homebrew', title, props)
@@ -428,13 +477,6 @@ const renderPage = async (req, res)=>{
return page; return page;
}; };
//Send rendered page
app.use(asyncHandler(async (req, res, next)=>{
const page = await renderPage(req, res);
if(!page) return;
res.send(page);
}));
//v=====----- Error-Handling Middleware -----=====v// //v=====----- Error-Handling Middleware -----=====v//
//Format Errors as plain objects so all fields will appear in the string sent //Format Errors as plain objects so all fields will appear in the string sent
const formatErrors = (key, value)=>{ const formatErrors = (key, value)=>{

View File

@@ -8,9 +8,16 @@ const Markdown = require('../shared/naturalcrit/markdown.js');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const asyncHandler = require('express-async-handler'); const asyncHandler = require('express-async-handler');
const { nanoid } = require('nanoid'); const { nanoid } = require('nanoid');
const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.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) => { // const getTopBrews = (cb) => {
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) { // HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
// cb(brews); // cb(brews);
@@ -37,6 +44,43 @@ const api = {
} }
return { id, googleId }; return { id, googleId };
}, },
//Get array of any of this user's brews tagged with `meta:theme`
getUsersBrewThemes : async (username)=>{
if(!username)
return {};
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)=>{ getBrew : (accessType, stubOnly = false)=>{
// Create middleware with the accessType passed in as part of the scope // Create middleware with the accessType passed in as part of the scope
return async (req, res, next)=>{ return async (req, res, next)=>{
@@ -55,7 +99,7 @@ const api = {
stub = stub?.toObject(); stub = stub?.toObject();
if(stub?.lock?.locked && accessType != 'edit') { if(stub?.lock?.locked && accessType != 'edit') {
throw { HBErrorCode: '100', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title }; throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title };
} }
// If there is a google id, try to find the google brew // If there is a google id, try to find the google brew
@@ -104,6 +148,20 @@ const api = {
next(); next();
}; };
}, },
getCSS : async (req, res)=>{
const { brew } = req;
if(!brew) return res.status(404).send('');
splitTextStyleAndMetadata(brew);
if(!brew.style) return res.status(404).send('');
res.set({
'Cache-Control' : 'no-cache',
'Content-Type' : 'text/css'
});
return res.status(200).send(brew.style);
},
mergeBrewText : (brew)=>{ mergeBrewText : (brew)=>{
let text = brew.text; let text = brew.text;
if(brew.style !== undefined) { if(brew.style !== undefined) {
@@ -142,7 +200,7 @@ const api = {
return modified; return modified;
}, },
excludeStubProps : (brew)=>{ excludeStubProps : (brew)=>{
const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount']; const propsToExclude = ['text', 'textBin'];
for (const prop of propsToExclude) { for (const prop of propsToExclude) {
brew[prop] = undefined; brew[prop] = undefined;
} }
@@ -209,6 +267,58 @@ const api = {
res.status(200).send(saved); 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, and an array of snippets, in render order
req.params.id : The shareId ( User theme ) or name ( static theme )
req.params.renderer : The Markdown renderer used for this theme */
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)=>{ updateBrew : async (req, res)=>{
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method // 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); const brewFromClient = api.excludePropsFromUpdate(req.body);
@@ -369,5 +479,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.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
router.delete('/api/:id', asyncHandler(api.deleteBrew)); router.delete('/api/:id', asyncHandler(api.deleteBrew));
router.get('/api/remove/:id', asyncHandler(api.deleteBrew)); router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
module.exports = api; module.exports = api;

View File

@@ -14,6 +14,9 @@ describe('Tests for api', ()=>{
let saved; let saved;
beforeEach(()=>{ beforeEach(()=>{
jest.resetModules();
jest.restoreAllMocks();
saved = undefined; saved = undefined;
saveFunc = jest.fn(async function() { saveFunc = jest.fn(async function() {
saved = { ...this, _id: '1' }; saved = { ...this, _id: '1' };
@@ -45,8 +48,10 @@ describe('Tests for api', ()=>{
model.mockImplementation((brew)=>modelBrew(brew)); model.mockImplementation((brew)=>modelBrew(brew));
res = { res = {
status : jest.fn(()=>res), status : jest.fn(()=>res),
send : jest.fn(()=>{}) send : jest.fn(()=>{}),
set : jest.fn(()=>{}),
setHeader : jest.fn(()=>{})
}; };
api = require('./homebrew.api'); api = require('./homebrew.api');
@@ -81,10 +86,6 @@ describe('Tests for api', ()=>{
}; };
}); });
afterEach(()=>{
jest.restoreAllMocks();
});
describe('getId', ()=>{ describe('getId', ()=>{
it('should return only id if google id is not present', ()=>{ it('should return only id if google id is not present', ()=>{
const { id, googleId } = api.getId({ const { id, googleId } = api.getId({
@@ -308,7 +309,7 @@ describe('Tests for api', ()=>{
const req = { brew: {} }; const req = { brew: {} };
const next = jest.fn(); const next = jest.fn();
await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '100', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' }); await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '51', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' });
}); });
}); });
@@ -408,8 +409,8 @@ brew`);
expect(sent).not.toEqual(googleBrew); expect(sent).not.toEqual(googleBrew);
expect(result.text).toBeUndefined(); expect(result.text).toBeUndefined();
expect(result.textBin).toBeUndefined(); expect(result.textBin).toBeUndefined();
expect(result.renderer).toBeUndefined(); expect(result.renderer).toBe('v3');
expect(result.pageCount).toBeUndefined(); expect(result.pageCount).toBe(1);
}); });
}); });
@@ -540,9 +541,9 @@ brew`);
description : '', description : '',
editId : expect.any(String), editId : expect.any(String),
gDrive : false, gDrive : false,
pageCount : undefined, pageCount : 1,
published : false, published : false,
renderer : undefined, renderer : 'V3',
lang : 'en', lang : 'en',
shareId : expect.any(String), shareId : expect.any(String),
googleId : expect.any(String), googleId : expect.any(String),
@@ -581,6 +582,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', ()=>{ describe('deleteBrew', ()=>{
it('should handle case where fetching the brew returns an error', async ()=>{ it('should handle case where fetching the brew returns an error', async ()=>{
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; }); api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
@@ -801,4 +917,66 @@ brew`);
expect(saved.googleId).toEqual(brew.googleId); expect(saved.googleId).toEqual(brew.googleId);
}); });
}); });
describe('Get CSS', ()=>{
it('should return brew style content as CSS text', async ()=>{
const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n````\n\n' };
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise(testBrew));
const fn = api.getBrew('share', true);
const req = { brew: {} };
const next = jest.fn();
await fn(req, null, next);
await api.getCSS(req, res);
expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith("\nI Have a style!\n");
expect(res.set).toHaveBeenCalledWith({
'Cache-Control' : 'no-cache',
'Content-Type' : 'text/css'
});
});
it('should return 404 when brew has no style content', async ()=>{
const testBrew = { title: 'test brew', text: 'I don\'t have a style!' };
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise(testBrew));
const fn = api.getBrew('share', true);
const req = { brew: {} };
const next = jest.fn();
await fn(req, null, next);
await api.getCSS(req, res);
expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style');
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith('');
});
it('should return 404 when brew does not exist', async ()=>{
const testBrew = { };
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise(testBrew));
const fn = api.getBrew('share', true);
const req = { brew: {} };
const next = jest.fn();
await fn(req, null, next);
await api.getCSS(req, res);
expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style');
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith('');
});
});
}); });

View File

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

102
server/vault.api.js Normal file
View File

@@ -0,0 +1,102 @@
const express = require('express');
const asyncHandler = require('express-async-handler');
const HomebrewModel = require('./homebrew.model.js').model;
const router = express.Router();
const titleConditions = (title)=>{
if(!title) return {};
return {
$text : {
$search : title,
$caseSensitive : false,
},
};
};
const authorConditions = (author)=>{
if(!author) return {};
return { authors: author };
};
const rendererConditions = (legacy, v3)=>{
if(legacy === 'true' && v3 !== 'true')
return { renderer: 'legacy' };
if(v3 === 'true' && legacy !== 'true')
return { renderer: 'V3' };
return {}; // If all renderers selected, renderer field not needed in query for speed
};
const findBrews = async (req, res)=>{
const title = req.query.title || '';
const author = req.query.author || '';
const page = Math.max(parseInt(req.query.page) || 1, 1);
const count = Math.max(parseInt(req.query.count) || 20, 10);
const skip = (page - 1) * count;
const combinedQuery = {
$and : [
{ published: true },
rendererConditions(req.query.legacy, req.query.v3),
titleConditions(title),
authorConditions(author)
],
};
const projection = {
editId : 0,
googleId : 0,
text : 0,
textBin : 0,
version : 0
};
await HomebrewModel.find(combinedQuery, projection)
.skip(skip)
.limit(count)
.maxTimeMS(5000)
.exec()
.then((brews)=>{
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const processedBrews = brews.map((brew)=>{
brew.authors = brew.authors.map((author)=>emailRegex.test(author) ? 'hidden' : author
);
return brew;
});
res.json({ brews: processedBrews, page });
})
.catch((error)=>{
throw { ...error, message: 'Error finding brews in Vault search', HBErrorCode: 90 };
});
};
const findTotal = async (req, res)=>{
const title = req.query.title || '';
const author = req.query.author || '';
const combinedQuery = {
$and : [
{ published: true },
rendererConditions(req.query.legacy, req.query.v3),
titleConditions(title),
authorConditions(author)
],
};
await HomebrewModel.countDocuments(combinedQuery)
.then((totalBrews)=>{
console.log(`when returning, the total of brews is ${totalBrews} for the query ${JSON.stringify(combinedQuery)}`);
res.json({ totalBrews });
})
.catch((error)=>{
throw { ...error, message: 'Error finding brews in Vault search findTotal function', HBErrorCode: 91 };
});
};
router.get('/api/vault/total', asyncHandler(findTotal));
router.get('/api/vault', asyncHandler(findBrews));
module.exports = router;

View File

@@ -1,5 +1,6 @@
const _ = require('lodash'); const _ = require('lodash');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const request = require('../client/homebrew/utils/request-middleware.js');
const splitTextStyleAndMetadata = (brew)=>{ const splitTextStyleAndMetadata = (brew)=>{
brew.text = brew.text.replaceAll('\r\n', '\n'); brew.text = brew.text.replaceAll('\r\n', '\n');
@@ -15,6 +16,11 @@ const splitTextStyleAndMetadata = (brew)=>{
brew.style = brew.text.slice(7, index - 1); brew.style = brew.text.slice(7, index - 1);
brew.text = brew.text.slice(index + 5); 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 = ()=>{ const printCurrentBrew = ()=>{
@@ -28,7 +34,25 @@ const printCurrentBrew = ()=>{
} }
}; };
const fetchThemeBundle = async (obj, renderer, theme)=>{
if(!renderer || !theme) return;
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 = { module.exports = {
splitTextStyleAndMetadata, splitTextStyleAndMetadata,
printCurrentBrew printCurrentBrew,
fetchThemeBundle,
}; };

View File

@@ -39,8 +39,10 @@ if(typeof window !== 'undefined'){
//Autocompletion //Autocompletion
require('codemirror/addon/hint/show-hint.js'); require('codemirror/addon/hint/show-hint.js');
const foldCode = require('./fold-code'); const foldPagesCode = require('./fold-pages');
foldCode.registerHomebreweryHelper(CodeMirror); foldPagesCode.registerHomebreweryHelper(CodeMirror);
const foldCSSCode = require('./fold-css');
foldCSSCode.registerHomebreweryHelper(CodeMirror);
} }
const CodeEditor = createClass({ const CodeEditor = createClass({
@@ -411,11 +413,11 @@ const CodeEditor = createClass({
foldOptions : function(cm){ foldOptions : function(cm){
return { return {
scanUp : true, scanUp : true,
rangeFinder : CodeMirror.fold.homebrewery, rangeFinder : this.props.language === 'css' ? CodeMirror.fold.homebrewerycss : CodeMirror.fold.homebrewery,
widget : (from, to)=>{ widget : (from, to)=>{
let text = ''; let text = '';
let currentLine = from.line; let currentLine = from.line;
const maxLength = 50; let maxLength = 50;
let foldPreviewText = ''; let foldPreviewText = '';
while (currentLine <= to.line && text.length <= maxLength) { while (currentLine <= to.line && text.length <= maxLength) {
@@ -430,10 +432,15 @@ const CodeEditor = createClass({
} }
} }
text = foldPreviewText || `Lines ${from.line+1}-${to.line+1}`; text = foldPreviewText || `Lines ${from.line+1}-${to.line+1}`;
text = text.replace('{', '').trim();
// Truncate data URLs at `data:`
const startOfData = text.indexOf('data:');
if(startOfData > 0)
maxLength = Math.min(startOfData + 5, maxLength);
text = text.trim();
if(text.length > maxLength) if(text.length > maxLength)
text = `${text.substr(0, maxLength)}...`; text = `${text.slice(0, maxLength)}...`;
return `\u21A4 ${text} \u21A6`; return `\u21A4 ${text} \u21A6`;
} }
@@ -450,3 +457,4 @@ const CodeEditor = createClass({
}); });
module.exports = CodeEditor; module.exports = CodeEditor;

View File

@@ -0,0 +1,44 @@
module.exports = {
registerHomebreweryHelper : function(CodeMirror) {
CodeMirror.registerHelper('fold', 'homebrewerycss', function(cm, start) {
// BRACE FOLDING
const startMatcher = /\{[ \t]*$/;
const endMatcher = /\}[ \t]*$/;
const activeLine = cm.getLine(start.line);
if(activeLine.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 and data-url folding
const importMatcher = /^@import.*?;/;
const dataURLMatcher = /url\(.*?data\:.*\)/;
if(activeLine.match(importMatcher) || activeLine.match(dataURLMatcher)) {
return {
from : CodeMirror.Pos(start.line, 0),
to : CodeMirror.Pos(start.line, activeLine.length)
};
}
return null;
});
}
};

View File

@@ -3,7 +3,7 @@ const _ = require('lodash');
const Marked = require('marked'); const Marked = require('marked');
const MarkedExtendedTables = require('marked-extended-tables'); const MarkedExtendedTables = require('marked-extended-tables');
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite'); const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id'); const { gfmHeadingId: MarkedGFMHeadingId, resetHeadings: MarkedGFMResetHeadingIDs } = require('marked-gfm-heading-id');
const { markedEmoji: MarkedEmojis } = require('marked-emoji'); const { markedEmoji: MarkedEmojis } = require('marked-emoji');
//Icon fonts included so they can appear in emoji autosuggest dropdown //Icon fonts included so they can appear in emoji autosuggest dropdown
@@ -28,17 +28,18 @@ const mathParser = new MathParser({
round : true, round : true,
floor : true, floor : true,
ceil : true, ceil : true,
abs : true,
sin : false, cos : false, tan : false, asin : false, acos : false, sin : false, cos : false, tan : false, asin : false, acos : false,
atan : false, sinh : false, cosh : false, tanh : false, asinh : false, atan : false, sinh : false, cosh : false, tanh : false, asinh : false,
acosh : false, atanh : false, sqrt : false, cbrt : false, log : false, acosh : false, atanh : false, sqrt : false, cbrt : false, log : false,
log2 : false, ln : false, lg : false, log10 : false, expm1 : false, log2 : false, ln : false, lg : false, log10 : false, expm1 : false,
log1p : false, abs : false, trunc : false, join : false, sum : false, log1p : false, trunc : false, join : false, sum : false, indexOf : false,
'-' : false, '+' : false, exp : false, not : false, length : false, '-' : false, '+' : false, exp : false, not : false, length : false,
'!' : false, sign : false, random : false, fac : false, min : false, '!' : false, sign : false, random : false, fac : false, min : false,
max : false, hypot : false, pyt : false, pow : false, atan2 : false, max : false, hypot : false, pyt : false, pow : false, atan2 : false,
'if' : false, gamma : false, roundTo : false, map : false, fold : false, 'if' : false, gamma : false, roundTo : false, map : false, fold : false,
filter : false, indexOf : false, filter : false,
remainder : false, factorial : false, remainder : false, factorial : false,
comparison : false, concatenate : false, comparison : false, concatenate : false,
@@ -46,6 +47,16 @@ const mathParser = new MathParser({
array : false, fndef : false array : false, fndef : false
} }
}); });
// Add sign function
mathParser.functions.sign = function (a) {
if(a >= 0) return '+';
return '-';
};
// Add signed function
mathParser.functions.signed = function (a) {
if(a >= 0) return `+${a}`;
return `${a}`;
};
//Processes the markdown within an HTML block if it's just a class-wrapper //Processes the markdown within an HTML block if it's just a class-wrapper
renderer.html = function (html) { renderer.html = function (html) {
@@ -75,7 +86,7 @@ renderer.link = function (href, title, text) {
if(href[0] == '#') { if(href[0] == '#') {
self = true; self = true;
} }
href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); href = cleanUrl(href);
if(href === null) { if(href === null) {
return text; return text;
@@ -91,6 +102,20 @@ renderer.link = function (href, title, text) {
return out; return out;
}; };
// Expose `src` attribute as `--HB_src` to make the URL accessible via CSS
renderer.image = function (href, title, text) {
href = cleanUrl(href);
if (href === null)
return text;
let out = `<img src="${href}" alt="${text}" style="--HB_src:url(${href});"`;
if (title)
out += ` title="${title}"`;
out += '>';
return out;
}
// Disable default reflink behavior, as it steps on our variables extension // Disable default reflink behavior, as it steps on our variables extension
tokenizer.def = function () { tokenizer.def = function () {
return undefined; return undefined;
@@ -345,6 +370,27 @@ const superSubScripts = {
} }
}; };
const forcedParagraphBreaks = {
name : 'hardBreaks',
level : 'block',
start(src) { return src.match(/\n:+$/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) {
const regex = /^(:+)(?:\n|$)/ym;
const match = regex.exec(src);
if(match?.length) {
return {
type : 'hardBreaks', // Should match "name" above
raw : match[0], // Text to consume from the source
length : match[1].length,
text : ''
};
}
},
renderer(token) {
return `<div class='blank'></div>`.repeat(token.length).concat('\n');
}
};
const definitionListsSingleLine = { const definitionListsSingleLine = {
name : 'definitionListsSingleLine', name : 'definitionListsSingleLine',
level : 'block', level : 'block',
@@ -389,9 +435,9 @@ const definitionListsSingleLine = {
const definitionListsMultiLine = { const definitionListsMultiLine = {
name : 'definitionListsMultiLine', name : 'definitionListsMultiLine',
level : 'block', level : 'block',
start(src) { return src.match(/\n[^\n]*\n::/m)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/\n[^\n]*\n::[^:\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::))|\n::(.(?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y; const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::[^:\n]))|\n::([^:\n](?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y;
let match; let match;
let endIndex = 0; let endIndex = 0;
const definitions = []; const definitions = [];
@@ -441,7 +487,7 @@ const replaceVar = function(input, hoist=false, allowUnresolved=false) {
const label = match[2]; const label = match[2];
//v=====--------------------< HANDLE MATH >-------------------=====v// //v=====--------------------< HANDLE MATH >-------------------=====v//
const mathRegex = /[a-z]+\(|[+\-*/^()]/g; const mathRegex = /[a-z]+\(|[+\-*/^(),]/g;
const matches = label.split(mathRegex); const matches = label.split(mathRegex);
const mathVars = matches.filter((match)=>isNaN(match))?.map((s)=>s.trim()); // Capture any variable names const mathVars = matches.filter((match)=>isNaN(match))?.map((s)=>s.trim()); // Capture any variable names
@@ -451,7 +497,7 @@ const replaceVar = function(input, hoist=false, allowUnresolved=false) {
mathVars?.forEach((variable)=>{ mathVars?.forEach((variable)=>{
const foundVar = lookupVar(variable, globalPageNumber, hoist); const foundVar = lookupVar(variable, globalPageNumber, hoist);
if(foundVar && foundVar.resolved && foundVar.content && !isNaN(foundVar.content)) // Only subsitute math values if fully resolved, not empty strings, and numbers if(foundVar && foundVar.resolved && foundVar.content && !isNaN(foundVar.content)) // Only subsitute math values if fully resolved, not empty strings, and numbers
replacedLabel = replacedLabel.replaceAll(variable, foundVar.content); replacedLabel = replacedLabel.replaceAll(new RegExp(`(?<!\\w)(${variable})(?!\\w)`, 'g'), foundVar.content);
}); });
try { try {
@@ -695,34 +741,27 @@ const MarkedEmojiOptions = {
renderer : (token)=>`<i class="${token.emoji}"></i>` renderer : (token)=>`<i class="${token.emoji}"></i>`
}; };
const tableTerminators = [
`:+\\n`, // hardBreak
` *{[^\n]+}`, // blockInjector
` *{{[^{\n]*\n.*?\n}}` // mustacheDiv
]
Marked.use(MarkedVariables()); Marked.use(MarkedVariables());
Marked.use({ extensions: [definitionListsMultiLine, definitionListsSingleLine, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] }); Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts,
mustacheSpans, mustacheDivs, mustacheInjectInline] });
Marked.use(mustacheInjectBlock); Marked.use(mustacheInjectBlock);
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false }); Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions)); Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
const nonWordAndColonTest = /[^\w:]/g; function cleanUrl(href) {
const cleanUrl = function (sanitize, base, href) { try {
if(sanitize) { href = encodeURI(href).replace(/%25/g, '%');
let prot; } catch {
try { return null;
prot = decodeURIComponent(unescape(href)) }
.replace(nonWordAndColonTest, '') return href;
.toLowerCase(); }
} catch (e) {
return null;
}
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
return null;
}
}
try {
href = encodeURI(href).replace(/%25/g, '%');
} catch (e) {
return null;
}
return href;
};
const escapeTest = /[&<>"']/; const escapeTest = /[&<>"']/;
const escapeReplace = /[&<>"']/g; const escapeReplace = /[&<>"']/g;
@@ -817,13 +856,15 @@ let globalPageNumber = 0;
module.exports = { module.exports = {
marked : Marked, marked : Marked,
render : (rawBrewText, pageNumber=1)=>{ render : (rawBrewText, pageNumber=0)=>{
globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order
varsQueue = []; //Could move into MarkedVariables() varsQueue = []; //Could move into MarkedVariables()
globalPageNumber = pageNumber; globalPageNumber = pageNumber;
if(pageNumber==0) {
MarkedGFMResetHeadingIDs();
}
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`) rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`);
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
const opts = Marked.defaults; const opts = Marked.defaults;
rawBrewText = opts.hooks.preprocess(rawBrewText); rawBrewText = opts.hooks.preprocess(rawBrewText);

View File

@@ -7,8 +7,9 @@ const SplitPane = createClass({
displayName : 'SplitPane', displayName : 'SplitPane',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
storageKey : 'naturalcrit-pane-split', storageKey : 'naturalcrit-pane-split',
onDragFinish : function(){} //fires when dragging onDragFinish : function(){}, //fires when dragging
showDividerButtons : true
}; };
}, },
@@ -142,9 +143,11 @@ const SplitPane = createClass({
width={this.state.currentDividerPos} width={this.state.currentDividerPos}
> >
{React.cloneElement(this.props.children[0], { {React.cloneElement(this.props.children[0], {
moveBrew : this.state.moveBrew, ...(this.props.showDividerButtons && {
moveSource : this.state.moveSource, moveBrew: this.state.moveBrew,
setMoveArrows : this.setMoveArrows moveSource: this.state.moveSource,
setMoveArrows: this.setMoveArrows,
}),
})} })}
</Pane> </Pane>
{this.renderDivider()} {this.renderDivider()}

View File

@@ -1,5 +1,5 @@
const stylelint = require('stylelint'); const stylelint = require('stylelint');
const { isNumber } = require('stylelint/lib/utils/validateTypes'); const { isNumber } = require('stylelint/lib/utils/validateTypes.cjs');
const { report, ruleMessages, validateOptions } = stylelint.utils; const { report, ruleMessages, validateOptions } = stylelint.utils;
const ruleName = 'naturalcrit/declaration-block-multi-line-min-declarations'; const ruleName = 'naturalcrit/declaration-block-multi-line-min-declarations';

View File

@@ -1,5 +1,5 @@
const stylelint = require('stylelint'); const stylelint = require('stylelint');
const { isNumber } = require('stylelint/lib/utils/validateTypes'); const { isNumber } = require('stylelint/lib/utils/validateTypes.cjs');
const { report, ruleMessages, validateOptions } = stylelint.utils; const { report, ruleMessages, validateOptions } = stylelint.utils;
const ruleName = 'naturalcrit/declaration-colon-min-space-before'; const ruleName = 'naturalcrit/declaration-colon-min-space-before';

View File

@@ -88,4 +88,16 @@ describe('Multiline Definition Lists', ()=>{
const rendered = Markdown.render(source).trim(); const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Inline definition 1</dd>\n<dt></dt><dd>Inline definition 2 (no DT)</dd>\n</dl>'); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Inline definition 1</dd>\n<dt></dt><dd>Inline definition 2 (no DT)</dd>\n</dl>');
}); });
test('Multiline Definition Term must have at least one non-empty Definition', function() {
const source = 'Term 1\n::';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<div class='blank'></div><div class='blank'></div>`);
});
test('Multiline Definition List must have at least one non-newline character after ::', function() {
const source = 'Term 1\n::\nDefinition 1\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<div class='blank'></div><div class='blank'></div>\n<p>Definition 1</p>`);
});
}); });

View File

@@ -0,0 +1,47 @@
/* eslint-disable max-lines */
const Markdown = require('naturalcrit/markdown.js');
describe('Hard Breaks', ()=>{
test('Single Break', function() {
const source = ':\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div>`);
});
test('Double Break', function() {
const source = '::\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div>`);
});
test('Triple Break', function() {
const source = ':::\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div><div class='blank'></div>`);
});
test('Many Break', function() {
const source = '::::::::::\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div>`);
});
test('Multiple sets of Breaks', function() {
const source = ':::\n:::\n:::';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div><div class='blank'></div>\n<div class='blank'></div><div class='blank'></div><div class='blank'></div>\n<div class='blank'></div><div class='blank'></div><div class='blank'></div>`);
});
test('Break directly between two paragraphs', function() {
const source = 'Line 1\n::\nLine 2';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Line 1</p>\n<div class='blank'></div><div class='blank'></div>\n<p>Line 2</p>`);
});
test('Ignored inside a code block', function() {
const source = '```\n\n:\n\n```\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<pre><code>\n:\n</code></pre>`);
});
});

View File

@@ -322,9 +322,9 @@ describe('Injection: When an injection tag follows an element', ()=>{
}); });
it('Renders an image element with injected style', function() { it('Renders an image element with injected style', function() {
const source = '![alt text](http://i.imgur.com/hMna6G0.png){position:absolute}'; const source = '![alt text](https://i.imgur.com/hMna6G0.png){position:absolute}';
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img style="position:absolute;" src="http://i.imgur.com/hMna6G0.png" alt="alt text"></p>'); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute;" src="https://i.imgur.com/hMna6G0.png" alt="alt text"></p>');
}); });
it('Renders an element modified by only the first of two consecutive injections', function() { it('Renders an element modified by only the first of two consecutive injections', function() {
@@ -343,19 +343,19 @@ describe('Injection: When an injection tag follows an element', ()=>{
it('Renders an image with added attributes', function() { it('Renders an image with added attributes', function() {
const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`; const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e"></p>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e"></p>`);
}); });
it('Renders an image with "=" in the url, and added attributes', function() { it('Renders an image with "=" in the url, and added attributes', function() {
const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png?auth=12345&height=1024) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`; const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png?auth=12345&height=1024) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png?auth=12345&height=1024" alt="homebrew mug" a="b and c" d="e"></p>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png?auth=12345&height=1024); position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png?auth=12345&height=1024" alt="homebrew mug" a="b and c" d="e"></p>`);
}); });
it('Renders an image and added attributes with "=" in the value, ', function() { it('Renders an image and added attributes with "=" in the value, ', function() {
const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e,otherUrl="url?auth=12345"}`; const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e,otherUrl="url?auth=12345"}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e" otherUrl="url?auth=12345"></p>`); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e" otherUrl="url?auth=12345"></p>`);
}); });
}); });

View File

@@ -315,21 +315,21 @@ describe('Normal Links and Images', ()=>{
const source = `![alt text](url)`; const source = `![alt text](url)`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p><img src="url" alt="alt text"></p>`.trimReturns()); <p><img src="url" alt="alt text" style="--HB_src:url(url);"></p>`.trimReturns());
}); });
it('Renders normal images with a title', function() { it('Renders normal images with a title', function() {
const source = 'An image ![alt text](url "and title")!'; const source = 'An image ![alt text](url "and title")!';
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p>An image <img src="url" alt="alt text" title="and title">!</p>`.trimReturns()); <p>An image <img src="url" alt="alt text" style="--HB_src:url(url);" title="and title">!</p>`.trimReturns());
}); });
it('Applies curly injectors to images', function() { it('Applies curly injectors to images', function() {
const source = `![alt text](url){width:100px}`; const source = `![alt text](url){width:100px}`;
const rendered = Markdown.render(source).trimReturns(); const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent` expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p><img style="width:100px;" src="url" alt="alt text"></p>`.trimReturns()); <p><img style="--HB_src:url(url); width:100px;" src="url" alt="alt text"></p>`.trimReturns());
}); });
it('Renders normal links', function() { it('Renders normal links', function() {
@@ -370,4 +370,36 @@ describe('Cross-page variables', ()=>{
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>two</p><p>one</p>\\page<p>two</p>'); expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>two</p><p>one</p>\\page<p>two</p>');
}); });
});
describe('Math function parameter handling', ()=>{
it('allows variables in single-parameter functions', function() {
const source = '[var]:4.1\n\n$[floor(var)]';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>4</p>`);
});
it('allows one variable and a number in two-parameter functions', function() {
const source = '[var]:4\n\n$[min(1,var)]';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>1</p>`);
});
it('allows two variables in two-parameter functions', function() {
const source = '[var1]:4\n\n[var2]:8\n\n$[min(var1,var2)]';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>4</p>`);
});
});
describe('Variable names that are subsets of other names', ()=>{
it('do not conflict with function names', function() {
const source = `[a]: -1\n\n$[abs(a)]`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered).toBe('<p>1</p>');
});
it('do not conflict with other variable names', function() {
const source = `[ab]: 2\n\n[aba]: 8\n\n[ba]: 4\n\n$[ab + aba + ba]`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered).toBe('<p>14</p>');
});
}); });

View File

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

View File

@@ -349,7 +349,7 @@ module.exports = [
/* Ink Friendly */ /* Ink Friendly */
*:is(.page,.monster,.note,.descriptive) { *:is(.page,.monster,.note,.descriptive) {
background : white !important; background : white !important;
filter : drop-shadow(0px 0px 3px #888) !important; box-shadow : 1px 4px 14px #888 !important;
} }
.page img { .page img {

View File

@@ -35,7 +35,7 @@ const getTOC = (pages)=>{
const ToCExclude = getComputedStyle(heading).getPropertyValue('--TOC'); const ToCExclude = getComputedStyle(heading).getPropertyValue('--TOC');
if(ToCExclude != 'exclude') { if(ToCExclude != 'exclude') {
recursiveAdd(heading.innerText.trim(), onPage, headerDepth.indexOf(heading.tagName), res); recursiveAdd(heading.textContent.trim(), onPage, headerDepth.indexOf(heading.tagName), res);
} }
}); });
return res; return res;

View File

@@ -382,6 +382,14 @@
.useColumns(0.96, @fillMode: balance); .useColumns(0.96, @fillMode: balance);
} }
//only for IOS devices
@supports (-webkit-touch-callout: none) {
.page .monster.frame {
background-repeat : no-repeat;
background-size : cover;
}
}
// ***************************** // *****************************
// * FOOTER // * FOOTER
// *****************************/ // *****************************/
@@ -459,6 +467,7 @@
margin-left : 1.5em; margin-left : 1.5em;
} }
} }
// ***************************** // *****************************
// * SPELL LIST // * SPELL LIST
// *****************************/ // *****************************/
@@ -883,6 +892,9 @@ h6,
.useColumns(0.96, @fillMode: balance); .useColumns(0.96, @fillMode: balance);
} }
} }
.toc.wide li {
break-inside: auto;
}
} }
// ***************************** // *****************************
@@ -907,6 +919,10 @@ h6,
.page h1 + * { margin-top : 0; } .page h1 + * { margin-top : 0; }
.page .descriptive.wide + * {
margin-top: 0;
}
//***************************** //*****************************
// * RUNE TABLE // * RUNE TABLE
// *****************************/ // *****************************/

View File

@@ -153,6 +153,18 @@ module.exports = [
gen : dedent` gen : dedent`
![cat warrior](https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg) {width:325px,mix-blend-mode:multiply}` ![cat warrior](https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg) {width:325px,mix-blend-mode:multiply}`
}, },
{
name : 'Image Wrap Left',
icon : 'fac image-wrap-left',
gen : dedent`
![homebrewery_mug](http://i.imgur.com/hMna6G0.png) {width:280px,margin-right:-3cm,wrapLeft}`
},
{
name : 'Image Wrap Right',
icon : 'fac image-wrap-right',
gen : dedent`
![homebrewery_mug](http://i.imgur.com/hMna6G0.png) {width:280px,margin-left:-3cm,wrapRight}`
},
{ {
name : 'Background Image', name : 'Background Image',
icon : 'fas fa-tree', icon : 'fas fa-tree',

View File

@@ -156,6 +156,19 @@ body { counter-reset : page-numbers; }
break-inside : avoid; break-inside : avoid;
} }
/* Wrap Text */
.wrapLeft {
shape-outside : var(--HB_src);
float : right;
shape-margin : 0.2cm;
}
.wrapRight {
shape-outside : var(--HB_src);
float : left;
shape-margin : 0.2cm;
}
/* Watermark */ /* Watermark */
.watermark { .watermark {
position : absolute; position : absolute;
@@ -236,7 +249,7 @@ body { counter-reset : page-numbers; }
left : 50%; left : 50%;
width : 50%; width : 50%;
height : 50%; height : 50%;
transform : translateX(-50%) translateY(50%) rotate(calc(-1deg * var(--rotation))) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY))); transform : translateX(-50%) translateY(50%) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY))) rotate(calc(-1deg * var(--rotation)));
} }
& img { & img {
position : absolute; position : absolute;

View File

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

View File

@@ -7,6 +7,7 @@
@noteBorderImage : url('/assets/noteBorder.png'); @noteBorderImage : url('/assets/noteBorder.png');
@descriptiveBoxImage : url('/assets/descriptiveBorder.png'); @descriptiveBoxImage : url('/assets/descriptiveBorder.png');
@monsterBlockBackground : url('/assets/parchmentBackgroundGrayscale.jpg'); @monsterBlockBackground : url('/assets/parchmentBackgroundGrayscale.jpg');
@monsterBlockOverlay : url('/assets/parchmentBackgroundOverlayed.jpg');
@monsterBorderImage : url('/assets/monsterBorderFancy.png'); @monsterBorderImage : url('/assets/monsterBorderFancy.png');
@codeBorderImage : url('/assets/codeBorder.png'); @codeBorderImage : url('/assets/codeBorder.png');
@classTableDecoration : url('/assets/classTableDecoration.png'); @classTableDecoration : url('/assets/classTableDecoration.png');

View File

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