0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-27 18:13:08 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
G.Ambatte
2bd06250fc Add @import folding 2024-07-20 14:50:39 +12:00
51 changed files with 3205 additions and 4351 deletions

View File

@@ -67,9 +67,6 @@ 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

79
.eslintrc.js Normal file
View File

@@ -0,0 +1,79 @@
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'],
}
};

View File

@@ -3,7 +3,7 @@
"stylelint-config-recess-order", "stylelint-config-recess-order",
"stylelint-config-recommended"], "stylelint-config-recommended"],
"plugins": [ "plugins": [
"@stylistic/stylelint-plugin", "stylelint-stylistic",
"./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:20-alpine FROM node:18-alpine
RUN apk --no-cache add git RUN apk --no-cache add git
ENV NODE_ENV=docker ENV NODE_ENV=docker

View File

@@ -84,145 +84,6 @@ 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).
### 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
@@ -270,6 +131,8 @@ 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
@@ -561,7 +424,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
@@ -619,7 +482,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
@@ -716,7 +579,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
}} }}
@@ -739,7 +602,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
}} }}
@@ -751,7 +614,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!)
@@ -895,7 +758,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
@@ -910,7 +773,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
@@ -993,7 +856,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
@@ -1019,7 +882,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.
@@ -1046,7 +909,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.

View File

@@ -7,7 +7,6 @@ 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');
@@ -19,6 +18,8 @@ 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,16 +57,14 @@ const BrewRenderer = (props)=>{
lang : '', lang : '',
errors : [], errors : [],
currentEditorPage : 0, currentEditorPage : 0,
themeBundle : {},
...props ...props
}; };
const [state, setState] = useState({ const [state, setState] = useState({
height : PAGE_HEIGHT, viewablePageNumber : 0,
isMounted : false, height : PAGE_HEIGHT,
visibility : 'hidden', isMounted : false,
zoom : 100, visibility : 'hidden',
currentPageNumber : 1,
}); });
const mainRef = useRef(null); const mainRef = useRef(null);
@@ -87,14 +86,11 @@ const BrewRenderer = (props)=>{
})); }));
}; };
const getCurrentPage = (e)=>{ const handleScroll = (e)=>{
const { scrollTop, clientHeight, scrollHeight } = e.target; const target = e.target;
const totalScrollableHeight = scrollHeight - clientHeight;
const currentPageNumber = Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length);
setState((prevState)=>({ setState((prevState)=>({
...prevState, ...prevState,
currentPageNumber : currentPageNumber || 1 viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * rawPages.length)
})); }));
}; };
@@ -105,12 +101,23 @@ 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.currentPageNumber) <= 3) if(Math.abs(index - state.viewablePageNumber) <= 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' />
@@ -118,9 +125,10 @@ 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);
const themeStyles = props.themeBundle?.joinedStyles ?? '<style>@import url("/themes/V3/Blank/style.css");</style>'; //return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${sanitizeScriptTags(props.style)}\n} </style>` }} />;
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `${themeStyles} \n\n <style> ${cleanStyle} </style>` }} />; return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />;
}; };
const renderPage = (pageText, index)=>{ const renderPage = (pageText, index)=>{
@@ -180,19 +188,15 @@ const BrewRenderer = (props)=>{
document.dispatchEvent(new MouseEvent('click')); document.dispatchEvent(new MouseEvent('click'));
}; };
//Toolbar settings: const rendererPath = props.renderer == 'V3' ? 'V3' : 'Legacy';
const handleZoom = (newZoom)=>{ const themePath = props.theme ?? '5ePHB';
setState((prevState)=>({ const baseThemePath = Themes[rendererPath][themePath].baseTheme;
...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={getCurrentPage}> ? <div className='brewRenderer' onScroll={handleScroll}>
<div className='pages'> <div className='pages'>
{renderDummyPage(1)} {renderDummyPage(1)}
</div> </div>
@@ -200,13 +204,11 @@ const BrewRenderer = (props)=>{
: null} : null}
<ErrorBar errors={props.errors} /> <ErrorBar errors={props.errors} />
<div className='popups' ref={mainRef}> <div className='popups'>
<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 }}
@@ -214,23 +216,30 @@ const BrewRenderer = (props)=>{
onClick={()=>{emitClick();}} onClick={()=>{emitClick();}}
> >
<div className={'brewRenderer'} <div className={'brewRenderer'}
onScroll={getCurrentPage} onScroll={handleScroll}
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'}`} style={{ zoom: `${state.zoom}%` }}> <div className='pages' lang={`${props.lang || 'en'}`}>
{renderPages()} {renderPages()}
</div> </div>
</> </>
} }
</div> </div>
</Frame> </Frame>
{renderPageInfo()}
</> </>
); );
}; };

View File

@@ -1,9 +1,8 @@
@import (multiple, less) 'shared/naturalcrit/styles/reset.less'; @import (multiple, less) 'shared/naturalcrit/styles/reset.less';
.brewRenderer { .brewRenderer {
overflow-y : scroll; will-change : transform;
will-change : transform; overflow-y : scroll;
padding-top : 30px;
:where(.pages) { :where(.pages) {
margin : 30px 0px; margin : 30px 0px;
& > :where(.page) { & > :where(.page) {
@@ -15,31 +14,66 @@
box-shadow : 1px 4px 14px #000000; box-shadow : 1px 4px 14px #000000;
} }
} }
&::-webkit-scrollbar { &::-webkit-scrollbar {
width : 20px; width: 20px;
&:horizontal { &:horizontal{
width : auto; height: 20px;
height : 20px; width:auto;
} }
&-thumb { &-thumb {
background : linear-gradient(90deg, #D3C1AF 15px, #00000000 15px); background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
&:horizontal { background : linear-gradient(0deg, #D3C1AF 15px, #00000000 15px); } &:horizontal{
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
}
} }
&-corner { visibility : hidden; } &-corner {
visibility: hidden;
}
}
}
.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 {
.pane { position : relative; } 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%;
padding-top : unset; overflow-y: unset;
overflow-y : unset;
.pages { .pages {
margin : 0px; margin: 0px;
& > .page { box-shadow : unset; } &>.page {
box-shadow: unset;
}
} }
} }
} }

View File

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

View File

@@ -1,162 +0,0 @@
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

@@ -1,103 +0,0 @@
@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,8 +59,6 @@ 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) {
@@ -84,19 +82,6 @@ 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;
@@ -134,19 +119,8 @@ 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)=>{ const customHighlights = codeMirror.getAllMarks().filter((mark)=>!mark.__isFold); //Don't undo code folding
// 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
@@ -158,11 +132,6 @@ 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$/))) {
@@ -275,7 +244,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) {
if(!marker.__isFold) marker.clear(); marker.clear();
}); });
codeMirror.markText(startPos, endPos, { className: 'emoji' }); codeMirror.markText(startPos, endPos, { className: 'emoji' });
} }
@@ -412,8 +381,7 @@ 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}/>
</>; </>;
} }
}, },
@@ -456,7 +424,6 @@ 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,7 +8,6 @@ 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');
@@ -28,7 +27,6 @@ const MetadataEditor = createClass({
return { return {
metadata : { metadata : {
editId : null, editId : null,
shareId : null,
title : '', title : '',
description : '', description : '',
thumbnail : '', thumbnail : '',
@@ -100,7 +98,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, 'renderer'); this.props.onChange(this.props.metadata);
}, },
handlePublish : function(val){ handlePublish : function(val){
this.props.onChange({ this.props.onChange({
@@ -112,7 +110,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, 'theme'); this.props.onChange(this.props.metadata);
}, },
handleLanguage : function(languageCode){ handleLanguage : function(languageCode){
@@ -193,42 +191,37 @@ 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(mergedThemes[renderer]), (theme)=>{ return _.map(_.values(Themes[renderer]), (theme)=>{
if(theme.path == this.props.metadata.shareId) return; return <div className='item' key={''} onClick={()=>this.handleTheme(theme)} title={''}>
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`; {`${theme.renderer} : ${theme.name}`}
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`; <img src={`/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={preview}/> <img src={`/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`}/>
</div> </div>
</div>; </div>;
}); });
}; };
const currentRenderer = this.props.metadata.renderer; const currentTheme = Themes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme];
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(currentRenderer == 'legacy') { if(this.props.metadata.renderer == 'legacy') {
dropdown = dropdown =
<Nav.dropdown className='disabled value' trigger='disabled'> <Nav.dropdown className='disabled value' trigger='disabled'>
<div> {`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i> </div> <div>
{`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i>
</div>
</Nav.dropdown>; </Nav.dropdown>;
} else { } else {
dropdown = dropdown =
<Nav.dropdown className='value' trigger='click'> <Nav.dropdown className='value' trigger='click'>
<div> {currentTheme.author ?? _.upperFirst(currentRenderer)} : {currentTheme.name} <i className='fas fa-caret-down'></i> </div> <div>
{`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`} <i className='fas fa-caret-down'></i>
{listThemes(currentRenderer)} </div>
{/*listThemes('Legacy')*/}
{listThemes('V3')}
</Nav.dropdown>; </Nav.dropdown>;
} }

View File

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

View File

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

View File

@@ -66,14 +66,13 @@ 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} userThemes={this.props.userThemes}/>} /> <Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
<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} userThemes={this.props.userThemes}/>} /> <Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} />} />
<Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } /> <Route path='/new' element={<WithRoute el={NewPage}/>} />
<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} disableMeta={true} />} /> <Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} /> <Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<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,18 +104,6 @@ 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,9 +21,6 @@
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,7 +1,6 @@
@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

@@ -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, fetchThemeBundle } = require('../../../../shared/helpers.js'); const { printCurrentBrew } = require('../../../../shared/helpers.js');
const googleDriveIcon = require('../../googleDrive.svg'); const googleDriveIcon = require('../../googleDrive.svg');
@@ -55,8 +55,7 @@ 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 : {}
}; };
}, },
@@ -88,8 +87,6 @@ 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() {
@@ -133,10 +130,7 @@ const EditPage = createClass({
}), ()=>{if(this.state.autoSave) this.trySave();}); }), ()=>{if(this.state.autoSave) this.trySave();});
}, },
handleMetaChange : function(metadata, field=undefined){ handleMetaChange : function(metadata){
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,
@@ -144,6 +138,7 @@ const EditPage = createClass({
}, },
isPending : true, isPending : true,
}), ()=>{if(this.state.autoSave) this.trySave();}); }), ()=>{if(this.state.autoSave) this.trySave();});
}, },
hasChanges : function(){ hasChanges : function(){
@@ -411,15 +406,12 @@ 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

@@ -136,19 +136,6 @@ 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}`,
// Brew locked by Administrators error // Brew locked by Administrators error
'100' : dedent` '100' : dedent`
## This brew has been locked. ## This brew has been locked.

View File

@@ -13,7 +13,6 @@ const HelpNavItem = require('../../navbar/help.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');
@@ -35,17 +34,12 @@ 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)
@@ -95,14 +89,12 @@ 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, fetchThemeBundle } = require('../../../../shared/helpers.js'); const { printCurrentBrew } = require('../../../../shared/helpers.js');
const BREWKEY = 'homebrewery-new'; const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style'; const STYLEKEY = 'homebrewery-new-style';
@@ -44,8 +44,7 @@ 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 : {}
}; };
}, },
@@ -78,15 +77,10 @@ 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);
@@ -128,10 +122,7 @@ const NewPage = createClass({
localStorage.setItem(STYLEKEY, style); localStorage.setItem(STYLEKEY, style);
}, },
handleMetaChange : function(metadata, field=undefined){ handleMetaChange : function(metadata){
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 },
}), ()=>{ }), ()=>{
@@ -151,6 +142,8 @@ 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) {
@@ -160,10 +153,12 @@ 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;
@@ -219,15 +214,12 @@ 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,27 +12,18 @@ 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, fetchThemeBundle } = require('../../../../shared/helpers.js'); const { printCurrentBrew } = 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() {
@@ -69,21 +60,13 @@ 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}>
this.props.disableMeta ? <Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
titleEl </MetadataNav>
:
<MetadataNav brew={this.props.brew}>
{titleEl}
</MetadataNav>
}
</Nav.section> </Nav.section>
<Nav.section> <Nav.section>
@@ -116,7 +99,6 @@ 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

@@ -1,69 +1,57 @@
.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 {
mask-image: url('../icons/position-top-left.svg'); content: url('../icons/position-top-left.svg');
} }
.position-top-right { .position-top-right {
mask-image: url('../icons/position-top-right.svg'); content: url('../icons/position-top-right.svg');
} }
.position-bottom-left { .position-bottom-left {
mask-image: url('../icons/position-bottom-left.svg'); content: url('../icons/position-bottom-left.svg');
} }
.position-bottom-right { .position-bottom-right {
mask-image: url('../icons/position-bottom-right.svg'); content: url('../icons/position-bottom-right.svg');
} }
.position-top { .position-top {
mask-image: url('../icons/position-top.svg'); content: url('../icons/position-top.svg');
} }
.position-right { .position-right {
mask-image: url('../icons/position-right.svg'); content: url('../icons/position-right.svg');
} }
.position-bottom { .position-bottom {
mask-image: url('../icons/position-bottom.svg'); content: url('../icons/position-bottom.svg');
} }
.position-left { .position-left {
mask-image: url('../icons/position-left.svg'); content: url('../icons/position-left.svg');
} }
.mask-edge { .mask-edge {
mask-image: url('../icons/mask-edge.svg'); content: url('../icons/mask-edge.svg');
} }
.mask-corner { .mask-corner {
mask-image: url('../icons/mask-corner.svg'); content: url('../icons/mask-corner.svg');
} }
.mask-center { .mask-center {
mask-image: url('../icons/mask-center.svg'); content: url('../icons/mask-center.svg');
} }
.book-front-cover { .book-front-cover {
mask-image: url('../icons/book-front-cover.svg'); content: url('../icons/book-front-cover.svg');
} }
.book-back-cover { .book-back-cover {
mask-image: url('../icons/book-back-cover.svg'); content: url('../icons/book-back-cover.svg');
} }
.book-inside-cover { .book-inside-cover {
mask-image: url('../icons/book-inside-cover.svg'); content: url('../icons/book-inside-cover.svg');
} }
.book-part-cover { .book-part-cover {
mask-image: url('../icons/book-part-cover.svg'); content: url('../icons/book-part-cover.svg');
} }
.davek { .davek {
mask-image: url('../icons/Davek.svg'); content: url('../icons/Davek.svg');
} }
.rellanic { .rellanic {
mask-image: url('../icons/Rellanic.svg'); content: url('../icons/Rellanic.svg');
} }
.iokharic { .iokharic {
mask-image: url('../icons/Iokharic.svg'); content: 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

@@ -1,15 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,12 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -4,7 +4,6 @@
"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"
} }

View File

@@ -1,71 +0,0 @@
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 }
}]
}
}
];

5405
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.14.2", "version": "3.13.1",
"engines": { "engines": {
"npm": "^10.2.x", "npm": "^10.2.x",
"node": "^20.8.x" "node": "^20.8.x"
@@ -15,16 +15,14 @@
"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", "lint": "eslint --fix **/*.{js,jsx}",
"lint:dry": "eslint", "lint:dry": "eslint **/*.{js,jsx}",
"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",
@@ -34,7 +32,6 @@
"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",
@@ -59,15 +56,15 @@
], ],
"coverageThreshold": { "coverageThreshold": {
"global": { "global": {
"statements": 50, "statements": 25,
"branches": 40, "branches": 10,
"functions": 40, "functions": 22,
"lines": 50 "lines": 25
}, },
"server/homebrew.api.js": { "server/homebrew.api.js": {
"statements": 70, "statements": 65,
"branches": 50, "branches": 50,
"functions": 65, "functions": 60,
"lines": 70 "lines": 70
} }
}, },
@@ -85,18 +82,18 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.24.7",
"@babel/plugin-transform-runtime": "^7.25.4", "@babel/plugin-transform-runtime": "^7.24.7",
"@babel/preset-env": "^7.25.4", "@babel/preset-env": "^7.24.7",
"@babel/preset-react": "^7.24.7", "@babel/preset-react": "^7.24.7",
"@googleapis/drive": "^8.13.0", "@googleapis/drive": "^8.11.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.6", "dompurify": "^3.1.5",
"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",
@@ -107,35 +104,34 @@
"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.2", "marked-emoji": "^1.4.1",
"marked-extended-tables": "^1.0.8", "marked-extended-tables": "^1.0.8",
"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.5.4", "mongoose": "^8.4.5",
"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.26.1", "react-router-dom": "6.24.1",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^10.1.0", "superagent": "^9.0.2",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/stylelint-plugin": "^3.0.1", "eslint": "^8.57.0",
"eslint": "^9.9.1", "eslint-plugin-jest": "^28.6.0",
"eslint-plugin-jest": "^28.8.0", "eslint-plugin-react": "^7.34.3",
"eslint-plugin-react": "^7.35.0",
"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": "^16.8.2", "stylelint": "^15.11.0",
"stylelint-config-recess-order": "^5.1.0", "stylelint-config-recess-order": "^4.6.0",
"stylelint-config-recommended": "^14.0.1", "stylelint-config-recommended": "^13.0.0",
"stylelint-stylistic": "^0.4.3",
"supertest": "^7.0.0" "supertest": "^7.0.0"
} }
} }

View File

@@ -9,12 +9,11 @@ const yaml = require('js-yaml');
const app = express(); const app = express();
const config = require('./config.js'); const config = require('./config.js');
const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = require('./homebrew.api.js'); const { homebrewApi, getBrew } = 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');
@@ -82,8 +81,7 @@ 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,
@@ -99,8 +97,7 @@ 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,
@@ -116,8 +113,7 @@ 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,
@@ -134,8 +130,7 @@ 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,
@@ -152,8 +147,7 @@ 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,
@@ -201,9 +195,6 @@ 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);
@@ -274,11 +265,9 @@ app.get('/user/:username', async (req, res, next)=>{
}); });
//Edit Page //Edit Page
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{ app.get('/edit/:id', asyncHandler(getBrew('edit')), (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.',
@@ -290,10 +279,10 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res,
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 from ID //New Page
app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
sanitizeBrew(req.brew, 'share'); sanitizeBrew(req.brew, 'share');
splitTextStyleAndMetadata(req.brew); splitTextStyleAndMetadata(req.brew);
const brew = { const brew = {
@@ -303,31 +292,17 @@ app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res,
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)=>{
@@ -424,16 +399,8 @@ if(isLocalEnvironment){
}); });
} }
//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 = {
@@ -451,8 +418,7 @@ 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)
@@ -462,6 +428,13 @@ 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,16 +8,9 @@ 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);
@@ -44,43 +37,6 @@ 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)=>{
@@ -148,20 +104,6 @@ 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) {
@@ -200,7 +142,7 @@ const api = {
return modified; return modified;
}, },
excludeStubProps : (brew)=>{ excludeStubProps : (brew)=>{
const propsToExclude = ['text', 'textBin']; const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount'];
for (const prop of propsToExclude) { for (const prop of propsToExclude) {
brew[prop] = undefined; brew[prop] = undefined;
} }
@@ -267,58 +209,6 @@ 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);
@@ -479,6 +369,5 @@ 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,9 +14,6 @@ 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' };
@@ -48,10 +45,8 @@ 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');
@@ -86,6 +81,10 @@ 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({
@@ -409,8 +408,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).toBe('v3'); expect(result.renderer).toBeUndefined();
expect(result.pageCount).toBe(1); expect(result.pageCount).toBeUndefined();
}); });
}); });
@@ -541,9 +540,9 @@ brew`);
description : '', description : '',
editId : expect.any(String), editId : expect.any(String),
gDrive : false, gDrive : false,
pageCount : 1, pageCount : undefined,
published : false, published : false,
renderer : 'V3', renderer : undefined,
lang : 'en', lang : 'en',
shareId : expect.any(String), shareId : expect.any(String),
googleId : expect.any(String), googleId : expect.any(String),
@@ -582,121 +581,6 @@ 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' }; });
@@ -917,66 +801,4 @@ 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, filter=null){ HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null){
const query = { authors: username, published: true, ...filter }; const query = { authors: username, published: true };
if(allowAccess){ if(allowAccess){
delete query.published; delete query.published;
} }

View File

@@ -1,6 +1,5 @@
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');
@@ -16,11 +15,6 @@ 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 = ()=>{
@@ -34,25 +28,7 @@ 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

@@ -417,7 +417,7 @@ const CodeEditor = createClass({
widget : (from, to)=>{ widget : (from, to)=>{
let text = ''; let text = '';
let currentLine = from.line; let currentLine = from.line;
let maxLength = 50; const maxLength = 50;
let foldPreviewText = ''; let foldPreviewText = '';
while (currentLine <= to.line && text.length <= maxLength) { while (currentLine <= to.line && text.length <= maxLength) {
@@ -432,15 +432,10 @@ 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.slice(0, maxLength)}...`; text = `${text.substr(0, maxLength)}...`;
return `\u21A4 ${text} \u21A6`; return `\u21A4 ${text} \u21A6`;
} }

View File

@@ -5,10 +5,10 @@ module.exports = {
// BRACE FOLDING // BRACE FOLDING
const startMatcher = /\{[ \t]*$/; const startMatcher = /\{[ \t]*$/;
const endMatcher = /\}[ \t]*$/; const endMatcher = /\}[ \t]*$/;
const activeLine = cm.getLine(start.line); const prevLine = cm.getLine(start.line);
if(activeLine.match(startMatcher)) { if(prevLine.match(startMatcher)) {
const lastLineNo = cm.lastLine(); const lastLineNo = cm.lastLine();
let end = start.line + 1; let end = start.line + 1;
let braceCount = 1; let braceCount = 1;
@@ -27,14 +27,14 @@ module.exports = {
}; };
} }
// @import and data-url folding // IMPORT FOLDING
const importMatcher = /^@import.*?;/;
const dataURLMatcher = /url\(.*?data\:.*\)/;
if(activeLine.match(importMatcher) || activeLine.match(dataURLMatcher)) { const importMatcher = /^@import.*?[;]/;
if(prevLine.match(importMatcher)) {
return { return {
from : CodeMirror.Pos(start.line, 0), from : CodeMirror.Pos(start.line, 0),
to : CodeMirror.Pos(start.line, activeLine.length) to : CodeMirror.Pos(start.line, cm.getLine(start.line).length)
}; };
} }

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, resetHeadings: MarkedGFMResetHeadingIDs } = require('marked-gfm-heading-id'); const { gfmHeadingId: MarkedGFMHeadingId } = 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,18 +28,17 @@ 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, trunc : false, join : false, sum : false, indexOf : false, log1p : false, abs : false, trunc : false, join : false, sum : 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, filter : false, indexOf : false,
remainder : false, factorial : false, remainder : false, factorial : false,
comparison : false, concatenate : false, comparison : false, concatenate : false,
@@ -47,16 +46,6 @@ 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) {
@@ -86,7 +75,7 @@ renderer.link = function (href, title, text) {
if(href[0] == '#') { if(href[0] == '#') {
self = true; self = true;
} }
href = cleanUrl(href); href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
if(href === null) { if(href === null) {
return text; return text;
@@ -356,27 +345,6 @@ 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',
@@ -421,9 +389,9 @@ const definitionListsSingleLine = {
const definitionListsMultiLine = { const definitionListsMultiLine = {
name : 'definitionListsMultiLine', name : 'definitionListsMultiLine',
level : 'block', level : 'block',
start(src) { return src.match(/\n[^\n]*\n::[^:\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/\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::)|(?:\n\n)|$))/y; const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::))|\n::(.(?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y;
let match; let match;
let endIndex = 0; let endIndex = 0;
const definitions = []; const definitions = [];
@@ -473,7 +441,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
@@ -483,7 +451,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(new RegExp(`(?<!\\w)(${variable})(?!\\w)`, 'g'), foundVar.content); replacedLabel = replacedLabel.replaceAll(variable, foundVar.content);
}); });
try { try {
@@ -728,20 +696,33 @@ const MarkedEmojiOptions = {
}; };
Marked.use(MarkedVariables()); Marked.use(MarkedVariables());
Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts, Marked.use({ extensions: [definitionListsMultiLine, definitionListsSingleLine, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] });
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({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions)); Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
function cleanUrl(href) { const nonWordAndColonTest = /[^\w:]/g;
try { const cleanUrl = function (sanitize, base, href) {
href = encodeURI(href).replace(/%25/g, '%'); if(sanitize) {
} catch { let prot;
return null; try {
} prot = decodeURIComponent(unescape(href))
return href; .replace(nonWordAndColonTest, '')
} .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;
@@ -836,15 +817,13 @@ let globalPageNumber = 0;
module.exports = { module.exports = {
marked : Marked, marked : Marked,
render : (rawBrewText, pageNumber=0)=>{ render : (rawBrewText, pageNumber=1)=>{
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

@@ -1,5 +1,5 @@
const stylelint = require('stylelint'); const stylelint = require('stylelint');
const { isNumber } = require('stylelint/lib/utils/validateTypes.cjs'); const { isNumber } = require('stylelint/lib/utils/validateTypes');
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.cjs'); const { isNumber } = require('stylelint/lib/utils/validateTypes');
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,16 +88,4 @@ 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

@@ -1,47 +0,0 @@
/* 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

@@ -371,35 +371,3 @@ describe('Cross-page variables', ()=>{
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" : "Blank", "baseTheme" : false,
"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;
box-shadow : 1px 4px 14px #888 !important; filter : drop-shadow(0px 0px 3px #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.textContent.trim(), onPage, headerDepth.indexOf(heading.tagName), res); recursiveAdd(heading.innerText.trim(), onPage, headerDepth.indexOf(heading.tagName), res);
} }
}); });
return res; return res;

View File

@@ -382,14 +382,6 @@
.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
// *****************************/ // *****************************/
@@ -467,7 +459,6 @@
margin-left : 1.5em; margin-left : 1.5em;
} }
} }
// ***************************** // *****************************
// * SPELL LIST // * SPELL LIST
// *****************************/ // *****************************/
@@ -892,9 +883,6 @@ h6,
.useColumns(0.96, @fillMode: balance); .useColumns(0.96, @fillMode: balance);
} }
} }
.toc.wide li {
break-inside: auto;
}
} }
// ***************************** // *****************************
@@ -919,10 +907,6 @@ h6,
.page h1 + * { margin-top : 0; } .page h1 + * { margin-top : 0; }
.page .descriptive.wide + * {
margin-top: 0;
}
//***************************** //*****************************
// * RUNE TABLE // * RUNE TABLE
// *****************************/ // *****************************/

View File

@@ -236,7 +236,7 @@ body { counter-reset : page-numbers; }
left : 50%; left : 50%;
width : 50%; width : 50%;
height : 50%; height : 50%;
transform : translateX(-50%) translateY(50%) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY))) rotate(calc(-1deg * var(--rotation))); transform : translateX(-50%) translateY(50%) rotate(calc(-1deg * var(--rotation))) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)));
} }
& img { & img {
position : absolute; position : absolute;

View File

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

View File

@@ -7,7 +7,6 @@
@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": "Blank", "baseTheme": false,
"baseSnippets": false, "baseSnippets": false,
"path": "5ePHB" "path": "5ePHB"
}, },
@@ -32,7 +32,7 @@
"Journal": { "Journal": {
"name": "Journal", "name": "Journal",
"renderer": "V3", "renderer": "V3",
"baseTheme": "Blank", "baseTheme": false,
"baseSnippets": "5ePHB", "baseSnippets": "5ePHB",
"path": "Journal" "path": "Journal"
} }