mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-03 08:22:42 +00:00
Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fix-vulnerability-admin-pages
This commit is contained in:
@@ -67,6 +67,9 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Test - Definition Lists
|
name: Test - Definition Lists
|
||||||
command: npm run test:definition-lists
|
command: npm run test:definition-lists
|
||||||
|
- run:
|
||||||
|
name: Test - Hard Breaks
|
||||||
|
command: npm run test:hard-breaks
|
||||||
- run:
|
- run:
|
||||||
name: Test - Variables
|
name: Test - Variables
|
||||||
command: npm run test:variables
|
command: npm run test:variables
|
||||||
|
|||||||
79
.eslintrc.js
79
.eslintrc.js
@@ -1,79 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root : true,
|
|
||||||
parserOptions : {
|
|
||||||
ecmaVersion : 2021,
|
|
||||||
sourceType : 'module',
|
|
||||||
ecmaFeatures : {
|
|
||||||
jsx : true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
env : {
|
|
||||||
browser : true,
|
|
||||||
node : true
|
|
||||||
},
|
|
||||||
plugins : ['react', 'jest'],
|
|
||||||
rules : {
|
|
||||||
/** Errors **/
|
|
||||||
'camelcase' : ['error', { properties: 'never' }],
|
|
||||||
//'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
|
||||||
'no-array-constructor' : 'error',
|
|
||||||
'no-iterator' : 'error',
|
|
||||||
'no-nested-ternary' : 'error',
|
|
||||||
'no-new-object' : 'error',
|
|
||||||
'no-proto' : 'error',
|
|
||||||
'react/jsx-no-bind' : ['error', { allowArrowFunctions: true }],
|
|
||||||
'react/jsx-uses-react' : 'error',
|
|
||||||
'react/prefer-es6-class' : ['error', 'never'],
|
|
||||||
'jest/valid-expect' : ['error', { maxArgs: 3 }],
|
|
||||||
|
|
||||||
/** Warnings **/
|
|
||||||
'max-lines' : ['warn', {
|
|
||||||
max : 200,
|
|
||||||
skipComments : true,
|
|
||||||
skipBlankLines : true,
|
|
||||||
}],
|
|
||||||
'max-depth' : ['warn', { max: 4 }],
|
|
||||||
'max-params' : ['warn', { max: 5 }],
|
|
||||||
'no-restricted-syntax' : ['warn', 'ClassDeclaration', 'SwitchStatement'],
|
|
||||||
'no-unused-vars' : ['warn', {
|
|
||||||
vars : 'all',
|
|
||||||
args : 'none',
|
|
||||||
varsIgnorePattern : 'config|_|cx|createClass'
|
|
||||||
}],
|
|
||||||
'react/jsx-uses-vars' : 'warn',
|
|
||||||
|
|
||||||
/** Fixable **/
|
|
||||||
'arrow-parens' : ['warn', 'always'],
|
|
||||||
'brace-style' : ['warn', '1tbs', { allowSingleLine: true }],
|
|
||||||
'jsx-quotes' : ['warn', 'prefer-single'],
|
|
||||||
'no-var' : 'warn',
|
|
||||||
'prefer-const' : 'warn',
|
|
||||||
'prefer-template' : 'warn',
|
|
||||||
'quotes' : ['warn', 'single', { 'allowTemplateLiterals': true }],
|
|
||||||
'semi' : ['warn', 'always'],
|
|
||||||
|
|
||||||
/** Whitespace **/
|
|
||||||
'array-bracket-spacing' : ['warn', 'never'],
|
|
||||||
'arrow-spacing' : ['warn', { before: false, after: false }],
|
|
||||||
'comma-spacing' : ['warn', { before: false, after: true }],
|
|
||||||
'indent' : ['warn', 'tab', { 'MemberExpression': 'off' }],
|
|
||||||
'keyword-spacing' : ['warn', {
|
|
||||||
before : true,
|
|
||||||
after : true,
|
|
||||||
overrides : {
|
|
||||||
if : { 'before': false, 'after': false }
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
'key-spacing' : ['warn', {
|
|
||||||
multiLine : { beforeColon: true, afterColon: true, align: 'colon' },
|
|
||||||
singleLine : { beforeColon: false, afterColon: true }
|
|
||||||
}],
|
|
||||||
'linebreak-style' : 'off',
|
|
||||||
'no-trailing-spaces' : 'warn',
|
|
||||||
'no-whitespace-before-property' : 'warn',
|
|
||||||
'object-curly-spacing' : ['warn', 'always'],
|
|
||||||
'react/jsx-indent-props' : ['warn', 'tab'],
|
|
||||||
'space-in-parens' : ['warn', 'never'],
|
|
||||||
'template-curly-spacing' : ['warn', 'never'],
|
|
||||||
}
|
|
||||||
};
|
|
||||||
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -1 +1,3 @@
|
|||||||
package-lock.json binary
|
package-lock.json binary
|
||||||
|
|
||||||
|
*.json text eol=lf
|
||||||
36
.github/pull_request_template.md
vendored
Normal file
36
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<!--
|
||||||
|
Before submitting a Pull Request, please consider the following to speed up reviews:
|
||||||
|
- 👷♀️ Create small PRs. Large PRs can usually be broken down into incremental PRs.
|
||||||
|
- 🚩 Do you already have several open PRs? Consider finishing or asking for help with existing PRs first.
|
||||||
|
- 🔧 Does your PR reference a discussed and approved issue, especially for personal or edge-case requests?
|
||||||
|
- 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
|
||||||
|
## Related Issues or Discussions
|
||||||
|
|
||||||
|
- Closes #
|
||||||
|
|
||||||
|
## QA Instructions, Screenshots, Recordings
|
||||||
|
|
||||||
|
_Please replace this line with instructions on how to test or view your changes, as well as any before/after
|
||||||
|
images for UI changes._
|
||||||
|
|
||||||
|
### Reviewer Checklist
|
||||||
|
|
||||||
|
_Please replace the list below with specific features you want reviewers to look at._
|
||||||
|
|
||||||
|
*Reviewers, refer to this list when testing features, or suggest new items *
|
||||||
|
- [ ] Verify new features are functional
|
||||||
|
- [ ] Feature A does X
|
||||||
|
- [ ] Feature B does Y
|
||||||
|
- [ ] Verify old features have not broken
|
||||||
|
- [ ] Feature Z can still be used
|
||||||
|
- [ ] Test for edge cases / try to break things
|
||||||
|
- [ ] Feature A handles negative numbers
|
||||||
|
- [ ] Identify opportunities for simplification and refactoring
|
||||||
|
- [ ] Check for code legibility and appropriate comments
|
||||||
|
|
||||||
|
<details><summary>Copy this list</summary>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"stylelint-config-recess-order",
|
"stylelint-config-recess-order",
|
||||||
"stylelint-config-recommended"],
|
"stylelint-config-recommended"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"stylelint-stylistic",
|
"@stylistic/stylelint-plugin",
|
||||||
"./stylelint_plugins/declaration-colon-align.js",
|
"./stylelint_plugins/declaration-colon-align.js",
|
||||||
"./stylelint_plugins/declaration-colon-min-space-before",
|
"./stylelint_plugins/declaration-colon-min-space-before",
|
||||||
"./stylelint_plugins/declaration-block-multi-line-min-declarations"
|
"./stylelint_plugins/declaration-block-multi-line-min-declarations"
|
||||||
@@ -16,32 +16,32 @@
|
|||||||
"font-family-no-missing-generic-family-keyword" : null,
|
"font-family-no-missing-generic-family-keyword" : null,
|
||||||
"font-weight-notation" : "named-where-possible",
|
"font-weight-notation" : "named-where-possible",
|
||||||
"font-family-name-quotes" : "always-unless-keyword",
|
"font-family-name-quotes" : "always-unless-keyword",
|
||||||
"stylistic/indentation" : "tab",
|
"@stylistic/indentation" : "tab",
|
||||||
"no-duplicate-selectors" : true,
|
"no-duplicate-selectors" : true,
|
||||||
"stylistic/color-hex-case" : "upper",
|
"@stylistic/color-hex-case" : "upper",
|
||||||
"color-hex-length" : "long",
|
"color-hex-length" : "long",
|
||||||
"stylistic/selector-combinator-space-after" : "always",
|
"@stylistic/selector-combinator-space-after" : "always",
|
||||||
"stylistic/selector-combinator-space-before" : "always",
|
"@stylistic/selector-combinator-space-before" : "always",
|
||||||
"stylistic/selector-attribute-operator-space-before" : "never",
|
"@stylistic/selector-attribute-operator-space-before" : "never",
|
||||||
"stylistic/selector-attribute-operator-space-after" : "never",
|
"@stylistic/selector-attribute-operator-space-after" : "never",
|
||||||
"stylistic/selector-attribute-brackets-space-inside" : "never",
|
"@stylistic/selector-attribute-brackets-space-inside" : "never",
|
||||||
"selector-attribute-quotes" : "always",
|
"selector-attribute-quotes" : "always",
|
||||||
"selector-pseudo-element-colon-notation" : "double",
|
"selector-pseudo-element-colon-notation" : "double",
|
||||||
"stylistic/selector-pseudo-class-parentheses-space-inside" : "never",
|
"@stylistic/selector-pseudo-class-parentheses-space-inside" : "never",
|
||||||
"stylistic/block-opening-brace-space-before" : "always",
|
"@stylistic/block-opening-brace-space-before" : "always",
|
||||||
"naturalcrit/declaration-colon-min-space-before" : 1,
|
"naturalcrit/declaration-colon-min-space-before" : 1,
|
||||||
"stylistic/declaration-block-trailing-semicolon" : "always",
|
"@stylistic/declaration-block-trailing-semicolon" : "always",
|
||||||
"stylistic/declaration-colon-space-after" : "always",
|
"@stylistic/declaration-colon-space-after" : "always",
|
||||||
"stylistic/number-leading-zero" : "always",
|
"@stylistic/number-leading-zero" : "always",
|
||||||
"function-url-quotes" : ["always", { "except": ["empty"] }],
|
"function-url-quotes" : ["always", { "except": ["empty"] }],
|
||||||
"function-url-scheme-disallowed-list" : ["data","http"],
|
"function-url-scheme-disallowed-list" : ["data","http"],
|
||||||
"comment-whitespace-inside" : "always",
|
"comment-whitespace-inside" : "always",
|
||||||
"stylistic/string-quotes" : "single",
|
"@stylistic/string-quotes" : "single",
|
||||||
"stylistic/media-feature-range-operator-space-before" : "always",
|
"@stylistic/media-feature-range-operator-space-before" : "always",
|
||||||
"stylistic/media-feature-range-operator-space-after" : "always",
|
"@stylistic/media-feature-range-operator-space-after" : "always",
|
||||||
"stylistic/media-feature-parentheses-space-inside" : "never",
|
"@stylistic/media-feature-parentheses-space-inside" : "never",
|
||||||
"stylistic/media-feature-colon-space-before" : "always",
|
"@stylistic/media-feature-colon-space-before" : "always",
|
||||||
"stylistic/media-feature-colon-space-after" : "always",
|
"@stylistic/media-feature-colon-space-after" : "always",
|
||||||
"naturalcrit/declaration-colon-align" : true,
|
"naturalcrit/declaration-colon-align" : true,
|
||||||
"naturalcrit/declaration-block-multi-line-min-declarations": 1
|
"naturalcrit/declaration-block-multi-line-min-declarations": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-alpine
|
FROM node:20-alpine
|
||||||
RUN apk --no-cache add git
|
RUN apk --no-cache add git
|
||||||
|
|
||||||
ENV NODE_ENV=docker
|
ENV NODE_ENV=docker
|
||||||
|
|||||||
249
changelog.md
249
changelog.md
@@ -84,6 +84,233 @@ pre {
|
|||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
|
### Wednesday 9/04/2024 - v3.15.0
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### 5e-Cleric, abquintic, calculuschild, Gazook89, G-Ambatte, Ericsheid, Kaiburr
|
||||||
|
|
||||||
|
* [x] New {{openSans **VAULT** {{fas,fa-dungeon}}}} page 🎉🎉🎉
|
||||||
|
:
|
||||||
|
All **PUBLISHED** brews ({{openSans :fas_circle_info: **Properties**}} menu) will be searchable, by title or author, and filtered by renderer. More features and adjustments will be coming.
|
||||||
|
:
|
||||||
|
Note: If any of your own brews are not showing up in search (particularly if stored on Google Drive), please edit and re-save to ensure our database has the data needed from document to be searchable.
|
||||||
|
|
||||||
|
Fixes issue [#697](https://github.com/naturalcrit/homebrewery/issues/697)
|
||||||
|
|
||||||
|
##### Gazook89
|
||||||
|
|
||||||
|
* [x] Auto-focus on text editor when switching editor tabs
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Wednesday 8/28/2024 - v3.14.3
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### calculuschild, G-Ambatte
|
||||||
|
|
||||||
|
* [x] New {{openSans **IMAGES → {{fac,image-wrap-left}} IMAGE WRAP LEFT/RIGHT**}} snippets
|
||||||
|
|
||||||
|
Fixes issue [#380](https://github.com/naturalcrit/homebrewery/issues/380)
|
||||||
|
|
||||||
|
* [x] Fix v3.14.2 bug with `꞉꞉꞉꞉` failing after tables
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Fix Account page crash when not logged in
|
||||||
|
|
||||||
|
Fixes issue [#3605](https://github.com/naturalcrit/homebrewery/issues/3605)
|
||||||
|
|
||||||
|
##### abquintic
|
||||||
|
|
||||||
|
* [x] Fix jump hotkeys conflicting with `CTRL + SHIFT`. Preview and Source movement shortcuts now use `CTRL + SHIFT + META + LEFT\RIGHTARROW`
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix display issue with image wrap icons
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
### Tuesday 8/27/2024 - v3.14.2
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### calculuschild
|
||||||
|
|
||||||
|
* [x] Reroute invalid urls to homepage
|
||||||
|
|
||||||
|
Fixes issues [#3269](https://github.com/naturalcrit/homebrewery/issues/3629)
|
||||||
|
|
||||||
|
* [x] Background dependency updates
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Add route to get brew styling via `/css/shareId`
|
||||||
|
|
||||||
|
Fixes issues [#1097](https://github.com/naturalcrit/homebrewery/issues/1097)
|
||||||
|
|
||||||
|
* [x] Fix `:emojis:` preventing code folding
|
||||||
|
|
||||||
|
Fixes issues [#3604](https://github.com/naturalcrit/homebrewery/issues/3604)
|
||||||
|
|
||||||
|
* [x] Fix mask image warping when rotated and stretched
|
||||||
|
|
||||||
|
Fixes issues [#3636](https://github.com/naturalcrit/homebrewery/issues/3636)
|
||||||
|
|
||||||
|
* [x] Fix Table of Contents uppercasing
|
||||||
|
|
||||||
|
Fixes issues [#3572](https://github.com/naturalcrit/homebrewery/issues/3572)
|
||||||
|
|
||||||
|
##### abquintic
|
||||||
|
|
||||||
|
* [x] Create globally unique Header IDs across pages
|
||||||
|
|
||||||
|
Fixes issues [#1430](https://github.com/naturalcrit/homebrewery/issues/1430)
|
||||||
|
|
||||||
|
* [x] Fix colon `꞉꞉꞉꞉` being parsed in codeblocks
|
||||||
|
|
||||||
|
* [x] Prevent crashes when loading undefined renderer or theme bundle
|
||||||
|
|
||||||
|
* [x] Add Jump-To hotkeys
|
||||||
|
|
||||||
|
* Use `CTRL/META + SHIFT + LEFTARROW` to brewJump
|
||||||
|
* Use `CTRL/META + SHIFT + RIGHTARROW` to sourceJump
|
||||||
|
|
||||||
|
* [x] Prevent reload from clobbering modified fresh clones
|
||||||
|
|
||||||
|
##### 5e-Cleric, Gazook89
|
||||||
|
|
||||||
|
* [x] Viewer tools for zoom/page navigation
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Tuesday 8/13/2024 - v3.14.1
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### abquintic
|
||||||
|
|
||||||
|
* [x] Allow Table of Contents to flow across columns
|
||||||
|
|
||||||
|
Fixes issues [#2563](https://github.com/naturalcrit/homebrewery/issues/2563)
|
||||||
|
|
||||||
|
* [x] Fix unusual margin spacing for adjacent `.descriptive` and `.wide` blocks
|
||||||
|
|
||||||
|
Fixes issues [#2688](https://github.com/naturalcrit/homebrewery/issues/2688)
|
||||||
|
|
||||||
|
* [x] Add code folding to :fas_paintbrush: {{openSans **STYLE**}} tab
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix edge case where Table of Contents generator changed capitalization of headings
|
||||||
|
|
||||||
|
Fixes issues [#3572](https://github.com/naturalcrit/homebrewery/issues/3572)
|
||||||
|
|
||||||
|
* [x] Fix **Ink Friendly** snippet causing unselectable PDF text
|
||||||
|
|
||||||
|
Fixes issues [#3563](https://github.com/naturalcrit/homebrewery/issues/3563)
|
||||||
|
|
||||||
|
* [x] Prevent brews selecting themselves as a theme
|
||||||
|
|
||||||
|
Fixes issues [#3614](https://github.com/naturalcrit/homebrewery/issues/3614)
|
||||||
|
|
||||||
|
* [x] Fix info pages (`/faq`, `/migrate`, etc.) showing blank authorship info
|
||||||
|
|
||||||
|
Fixes issues [#3568](https://github.com/naturalcrit/homebrewery/issues/3568)
|
||||||
|
|
||||||
|
* [x] Add `abs()`, `sign()` and `signed()` functions to variable syntax math handler
|
||||||
|
|
||||||
|
Fixes issues [#3537](https://github.com/naturalcrit/homebrewery/issues/3537)
|
||||||
|
|
||||||
|
* [x] Fix variable math handler not processing commas (i.e., in `$[max(varA,varB)]`
|
||||||
|
|
||||||
|
Fixes issues [#3613](https://github.com/naturalcrit/homebrewery/issues/3613)
|
||||||
|
|
||||||
|
* [x] Fix variable math handler scrambling variables with names that are subsets of other variables
|
||||||
|
|
||||||
|
Fixes issues [#3622](https://github.com/naturalcrit/homebrewery/issues/3622)
|
||||||
|
|
||||||
|
##### calculuschild
|
||||||
|
|
||||||
|
* [x] Fix `/migrate` page using an editor context instead of share context
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Fix Monster Stat Blocks losing color in Safari
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
### Monday 7/29/2024 - v3.14.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### abquintic, calculuschild
|
||||||
|
|
||||||
|
* [x] Alternative Brew Themes, including importing other brews as a base theme.
|
||||||
|
|
||||||
|
- In the :fas_circle_info: **Properties** menu, find the new {{openSans **THEME**}} dropdown. It lists Brew Themes, including a new **Blank** theme as a simpler basis for custom styling.
|
||||||
|
- Brews tagged with `meta:theme` will appear in the Brew Themes list. Selecting one loads its :fas_paintbrush: **Style** tab contents as the CSS basis for the current brew, allowing one brew to style multiple documents.
|
||||||
|
- Brews with `meta:theme` can also select their own Theme, i.e. layering Themes on top of each other.
|
||||||
|
- The next goal is to make **Published** Themes shareable between users.
|
||||||
|
|
||||||
|
|
||||||
|
Fixes issues [#1899](https://github.com/naturalcrit/homebrewery/issues/1899), [#3085](https://github.com/naturalcrit/homebrewery/issues/3085)
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix Drop-cap font becoming corrupted when Bold
|
||||||
|
|
||||||
|
Fixes issues [#3551](https://github.com/naturalcrit/homebrewery/issues/3551)
|
||||||
|
|
||||||
|
* [x] Fixes to UI styling
|
||||||
|
|
||||||
|
Fixes issues [#3568](https://github.com/naturalcrit/homebrewery/issues/3568)
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
### Saturday 6/7/2024 - v3.13.1
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### calculuschild, G-Ambatte
|
||||||
|
|
||||||
|
* [x] Hotfixes for issues with v3.13.0
|
||||||
|
|
||||||
|
Fixes issues [#3559](https://github.com/naturalcrit/homebrewery/issues/3559), [#3552](https://github.com/naturalcrit/homebrewery/issues/3552), [#3554](https://github.com/naturalcrit/homebrewery/issues/3554)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Friday 28/6/2024 - v3.13.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### calculuschild
|
||||||
|
|
||||||
|
* [x] Add `:emoji:` Markdown syntax, with autosuggest; start typing after the first `:` for matching emojis from
|
||||||
|
:fab_font_awesome: FontAwesome, :df_d20: DiceFont, :ei_action: ElderberryInn, and a subset of :gi_broadsword: GameIcons
|
||||||
|
|
||||||
|
* [x] Fix `{curly injection}` to append to, rather than erase and replace target CSS
|
||||||
|
* [x] {{openSans **GET PDF**}} {{fa,fa-file-pdf}} now opens the print dialog directly, rather than redirecting to a separate page
|
||||||
|
|
||||||
|
##### Gazook
|
||||||
|
|
||||||
|
* [x] Several small style tweaks to the UI
|
||||||
|
* [x] Cleaning and refactoring several large pieces of code
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] For error pages, add links to user account and `/share` page if available
|
||||||
|
|
||||||
|
Fixes issue [#3298](https://github.com/naturalcrit/homebrewery/issues/3298)
|
||||||
|
|
||||||
|
* [x] Change FrontCover title to use stroke outline instead of faking it with dozens of shadows
|
||||||
|
* [x] Cleaning and refactoring several large pieces of CSS
|
||||||
|
|
||||||
|
##### abquintic
|
||||||
|
|
||||||
|
* [x] Added additional {{openSans **TABLE OF CONTENTS**}} snippet options. Explicitly include or exclude items from the ToC generation via CSS properties
|
||||||
|
`--TOC:exclude` or `--TOC:include`, or change the included header depth from 3 to 6 (default 3) with `tocDepthH6`
|
||||||
|
|
||||||
|
##### MurdoMaclachlan *(new contributor!)*
|
||||||
|
|
||||||
|
* [x] Added "proficiency bonus" to Monster Stat Block snippet.
|
||||||
|
|
||||||
|
Fixes issue [#3397](https://github.com/naturalcrit/homebrewery/issues/3397)
|
||||||
|
}}
|
||||||
|
|
||||||
### Monday 18/3/2024 - v3.12.0
|
### Monday 18/3/2024 - v3.12.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -375,7 +602,7 @@ Fixes issue [#2729](https://github.com/naturalcrit/homebrewery/issues/2729),
|
|||||||
### Thursday 17/08/2023 - v3.9.2
|
### Thursday 17/08/2023 - v3.9.2
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### Calculuschild
|
##### calculuschild
|
||||||
|
|
||||||
* [x] Fix links to certain old Google Drive files
|
* [x] Fix links to certain old Google Drive files
|
||||||
|
|
||||||
@@ -433,7 +660,7 @@ Fixes issue [#1924](https://github.com/naturalcrit/homebrewery/issues/1924)
|
|||||||
### Friday 02/06/2023 - v3.9.0
|
### Friday 02/06/2023 - v3.9.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### Calculuschild
|
##### calculuschild
|
||||||
|
|
||||||
* [x] Fix some files not showing up on userpage when user has a large number of brews in Google Drive
|
* [x] Fix some files not showing up on userpage when user has a large number of brews in Google Drive
|
||||||
|
|
||||||
@@ -530,7 +757,7 @@ Fixes issues [#2731](https://github.com/naturalcrit/homebrewery/issues/2731)
|
|||||||
### Monday 13/03/2023 - v3.7.2
|
### Monday 13/03/2023 - v3.7.2
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### Calculuschild
|
##### calculuschild
|
||||||
|
|
||||||
* [x] Fix wide Monster Stat Blocks not spanning columns on Legacy
|
* [x] Fix wide Monster Stat Blocks not spanning columns on Legacy
|
||||||
}}
|
}}
|
||||||
@@ -553,7 +780,7 @@ Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569)
|
|||||||
* [x] Updated the Google Drive icon
|
* [x] Updated the Google Drive icon
|
||||||
* [x] Backend fix to unit tests failing intermittently
|
* [x] Backend fix to unit tests failing intermittently
|
||||||
|
|
||||||
##### Calculuschild
|
##### calculuschild
|
||||||
|
|
||||||
* [x] Fix PDF pixelation on CoverPage text outlines
|
* [x] Fix PDF pixelation on CoverPage text outlines
|
||||||
}}
|
}}
|
||||||
@@ -565,7 +792,7 @@ Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569)
|
|||||||
**NOTE:** Some new snippets will now show a {{beta BETA}} tag. Feel free to use them, but be aware we may change how they work depending on your feedback.
|
**NOTE:** Some new snippets will now show a {{beta BETA}} tag. Feel free to use them, but be aware we may change how they work depending on your feedback.
|
||||||
}}
|
}}
|
||||||
|
|
||||||
##### Calculuschild
|
##### calculuschild
|
||||||
|
|
||||||
* [x] New {{openSans **IMAGES → WATERCOLOR EDGE** {{fac,mask-edge}} }} and {{openSans **WATERCOLOR CORNER** {{fac,mask-corner}} }} snippets for V3, which adds a stylish watercolor texture to the edge of your images! (Thanks to /u/flamableconcrete on Reddit for providing these image masks!)
|
* [x] New {{openSans **IMAGES → WATERCOLOR EDGE** {{fac,mask-edge}} }} and {{openSans **WATERCOLOR CORNER** {{fac,mask-corner}} }} snippets for V3, which adds a stylish watercolor texture to the edge of your images! (Thanks to /u/flamableconcrete on Reddit for providing these image masks!)
|
||||||
|
|
||||||
@@ -709,7 +936,7 @@ Fixes issues [#1670](https://github.com/naturalcrit/homebrewery/issues/1670)
|
|||||||
### Thursday 28/10/2022 - v3.3.1
|
### Thursday 28/10/2022 - v3.3.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### Calculuschild
|
##### calculuschild
|
||||||
|
|
||||||
* [x] Fixes to several broken CSS styles from v3.3.0
|
* [x] Fixes to several broken CSS styles from v3.3.0
|
||||||
|
|
||||||
@@ -724,7 +951,7 @@ Fixes issues [#2468](https://github.com/naturalcrit/homebrewery/issues/2468)
|
|||||||
### Friday 19/10/2022 - v3.3.0
|
### Friday 19/10/2022 - v3.3.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### Calculuschild
|
##### calculuschild
|
||||||
|
|
||||||
* [x] Fix for tables broken by Chrome v106
|
* [x] Fix for tables broken by Chrome v106
|
||||||
|
|
||||||
@@ -807,7 +1034,7 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
|||||||
### Wednesday 31/08/2022 - v3.2.1
|
### Wednesday 31/08/2022 - v3.2.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### Calculuschild
|
##### calculuschild
|
||||||
|
|
||||||
* [x] Reference Links should now work inside tables
|
* [x] Reference Links should now work inside tables
|
||||||
|
|
||||||
@@ -833,7 +1060,7 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
|||||||
### Saturday 27/08/2022 - v3.2.0
|
### Saturday 27/08/2022 - v3.2.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### Calculuschild
|
##### calculuschild
|
||||||
|
|
||||||
* [x] The V3 renderer is now the default for new brews.
|
* [x] The V3 renderer is now the default for new brews.
|
||||||
|
|
||||||
@@ -860,7 +1087,7 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
|||||||
### Thursday 09/06/2022 - v3.1.1
|
### Thursday 09/06/2022 - v3.1.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### Calculuschild:
|
##### calculuschild:
|
||||||
|
|
||||||
* [x] Fixed class table decorations appearing on top of the table in PDF output.
|
* [x] Fixed class table decorations appearing on top of the table in PDF output.
|
||||||
|
|
||||||
@@ -1704,4 +1931,4 @@ Massive changelog incoming:
|
|||||||
|
|
||||||
* Added `phb.standalone.css` plus a build system for creating it
|
* Added `phb.standalone.css` plus a build system for creating it
|
||||||
* Added page numbers and footer text
|
* Added page numbers and footer text
|
||||||
* Page accent now flips each page
|
* Page accent now flips each page
|
||||||
|
|||||||
29
client/components/dialog.jsx
Normal file
29
client/components/dialog.jsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Dialog box, for popups and modal blocking messages
|
||||||
|
const React = require('react');
|
||||||
|
const { useRef, useEffect } = React;
|
||||||
|
|
||||||
|
function Dialog({ dismissKey, closeText = 'Close', blocking = false, ...rest }) {
|
||||||
|
const dialogRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
if(!dismissKey || !localStorage.getItem(dismissKey)) {
|
||||||
|
blocking ? dialogRef.current?.showModal() : dialogRef.current?.show();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const dismiss = ()=>{
|
||||||
|
dismissKey && localStorage.setItem(dismissKey, true);
|
||||||
|
dialogRef.current?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog ref={dialogRef} onCancel={dismiss} {...rest}>
|
||||||
|
{rest.children}
|
||||||
|
<button className='dismiss' onClick={dismiss}>
|
||||||
|
{closeText}
|
||||||
|
</button>
|
||||||
|
</dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dialog;
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./brewRenderer.less');
|
require('./brewRenderer.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { useState, useRef, useEffect } = React;
|
const { useState, useRef, useEffect, useCallback } = React;
|
||||||
const _ = require('lodash');
|
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');
|
||||||
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
|
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
|
||||||
const Frame = require('react-frame-component').default;
|
const Frame = require('react-frame-component').default;
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
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`
|
||||||
@@ -36,7 +36,7 @@ const BrewPage = (props)=>{
|
|||||||
index : 0,
|
index : 0,
|
||||||
...props
|
...props
|
||||||
};
|
};
|
||||||
const cleanText = DOMPurify.sanitize(props.contents, purifyConfig);
|
const cleanText = props.contents; //DOMPurify.sanitize(props.contents, purifyConfig);
|
||||||
return <div className={props.className} id={`p${props.index + 1}`} >
|
return <div className={props.className} id={`p${props.index + 1}`} >
|
||||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
|
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
|
||||||
</div>;
|
</div>;
|
||||||
@@ -49,21 +49,25 @@ let rawPages = [];
|
|||||||
|
|
||||||
const BrewRenderer = (props)=>{
|
const BrewRenderer = (props)=>{
|
||||||
props = {
|
props = {
|
||||||
text : '',
|
text : '',
|
||||||
style : '',
|
style : '',
|
||||||
renderer : 'legacy',
|
renderer : 'legacy',
|
||||||
theme : '5ePHB',
|
theme : '5ePHB',
|
||||||
lang : '',
|
lang : '',
|
||||||
errors : [],
|
errors : [],
|
||||||
currentEditorPage : 0,
|
currentEditorCursorPageNum : 0,
|
||||||
|
currentEditorViewPageNum : 0,
|
||||||
|
currentBrewRendererPageNum : 0,
|
||||||
|
themeBundle : {},
|
||||||
|
onPageChange : ()=>{},
|
||||||
...props
|
...props
|
||||||
};
|
};
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
viewablePageNumber : 0,
|
height : PAGE_HEIGHT,
|
||||||
height : PAGE_HEIGHT,
|
isMounted : false,
|
||||||
isMounted : false,
|
visibility : 'hidden',
|
||||||
visibility : 'hidden',
|
zoom : 100
|
||||||
});
|
});
|
||||||
|
|
||||||
const mainRef = useRef(null);
|
const mainRef = useRef(null);
|
||||||
@@ -85,38 +89,27 @@ const BrewRenderer = (props)=>{
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScroll = (e)=>{
|
const updateCurrentPage = useCallback(_.throttle((e)=>{
|
||||||
const target = e.target;
|
const { scrollTop, clientHeight, scrollHeight } = e.target;
|
||||||
setState((prevState)=>({
|
const totalScrollableHeight = scrollHeight - clientHeight;
|
||||||
...prevState,
|
const currentPageNumber = Math.max(Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length), 1);
|
||||||
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * rawPages.length)
|
|
||||||
}));
|
props.onPageChange(currentPageNumber);
|
||||||
};
|
}, 200), []);
|
||||||
|
|
||||||
const isInView = (index)=>{
|
const isInView = (index)=>{
|
||||||
if(!state.isMounted)
|
if(!state.isMounted)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if(index == props.currentEditorPage) //Already rendered before this step
|
if(index == props.currentEditorCursorPageNum - 1) //Already rendered before this step
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if(Math.abs(index - state.viewablePageNumber) <= 3)
|
if(Math.abs(index - props.currentBrewRendererPageNum - 1) <= 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' />
|
||||||
@@ -124,10 +117,9 @@ const BrewRenderer = (props)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderStyle = ()=>{
|
const renderStyle = ()=>{
|
||||||
if(!props.style) return;
|
const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig);
|
||||||
const cleanStyle = 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)=>{
|
||||||
@@ -149,7 +141,7 @@ const BrewRenderer = (props)=>{
|
|||||||
renderedPages.length = 0;
|
renderedPages.length = 0;
|
||||||
|
|
||||||
// Render currently-edited page first so cross-page effects (variables, links) can propagate out first
|
// Render currently-edited page first so cross-page effects (variables, links) can propagate out first
|
||||||
renderedPages[props.currentEditorPage] = renderPage(rawPages[props.currentEditorPage], props.currentEditorPage);
|
renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1);
|
||||||
|
|
||||||
_.forEach(rawPages, (page, index)=>{
|
_.forEach(rawPages, (page, index)=>{
|
||||||
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){
|
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){
|
||||||
@@ -159,6 +151,16 @@ const BrewRenderer = (props)=>{
|
|||||||
return renderedPages;
|
return renderedPages;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleControlKeys = (e)=>{
|
||||||
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
|
const P_KEY = 80;
|
||||||
|
if(e.keyCode == P_KEY && props.allowPrint) printCurrentBrew();
|
||||||
|
if(e.keyCode == P_KEY) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
|
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
|
||||||
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
|
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
|
||||||
updateSize();
|
updateSize();
|
||||||
@@ -177,21 +179,33 @@ const BrewRenderer = (props)=>{
|
|||||||
document.dispatchEvent(new MouseEvent('click'));
|
document.dispatchEvent(new MouseEvent('click'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererPath = props.renderer == 'V3' ? 'V3' : 'Legacy';
|
//Toolbar settings:
|
||||||
const themePath = props.theme ?? '5ePHB';
|
const handleZoom = (newZoom)=>{
|
||||||
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
setState((prevState)=>({
|
||||||
|
...prevState,
|
||||||
|
zoom : newZoom
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/*render dummy page while iFrame is mounting.*/}
|
{/*render dummy page while iFrame is mounting.*/}
|
||||||
{!state.isMounted
|
{!state.isMounted
|
||||||
? <div className='brewRenderer' onScroll={handleScroll}>
|
? <div className='brewRenderer' onScroll={updateCurrentPage}>
|
||||||
<div className='pages'>
|
<div className='pages'>
|
||||||
{renderDummyPage(1)}
|
{renderDummyPage(1)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
|
<ErrorBar errors={props.errors} />
|
||||||
|
<div className='popups' ref={mainRef}>
|
||||||
|
<RenderWarnings />
|
||||||
|
<NotificationPopup />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ToolBar onZoomChange={handleZoom} currentPage={props.currentBrewRendererPageNum} 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 }}
|
||||||
@@ -199,33 +213,23 @@ const BrewRenderer = (props)=>{
|
|||||||
onClick={()=>{emitClick();}}
|
onClick={()=>{emitClick();}}
|
||||||
>
|
>
|
||||||
<div className={'brewRenderer'}
|
<div className={'brewRenderer'}
|
||||||
onScroll={handleScroll}
|
onScroll={updateCurrentPage}
|
||||||
|
onKeyDown={handleControlKeys}
|
||||||
|
tabIndex={-1}
|
||||||
style={{ height: state.height }}>
|
style={{ height: state.height }}>
|
||||||
|
|
||||||
<ErrorBar errors={props.errors} />
|
|
||||||
<div className='popups'>
|
|
||||||
<RenderWarnings />
|
|
||||||
<NotificationPopup />
|
|
||||||
</div>
|
|
||||||
<link href={`/themes/${rendererPath}/Blank/style.css`} type='text/css' rel='stylesheet'/>
|
|
||||||
{baseThemePath &&
|
|
||||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type='text/css' rel='stylesheet'/>
|
|
||||||
}
|
|
||||||
<link href={`/themes/${rendererPath}/${themePath}/style.css`} type='text/css' rel='stylesheet'/>
|
|
||||||
|
|
||||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||||
{state.isMounted
|
{state.isMounted
|
||||||
&&
|
&&
|
||||||
<>
|
<>
|
||||||
{renderStyle()}
|
{renderStyle()}
|
||||||
<div className='pages' lang={`${props.lang || 'en'}`}>
|
<div className='pages' lang={`${props.lang || 'en'}`} style={{ zoom: `${state.zoom}%` }}>
|
||||||
{renderPages()}
|
{renderPages()}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Frame>
|
</Frame>
|
||||||
{renderPageInfo()}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
|
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
|
||||||
|
|
||||||
.brewRenderer {
|
.brewRenderer {
|
||||||
will-change : transform;
|
overflow-y : scroll;
|
||||||
overflow-y : scroll;
|
will-change : transform;
|
||||||
|
padding-top : 30px;
|
||||||
:where(.pages) {
|
:where(.pages) {
|
||||||
margin : 30px 0px;
|
margin : 30px 0px;
|
||||||
& > :where(.page) {
|
& > :where(.page) {
|
||||||
@@ -14,53 +15,31 @@
|
|||||||
box-shadow : 1px 4px 14px #000000;
|
box-shadow : 1px 4px 14px #000000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 20px;
|
width : 20px;
|
||||||
&:horizontal{
|
&:horizontal {
|
||||||
height: 20px;
|
width : auto;
|
||||||
width:auto;
|
height : 20px;
|
||||||
}
|
}
|
||||||
&-thumb {
|
&-thumb {
|
||||||
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
|
background : linear-gradient(90deg, #D3C1AF 15px, #00000000 15px);
|
||||||
&:horizontal{
|
&:horizontal { background : linear-gradient(0deg, #D3C1AF 15px, #00000000 15px); }
|
||||||
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&-corner {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
}
|
||||||
|
&-corner { visibility : hidden; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane { position : relative; }
|
.pane { position : relative; }
|
||||||
.pageInfo {
|
|
||||||
position : absolute;
|
@media print {
|
||||||
right : 17px;
|
.toolBar { display : none; }
|
||||||
bottom : 0;
|
.brewRenderer {
|
||||||
z-index : 1000;
|
height : 100%;
|
||||||
font-size : 10px;
|
padding-top : unset;
|
||||||
font-weight : 800;
|
overflow-y : unset;
|
||||||
color : white;
|
.pages {
|
||||||
background-color : #333333;
|
margin : 0px;
|
||||||
div {
|
& > .page { box-shadow : unset; }
|
||||||
display : inline-block;
|
}
|
||||||
padding : 8px 10px;
|
|
||||||
&:not(:last-child) { border-right : 1px solid #666666; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ppr_msg {
|
|
||||||
position : absolute;
|
|
||||||
bottom : 0;
|
|
||||||
left : 0px;
|
|
||||||
z-index : 1000;
|
|
||||||
padding : 8px 10px;
|
|
||||||
font-size : 10px;
|
|
||||||
font-weight : 800;
|
|
||||||
color : white;
|
|
||||||
background-color : #333333;
|
|
||||||
}
|
|
||||||
@@ -1,80 +1,46 @@
|
|||||||
require('./notificationPopup.less');
|
require('./notificationPopup.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const DISMISS_KEY = 'dismiss_notification12-04-23';
|
import Dialog from '../../../components/dialog.jsx';
|
||||||
|
|
||||||
const NotificationPopup = createClass({
|
const DISMISS_KEY = 'dismiss_notification04-09-24';
|
||||||
displayName : 'NotificationPopup',
|
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
|
||||||
getInitialState : function() {
|
|
||||||
return {
|
|
||||||
notifications : {}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
componentDidMount : function() {
|
|
||||||
this.checkNotifications();
|
|
||||||
window.addEventListener('resize', this.checkNotifications);
|
|
||||||
},
|
|
||||||
componentWillUnmount : function() {
|
|
||||||
window.removeEventListener('resize', this.checkNotifications);
|
|
||||||
},
|
|
||||||
notifications : {
|
|
||||||
psa : function(){
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<li key='psa'>
|
|
||||||
<em>Don't store IMAGES in Google Drive</em><br />
|
|
||||||
Google Drive is not an image service, and will block images from being used
|
|
||||||
in brews if they get more views than expected. Google has confirmed they won't fix
|
|
||||||
this, so we recommend you look for another image hosting service such as imgur, ImgBB or Google Photos.
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li key='googleDriveFolder'>
|
const NotificationPopup = ()=>{
|
||||||
<em>Don't delete your Homebrewery folder on Google Drive!</em> <br />
|
return <Dialog className='notificationPopup' dismissKey={DISMISS_KEY} closeText={DISMISS_BUTTON} >
|
||||||
We have had several reports of users losing their brews, not realizing
|
<div className='header'>
|
||||||
that they had deleted the files on their Google Drive. If you have a Homebrewery folder
|
<i className='fas fa-info-circle info'></i>
|
||||||
on your Google Drive with *.txt files inside, <em>do not delete it</em>!
|
<h3>Notice</h3>
|
||||||
We cannot help you recover files that you have deleted from your own
|
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
|
||||||
Google Drive.
|
</div>
|
||||||
</li>
|
<ul>
|
||||||
|
<li key='Vault'>
|
||||||
|
<em>Search brews with our new page!</em><br />
|
||||||
|
We have been working very hard in making this possible, now you can share your work and look at it in the new <a href='/vault'>Vault</a> page!
|
||||||
|
All PUBLISHED brews will be available to anyone searching there, by title or author, and filtering by renderer.
|
||||||
|
|
||||||
<li key='faq'>
|
More features will be coming.
|
||||||
<em>Protect your work! </em> <br />
|
</li>
|
||||||
If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews!
|
|
||||||
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
|
|
||||||
See the FAQ
|
|
||||||
</a> to learn how to avoid losing your work!
|
|
||||||
</li>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
checkNotifications : function(){
|
|
||||||
const hideDismiss = localStorage.getItem(DISMISS_KEY);
|
|
||||||
if(hideDismiss) return this.setState({ notifications: {} });
|
|
||||||
|
|
||||||
this.setState({
|
<li key='googleDriveFolder'>
|
||||||
notifications : _.mapValues(this.notifications, (fn)=>{ return fn(); }) //Convert notification functions into their return text value
|
<em>Don't delete your Homebrewery folder on Google Drive!</em> <br />
|
||||||
});
|
We have had several reports of users losing their brews, not realizing
|
||||||
},
|
that they had deleted the files on their Google Drive. If you have a Homebrewery folder
|
||||||
dismiss : function(){
|
on your Google Drive with *.txt files inside, <em>do not delete it</em>!
|
||||||
localStorage.setItem(DISMISS_KEY, true);
|
We cannot help you recover files that you have deleted from your own
|
||||||
this.checkNotifications();
|
Google Drive.
|
||||||
},
|
</li>
|
||||||
render : function(){
|
|
||||||
if(_.isEmpty(this.state.notifications)) return null;
|
|
||||||
|
|
||||||
return <div className='notificationPopup'>
|
<li key='faq'>
|
||||||
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
|
<em>Protect your work! </em> <br />
|
||||||
<i className='fas fa-info-circle info' />
|
If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews!
|
||||||
<div className='header'>
|
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
|
||||||
<h3>Notice</h3>
|
See the FAQ
|
||||||
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
|
</a> to learn how to avoid losing your work!
|
||||||
</div>
|
</li>
|
||||||
<ul>{_.values(this.state.notifications)}</ul>
|
</ul>
|
||||||
</div>;
|
</Dialog>;
|
||||||
}
|
};
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = NotificationPopup;
|
module.exports = NotificationPopup;
|
||||||
|
|||||||
@@ -1,64 +1,61 @@
|
|||||||
.popups{
|
.popups {
|
||||||
position : fixed;
|
position : fixed;
|
||||||
top : @navbarHeight;
|
top : calc(@navbarHeight + @viewerToolsHeight);
|
||||||
right : 15px;
|
right : 24px;
|
||||||
z-index : 10001;
|
z-index : 10001;
|
||||||
width : 450px;
|
width : 450px;
|
||||||
|
margin-top : 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notificationPopup{
|
.notificationPopup {
|
||||||
position : relative;
|
position : relative;
|
||||||
display : inline-block;
|
|
||||||
width : 100%;
|
width : 100%;
|
||||||
padding : 15px;
|
padding : 15px;
|
||||||
padding-bottom : 10px;
|
padding-bottom : 10px;
|
||||||
padding-left : 25px;
|
padding-left : 25px;
|
||||||
background-color : @blue;
|
|
||||||
color : white;
|
color : white;
|
||||||
a{
|
background-color : @blue;
|
||||||
color : #e0e5c1;
|
border : none;
|
||||||
|
&[open] { display : inline-block; }
|
||||||
|
a {
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
|
color : #E0E5C1;
|
||||||
}
|
}
|
||||||
i.info{
|
i.info {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 12px;
|
top : 12px;
|
||||||
left : 12px;
|
left : 12px;
|
||||||
opacity : 0.8;
|
|
||||||
font-size : 2.5em;
|
font-size : 2.5em;
|
||||||
|
opacity : 0.8;
|
||||||
}
|
}
|
||||||
i.dismiss{
|
button.dismiss {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 10px;
|
top : 10px;
|
||||||
right : 10px;
|
right : 10px;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
opacity : 0.6;
|
background-color : transparent;
|
||||||
&:hover{
|
opacity : 0.6;
|
||||||
opacity : 1;
|
&:hover { opacity : 1; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.header {
|
.header { padding-left : 50px; }
|
||||||
padding-left : 50px;
|
small {
|
||||||
}
|
|
||||||
small{
|
|
||||||
opacity : 0.7;
|
|
||||||
font-size : 0.6em;
|
font-size : 0.6em;
|
||||||
|
opacity : 0.7;
|
||||||
}
|
}
|
||||||
h3{
|
h3 {
|
||||||
font-size : 1.1em;
|
font-size : 1.1em;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
}
|
}
|
||||||
ul{
|
ul {
|
||||||
margin-top : 15px;
|
margin-top : 15px;
|
||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
list-style-position : outside;
|
list-style-position : outside;
|
||||||
list-style-type : disc;
|
list-style-type : disc;
|
||||||
li{
|
li {
|
||||||
|
margin-top : 1.4em;
|
||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
line-height : 1.4em;
|
line-height : 1.4em;
|
||||||
margin-top : 1.4em;
|
em { font-weight : 800; }
|
||||||
em{
|
|
||||||
font-weight : 800;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
164
client/homebrew/brewRenderer/toolBar/toolBar.jsx
Normal file
164
client/homebrew/brewRenderer/toolBar/toolBar.jsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
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);
|
||||||
|
const [toolsVisible, setToolsVisible] = useState(true);
|
||||||
|
|
||||||
|
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 ${toolsVisible ? 'visible' : 'hidden'}`}>
|
||||||
|
<button className='toggleButton' title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
|
||||||
|
{/*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;
|
||||||
128
client/homebrew/brewRenderer/toolBar/toolBar.less
Normal file
128
client/homebrew/brewRenderer/toolBar/toolBar.less
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
@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;
|
||||||
|
& > *:not(.toggleButton) {
|
||||||
|
opacity: 1;
|
||||||
|
transition: all .2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
width: 32px;
|
||||||
|
transition: all .3s ease;
|
||||||
|
flex-wrap:nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: unset;
|
||||||
|
opacity: .5;
|
||||||
|
& > *:not(.toggleButton) {
|
||||||
|
opacity: 0;
|
||||||
|
transition: all .2s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.toggleButton {
|
||||||
|
z-index : 5;
|
||||||
|
position:absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 32px;
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./editor.less');
|
require('./editor.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
const Markdown = require('../../../shared/naturalcrit/markdown.js');
|
const Markdown = require('../../../shared/naturalcrit/markdown.js');
|
||||||
|
|
||||||
@@ -22,6 +21,7 @@ const DEFAULT_STYLE_TEXT = dedent`
|
|||||||
color: black;
|
color: black;
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
let isJumping = false;
|
||||||
|
|
||||||
const Editor = createClass({
|
const Editor = createClass({
|
||||||
displayName : 'Editor',
|
displayName : 'Editor',
|
||||||
@@ -37,8 +37,15 @@ const Editor = createClass({
|
|||||||
onMetaChange : ()=>{},
|
onMetaChange : ()=>{},
|
||||||
reportError : ()=>{},
|
reportError : ()=>{},
|
||||||
|
|
||||||
|
onCursorPageChange : ()=>{},
|
||||||
|
onViewPageChange : ()=>{},
|
||||||
|
|
||||||
editorTheme : 'default',
|
editorTheme : 'default',
|
||||||
renderer : 'legacy'
|
renderer : 'legacy',
|
||||||
|
|
||||||
|
currentEditorCursorPageNum : 1,
|
||||||
|
currentEditorViewPageNum : 1,
|
||||||
|
currentBrewRendererPageNum : 1,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
@@ -56,9 +63,15 @@ const Editor = createClass({
|
|||||||
isMeta : function() {return this.state.view == 'meta';},
|
isMeta : function() {return this.state.view == 'meta';},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
|
this.codeEditor.current.codeMirror.on('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor());});
|
||||||
|
this.codeEditor.current.codeMirror.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());}, 200));
|
||||||
|
|
||||||
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
|
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
|
||||||
if(editorTheme) {
|
if(editorTheme) {
|
||||||
@@ -73,13 +86,35 @@ const Editor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
||||||
|
|
||||||
this.highlightCustomMarkdown();
|
this.highlightCustomMarkdown();
|
||||||
if(prevProps.moveBrew !== this.props.moveBrew) {
|
if(prevProps.moveBrew !== this.props.moveBrew)
|
||||||
this.brewJump();
|
this.brewJump();
|
||||||
};
|
|
||||||
if(prevProps.moveSource !== this.props.moveSource) {
|
if(prevProps.moveSource !== this.props.moveSource)
|
||||||
this.sourceJump();
|
this.sourceJump();
|
||||||
};
|
|
||||||
|
if(this.props.liveScroll) {
|
||||||
|
if(prevProps.currentBrewRendererPageNum !== this.props.currentBrewRendererPageNum) {
|
||||||
|
this.sourceJump(this.props.currentBrewRendererPageNum, false);
|
||||||
|
} else if(prevProps.currentEditorViewPageNum !== this.props.currentEditorViewPageNum) {
|
||||||
|
this.brewJump(this.props.currentEditorViewPageNum, false);
|
||||||
|
} else if(prevProps.currentEditorCursorPageNum !== this.props.currentEditorCursorPageNum) {
|
||||||
|
this.brewJump(this.props.currentEditorCursorPageNum, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleControlKeys : function(e){
|
||||||
|
if(!(e.ctrlKey && e.metaKey && e.shiftKey)) return;
|
||||||
|
const LEFTARROW_KEY = 37;
|
||||||
|
const RIGHTARROW_KEY = 39;
|
||||||
|
if(e.keyCode == RIGHTARROW_KEY) this.brewJump();
|
||||||
|
if(e.keyCode == LEFTARROW_KEY) this.sourceJump();
|
||||||
|
if(e.keyCode == LEFTARROW_KEY || e.keyCode == RIGHTARROW_KEY) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateEditorSize : function() {
|
updateEditorSize : function() {
|
||||||
@@ -90,6 +125,20 @@ const Editor = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateCurrentCursorPage : function(cursor) {
|
||||||
|
const lines = this.props.brew.text.split('\n').slice(0, cursor.line + 1);
|
||||||
|
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
|
||||||
|
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
||||||
|
this.props.onCursorPageChange(currentPage);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCurrentViewPage : function(topScrollLine) {
|
||||||
|
const lines = this.props.brew.text.split('\n').slice(0, topScrollLine + 1);
|
||||||
|
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
|
||||||
|
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
||||||
|
this.props.onViewPageChange(currentPage);
|
||||||
|
},
|
||||||
|
|
||||||
handleInject : function(injectText){
|
handleInject : function(injectText){
|
||||||
this.codeEditor.current?.injectText(injectText, false);
|
this.codeEditor.current?.injectText(injectText, false);
|
||||||
},
|
},
|
||||||
@@ -98,19 +147,10 @@ const Editor = createClass({
|
|||||||
this.props.setMoveArrows(newView === 'text');
|
this.props.setMoveArrows(newView === 'text');
|
||||||
this.setState({
|
this.setState({
|
||||||
view : newView
|
view : newView
|
||||||
}, this.updateEditorSize); //TODO: not sure if updateeditorsize needed
|
}, ()=>{
|
||||||
},
|
this.codeEditor.current?.codeMirror.focus();
|
||||||
|
this.updateEditorSize();
|
||||||
getCurrentPage : function(){
|
}); //TODO: not sure if updateeditorsize needed
|
||||||
const lines = this.props.brew.text.split('\n').slice(0, this.codeEditor.current.getCursorPosition().line + 1);
|
|
||||||
return _.reduce(lines, (r, line)=>{
|
|
||||||
if(
|
|
||||||
(this.props.renderer == 'legacy' && line.indexOf('\\page') !== -1)
|
|
||||||
||
|
|
||||||
(this.props.renderer == 'V3' && line.match(/^\\page$/))
|
|
||||||
) r++;
|
|
||||||
return r;
|
|
||||||
}, 1);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
highlightCustomMarkdown : function(){
|
highlightCustomMarkdown : function(){
|
||||||
@@ -119,8 +159,19 @@ const Editor = createClass({
|
|||||||
const codeMirror = this.codeEditor.current.codeMirror;
|
const codeMirror = this.codeEditor.current.codeMirror;
|
||||||
|
|
||||||
codeMirror.operation(()=>{ // Batch CodeMirror styling
|
codeMirror.operation(()=>{ // Batch CodeMirror styling
|
||||||
|
|
||||||
|
const foldLines = [];
|
||||||
|
|
||||||
//reset custom text styles
|
//reset custom text styles
|
||||||
const customHighlights = codeMirror.getAllMarks().filter((mark)=>!mark.__isFold); //Don't undo code folding
|
const customHighlights = codeMirror.getAllMarks().filter((mark)=>{
|
||||||
|
// Record details of folded sections
|
||||||
|
if(mark.__isFold) {
|
||||||
|
const fold = mark.find();
|
||||||
|
foldLines.push({ from: fold.from?.line, to: fold.to?.line });
|
||||||
|
}
|
||||||
|
return !mark.__isFold;
|
||||||
|
}); //Don't undo code folding
|
||||||
|
|
||||||
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
||||||
|
|
||||||
let editorPageCount = 2; // start page count from page 2
|
let editorPageCount = 2; // start page count from page 2
|
||||||
@@ -132,6 +183,11 @@ const Editor = createClass({
|
|||||||
codeMirror.removeLineClass(lineNumber, 'text');
|
codeMirror.removeLineClass(lineNumber, 'text');
|
||||||
codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
|
codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
|
||||||
|
|
||||||
|
// Don't process lines inside folded text
|
||||||
|
// If the current lineNumber is inside any folded marks, skip line styling
|
||||||
|
if(foldLines.some((fold)=>lineNumber >= fold.from && lineNumber <= fold.to))
|
||||||
|
return;
|
||||||
|
|
||||||
// Styling for \page breaks
|
// Styling for \page breaks
|
||||||
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
||||||
(this.props.renderer == 'V3' && line.match(/^\\page$/))) {
|
(this.props.renderer == 'V3' && line.match(/^\\page$/))) {
|
||||||
@@ -155,7 +211,7 @@ const Editor = createClass({
|
|||||||
|
|
||||||
// definition lists
|
// definition lists
|
||||||
if(line.includes('::')){
|
if(line.includes('::')){
|
||||||
if(/^:*$/.test(line) == true){ return };
|
if(/^:*$/.test(line) == true){ return; };
|
||||||
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
|
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
|
||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(line)) != null){
|
while ((match = regex.exec(line)) != null){
|
||||||
@@ -163,10 +219,10 @@ const Editor = createClass({
|
|||||||
codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' });
|
codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' });
|
||||||
codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' });
|
codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' });
|
||||||
const ddIndex = match.indices[2][0];
|
const ddIndex = match.indices[2][0];
|
||||||
let colons = /::/g;
|
const colons = /::/g;
|
||||||
let colonMatches = colons.exec(match[2]);
|
const colonMatches = colons.exec(match[2]);
|
||||||
if(colonMatches !== null){
|
if(colonMatches !== null){
|
||||||
codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight'} )
|
codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,12 +232,12 @@ const Editor = createClass({
|
|||||||
let startIndex = line.indexOf('^');
|
let startIndex = line.indexOf('^');
|
||||||
const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy;
|
const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy;
|
||||||
const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy;
|
const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy;
|
||||||
|
|
||||||
while (startIndex >= 0) {
|
while (startIndex >= 0) {
|
||||||
superRegex.lastIndex = subRegex.lastIndex = startIndex;
|
superRegex.lastIndex = subRegex.lastIndex = startIndex;
|
||||||
let isSuper = false;
|
let isSuper = false;
|
||||||
let match = subRegex.exec(line) || superRegex.exec(line);
|
const match = subRegex.exec(line) || superRegex.exec(line);
|
||||||
if (match) {
|
if(match) {
|
||||||
isSuper = !subRegex.lastIndex;
|
isSuper = !subRegex.lastIndex;
|
||||||
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' });
|
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' });
|
||||||
}
|
}
|
||||||
@@ -231,20 +287,20 @@ const Editor = createClass({
|
|||||||
|
|
||||||
while (startIndex >= 0) {
|
while (startIndex >= 0) {
|
||||||
emojiRegex.lastIndex = startIndex;
|
emojiRegex.lastIndex = startIndex;
|
||||||
let match = emojiRegex.exec(line);
|
const match = emojiRegex.exec(line);
|
||||||
if (match) {
|
if(match) {
|
||||||
let tokens = Markdown.marked.lexer(match[0]);
|
let tokens = Markdown.marked.lexer(match[0]);
|
||||||
tokens = tokens[0].tokens.filter(t => t.type == 'emoji')
|
tokens = tokens[0].tokens.filter((t)=>t.type == 'emoji');
|
||||||
if (!tokens.length)
|
if(!tokens.length)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let startPos = { line: lineNumber, ch: match.index };
|
const startPos = { line: lineNumber, ch: match.index };
|
||||||
let endPos = { line: lineNumber, ch: match.index + match[0].length };
|
const endPos = { line: lineNumber, ch: match.index + match[0].length };
|
||||||
|
|
||||||
// Iterate over conflicting marks and clear them
|
// Iterate over conflicting marks and clear them
|
||||||
var marks = codeMirror.findMarks(startPos, endPos);
|
const marks = codeMirror.findMarks(startPos, endPos);
|
||||||
marks.forEach(function(marker) {
|
marks.forEach(function(marker) {
|
||||||
marker.clear();
|
if(!marker.__isFold) marker.clear();
|
||||||
});
|
});
|
||||||
codeMirror.markText(startPos, endPos, { className: 'emoji' });
|
codeMirror.markText(startPos, endPos, { className: 'emoji' });
|
||||||
}
|
}
|
||||||
@@ -257,75 +313,93 @@ const Editor = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
brewJump : function(targetPage=this.getCurrentPage()){
|
brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){
|
||||||
if(!window) return;
|
if(!window || isJumping)
|
||||||
// console.log(`Scroll to: p${targetPage}`);
|
return;
|
||||||
|
|
||||||
|
// Get current brewRenderer scroll position and calculate target position
|
||||||
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
|
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
|
||||||
const currentPos = brewRenderer.scrollTop;
|
const currentPos = brewRenderer.scrollTop;
|
||||||
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
|
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
|
||||||
const interimPos = targetPos >= 0 ? -30 : 30;
|
|
||||||
|
|
||||||
const bounceDelay = 100;
|
const checkIfScrollComplete = ()=>{
|
||||||
const scrollDelay = 500;
|
let scrollingTimeout;
|
||||||
|
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
|
||||||
if(!this.throttleBrewMove) {
|
scrollingTimeout = setTimeout(()=>{
|
||||||
this.throttleBrewMove = _.throttle((currentPos, interimPos, targetPos)=>{
|
isJumping = false;
|
||||||
brewRenderer.scrollTo({ top: currentPos + interimPos, behavior: 'smooth' });
|
brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
|
||||||
setTimeout(()=>{
|
}, 150); // If 150 ms pass without a brewRenderer scroll event, assume scrolling is done
|
||||||
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
|
|
||||||
}, bounceDelay);
|
|
||||||
}, scrollDelay, { leading: true, trailing: false });
|
|
||||||
};
|
};
|
||||||
this.throttleBrewMove(currentPos, interimPos, targetPos);
|
|
||||||
|
|
||||||
// const hashPage = (page != 1) ? `p${page}` : '';
|
isJumping = true;
|
||||||
// window.location.hash = hashPage;
|
checkIfScrollComplete();
|
||||||
|
brewRenderer.addEventListener('scroll', checkIfScrollComplete);
|
||||||
|
|
||||||
|
if(smooth) {
|
||||||
|
const bouncePos = targetPos >= 0 ? -30 : 30; //Do a little bounce before scrolling
|
||||||
|
const bounceDelay = 100;
|
||||||
|
const scrollDelay = 500;
|
||||||
|
|
||||||
|
if(!this.throttleBrewMove) {
|
||||||
|
this.throttleBrewMove = _.throttle((currentPos, bouncePos, targetPos)=>{
|
||||||
|
brewRenderer.scrollTo({ top: currentPos + bouncePos, behavior: 'smooth' });
|
||||||
|
setTimeout(()=>{
|
||||||
|
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
|
||||||
|
}, bounceDelay);
|
||||||
|
}, scrollDelay, { leading: true, trailing: false });
|
||||||
|
};
|
||||||
|
this.throttleBrewMove(currentPos, bouncePos, targetPos);
|
||||||
|
} else {
|
||||||
|
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'instant', block: 'start' });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
sourceJump : function(targetLine=null){
|
sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){
|
||||||
if(this.isText()) {
|
if(!this.isText || isJumping)
|
||||||
if(targetLine == null) {
|
return;
|
||||||
targetLine = 0;
|
|
||||||
|
|
||||||
const pageCollection = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('page');
|
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
|
||||||
const brewRendererHeight = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0).getBoundingClientRect().height;
|
const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit);
|
||||||
|
const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
|
||||||
|
|
||||||
let currentPage = 1;
|
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
|
||||||
for (const page of pageCollection) {
|
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
|
||||||
if(page.getBoundingClientRect().bottom > (brewRendererHeight / 2)) {
|
|
||||||
currentPage = parseInt(page.id.slice(1)) || 1;
|
const checkIfScrollComplete = ()=>{
|
||||||
break;
|
let scrollingTimeout;
|
||||||
}
|
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
|
||||||
|
scrollingTimeout = setTimeout(()=>{
|
||||||
|
isJumping = false;
|
||||||
|
this.codeEditor.current.codeMirror.off('scroll', checkIfScrollComplete);
|
||||||
|
}, 150); // If 150 ms pass without a scroll event, assume scrolling is done
|
||||||
|
};
|
||||||
|
|
||||||
|
isJumping = true;
|
||||||
|
checkIfScrollComplete();
|
||||||
|
this.codeEditor.current.codeMirror.on('scroll', checkIfScrollComplete);
|
||||||
|
|
||||||
|
if(smooth) {
|
||||||
|
//Scroll 1/10 of the way every 10ms until 1px off.
|
||||||
|
const incrementalScroll = setInterval(()=>{
|
||||||
|
currentY += (targetY - currentY) / 10;
|
||||||
|
this.codeEditor.current.codeMirror.scrollTo(null, currentY);
|
||||||
|
|
||||||
|
// Update target: target height is not accurate until within +-10 lines of the visible window
|
||||||
|
if(Math.abs(targetY - currentY > 100))
|
||||||
|
targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
|
||||||
|
|
||||||
|
// End when close enough
|
||||||
|
if(Math.abs(targetY - currentY) < 1) {
|
||||||
|
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
|
||||||
|
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
|
||||||
|
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
|
||||||
|
clearInterval(incrementalScroll);
|
||||||
}
|
}
|
||||||
|
}, 10);
|
||||||
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
|
} else {
|
||||||
const textString = this.props.brew.text.split(textSplit).slice(0, currentPage-1).join(textSplit);
|
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
|
||||||
const textPosition = textString.length;
|
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
|
||||||
const lineCount = textString.match('\n') ? textString.slice(0, textPosition).split('\n').length : 0;
|
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
|
||||||
|
|
||||||
targetLine = lineCount - 1; //Scroll to `\page`, which is one line back.
|
|
||||||
|
|
||||||
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
|
|
||||||
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
|
|
||||||
|
|
||||||
//Scroll 1/10 of the way every 10ms until 1px off.
|
|
||||||
const incrementalScroll = setInterval(()=>{
|
|
||||||
currentY += (targetY - currentY) / 10;
|
|
||||||
this.codeEditor.current.codeMirror.scrollTo(null, currentY);
|
|
||||||
|
|
||||||
// Update target: target height is not accurate until within +-10 lines of the visible window
|
|
||||||
if(Math.abs(targetY - currentY > 100))
|
|
||||||
targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
|
|
||||||
|
|
||||||
// End when close enough
|
|
||||||
if(Math.abs(targetY - currentY) < 1) {
|
|
||||||
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
|
|
||||||
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
|
|
||||||
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
|
|
||||||
clearInterval(incrementalScroll);
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -367,7 +441,7 @@ const Editor = createClass({
|
|||||||
view={this.state.view}
|
view={this.state.view}
|
||||||
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||||
onChange={this.props.onStyleChange}
|
onChange={this.props.onStyleChange}
|
||||||
enableFolding={false}
|
enableFolding={true}
|
||||||
editorTheme={this.state.editorTheme}
|
editorTheme={this.state.editorTheme}
|
||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent} />
|
||||||
</>;
|
</>;
|
||||||
@@ -381,7 +455,8 @@ const Editor = createClass({
|
|||||||
<MetadataEditor
|
<MetadataEditor
|
||||||
metadata={this.props.brew}
|
metadata={this.props.brew}
|
||||||
onChange={this.props.onMetaChange}
|
onChange={this.props.onMetaChange}
|
||||||
reportError={this.props.reportError}/>
|
reportError={this.props.reportError}
|
||||||
|
userThemes={this.props.userThemes}/>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -424,7 +499,10 @@ const Editor = createClass({
|
|||||||
historySize={this.historySize()}
|
historySize={this.historySize()}
|
||||||
currentEditorTheme={this.state.editorTheme}
|
currentEditorTheme={this.state.editorTheme}
|
||||||
updateEditorTheme={this.updateEditorTheme}
|
updateEditorTheme={this.updateEditorTheme}
|
||||||
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} />
|
snippetBundle={this.props.snippetBundle}
|
||||||
|
cursorPos={this.codeEditor.current?.getCursorPosition() || {}}
|
||||||
|
updateBrew={this.props.updateBrew}
|
||||||
|
/>
|
||||||
|
|
||||||
{this.renderEditor()}
|
{this.renderEditor()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
|||||||
const Combobox = require('client/components/combobox.jsx');
|
const Combobox = require('client/components/combobox.jsx');
|
||||||
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||||
|
|
||||||
|
|
||||||
const Themes = require('themes/themes.json');
|
const Themes = require('themes/themes.json');
|
||||||
const validations = require('./validations.js');
|
const validations = require('./validations.js');
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ const MetadataEditor = createClass({
|
|||||||
return {
|
return {
|
||||||
metadata : {
|
metadata : {
|
||||||
editId : null,
|
editId : null,
|
||||||
|
shareId : null,
|
||||||
title : '',
|
title : '',
|
||||||
description : '',
|
description : '',
|
||||||
thumbnail : '',
|
thumbnail : '',
|
||||||
@@ -98,7 +100,7 @@ const MetadataEditor = createClass({
|
|||||||
if(renderer == 'legacy')
|
if(renderer == 'legacy')
|
||||||
this.props.metadata.theme = '5ePHB';
|
this.props.metadata.theme = '5ePHB';
|
||||||
}
|
}
|
||||||
this.props.onChange(this.props.metadata);
|
this.props.onChange(this.props.metadata, 'renderer');
|
||||||
},
|
},
|
||||||
handlePublish : function(val){
|
handlePublish : function(val){
|
||||||
this.props.onChange({
|
this.props.onChange({
|
||||||
@@ -110,7 +112,7 @@ const MetadataEditor = createClass({
|
|||||||
handleTheme : function(theme){
|
handleTheme : function(theme){
|
||||||
this.props.metadata.renderer = theme.renderer;
|
this.props.metadata.renderer = theme.renderer;
|
||||||
this.props.metadata.theme = theme.path;
|
this.props.metadata.theme = theme.path;
|
||||||
this.props.onChange(this.props.metadata);
|
this.props.onChange(this.props.metadata, 'theme');
|
||||||
},
|
},
|
||||||
|
|
||||||
handleLanguage : function(languageCode){
|
handleLanguage : function(languageCode){
|
||||||
@@ -191,37 +193,42 @@ const MetadataEditor = createClass({
|
|||||||
renderThemeDropdown : function(){
|
renderThemeDropdown : function(){
|
||||||
if(!global.enable_themes) return;
|
if(!global.enable_themes) return;
|
||||||
|
|
||||||
|
const mergedThemes = _.merge(Themes, this.props.userThemes);
|
||||||
|
|
||||||
const listThemes = (renderer)=>{
|
const listThemes = (renderer)=>{
|
||||||
return _.map(_.values(Themes[renderer]), (theme)=>{
|
return _.map(_.values(mergedThemes[renderer]), (theme)=>{
|
||||||
return <div className='item' key={''} onClick={()=>this.handleTheme(theme)} title={''}>
|
if(theme.path == this.props.metadata.shareId) return;
|
||||||
{`${theme.renderer} : ${theme.name}`}
|
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`;
|
||||||
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`}/>
|
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`;
|
||||||
|
return <div className='item' key={`${renderer}_${theme.name}`} onClick={()=>this.handleTheme(theme)} title={''}>
|
||||||
|
{theme.author ?? renderer} : {theme.name}
|
||||||
|
<div className='texture-container'>
|
||||||
|
<img src={texture}/>
|
||||||
|
</div>
|
||||||
<div className='preview'>
|
<div className='preview'>
|
||||||
<h6>{`${theme.name}`} preview</h6>
|
<h6>{theme.name} preview</h6>
|
||||||
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`}/>
|
<img src={preview}/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentTheme = Themes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme];
|
const currentRenderer = this.props.metadata.renderer;
|
||||||
|
const currentTheme = mergedThemes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme]
|
||||||
|
?? { name: `!!! THEME MISSING !!! ID=${this.props.metadata.theme}` };
|
||||||
let dropdown;
|
let dropdown;
|
||||||
|
|
||||||
if(this.props.metadata.renderer == 'legacy') {
|
if(currentRenderer == 'legacy') {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown className='disabled value' trigger='disabled'>
|
<Nav.dropdown className='disabled value' trigger='disabled'>
|
||||||
<div>
|
<div> {`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i> </div>
|
||||||
{`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i>
|
|
||||||
</div>
|
|
||||||
</Nav.dropdown>;
|
</Nav.dropdown>;
|
||||||
} else {
|
} else {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown className='value' trigger='click'>
|
<Nav.dropdown className='value' trigger='click'>
|
||||||
<div>
|
<div> {currentTheme.author ?? _.upperFirst(currentRenderer)} : {currentTheme.name} <i className='fas fa-caret-down'></i> </div>
|
||||||
{`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`} <i className='fas fa-caret-down'></i>
|
|
||||||
</div>
|
{listThemes(currentRenderer)}
|
||||||
{/*listThemes('Legacy')*/}
|
|
||||||
{listThemes('V3')}
|
|
||||||
</Nav.dropdown>;
|
</Nav.dropdown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,327 +1,309 @@
|
|||||||
@import 'naturalcrit/styles/colors.less';
|
@import 'naturalcrit/styles/colors.less';
|
||||||
|
|
||||||
.metadataEditor{
|
.metadataEditor {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
z-index : 5;
|
z-index : 5;
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
padding : 25px;
|
|
||||||
background-color : #999;
|
|
||||||
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
||||||
|
padding : 25px;
|
||||||
overflow-y : auto;
|
overflow-y : auto;
|
||||||
|
background-color : #999999;
|
||||||
|
|
||||||
.sectionHead {
|
.sectionHead {
|
||||||
font-weight: 1000;
|
margin : 20px 0;
|
||||||
margin: 20px 0;
|
font-weight : 1000;
|
||||||
|
|
||||||
&:first-of-type {
|
&:first-of-type { margin-top : 0; }
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& > div {
|
& > div { margin-bottom : 10px; }
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-group {
|
.field-group {
|
||||||
display: flex;
|
display : flex;
|
||||||
width: 100%;
|
flex-wrap : wrap;
|
||||||
flex-wrap: wrap;
|
gap : 10px;
|
||||||
gap: 10px;
|
width : 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-column {
|
.field-column {
|
||||||
display: flex;
|
display : flex;
|
||||||
flex-direction: column;
|
flex : 5 0 200px;
|
||||||
flex: 5 0 200px;
|
flex-direction : column;
|
||||||
gap: 10px;
|
gap : 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.field{
|
.field {
|
||||||
|
position : relative;
|
||||||
display : flex;
|
display : flex;
|
||||||
flex-wrap : wrap;
|
flex-wrap : wrap;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
min-width : 200px;
|
min-width : 200px;
|
||||||
position : relative;
|
& > label {
|
||||||
&>label{
|
|
||||||
width : 80px;
|
width : 80px;
|
||||||
font-size : 11px;
|
font-size : 11px;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
line-height : 1.8em;
|
line-height : 1.8em;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
}
|
}
|
||||||
&>.value{
|
& > .value {
|
||||||
flex : 1 1 auto;
|
flex : 1 1 auto;
|
||||||
width : 50px;
|
width : 50px;
|
||||||
&:invalid {
|
&:invalid { background : #FFB9B9; }
|
||||||
background : #ffb9b9;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
input[type='text'], textarea {
|
input[type='text'], textarea {
|
||||||
border : 1px solid gray;
|
border : 1px solid gray;
|
||||||
&:focus {
|
&:focus { outline : 1px solid #444444; }
|
||||||
outline: 1px solid #444;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&.thumbnail{
|
&.thumbnail {
|
||||||
height : 1.4em;
|
height : 1.4em;
|
||||||
label{
|
label { line-height : 2.0em; }
|
||||||
line-height: 2.0em;
|
.value {
|
||||||
|
overflow : hidden;
|
||||||
|
text-overflow : ellipsis;
|
||||||
}
|
}
|
||||||
.value{
|
button {
|
||||||
overflow: hidden;
|
padding : 0px 5px;
|
||||||
text-overflow: ellipsis;
|
color : white;
|
||||||
}
|
background-color : black;
|
||||||
button{
|
border : 1px solid #999999;
|
||||||
border: 1px solid #999;
|
&:hover { background-color : #777777; }
|
||||||
color: white;
|
|
||||||
padding: 0px 5px;
|
|
||||||
background-color: black;
|
|
||||||
&:hover{
|
|
||||||
background-color: #777;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.description {
|
&.description {
|
||||||
flex: 1;
|
flex : 1;
|
||||||
textarea.value {
|
textarea.value {
|
||||||
resize : none;
|
|
||||||
height : auto;
|
height : auto;
|
||||||
font-family : 'Open Sans', sans-serif;
|
font-family : 'Open Sans', sans-serif;
|
||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
|
resize : none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.language .language-dropdown {
|
&.language .language-dropdown {
|
||||||
max-width : 150px;
|
|
||||||
z-index : 200;
|
z-index : 200;
|
||||||
|
max-width : 150px;
|
||||||
}
|
}
|
||||||
small {
|
small {
|
||||||
|
display : inline-block;
|
||||||
font-size : 0.6em;
|
font-size : 0.6em;
|
||||||
font-style : italic;
|
font-style : italic;
|
||||||
line-height : 1.4em;
|
line-height : 1.4em;
|
||||||
display : inline-block;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.thumbnail-preview {
|
.thumbnail-preview {
|
||||||
position: relative;
|
position : relative;
|
||||||
justify-self: center;
|
flex : 1 1;
|
||||||
width: 80px;
|
justify-self : center;
|
||||||
height: min-content;
|
width : 80px;
|
||||||
flex: 1 1;
|
height : min-content;
|
||||||
max-height: 115px;
|
max-height : 115px;
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio : 1 / 1;
|
||||||
object-fit: contain;
|
object-fit : contain;
|
||||||
background-color: #AAA;
|
background-color : #AAAAAA;
|
||||||
}
|
}
|
||||||
|
|
||||||
.systems.field .value{
|
.systems.field .value {
|
||||||
label{
|
label {
|
||||||
vertical-align : middle;
|
|
||||||
margin-right : 15px;
|
|
||||||
cursor : pointer;
|
|
||||||
font-size : 0.7em;
|
|
||||||
font-weight : 800;
|
|
||||||
user-select : none;
|
|
||||||
white-space : nowrap;
|
|
||||||
display : inline-flex;
|
display : inline-flex;
|
||||||
align-items : center;
|
align-items : center;
|
||||||
}
|
margin-right : 15px;
|
||||||
a {
|
font-size : 0.7em;
|
||||||
font-size : 0.7em;
|
font-weight : 800;
|
||||||
font-weight : 800;
|
white-space : nowrap;
|
||||||
display : inline-flex;
|
|
||||||
}
|
|
||||||
input{
|
|
||||||
vertical-align : middle;
|
vertical-align : middle;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
|
user-select : none;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
display : inline-flex;
|
||||||
|
font-size : 0.7em;
|
||||||
|
font-weight : 800;
|
||||||
|
}
|
||||||
|
input {
|
||||||
margin : 3px;
|
margin : 3px;
|
||||||
|
vertical-align : middle;
|
||||||
|
cursor : pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.publish.field .value{
|
.publish.field .value {
|
||||||
position : relative;
|
position : relative;
|
||||||
margin-bottom: 15px;
|
margin-bottom : 15px;
|
||||||
button{
|
button { width : 100%; }
|
||||||
width:100%;
|
button.publish {
|
||||||
}
|
|
||||||
button.publish{
|
|
||||||
.button(@blueLight);
|
.button(@blueLight);
|
||||||
}
|
}
|
||||||
button.unpublish{
|
button.unpublish {
|
||||||
.button(@silver);
|
.button(@silver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete.field .value{
|
.delete.field .value {
|
||||||
button{
|
button {
|
||||||
.button(@red);
|
.button(@red);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.authors.field .value{
|
.authors.field .value {
|
||||||
font-size: 0.8em;
|
font-size : 0.8em;
|
||||||
line-height : 1.5em;
|
line-height : 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.themes.field{
|
.themes.field {
|
||||||
font-size : 13.33px;
|
font-size : 13.33px;
|
||||||
.navDropdownContainer {
|
.navDropdownContainer {
|
||||||
background-color : white;
|
position : relative;
|
||||||
position : relative;
|
z-index : 100;
|
||||||
z-index : 100;
|
background-color : white;
|
||||||
&.disabled {
|
&.disabled {
|
||||||
font-style :italic;
|
font-style : italic;
|
||||||
font-style : italic;
|
color : dimgray;
|
||||||
background-color : darkgray;
|
background-color : darkgray;
|
||||||
color : dimgray;
|
|
||||||
}
|
}
|
||||||
&>div:first-child {
|
& > div:first-child {
|
||||||
border : 2px solid rgb(118,118,118);
|
padding : 6px 3px;
|
||||||
padding : 6px 3px;
|
background-color : inherit;
|
||||||
background-color : inherit;
|
border : 2px solid rgb(118,118,118);
|
||||||
i {
|
i { float : right; }
|
||||||
float : right;
|
|
||||||
}
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color : @blue;
|
color : white;
|
||||||
color : white;
|
background-color : @blue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.navDropdown .item > p {
|
||||||
|
width : 45%;
|
||||||
|
height : 1.1em;
|
||||||
|
overflow : hidden;
|
||||||
|
text-overflow : ellipsis;
|
||||||
|
white-space : nowrap;
|
||||||
|
}
|
||||||
.navDropdown {
|
.navDropdown {
|
||||||
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
|
||||||
position : absolute;
|
position : absolute;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
|
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||||
.item {
|
.item {
|
||||||
padding : 3px 3px;
|
|
||||||
border-top : 1px solid rgb(118, 118, 118);
|
|
||||||
position : relative;
|
position : relative;
|
||||||
|
padding : 3px 3px;
|
||||||
overflow : visible;
|
overflow : visible;
|
||||||
background-color : white;
|
background-color : white;
|
||||||
|
border-top : 1px solid rgb(118, 118, 118);
|
||||||
.preview {
|
.preview {
|
||||||
display : flex;
|
position : absolute;
|
||||||
flex-direction: column;
|
top : 0;
|
||||||
background : #ccc;
|
right : 0;
|
||||||
border-radius : 5px;
|
z-index : 1;
|
||||||
box-shadow : 0 0 5px black;
|
display : flex;
|
||||||
width : 200px;
|
flex-direction : column;
|
||||||
color :black;
|
width : 200px;
|
||||||
position : absolute;
|
overflow : hidden;
|
||||||
top : 0;
|
color : black;
|
||||||
right : 0;
|
background : #CCCCCC;
|
||||||
opacity : 0;
|
border-radius : 5px;
|
||||||
transition : opacity 250ms ease;
|
box-shadow : 0 0 5px black;
|
||||||
z-index : 1;
|
opacity : 0;
|
||||||
overflow :hidden;
|
transition : opacity 250ms ease;
|
||||||
h6 {
|
h6 {
|
||||||
font-weight : 900;
|
padding-block : 0.5em;
|
||||||
padding-inline:1em;
|
padding-inline : 1em;
|
||||||
padding-block :.5em;
|
font-weight : 900;
|
||||||
border-bottom :2px solid hsl(0,0%,40%);
|
border-bottom : 2px solid hsl(0,0%,40%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color : @blue;
|
color : white;
|
||||||
color : white;
|
background-color : @blue;
|
||||||
}
|
}
|
||||||
&:hover > .preview {
|
&:hover > .preview { opacity : 1; }
|
||||||
opacity: 1;
|
.texture-container {
|
||||||
}
|
position : absolute;
|
||||||
>img {
|
top : 0;
|
||||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
left : 0;
|
||||||
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
width : 100%;
|
||||||
position : absolute;
|
height : 100%;
|
||||||
right : 0;
|
min-height : 100%;
|
||||||
top : 0px;
|
overflow : hidden;
|
||||||
width : 50%;
|
> img {
|
||||||
height : 100%;
|
position : absolute;
|
||||||
|
top : 0px;
|
||||||
|
right : 0;
|
||||||
|
width : 50%;
|
||||||
|
min-height : 100%;
|
||||||
|
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||||
|
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.field .list {
|
.field .list {
|
||||||
display: flex;
|
display : flex;
|
||||||
flex: 1 0;
|
flex : 1 0;
|
||||||
flex-wrap: wrap;
|
flex-wrap : wrap;
|
||||||
|
|
||||||
> * {
|
> * { flex : 0 0 auto; }
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#groupedIcon {
|
#groupedIcon {
|
||||||
#backgroundColors;
|
#backgroundColors;
|
||||||
display: inline-block;
|
position : relative;
|
||||||
height: ~"calc(100% + 0.6em)";
|
top : -0.3em;
|
||||||
position: relative;
|
right : -0.3em;
|
||||||
top: -0.3em;
|
display : inline-block;
|
||||||
right: -0.3em;
|
min-width : 20px;
|
||||||
cursor: pointer;
|
height : ~'calc(100% + 0.6em)';
|
||||||
min-width: 20px;
|
color : white;
|
||||||
text-align: center;
|
text-align : center;
|
||||||
color: white;
|
cursor : pointer;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
position: relative;
|
position : relative;
|
||||||
top: 50%;
|
top : 50%;
|
||||||
transform: translateY(-50%);
|
transform : translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child) { border-right : 1px solid black; }
|
||||||
border-right: 1px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
&:last-child { border-radius : 0 0.5em 0.5em 0; }
|
||||||
border-radius: 0 0.5em 0.5em 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
background-color: #dddddd;
|
padding : 0.3em;
|
||||||
border-radius: .5em;
|
margin : 2px;
|
||||||
font-size: .9em;
|
font-size : 0.9em;
|
||||||
margin: 2px;
|
background-color : #DDDDDD;
|
||||||
padding: .3em;
|
border-radius : 0.5em;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
#groupedIcon
|
#groupedIcon; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group {
|
.input-group {
|
||||||
height: ~"calc(.9em + 4px + .6em)";
|
height : ~'calc(.9em + 4px + .6em)';
|
||||||
|
|
||||||
input {
|
input { border-radius : 0.5em 0 0 0.5em; }
|
||||||
border-radius: .5em 0 0 .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:last-child {
|
input:last-child { border-radius : 0.5em; }
|
||||||
border-radius: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
width: 7.5vw;
|
width : 7.5vw;
|
||||||
min-width: 75px;
|
min-width : 75px;
|
||||||
height: 100%;
|
height : 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invalid:focus {
|
.invalid:focus { background-color : pink; }
|
||||||
background-color: pink;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
#groupedIcon;
|
#groupedIcon;
|
||||||
height: 97%;
|
top : -0.54em;
|
||||||
font-size: .8em;
|
right : 1px;
|
||||||
right: 1px;
|
height : 97%;
|
||||||
top: -.54em;
|
font-size : 0.8em;
|
||||||
|
|
||||||
i {
|
i { font-size : 1.125em; }
|
||||||
font-size: 1.125em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
|
import { getHistoryItems, historyExists } from '../../utils/versionHistory.js';
|
||||||
|
|
||||||
//Import all themes
|
//Import all themes
|
||||||
|
|
||||||
const Themes = require('themes/themes.json');
|
|
||||||
|
|
||||||
const ThemeSnippets = {};
|
const ThemeSnippets = {};
|
||||||
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
|
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
|
||||||
ThemeSnippets['V3_5ePHB'] = require('themes/V3/5ePHB/snippets.js');
|
ThemeSnippets['V3_5ePHB'] = require('themes/V3/5ePHB/snippets.js');
|
||||||
@@ -40,7 +39,9 @@ const Snippetbar = createClass({
|
|||||||
foldCode : ()=>{},
|
foldCode : ()=>{},
|
||||||
unfoldCode : ()=>{},
|
unfoldCode : ()=>{},
|
||||||
updateEditorTheme : ()=>{},
|
updateEditorTheme : ()=>{},
|
||||||
cursorPos : {}
|
cursorPos : {},
|
||||||
|
snippetBundle : [],
|
||||||
|
updateBrew : ()=>{}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -48,53 +49,50 @@ const Snippetbar = createClass({
|
|||||||
return {
|
return {
|
||||||
renderer : this.props.renderer,
|
renderer : this.props.renderer,
|
||||||
themeSelector : false,
|
themeSelector : false,
|
||||||
snippets : []
|
snippets : [],
|
||||||
|
historyExists : false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount : async function() {
|
componentDidMount : async function() {
|
||||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
const snippets = this.compileSnippets();
|
||||||
const themePath = this.props.theme ?? '5ePHB';
|
|
||||||
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
|
|
||||||
snippets = this.compileSnippets(rendererPath, themePath, snippets);
|
|
||||||
this.setState({
|
this.setState({
|
||||||
snippets : snippets
|
snippets : snippets
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate : async function(prevProps) {
|
componentDidUpdate : async function(prevProps) {
|
||||||
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme) {
|
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
|
||||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
|
||||||
const themePath = this.props.theme ?? '5ePHB';
|
|
||||||
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
|
|
||||||
snippets = this.compileSnippets(rendererPath, themePath, snippets);
|
|
||||||
this.setState({
|
this.setState({
|
||||||
snippets : snippets
|
snippets : this.compileSnippets()
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
historyExists : historyExists(this.props.brew)
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mergeCustomizer : function(oldValue, newValue, key) {
|
||||||
mergeCustomizer : function(valueA, valueB, key) {
|
|
||||||
if(key == 'snippets') {
|
if(key == 'snippets') {
|
||||||
const result = _.reverse(_.unionBy(_.reverse(valueB), _.reverse(valueA), 'name')); // Join snippets together, with preference for the current theme over the base theme
|
const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme
|
||||||
return _.filter(result, 'gen'); //Only keep snippets with a 'gen' property.
|
return result.filter((snip)=>snip.gen || snip.subsnippets);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
compileSnippets : function(rendererPath, themePath, snippets) {
|
compileSnippets : function() {
|
||||||
let compiledSnippets = snippets;
|
let compiledSnippets = [];
|
||||||
const baseSnippetsPath = Themes[rendererPath][themePath].baseSnippets;
|
|
||||||
|
|
||||||
const objB = _.keyBy(compiledSnippets, 'groupName');
|
let oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||||
|
|
||||||
if(baseSnippetsPath) {
|
for (let snippets of this.props.snippetBundle) {
|
||||||
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_${baseSnippetsPath}`]), 'groupName');
|
if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name
|
||||||
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
|
snippets = ThemeSnippets[snippets];
|
||||||
compiledSnippets = this.compileSnippets(rendererPath, baseSnippetsPath, _.cloneDeep(compiledSnippets));
|
|
||||||
} else {
|
const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName');
|
||||||
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_Blank`]), 'groupName');
|
compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer));
|
||||||
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
|
|
||||||
|
oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||||
}
|
}
|
||||||
return compiledSnippets;
|
return compiledSnippets;
|
||||||
},
|
},
|
||||||
@@ -146,6 +144,36 @@ const Snippetbar = createClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
replaceContent : function(item){
|
||||||
|
return this.props.updateBrew(item);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHistoryItems : function() {
|
||||||
|
const historyItems = getHistoryItems(this.props.brew);
|
||||||
|
|
||||||
|
return <div className='dropdown'>
|
||||||
|
{_.map(historyItems, (item, index)=>{
|
||||||
|
if(!item.savedAt) return;
|
||||||
|
|
||||||
|
const saveTime = new Date(item.savedAt);
|
||||||
|
const diffMs = new Date() - saveTime;
|
||||||
|
const diffSecs = Math.floor(diffMs / 1000);
|
||||||
|
|
||||||
|
let diffString = `about ${diffSecs} seconds ago`;
|
||||||
|
|
||||||
|
if(diffSecs > 60) diffString = `about ${Math.floor(diffSecs / 60)} minutes ago`;
|
||||||
|
if(diffSecs > (60 * 60)) diffString = `about ${Math.floor(diffSecs / (60 * 60))} hours ago`;
|
||||||
|
if(diffSecs > (24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (24 * 60 * 60))} days ago`;
|
||||||
|
if(diffSecs > (7 * 24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (7 * 24 * 60 * 60))} weeks ago`;
|
||||||
|
|
||||||
|
return <div className='snippet' key={index} onClick={()=>{this.replaceContent(item);}} >
|
||||||
|
<i className={`fas fa-${index+1}`} />
|
||||||
|
<span className='name' title={saveTime.toISOString()}>v{item.version} : {diffString}</span>
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
renderEditorButtons : function(){
|
renderEditorButtons : function(){
|
||||||
if(!this.props.showEditButtons) return;
|
if(!this.props.showEditButtons) return;
|
||||||
|
|
||||||
@@ -166,6 +194,10 @@ const Snippetbar = createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <div className='editors'>
|
return <div className='editors'>
|
||||||
|
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`} >
|
||||||
|
<i className='fas fa-clock-rotate-left' />
|
||||||
|
{this.state.historyExists && this.renderHistoryItems() }
|
||||||
|
</div>
|
||||||
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
||||||
onClick={this.props.undo} >
|
onClick={this.props.undo} >
|
||||||
<i className='fas fa-undo' />
|
<i className='fas fa-undo' />
|
||||||
|
|||||||
@@ -53,6 +53,21 @@
|
|||||||
font-size : 0.75em;
|
font-size : 0.75em;
|
||||||
color : inherit;
|
color : inherit;
|
||||||
}
|
}
|
||||||
|
&.history {
|
||||||
|
.tooltipLeft('History');
|
||||||
|
font-size : 0.75em;
|
||||||
|
color : grey;
|
||||||
|
position : relative;
|
||||||
|
&.active {
|
||||||
|
color : inherit;
|
||||||
|
}
|
||||||
|
&>.dropdown{
|
||||||
|
right : -1px;
|
||||||
|
&>.snippet{
|
||||||
|
padding-right : 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
&.editorTheme {
|
&.editorTheme {
|
||||||
.tooltipLeft('Editor Themes');
|
.tooltipLeft('Editor Themes');
|
||||||
font-size : 0.75em;
|
font-size : 0.75em;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const UserPage = require('./pages/userPage/userPage.jsx');
|
|||||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||||
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
const VaultPage = require('./pages/vaultPage/vaultPage.jsx');
|
||||||
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
||||||
|
|
||||||
const WithRoute = (props)=>{
|
const WithRoute = (props)=>{
|
||||||
@@ -67,20 +67,20 @@ const Homebrew = createClass({
|
|||||||
<Router location={this.props.url}>
|
<Router location={this.props.url}>
|
||||||
<div className='homebrew'>
|
<div className='homebrew'>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
|
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
||||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} />} />
|
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
||||||
<Route path='/new' element={<WithRoute el={NewPage}/>} />
|
<Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } />
|
||||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
||||||
<Route path='/print/:id' element={<WithRoute el={PrintPage} brew={this.props.brew} />} />
|
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
|
||||||
<Route path='/print' element={<WithRoute el={PrintPage} />} />
|
<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} />} />
|
||||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
@@ -88,15 +88,4 @@ const Homebrew = createClass({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = Homebrew;
|
module.exports = Homebrew;
|
||||||
|
|
||||||
//TODO: Nicer Error page instead of just "cant get that"
|
|
||||||
// '/share/:id' : (args)=>{
|
|
||||||
// if(!this.props.brew.shareId){
|
|
||||||
// return <ErrorPage errorId={args.id}/>;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return <SharePage
|
|
||||||
// id={args.id}
|
|
||||||
// brew={this.props.brew} />;
|
|
||||||
// },
|
|
||||||
@@ -104,6 +104,18 @@ const ErrorNavItem = createClass({
|
|||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(HBErrorCode === '09') {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
Looks like there was a problem retreiving
|
||||||
|
the theme, or a theme that it inherits,
|
||||||
|
for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
|
||||||
|
{response.body.brewId}</a> still exists!
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
<div className='errorContainer'>
|
<div className='errorContainer'>
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
font-size : 10px;
|
font-size : 10px;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
|
.lowercase {
|
||||||
|
text-transform : none;
|
||||||
|
}
|
||||||
a{
|
a{
|
||||||
color : @teal;
|
color : @teal;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@import 'naturalcrit/styles/colors.less';
|
@import 'naturalcrit/styles/colors.less';
|
||||||
|
|
||||||
@navbarHeight : 28px;
|
@navbarHeight : 28px;
|
||||||
|
@viewerToolsHeight : 32px;
|
||||||
|
|
||||||
@keyframes pinkColoring {
|
@keyframes pinkColoring {
|
||||||
0% { color : pink; }
|
0% { color : pink; }
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const { printCurrentBrew } = require('../../../shared/helpers.js');
|
||||||
|
|
||||||
module.exports = function(props){
|
module.exports = function(){
|
||||||
return <Nav.item newTab={true} href={`/print/${props.shareId}?dialog=true`} color='purple' icon='far fa-file-pdf'>
|
return <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'>
|
||||||
get PDF
|
get PDF
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
};
|
};
|
||||||
|
|||||||
17
client/homebrew/navbar/vault.navitem.jsx
Normal file
17
client/homebrew/navbar/vault.navitem.jsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const React = require('react');
|
||||||
|
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
|
module.exports = function (props) {
|
||||||
|
return (
|
||||||
|
<Nav.item
|
||||||
|
color='purple'
|
||||||
|
icon='fas fa-dungeon'
|
||||||
|
href='/vault'
|
||||||
|
newTab={false}
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
Vault
|
||||||
|
</Nav.item>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -19,7 +19,8 @@ const BrewItem = createClass({
|
|||||||
stubbed : true
|
stubbed : true
|
||||||
},
|
},
|
||||||
updateListFilter : ()=>{},
|
updateListFilter : ()=>{},
|
||||||
reportError : ()=>{}
|
reportError : ()=>{},
|
||||||
|
renderStorage : true
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -95,6 +96,7 @@ const BrewItem = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderStorageIcon : function(){
|
renderStorageIcon : function(){
|
||||||
|
if(!this.props.renderStorage) return;
|
||||||
if(this.props.brew.googleId) {
|
if(this.props.brew.googleId) {
|
||||||
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
|
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
|
||||||
<a href={this.props.brew.webViewLink} target='_blank'>
|
<a href={this.props.brew.webViewLink} target='_blank'>
|
||||||
@@ -142,10 +144,14 @@ const BrewItem = createClass({
|
|||||||
}
|
}
|
||||||
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
||||||
<i className='fas fa-user'/> {brew.authors?.map((author, index)=>(
|
<i className='fas fa-user'/> {brew.authors?.map((author, index)=>(
|
||||||
<>
|
<React.Fragment key={index}>
|
||||||
<a key={index} href={`/user/${author}`}>{author}</a>
|
{author === 'hidden'
|
||||||
{index < brew.authors.length - 1 && ', '}
|
? <span title="Username contained an email address; hidden to protect user's privacy">{author}</span>
|
||||||
</>))}
|
: <a href={`/user/${author}`}>{author}</a>
|
||||||
|
}
|
||||||
|
{index < brew.authors.length - 1 && ', '}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
||||||
|
|||||||
@@ -119,11 +119,12 @@
|
|||||||
text-align : center;
|
text-align : center;
|
||||||
a{
|
a{
|
||||||
.animate(opacity);
|
.animate(opacity);
|
||||||
display : block;
|
display : block;
|
||||||
margin : 8px 0px;
|
margin : 8px 0px;
|
||||||
opacity : 0.6;
|
opacity : 0.6;
|
||||||
font-size : 1.3em;
|
font-size : 1.3em;
|
||||||
color : white;
|
color : white;
|
||||||
|
text-decoration : unset;
|
||||||
&:hover{
|
&:hover{
|
||||||
opacity : 1;
|
opacity : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
require('./editPage.less');
|
require('./editPage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
|
||||||
const request = require('../../utils/request-middleware.js');
|
const request = require('../../utils/request-middleware.js');
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ const Navbar = require('../../navbar/navbar.jsx');
|
|||||||
|
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
@@ -20,9 +21,14 @@ const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
|||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
|
||||||
|
|
||||||
const googleDriveIcon = require('../../googleDrive.svg');
|
const googleDriveIcon = require('../../googleDrive.svg');
|
||||||
|
|
||||||
@@ -38,20 +44,24 @@ const EditPage = createClass({
|
|||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
brew : this.props.brew,
|
brew : this.props.brew,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
isPending : false,
|
isPending : false,
|
||||||
alertTrashedGoogleBrew : this.props.brew.trashed,
|
alertTrashedGoogleBrew : this.props.brew.trashed,
|
||||||
alertLoginToTransfer : false,
|
alertLoginToTransfer : false,
|
||||||
saveGoogle : this.props.brew.googleId ? true : false,
|
saveGoogle : this.props.brew.googleId ? true : false,
|
||||||
confirmGoogleTransfer : false,
|
confirmGoogleTransfer : false,
|
||||||
error : null,
|
error : null,
|
||||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
htmlErrors : Markdown.validate(this.props.brew.text),
|
||||||
url : '',
|
url : '',
|
||||||
autoSave : true,
|
autoSave : true,
|
||||||
autoSaveWarning : false,
|
autoSaveWarning : false,
|
||||||
unsavedTime : new Date(),
|
unsavedTime : new Date(),
|
||||||
currentEditorPage : 0
|
currentEditorViewPageNum : 1,
|
||||||
|
currentEditorCursorPageNum : 1,
|
||||||
|
currentBrewRendererPageNum : 1,
|
||||||
|
displayLockMessage : this.props.brew.lock || false,
|
||||||
|
themeBundle : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -83,6 +93,8 @@ const EditPage = createClass({
|
|||||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||||
|
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
document.addEventListener('keydown', this.handleControlKeys);
|
||||||
},
|
},
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
@@ -95,7 +107,7 @@ const EditPage = createClass({
|
|||||||
const S_KEY = 83;
|
const S_KEY = 83;
|
||||||
const P_KEY = 80;
|
const P_KEY = 80;
|
||||||
if(e.keyCode == S_KEY) this.trySave(true);
|
if(e.keyCode == S_KEY) this.trySave(true);
|
||||||
if(e.keyCode == P_KEY) window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
|
if(e.keyCode == P_KEY) printCurrentBrew();
|
||||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -106,16 +118,27 @@ const EditPage = createClass({
|
|||||||
this.editor.current.update();
|
this.editor.current.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleEditorViewPageChange : function(pageNumber){
|
||||||
|
this.setState({ currentEditorViewPageNum: pageNumber });
|
||||||
|
},
|
||||||
|
|
||||||
|
handleEditorCursorPageChange : function(pageNumber){
|
||||||
|
this.setState({ currentEditorCursorPageNum: pageNumber });
|
||||||
|
},
|
||||||
|
|
||||||
|
handleBrewRendererPageChange : function(pageNumber){
|
||||||
|
this.setState({ currentBrewRendererPageNum: pageNumber });
|
||||||
|
},
|
||||||
|
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
//If there are errors, run the validator on every change to give quick feedback
|
//If there are errors, run the validator on every change to give quick feedback
|
||||||
let htmlErrors = this.state.htmlErrors;
|
let htmlErrors = this.state.htmlErrors;
|
||||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, text: text },
|
brew : { ...prevState.brew, text: text },
|
||||||
isPending : true,
|
isPending : true,
|
||||||
htmlErrors : htmlErrors,
|
htmlErrors : htmlErrors,
|
||||||
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -126,7 +149,10 @@ const EditPage = createClass({
|
|||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMetaChange : function(metadata){
|
handleMetaChange : function(metadata, field=undefined){
|
||||||
|
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
||||||
|
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : {
|
brew : {
|
||||||
...prevState.brew,
|
...prevState.brew,
|
||||||
@@ -134,13 +160,22 @@ const EditPage = createClass({
|
|||||||
},
|
},
|
||||||
isPending : true,
|
isPending : true,
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
hasChanges : function(){
|
hasChanges : function(){
|
||||||
return !_.isEqual(this.state.brew, this.savedBrew);
|
return !_.isEqual(this.state.brew, this.savedBrew);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateBrew : function(newData){
|
||||||
|
this.setState((prevState)=>({
|
||||||
|
brew : {
|
||||||
|
...prevState.brew,
|
||||||
|
style : newData.style,
|
||||||
|
text : newData.text
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
trySave : function(immediate=false){
|
trySave : function(immediate=false){
|
||||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||||
if(this.hasChanges()){
|
if(this.hasChanges()){
|
||||||
@@ -193,6 +228,9 @@ const EditPage = createClass({
|
|||||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
updateHistory(this.state.brew);
|
||||||
|
versionHistoryGarbageCollection();
|
||||||
|
|
||||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||||
|
|
||||||
const brew = this.state.brew;
|
const brew = this.state.brew;
|
||||||
@@ -378,7 +416,7 @@ const EditPage = createClass({
|
|||||||
post to reddit
|
post to reddit
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
</Nav.dropdown>
|
</Nav.dropdown>
|
||||||
<PrintLink shareId={this.processShareId()} />
|
<PrintNavItem />
|
||||||
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
||||||
<Account />
|
<Account />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
@@ -392,6 +430,7 @@ const EditPage = createClass({
|
|||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
|
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
|
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />}
|
||||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||||
<Editor
|
<Editor
|
||||||
ref={this.editor}
|
ref={this.editor}
|
||||||
@@ -401,15 +440,28 @@ 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}
|
||||||
|
updateBrew={this.updateBrew}
|
||||||
|
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||||
|
onViewPageChange={this.handleEditorViewPageChange}
|
||||||
|
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||||
|
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||||
|
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||||
/>
|
/>
|
||||||
<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}
|
onPageChange={this.handleBrewRendererPageChange}
|
||||||
|
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||||
|
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||||
|
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||||
|
allowPrint={true}
|
||||||
/>
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
require('./lockNotification.less');
|
||||||
|
const React = require('react');
|
||||||
|
import Dialog from '../../../../components/dialog.jsx';
|
||||||
|
|
||||||
|
function LockNotification(props) {
|
||||||
|
props = {
|
||||||
|
shareId : 0,
|
||||||
|
disableLock : ()=>{},
|
||||||
|
message : '',
|
||||||
|
...props
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeLock = ()=>{
|
||||||
|
alert(`Not yet implemented - ID ${props.shareId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Dialog className='lockNotification' blocking closeText='CONTINUE TO EDITOR' >
|
||||||
|
<h1>BREW LOCKED</h1>
|
||||||
|
<p>This brew been locked by the Administrators. It will not be accessible by any method other than the Editor until the lock is removed.</p>
|
||||||
|
<hr />
|
||||||
|
<h3>LOCK REASON</h3>
|
||||||
|
<p>{props.message || 'Unable to retrieve Lock Message'}</p>
|
||||||
|
<hr />
|
||||||
|
<p>Once you have resolved this issue, click REQUEST LOCK REMOVAL to notify the Administrators for review.</p>
|
||||||
|
<p>Click CONTINUE TO EDITOR to temporarily hide this notification; it will reappear the next time the page is reloaded.</p>
|
||||||
|
<button onClick={removeLock}>REQUEST LOCK REMOVAL</button>
|
||||||
|
</Dialog>;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = LockNotification;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
.lockNotification {
|
||||||
|
z-index : 1;
|
||||||
|
width : 80%;
|
||||||
|
padding : 10px;
|
||||||
|
margin : 5% 10%;
|
||||||
|
line-height : 1.5em;
|
||||||
|
color : black;
|
||||||
|
text-align : center;
|
||||||
|
background-color : #CCCCCC;
|
||||||
|
|
||||||
|
&::backdrop { background-color : #000000AA; }
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin : 10px;
|
||||||
|
color : white;
|
||||||
|
background-color : #333333;
|
||||||
|
|
||||||
|
&:hover { background-color : #777777; }
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h3 {
|
||||||
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
font-weight : 800;
|
||||||
|
}
|
||||||
|
h1 { font-size : 24px; }
|
||||||
|
h3 { font-size : 18px; }
|
||||||
|
}
|
||||||
@@ -2,6 +2,9 @@ const dedent = require('dedent-tabs').default;
|
|||||||
|
|
||||||
const loginUrl = 'https://www.naturalcrit.com/login';
|
const loginUrl = 'https://www.naturalcrit.com/login';
|
||||||
|
|
||||||
|
//001-050 : Brew errors
|
||||||
|
//050-100 : Other pages errors
|
||||||
|
|
||||||
const errorIndex = (props)=>{
|
const errorIndex = (props)=>{
|
||||||
return {
|
return {
|
||||||
// Default catch all
|
// Default catch all
|
||||||
@@ -136,11 +139,32 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
**Brew ID:** ${props.brew.brewId}`,
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
// Theme load error
|
||||||
|
'09' : dedent`
|
||||||
|
## No Homebrewery theme document could be found.
|
||||||
|
|
||||||
|
The server could not locate the Homebrewery document. It was likely deleted by
|
||||||
|
its owner.
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
**Requested access:** ${props.brew.accessType}
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
//account page when account is not defined
|
||||||
|
'50' : dedent`
|
||||||
|
## You are not signed in
|
||||||
|
|
||||||
|
You are trying to access the account page, but are not signed in to an account.
|
||||||
|
|
||||||
|
Please login or signup at our [login page](https://www.naturalcrit.com/login?redirect=https://homebrewery.naturalcrit.com/account).`,
|
||||||
|
|
||||||
// Brew locked by Administrators error
|
// Brew locked by Administrators error
|
||||||
'100' : dedent`
|
'51' : dedent`
|
||||||
## This brew has been locked.
|
## This brew has been locked.
|
||||||
|
|
||||||
Please contact the Administrators to unlock this document.
|
Only an author may request that this lock is removed.
|
||||||
|
|
||||||
:
|
:
|
||||||
|
|
||||||
@@ -152,7 +176,7 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
// ####### Admin pages errors #######
|
// ####### Admin pages errors #######
|
||||||
|
|
||||||
'401': dedent`
|
'52': dedent`
|
||||||
## Authorization Required
|
## Authorization Required
|
||||||
You need to provide correct administrator credentials to access this page.
|
You need to provide correct administrator credentials to access this page.
|
||||||
|
|
||||||
@@ -162,7 +186,7 @@ const errorIndex = (props)=>{
|
|||||||
[reddit mod mail](https://old.reddit.com/message/compose/?to=/r/homebrewery) or
|
[reddit mod mail](https://old.reddit.com/message/compose/?to=/r/homebrewery) or
|
||||||
in the [hb_issues channel of the Discord of Many Things server](https://discord.gg/domt).
|
in the [hb_issues channel of the Discord of Many Things server](https://discord.gg/domt).
|
||||||
`,
|
`,
|
||||||
'403': dedent`
|
'53': dedent`
|
||||||
## Access Denied
|
## Access Denied
|
||||||
|
|
||||||
The credentials you entered are not correct, you may try again, attention, there is a limited number of tries before you are blocked.
|
The credentials you entered are not correct, you may try again, attention, there is a limited number of tries before you are blocked.
|
||||||
@@ -177,7 +201,7 @@ const errorIndex = (props)=>{
|
|||||||
as so at our subreddit or discord you will find in the home page.
|
as so at our subreddit or discord you will find in the home page.
|
||||||
`,
|
`,
|
||||||
|
|
||||||
'470' : dedent`
|
'53' : dedent`
|
||||||
## You have runned out of attempts
|
## You have runned out of attempts
|
||||||
|
|
||||||
You have failed to provide correct credentials to access the page too many times, and you have run out of attempts.
|
You have failed to provide correct credentials to access the page too many times, and you have run out of attempts.
|
||||||
@@ -195,7 +219,11 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
In any case, your attempts have been logged, and you will not be capable of doing any more attempt for now.
|
In any case, your attempts have been logged, and you will not be capable of doing any more attempt for now.
|
||||||
`,
|
`,
|
||||||
|
'90' : dedent` An unexpected error occurred while looking for these brews.
|
||||||
|
Try again in a few minutes.`,
|
||||||
|
|
||||||
|
'91' : dedent` An unexpected error occurred while trying to get the total of brews.`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = errorIndex;
|
module.exports = errorIndex;
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
//TODO: Depricate
|
|
||||||
|
|
||||||
module.exports = function(shareId){
|
|
||||||
return function(event){
|
|
||||||
event = event || window.event;
|
|
||||||
if((event.ctrlKey || event.metaKey) && event.keyCode == 80){
|
|
||||||
const win = window.open(`/homebrew/print/${shareId}?dialog=true`, '_blank');
|
|
||||||
win.focus();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
require('./homePage.less');
|
require('./homePage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const request = require('../../utils/request-middleware.js');
|
const request = require('../../utils/request-middleware.js');
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
@@ -10,10 +9,11 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
|||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
|
const { fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
@@ -31,15 +31,22 @@ const HomePage = createClass({
|
|||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
brew : this.props.brew,
|
brew : this.props.brew,
|
||||||
welcomeText : this.props.brew.text,
|
welcomeText : this.props.brew.text,
|
||||||
error : undefined,
|
error : undefined,
|
||||||
currentEditorPage : 0
|
currentEditorViewPageNum : 1,
|
||||||
|
currentEditorCursorPageNum : 1,
|
||||||
|
currentBrewRendererPageNum : 1,
|
||||||
|
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)
|
||||||
@@ -55,10 +62,22 @@ const HomePage = createClass({
|
|||||||
handleSplitMove : function(){
|
handleSplitMove : function(){
|
||||||
this.editor.current.update();
|
this.editor.current.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleEditorViewPageChange : function(pageNumber){
|
||||||
|
this.setState({ currentEditorViewPageNum: pageNumber });
|
||||||
|
},
|
||||||
|
|
||||||
|
handleEditorCursorPageChange : function(pageNumber){
|
||||||
|
this.setState({ currentEditorCursorPageNum: pageNumber });
|
||||||
|
},
|
||||||
|
|
||||||
|
handleBrewRendererPageChange : function(pageNumber){
|
||||||
|
this.setState({ currentBrewRendererPageNum: pageNumber });
|
||||||
|
},
|
||||||
|
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, text: text },
|
brew : { ...prevState.brew, text: text },
|
||||||
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
@@ -70,6 +89,7 @@ const HomePage = createClass({
|
|||||||
}
|
}
|
||||||
<NewBrewItem />
|
<NewBrewItem />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
|
<VaultNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<AccountNavItem />
|
<AccountNavItem />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
@@ -89,12 +109,22 @@ 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}
|
||||||
|
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||||
|
onViewPageChange={this.handleEditorViewPageChange}
|
||||||
|
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||||
|
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||||
|
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||||
/>
|
/>
|
||||||
<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}
|
onPageChange={this.handleBrewRendererPageChange}
|
||||||
|
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||||
|
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||||
|
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||||
|
themeBundle={this.state.themeBundle}
|
||||||
/>
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const request = require('../../utils/request-middleware.js');
|
|||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
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');
|
||||||
@@ -18,6 +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 BREWKEY = 'homebrewery-new';
|
const BREWKEY = 'homebrewery-new';
|
||||||
const STYLEKEY = 'homebrewery-new-style';
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
@@ -37,12 +39,15 @@ const NewPage = createClass({
|
|||||||
const brew = this.props.brew;
|
const brew = this.props.brew;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
brew : brew,
|
brew : brew,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
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
|
currentEditorViewPageNum : 1,
|
||||||
|
currentEditorCursorPageNum : 1,
|
||||||
|
currentBrewRendererPageNum : 1,
|
||||||
|
themeBundle : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -75,10 +80,15 @@ const NewPage = createClass({
|
|||||||
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||||
|
|
||||||
localStorage.setItem(BREWKEY, brew.text);
|
localStorage.setItem(BREWKEY, brew.text);
|
||||||
if(brew.style)
|
if(brew.style)
|
||||||
localStorage.setItem(STYLEKEY, brew.style);
|
localStorage.setItem(STYLEKEY, brew.style);
|
||||||
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
|
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
|
||||||
|
if(window.location.pathname != '/new') {
|
||||||
|
window.history.replaceState({}, window.location.title, '/new/');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
document.removeEventListener('keydown', this.handleControlKeys);
|
document.removeEventListener('keydown', this.handleControlKeys);
|
||||||
@@ -89,7 +99,7 @@ const NewPage = createClass({
|
|||||||
const S_KEY = 83;
|
const S_KEY = 83;
|
||||||
const P_KEY = 80;
|
const P_KEY = 80;
|
||||||
if(e.keyCode == S_KEY) this.save();
|
if(e.keyCode == S_KEY) this.save();
|
||||||
if(e.keyCode == P_KEY) this.print();
|
if(e.keyCode == P_KEY) printCurrentBrew();
|
||||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -100,15 +110,26 @@ const NewPage = createClass({
|
|||||||
this.editor.current.update();
|
this.editor.current.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleEditorViewPageChange : function(pageNumber){
|
||||||
|
this.setState({ currentEditorViewPageNum: pageNumber });
|
||||||
|
},
|
||||||
|
|
||||||
|
handleEditorCursorPageChange : function(pageNumber){
|
||||||
|
this.setState({ currentEditorCursorPageNum: pageNumber });
|
||||||
|
},
|
||||||
|
|
||||||
|
handleBrewRendererPageChange : function(pageNumber){
|
||||||
|
this.setState({ currentBrewRendererPageNum: pageNumber });
|
||||||
|
},
|
||||||
|
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
//If there are errors, run the validator on every change to give quick feedback
|
//If there are errors, run the validator on every change to give quick feedback
|
||||||
let htmlErrors = this.state.htmlErrors;
|
let htmlErrors = this.state.htmlErrors;
|
||||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, text: text },
|
brew : { ...prevState.brew, text: text },
|
||||||
htmlErrors : htmlErrors,
|
htmlErrors : htmlErrors,
|
||||||
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
|
||||||
}));
|
}));
|
||||||
localStorage.setItem(BREWKEY, text);
|
localStorage.setItem(BREWKEY, text);
|
||||||
},
|
},
|
||||||
@@ -120,7 +141,10 @@ const NewPage = createClass({
|
|||||||
localStorage.setItem(STYLEKEY, style);
|
localStorage.setItem(STYLEKEY, style);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMetaChange : function(metadata){
|
handleMetaChange : function(metadata, field=undefined){
|
||||||
|
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
||||||
|
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, ...metadata },
|
brew : { ...prevState.brew, ...metadata },
|
||||||
}), ()=>{
|
}), ()=>{
|
||||||
@@ -140,8 +164,6 @@ const NewPage = createClass({
|
|||||||
isSaving : true
|
isSaving : true
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('saving new brew');
|
|
||||||
|
|
||||||
let brew = this.state.brew;
|
let brew = this.state.brew;
|
||||||
// Split out CSS to Style if CSS codefence exists
|
// Split out CSS to Style if CSS codefence exists
|
||||||
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
|
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
|
||||||
@@ -151,12 +173,10 @@ const NewPage = createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||||
|
|
||||||
const res = await request
|
const res = await request
|
||||||
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
|
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||||
.send(brew)
|
.send(brew)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err);
|
|
||||||
this.setState({ isSaving: false, error: err });
|
this.setState({ isSaving: false, error: err });
|
||||||
});
|
});
|
||||||
if(!res) return;
|
if(!res) return;
|
||||||
@@ -180,16 +200,6 @@ const NewPage = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
print : function(){
|
|
||||||
window.open('/print?dialog=true&local=print', '_blank');
|
|
||||||
},
|
|
||||||
|
|
||||||
renderLocalPrintButton : function(){
|
|
||||||
return <Nav.item color='purple' icon='far fa-file-pdf' onClick={this.print}>
|
|
||||||
get PDF
|
|
||||||
</Nav.item>;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
return <Navbar>
|
return <Navbar>
|
||||||
|
|
||||||
@@ -202,7 +212,7 @@ const NewPage = createClass({
|
|||||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
this.renderSaveButton()
|
this.renderSaveButton()
|
||||||
}
|
}
|
||||||
{this.renderLocalPrintButton()}
|
<PrintNavItem />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<AccountNavItem />
|
<AccountNavItem />
|
||||||
@@ -222,15 +232,27 @@ 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}
|
||||||
|
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||||
|
onViewPageChange={this.handleEditorViewPageChange}
|
||||||
|
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||||
|
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||||
|
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||||
/>
|
/>
|
||||||
<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}
|
onPageChange={this.handleBrewRendererPageChange}
|
||||||
|
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||||
|
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||||
|
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||||
|
allowPrint={true}
|
||||||
/>
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
require('./printPage.less');
|
|
||||||
const React = require('react');
|
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const { Meta } = require('vitreum/headtags');
|
|
||||||
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
|
||||||
|
|
||||||
const Themes = require('themes/themes.json');
|
|
||||||
|
|
||||||
const BREWKEY = 'homebrewery-new';
|
|
||||||
const STYLEKEY = 'homebrewery-new-style';
|
|
||||||
const METAKEY = 'homebrewery-new-meta';
|
|
||||||
|
|
||||||
const PrintPage = createClass({
|
|
||||||
displayName : 'PrintPage',
|
|
||||||
getDefaultProps : function() {
|
|
||||||
return {
|
|
||||||
query : {},
|
|
||||||
brew : {
|
|
||||||
text : '',
|
|
||||||
style : '',
|
|
||||||
renderer : 'legacy',
|
|
||||||
lang : ''
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function() {
|
|
||||||
return {
|
|
||||||
brew : {
|
|
||||||
text : this.props.brew.text || '',
|
|
||||||
style : this.props.brew.style || undefined,
|
|
||||||
renderer : this.props.brew.renderer || 'legacy',
|
|
||||||
theme : this.props.brew.theme || '5ePHB',
|
|
||||||
lang : this.props.brew.lang || 'en'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount : function() {
|
|
||||||
if(this.props.query.local == 'print'){
|
|
||||||
const brewStorage = localStorage.getItem(BREWKEY);
|
|
||||||
const styleStorage = localStorage.getItem(STYLEKEY);
|
|
||||||
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
|
|
||||||
|
|
||||||
this.setState((prevState, prevProps)=>{
|
|
||||||
return {
|
|
||||||
brew : {
|
|
||||||
text : brewStorage,
|
|
||||||
style : styleStorage,
|
|
||||||
renderer : metaStorage?.renderer || 'legacy',
|
|
||||||
theme : metaStorage?.theme || '5ePHB',
|
|
||||||
lang : metaStorage?.lang || 'en'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.props.query.dialog) window.print();
|
|
||||||
},
|
|
||||||
|
|
||||||
renderStyle : function() {
|
|
||||||
if(!this.state.brew.style) return;
|
|
||||||
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.state.brew.style}\n} </style>` }} />;
|
|
||||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>\n${this.state.brew.style}\n</style>` }} />;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderPages : function(){
|
|
||||||
if(this.state.brew.renderer == 'legacy') {
|
|
||||||
return _.map(this.state.brew.text.split('\\page'), (pageText, index)=>{
|
|
||||||
return <div
|
|
||||||
className='phb page'
|
|
||||||
id={`p${index + 1}`}
|
|
||||||
dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }}
|
|
||||||
key={index} />;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return _.map(this.state.brew.text.split(/^\\page$/gm), (pageText, index)=>{
|
|
||||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
|
||||||
return (
|
|
||||||
<div className='page' id={`p${index + 1}`} key={index} >
|
|
||||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function(){
|
|
||||||
const rendererPath = this.state.brew.renderer == 'V3' ? 'V3' : 'Legacy';
|
|
||||||
const themePath = this.state.brew.theme ?? '5ePHB';
|
|
||||||
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
|
||||||
|
|
||||||
return <div>
|
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
|
||||||
<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 */}
|
|
||||||
{this.renderStyle()}
|
|
||||||
<div className='pages' lang={this.state.brew.lang}>
|
|
||||||
{this.renderPages()}
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = PrintPage;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.printPage{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -6,25 +6,33 @@ const { Meta } = require('vitreum/headtags');
|
|||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
||||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
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 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() {
|
||||||
@@ -35,7 +43,7 @@ const SharePage = createClass({
|
|||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
const P_KEY = 80;
|
const P_KEY = 80;
|
||||||
if(e.keyCode == P_KEY){
|
if(e.keyCode == P_KEY){
|
||||||
window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
|
if(e.keyCode == P_KEY) printCurrentBrew();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -61,18 +69,26 @@ const SharePage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
|
const titleStyle = this.props.disableMeta ? { cursor: 'default' } : {};
|
||||||
|
const titleEl = <Nav.item className='brewTitle' style={titleStyle}>{this.props.brew.title}</Nav.item>;
|
||||||
|
|
||||||
return <div className='sharePage sitePage'>
|
return <div className='sharePage sitePage'>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav.section className='titleSection'>
|
<Nav.section className='titleSection'>
|
||||||
<MetadataNav brew={this.props.brew}>
|
{
|
||||||
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
this.props.disableMeta ?
|
||||||
</MetadataNav>
|
titleEl
|
||||||
|
:
|
||||||
|
<MetadataNav brew={this.props.brew}>
|
||||||
|
{titleEl}
|
||||||
|
</MetadataNav>
|
||||||
|
}
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.props.brew.shareId && <>
|
{this.props.brew.shareId && <>
|
||||||
<PrintLink shareId={this.processShareId()} />
|
<PrintNavItem/>
|
||||||
<Nav.dropdown>
|
<Nav.dropdown>
|
||||||
<Nav.item color='red' icon='fas fa-code'>
|
<Nav.item color='red' icon='fas fa-code'>
|
||||||
source
|
source
|
||||||
@@ -95,7 +111,14 @@ const SharePage = createClass({
|
|||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<BrewRenderer text={this.props.brew.text} style={this.props.brew.style} renderer={this.props.brew.renderer} theme={this.props.brew.theme} />
|
<BrewRenderer
|
||||||
|
text={this.props.brew.text}
|
||||||
|
style={this.props.brew.style}
|
||||||
|
renderer={this.props.brew.renderer}
|
||||||
|
theme={this.props.brew.theme}
|
||||||
|
themeBundle={this.state.themeBundle}
|
||||||
|
allowPrint={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const Account = require('../../navbar/account.navitem.jsx');
|
|||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
|
const VaultNavitem = require('../../navbar/vault.navitem.jsx');
|
||||||
|
|
||||||
const UserPage = createClass({
|
const UserPage = createClass({
|
||||||
displayName : 'UserPage',
|
displayName : 'UserPage',
|
||||||
@@ -66,6 +67,7 @@ const UserPage = createClass({
|
|||||||
}
|
}
|
||||||
<NewBrew />
|
<NewBrew />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
|
<VaultNavitem/>
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<Account />
|
<Account />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|||||||
396
client/homebrew/pages/vaultPage/vaultPage.jsx
Normal file
396
client/homebrew/pages/vaultPage/vaultPage.jsx
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
require('./vaultPage.less');
|
||||||
|
|
||||||
|
const React = require('react');
|
||||||
|
const { useState, useEffect, useRef } = React;
|
||||||
|
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
|
||||||
|
const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx');
|
||||||
|
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
|
||||||
|
|
||||||
|
const request = require('../../utils/request-middleware.js');
|
||||||
|
|
||||||
|
const VaultPage = (props)=>{
|
||||||
|
const [pageState, setPageState] = useState(parseInt(props.query.page) || 1);
|
||||||
|
|
||||||
|
//Response state
|
||||||
|
const [brewCollection, setBrewCollection] = useState(null);
|
||||||
|
const [totalBrews, setTotalBrews] = useState(null);
|
||||||
|
const [searching, setSearching] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
|
||||||
|
const titleRef = useRef(null);
|
||||||
|
const authorRef = useRef(null);
|
||||||
|
const countRef = useRef(null);
|
||||||
|
const v3Ref = useRef(null);
|
||||||
|
const legacyRef = useRef(null);
|
||||||
|
const submitButtonRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
disableSubmitIfFormInvalid();
|
||||||
|
loadPage(pageState, true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateStateWithBrews = (brews, page)=>{
|
||||||
|
setBrewCollection(brews || null);
|
||||||
|
setPageState(parseInt(page) || 1);
|
||||||
|
setSearching(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUrl = (titleValue, authorValue, countValue, v3Value, legacyValue, page)=>{
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const urlParams = new URLSearchParams(url.search);
|
||||||
|
|
||||||
|
urlParams.set('title', titleValue);
|
||||||
|
urlParams.set('author', authorValue);
|
||||||
|
urlParams.set('count', countValue);
|
||||||
|
urlParams.set('v3', v3Value);
|
||||||
|
urlParams.set('legacy', legacyValue);
|
||||||
|
urlParams.set('page', page);
|
||||||
|
|
||||||
|
url.search = urlParams.toString();
|
||||||
|
window.history.replaceState(null, '', url.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const performSearch = async (title, author, count, v3, legacy, page)=>{
|
||||||
|
updateUrl(title, author, count, v3, legacy, page);
|
||||||
|
|
||||||
|
const response = await request.get(
|
||||||
|
`/api/vault?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}&count=${count}&page=${page}`
|
||||||
|
).catch((error)=>{
|
||||||
|
console.log('error at loadPage: ', error);
|
||||||
|
setError(error);
|
||||||
|
updateStateWithBrews([], 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(response.ok)
|
||||||
|
updateStateWithBrews(response.body.brews, page);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadTotal = async (title, author, v3, legacy)=>{
|
||||||
|
setTotalBrews(null);
|
||||||
|
|
||||||
|
const response = await request.get(
|
||||||
|
`/api/vault/total?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}`
|
||||||
|
).catch((error)=>{
|
||||||
|
console.log('error at loadTotal: ', error);
|
||||||
|
setError(error);
|
||||||
|
updateStateWithBrews([], 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(response.ok)
|
||||||
|
setTotalBrews(response.body.totalBrews);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPage = async (page, updateTotal)=>{
|
||||||
|
if(!validateForm())
|
||||||
|
return;
|
||||||
|
|
||||||
|
setSearching(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const title = titleRef.current.value || '';
|
||||||
|
const author = authorRef.current.value || '';
|
||||||
|
const count = countRef.current.value || 10;
|
||||||
|
const v3 = v3Ref.current.checked != false;
|
||||||
|
const legacy = legacyRef.current.checked != false;
|
||||||
|
|
||||||
|
performSearch(title, author, count, v3, legacy, page);
|
||||||
|
|
||||||
|
if(updateTotal)
|
||||||
|
loadTotal(title, author, v3, legacy);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderNavItems = ()=>(
|
||||||
|
<Navbar>
|
||||||
|
<Nav.section>
|
||||||
|
<Nav.item className='brewTitle'>
|
||||||
|
Vault: Search for brews
|
||||||
|
</Nav.item>
|
||||||
|
</Nav.section>
|
||||||
|
<Nav.section>
|
||||||
|
<NewBrew />
|
||||||
|
<HelpNavItem />
|
||||||
|
<RecentNavItem />
|
||||||
|
<Account />
|
||||||
|
</Nav.section>
|
||||||
|
</Navbar>
|
||||||
|
);
|
||||||
|
|
||||||
|
const validateForm = ()=>{
|
||||||
|
//form validity: title or author must be written, and at least one renderer set
|
||||||
|
const isTitleValid = titleRef.current.validity.valid && titleRef.current.value;
|
||||||
|
const isAuthorValid = authorRef.current.validity.valid && authorRef.current.value;
|
||||||
|
const isCheckboxChecked = legacyRef.current.checked || v3Ref.current.checked;
|
||||||
|
|
||||||
|
const isFormValid = (isTitleValid || isAuthorValid) && isCheckboxChecked;
|
||||||
|
|
||||||
|
return isFormValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableSubmitIfFormInvalid = ()=>{
|
||||||
|
submitButtonRef.current.disabled = !validateForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderForm = ()=>(
|
||||||
|
<div className='brewLookup'>
|
||||||
|
<h2 className='formTitle'>Brew Lookup</h2>
|
||||||
|
<div className='formContents'>
|
||||||
|
<label>
|
||||||
|
Title of the brew
|
||||||
|
<input
|
||||||
|
ref={titleRef}
|
||||||
|
type='text'
|
||||||
|
name='title'
|
||||||
|
defaultValue={props.query.title || ''}
|
||||||
|
onKeyUp={disableSubmitIfFormInvalid}
|
||||||
|
pattern='.{3,}'
|
||||||
|
title='At least 3 characters'
|
||||||
|
onKeyDown={(e)=>{
|
||||||
|
if(e.key === 'Enter' && !submitButtonRef.current.disabled)
|
||||||
|
loadPage(1, true);
|
||||||
|
}}
|
||||||
|
placeholder='v3 Reference Document'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Author of the brew
|
||||||
|
<input
|
||||||
|
ref={authorRef}
|
||||||
|
type='text'
|
||||||
|
name='author'
|
||||||
|
pattern='.{1,}'
|
||||||
|
defaultValue={props.query.author || ''}
|
||||||
|
onKeyUp={disableSubmitIfFormInvalid}
|
||||||
|
onKeyDown={(e)=>{
|
||||||
|
if(e.key === 'Enter' && !submitButtonRef.current.disabled)
|
||||||
|
loadPage(1, true);
|
||||||
|
}}
|
||||||
|
placeholder='Username'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Results per page
|
||||||
|
<select ref={countRef} name='count' defaultValue={props.query.count || 20}>
|
||||||
|
<option value='10'>10</option>
|
||||||
|
<option value='20'>20</option>
|
||||||
|
<option value='40'>40</option>
|
||||||
|
<option value='60'>60</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
className='renderer'
|
||||||
|
ref={v3Ref}
|
||||||
|
type='checkbox'
|
||||||
|
defaultChecked={props.query.v3 !== 'false'}
|
||||||
|
onChange={disableSubmitIfFormInvalid}
|
||||||
|
/>
|
||||||
|
Search for v3 brews
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
className='renderer'
|
||||||
|
ref={legacyRef}
|
||||||
|
type='checkbox'
|
||||||
|
defaultChecked={props.query.legacy !== 'false'}
|
||||||
|
onChange={disableSubmitIfFormInvalid}
|
||||||
|
/>
|
||||||
|
Search for legacy brews
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id='searchButton'
|
||||||
|
ref={submitButtonRef}
|
||||||
|
onClick={()=>{
|
||||||
|
loadPage(1, true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
<i
|
||||||
|
className={searching ? 'fas fa-spin fa-spinner': 'fas fa-search'}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<legend>
|
||||||
|
<h3>Tips and tricks</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Only <b>published</b> brews are searchable via this tool
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Usernames are case-sensitive
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Use <code>"word"</code> to match an exact string,
|
||||||
|
and <code>-</code> to exclude words (at least one word must not be negated)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Some common words like "a", "after", "through", "itself", "here", etc.,
|
||||||
|
are ignored in searches. The full list can be found
|
||||||
|
<a href='https://github.com/mongodb/mongo/blob/0e3b3ca8480ddddf5d0105d11a94bd4698335312/src/mongo/db/fts/stop_words_english.txt'>
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<small>New features will be coming, such as filters and search by tags.</small>
|
||||||
|
</legend>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderPaginationControls = ()=>{
|
||||||
|
if(!totalBrews) return null;
|
||||||
|
|
||||||
|
const countInt = parseInt(props.query.count || 20);
|
||||||
|
const totalPages = Math.ceil(totalBrews / countInt);
|
||||||
|
|
||||||
|
let startPage, endPage;
|
||||||
|
if(pageState <= 6) {
|
||||||
|
startPage = 1;
|
||||||
|
endPage = Math.min(totalPages, 10);
|
||||||
|
} else if(pageState + 4 >= totalPages) {
|
||||||
|
startPage = Math.max(1, totalPages - 9);
|
||||||
|
endPage = totalPages;
|
||||||
|
} else {
|
||||||
|
startPage = pageState - 5;
|
||||||
|
endPage = pageState + 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagesAroundCurrent = new Array(endPage - startPage + 1)
|
||||||
|
.fill()
|
||||||
|
.map((_, index)=>(
|
||||||
|
<a
|
||||||
|
key={startPage + index}
|
||||||
|
className={`pageNumber ${
|
||||||
|
pageState === startPage + index ? 'currentPage' : ''
|
||||||
|
}`}
|
||||||
|
onClick={()=>loadPage(startPage + index, false)}
|
||||||
|
>
|
||||||
|
{startPage + index}
|
||||||
|
</a>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='paginationControls'>
|
||||||
|
<button
|
||||||
|
className='previousPage'
|
||||||
|
onClick={()=>loadPage(pageState - 1, false)}
|
||||||
|
disabled={pageState === startPage}
|
||||||
|
>
|
||||||
|
<i className='fa-solid fa-chevron-left'></i>
|
||||||
|
</button>
|
||||||
|
<ol className='pages'>
|
||||||
|
{startPage > 1 && (
|
||||||
|
<a
|
||||||
|
className='pageNumber firstPage'
|
||||||
|
onClick={()=>loadPage(1, false)}
|
||||||
|
>
|
||||||
|
1 ...
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{pagesAroundCurrent}
|
||||||
|
{endPage < totalPages && (
|
||||||
|
<a
|
||||||
|
className='pageNumber lastPage'
|
||||||
|
onClick={()=>loadPage(totalPages, false)}
|
||||||
|
>
|
||||||
|
... {totalPages}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
<button
|
||||||
|
className='nextPage'
|
||||||
|
onClick={()=>loadPage(pageState + 1, false)}
|
||||||
|
disabled={pageState === totalPages}
|
||||||
|
>
|
||||||
|
<i className='fa-solid fa-chevron-right'></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFoundBrews = ()=>{
|
||||||
|
if(searching) {
|
||||||
|
return (
|
||||||
|
<div className='foundBrews searching'>
|
||||||
|
<h3 className='searchAnim'>Searching</h3>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(error) {
|
||||||
|
const errorText = ErrorIndex()[error.HBErrorCode.toString()] || '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='foundBrews noBrews'>
|
||||||
|
<h3>Error: {errorText}</h3>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!brewCollection) {
|
||||||
|
return (
|
||||||
|
<div className='foundBrews noBrews'>
|
||||||
|
<h3>No search yet</h3>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(brewCollection.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className='foundBrews noBrews'>
|
||||||
|
<h3>No brews found</h3>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='foundBrews'>
|
||||||
|
<span className='totalBrews'>
|
||||||
|
{`Brews found: `}
|
||||||
|
<span>{totalBrews}</span>
|
||||||
|
</span>
|
||||||
|
{brewCollection.map((brew, index)=>{
|
||||||
|
return (
|
||||||
|
<BrewItem
|
||||||
|
brew={{ ...brew }}
|
||||||
|
key={index}
|
||||||
|
reportError={props.reportError}
|
||||||
|
renderStorage={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{renderPaginationControls()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='vaultPage'>
|
||||||
|
<link href='/themes/V3/Blank/style.css' rel='stylesheet' />
|
||||||
|
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet' />
|
||||||
|
{renderNavItems()}
|
||||||
|
<div className='content'>
|
||||||
|
<SplitPane showDividerButtons={false}>
|
||||||
|
<div className='form dataGroup'>{renderForm()}</div>
|
||||||
|
|
||||||
|
<div className='resultsContainer dataGroup'>
|
||||||
|
{renderFoundBrews()}
|
||||||
|
</div>
|
||||||
|
</SplitPane>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = VaultPage;
|
||||||
362
client/homebrew/pages/vaultPage/vaultPage.less
Normal file
362
client/homebrew/pages/vaultPage/vaultPage.less
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
.vaultPage {
|
||||||
|
height : 100%;
|
||||||
|
overflow-y : hidden;
|
||||||
|
background-color : #2C3E50;
|
||||||
|
|
||||||
|
*:not(input) { user-select : none; }
|
||||||
|
|
||||||
|
.content {
|
||||||
|
background : #2C3E50;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.dataGroup {
|
||||||
|
width : 100%;
|
||||||
|
height : 100%;
|
||||||
|
background : white;
|
||||||
|
|
||||||
|
&.form .brewLookup {
|
||||||
|
position : relative;
|
||||||
|
padding : 50px clamp(20px, 4vw, 50px);
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size : 10pt;
|
||||||
|
color : #555555;
|
||||||
|
|
||||||
|
a { color : #333333; }
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding-inline : 5px;
|
||||||
|
background : lightgrey;
|
||||||
|
border-radius : 5px;
|
||||||
|
font-family : monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
font-family : 'CodeBold';
|
||||||
|
letter-spacing : 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
h3 {
|
||||||
|
margin-block : 30px 20px;
|
||||||
|
font-size : 20px;
|
||||||
|
text-align : center;
|
||||||
|
border-bottom : 2px solid;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
padding-inline : 30px 10px;
|
||||||
|
li {
|
||||||
|
margin-block : 5px;
|
||||||
|
line-height : calc(1em + 5px);
|
||||||
|
list-style : disc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position : absolute;
|
||||||
|
top : 0;
|
||||||
|
right : 0;
|
||||||
|
left : 0;
|
||||||
|
display : block;
|
||||||
|
padding : 10px;
|
||||||
|
font-weight : 900;
|
||||||
|
color : white;
|
||||||
|
white-space : pre-wrap;
|
||||||
|
content : 'Error:\A At least one renderer should be enabled to make a search';
|
||||||
|
background : rgb(255, 60, 60);
|
||||||
|
opacity : 0;
|
||||||
|
transition : opacity 0.5s;
|
||||||
|
}
|
||||||
|
&:not(:has(input[type='checkbox']:checked))::after { opacity : 1; }
|
||||||
|
|
||||||
|
.formTitle {
|
||||||
|
margin : 20px 0;
|
||||||
|
font-size : 30px;
|
||||||
|
color : black;
|
||||||
|
text-align : center;
|
||||||
|
border-bottom : 2px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formContents {
|
||||||
|
position : relative;
|
||||||
|
display : flex;
|
||||||
|
flex-direction : column;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display : flex;
|
||||||
|
align-items : center;
|
||||||
|
margin : 10px 0;
|
||||||
|
}
|
||||||
|
select { margin : 0 10px; }
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin : 0 10px;
|
||||||
|
|
||||||
|
&:invalid { background : rgb(255, 188, 181); }
|
||||||
|
|
||||||
|
&[type='checkbox'] {
|
||||||
|
position : relative;
|
||||||
|
display : inline-block;
|
||||||
|
width : 50px;
|
||||||
|
height : 30px;
|
||||||
|
font-family : 'WalterTurncoat';
|
||||||
|
font-size : 20px;
|
||||||
|
font-weight : 800;
|
||||||
|
color : white;
|
||||||
|
letter-spacing : 2px;
|
||||||
|
appearance : none;
|
||||||
|
background : red;
|
||||||
|
isolation : isolate;
|
||||||
|
border-radius : 5px;
|
||||||
|
|
||||||
|
&::before,&::after {
|
||||||
|
position : absolute;
|
||||||
|
inset : 0;
|
||||||
|
z-index : 5;
|
||||||
|
padding-top : 2px;
|
||||||
|
text-align : center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display : block;
|
||||||
|
content : 'No';
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display : none;
|
||||||
|
content : 'Yes';
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked {
|
||||||
|
background : green;
|
||||||
|
|
||||||
|
&::before { display : none; }
|
||||||
|
&::after { display : block; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchButton {
|
||||||
|
position : absolute;
|
||||||
|
right : 20px;
|
||||||
|
bottom : 0;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-left : 10px;
|
||||||
|
animation-duration : 1000s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.resultsContainer {
|
||||||
|
display : flex;
|
||||||
|
flex-direction : column;
|
||||||
|
height : 100%;
|
||||||
|
overflow-y : auto;
|
||||||
|
font-family : 'BookInsanityRemake';
|
||||||
|
font-size : 0.34cm;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-family : 'Open Sans';
|
||||||
|
font-weight : 900;
|
||||||
|
color : white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foundBrews {
|
||||||
|
position : relative;
|
||||||
|
width : 100%;
|
||||||
|
height : 100%;
|
||||||
|
max-height : 100%;
|
||||||
|
padding : 50px 50px 70px 50px;
|
||||||
|
overflow-y : scroll;
|
||||||
|
background-color : #2C3E50;
|
||||||
|
|
||||||
|
h3 { font-size : 25px; }
|
||||||
|
|
||||||
|
&.noBrews {
|
||||||
|
display : grid;
|
||||||
|
place-items : center;
|
||||||
|
color : white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.searching {
|
||||||
|
display : grid;
|
||||||
|
place-items : center;
|
||||||
|
color : white;
|
||||||
|
|
||||||
|
h3 { position : relative; }
|
||||||
|
|
||||||
|
h3.searchAnim::after {
|
||||||
|
position : absolute;
|
||||||
|
top : 50%;
|
||||||
|
right : 0;
|
||||||
|
width : max-content;
|
||||||
|
height : 1em;
|
||||||
|
content : '';
|
||||||
|
translate : calc(100% + 5px) -50%;
|
||||||
|
animation : trailingDots 2s ease infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.totalBrews {
|
||||||
|
position : fixed;
|
||||||
|
right : 0;
|
||||||
|
bottom : 0;
|
||||||
|
z-index : 1000;
|
||||||
|
padding : 8px 10px;
|
||||||
|
font-family : 'Open Sans';
|
||||||
|
font-size : 11px;
|
||||||
|
font-weight : 800;
|
||||||
|
color : white;
|
||||||
|
background-color : #333333;
|
||||||
|
|
||||||
|
.searchAnim {
|
||||||
|
position : relative;
|
||||||
|
display : inline-block;
|
||||||
|
width : 3ch;
|
||||||
|
height : 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchAnim::after {
|
||||||
|
position : absolute;
|
||||||
|
top : 50%;
|
||||||
|
right : 0;
|
||||||
|
width : max-content;
|
||||||
|
height : 1em;
|
||||||
|
content : '';
|
||||||
|
translate : -50% -50%;
|
||||||
|
animation : trailingDots 2s ease infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brewItem {
|
||||||
|
width : 47%;
|
||||||
|
margin-right : 40px;
|
||||||
|
color : black;
|
||||||
|
isolation:isolate;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
position:absolute;
|
||||||
|
inset:0;
|
||||||
|
display:block;
|
||||||
|
content:'';
|
||||||
|
background-image : url('/assets/parchmentBackground.jpg');
|
||||||
|
z-index:-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(even of .brewItem) { margin-right : 0; }
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-family : 'MrEavesRemake';
|
||||||
|
font-size : 0.75cm;
|
||||||
|
font-weight : 800;
|
||||||
|
line-height : 0.988em;
|
||||||
|
color : var(--HB_Color_HeaderText);
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
font-family : 'ScalySansRemake';
|
||||||
|
font-size : 1.2em;
|
||||||
|
position:relative;
|
||||||
|
z-index:2;
|
||||||
|
|
||||||
|
>span {
|
||||||
|
margin-right : 12px;
|
||||||
|
line-height : 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.links {
|
||||||
|
z-index:2;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 0px;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
z-index:1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginationControls {
|
||||||
|
position : absolute;
|
||||||
|
left : 50%;
|
||||||
|
display : grid;
|
||||||
|
grid-template-areas : 'previousPage currentPage nextPage';
|
||||||
|
grid-template-columns : 50px 1fr 50px;
|
||||||
|
place-items : center;
|
||||||
|
width : auto;
|
||||||
|
translate : -50%;
|
||||||
|
|
||||||
|
.pages {
|
||||||
|
display : flex;
|
||||||
|
grid-area : currentPage;
|
||||||
|
justify-content : space-evenly;
|
||||||
|
width : 100%;
|
||||||
|
height : 100%;
|
||||||
|
padding : 5px 8px;
|
||||||
|
text-align : center;
|
||||||
|
|
||||||
|
.pageNumber {
|
||||||
|
margin-inline : 1vw;
|
||||||
|
font-family : 'Open Sans';
|
||||||
|
font-weight : 900;
|
||||||
|
color : white;
|
||||||
|
text-underline-position : under;
|
||||||
|
text-wrap : nowrap;
|
||||||
|
cursor : pointer;
|
||||||
|
|
||||||
|
&.currentPage {
|
||||||
|
color : gold;
|
||||||
|
text-decoration : underline;
|
||||||
|
pointer-events : none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.firstPage { margin-right : -5px; }
|
||||||
|
|
||||||
|
&.lastPage { margin-left : -5px; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width : max-content;
|
||||||
|
|
||||||
|
&.previousPage { grid-area : previousPage; }
|
||||||
|
|
||||||
|
&.nextPage { grid-area : nextPage; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes trailingDots {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
32% { content : ' .'; }
|
||||||
|
|
||||||
|
33%,
|
||||||
|
65% { content : ' ..'; }
|
||||||
|
|
||||||
|
66%,
|
||||||
|
100% { content : ' ...'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// media query for when the page is smaller than 1079 px in width
|
||||||
|
@media screen and (max-width : 1079px) {
|
||||||
|
.vaultPage .content {
|
||||||
|
|
||||||
|
.dataGroup.form .brewLookup { padding : 1px 20px 20px 10px; }
|
||||||
|
|
||||||
|
.dataGroup.resultsContainer .foundBrews .brewItem {
|
||||||
|
width : 100%;
|
||||||
|
margin-inline : auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
116
client/homebrew/utils/versionHistory.js
Normal file
116
client/homebrew/utils/versionHistory.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
export const HISTORY_PREFIX = 'HOMEBREWERY-HISTORY';
|
||||||
|
export const HISTORY_SLOTS = 5;
|
||||||
|
|
||||||
|
// History values in minutes
|
||||||
|
const DEFAULT_HISTORY_SAVE_DELAYS = {
|
||||||
|
'0' : 0,
|
||||||
|
'1' : 2,
|
||||||
|
'2' : 10,
|
||||||
|
'3' : 60,
|
||||||
|
'4' : 12 * 60,
|
||||||
|
'5' : 2 * 24 * 60
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_GARBAGE_COLLECT_DELAY = 28 * 24 * 60;
|
||||||
|
|
||||||
|
const HISTORY_SAVE_DELAYS = global.config?.historyData?.HISTORY_SAVE_DELAYS ?? DEFAULT_HISTORY_SAVE_DELAYS;
|
||||||
|
const GARBAGE_COLLECT_DELAY = global.config?.historyData?.GARBAGE_COLLECT_DELAY ?? DEFAULT_GARBAGE_COLLECT_DELAY;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function getKeyBySlot(brew, slot){
|
||||||
|
return `${HISTORY_PREFIX}-${brew.shareId}-${slot}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getVersionBySlot(brew, slot){
|
||||||
|
// Read stored brew data
|
||||||
|
// - If it exists, parse data to object
|
||||||
|
// - If it doesn't exist, pass default object
|
||||||
|
const key = getKeyBySlot(brew, slot);
|
||||||
|
const storedVersion = localStorage.getItem(key);
|
||||||
|
const output = storedVersion ? JSON.parse(storedVersion) : { expireAt: '2000-01-01T00:00:00.000Z', shareId: brew.shareId, noData: true };
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateStoredBrew(brew, slot = 0) {
|
||||||
|
const archiveBrew = {
|
||||||
|
title : brew.title,
|
||||||
|
text : brew.text,
|
||||||
|
style : brew.style,
|
||||||
|
version : brew.version,
|
||||||
|
shareId : brew.shareId,
|
||||||
|
savedAt : brew?.savedAt || new Date(),
|
||||||
|
expireAt : new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
archiveBrew.expireAt.setMinutes(archiveBrew.expireAt.getMinutes() + HISTORY_SAVE_DELAYS[slot]);
|
||||||
|
|
||||||
|
const key = getKeyBySlot(brew, slot);
|
||||||
|
localStorage.setItem(key, JSON.stringify(archiveBrew));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function historyExists(brew){
|
||||||
|
return Object.keys(localStorage)
|
||||||
|
.some((key)=>{
|
||||||
|
return key.startsWith(`${HISTORY_PREFIX}-${brew.shareId}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadHistory(brew){
|
||||||
|
const history = {};
|
||||||
|
|
||||||
|
// Load data from local storage to History object
|
||||||
|
for (let i = 1; i <= HISTORY_SLOTS; i++){
|
||||||
|
history[i] = getVersionBySlot(brew, i);
|
||||||
|
};
|
||||||
|
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateHistory(brew) {
|
||||||
|
const history = loadHistory(brew);
|
||||||
|
|
||||||
|
// Walk each version position
|
||||||
|
for (let slot = HISTORY_SLOTS; slot > 0; slot--){
|
||||||
|
const storedVersion = history[slot];
|
||||||
|
|
||||||
|
// If slot has expired, update all lower slots and break
|
||||||
|
if(new Date() >= new Date(storedVersion.expireAt)){
|
||||||
|
for (let updateSlot = slot - 1; updateSlot>0; updateSlot--){
|
||||||
|
// Move data from updateSlot to updateSlot + 1
|
||||||
|
!history[updateSlot]?.noData && updateStoredBrew(history[updateSlot], updateSlot + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the most recent brew
|
||||||
|
updateStoredBrew(brew, 1);
|
||||||
|
|
||||||
|
// Break out of data checks because we found an expired value
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getHistoryItems(brew){
|
||||||
|
const historyArray = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= HISTORY_SLOTS; i++){
|
||||||
|
historyArray.push(getVersionBySlot(brew, i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return historyArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function versionHistoryGarbageCollection(){
|
||||||
|
Object.keys(localStorage)
|
||||||
|
.filter((key)=>{
|
||||||
|
return key.startsWith(HISTORY_PREFIX);
|
||||||
|
})
|
||||||
|
.forEach((key)=>{
|
||||||
|
const collectAt = new Date(JSON.parse(localStorage.getItem(key)).savedAt);
|
||||||
|
collectAt.setMinutes(collectAt.getMinutes() + GARBAGE_COLLECT_DELAY);
|
||||||
|
if(new Date() > collectAt){
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,57 +1,75 @@
|
|||||||
.fac {
|
.fac {
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
|
background-color : currentColor;
|
||||||
|
mask-size : contain;
|
||||||
|
mask-repeat : no-repeat;
|
||||||
|
mask-position : center;
|
||||||
|
width : 1em;
|
||||||
|
aspect-ratio : 1;
|
||||||
}
|
}
|
||||||
.position-top-left {
|
.position-top-left {
|
||||||
content: url('../icons/position-top-left.svg');
|
mask-image: url('../icons/position-top-left.svg');
|
||||||
}
|
}
|
||||||
.position-top-right {
|
.position-top-right {
|
||||||
content: url('../icons/position-top-right.svg');
|
mask-image: url('../icons/position-top-right.svg');
|
||||||
}
|
}
|
||||||
.position-bottom-left {
|
.position-bottom-left {
|
||||||
content: url('../icons/position-bottom-left.svg');
|
mask-image: url('../icons/position-bottom-left.svg');
|
||||||
}
|
}
|
||||||
.position-bottom-right {
|
.position-bottom-right {
|
||||||
content: url('../icons/position-bottom-right.svg');
|
mask-image: url('../icons/position-bottom-right.svg');
|
||||||
}
|
}
|
||||||
.position-top {
|
.position-top {
|
||||||
content: url('../icons/position-top.svg');
|
mask-image: url('../icons/position-top.svg');
|
||||||
}
|
}
|
||||||
.position-right {
|
.position-right {
|
||||||
content: url('../icons/position-right.svg');
|
mask-image: url('../icons/position-right.svg');
|
||||||
}
|
}
|
||||||
.position-bottom {
|
.position-bottom {
|
||||||
content: url('../icons/position-bottom.svg');
|
mask-image: url('../icons/position-bottom.svg');
|
||||||
}
|
}
|
||||||
.position-left {
|
.position-left {
|
||||||
content: url('../icons/position-left.svg');
|
mask-image: url('../icons/position-left.svg');
|
||||||
}
|
}
|
||||||
.mask-edge {
|
.mask-edge {
|
||||||
content: url('../icons/mask-edge.svg');
|
mask-image: url('../icons/mask-edge.svg');
|
||||||
}
|
}
|
||||||
.mask-corner {
|
.mask-corner {
|
||||||
content: url('../icons/mask-corner.svg');
|
mask-image: url('../icons/mask-corner.svg');
|
||||||
}
|
}
|
||||||
.mask-center {
|
.mask-center {
|
||||||
content: url('../icons/mask-center.svg');
|
mask-image: url('../icons/mask-center.svg');
|
||||||
}
|
}
|
||||||
.book-front-cover {
|
.book-front-cover {
|
||||||
content: url('../icons/book-front-cover.svg');
|
mask-image: url('../icons/book-front-cover.svg');
|
||||||
}
|
}
|
||||||
.book-back-cover {
|
.book-back-cover {
|
||||||
content: url('../icons/book-back-cover.svg');
|
mask-image: url('../icons/book-back-cover.svg');
|
||||||
}
|
}
|
||||||
.book-inside-cover {
|
.book-inside-cover {
|
||||||
content: url('../icons/book-inside-cover.svg');
|
mask-image: url('../icons/book-inside-cover.svg');
|
||||||
}
|
}
|
||||||
.book-part-cover {
|
.book-part-cover {
|
||||||
content: url('../icons/book-part-cover.svg');
|
mask-image: url('../icons/book-part-cover.svg');
|
||||||
|
}
|
||||||
|
.image-wrap-left {
|
||||||
|
mask-image: url('../icons/image-wrap-left.svg');
|
||||||
|
}
|
||||||
|
.image-wrap-right {
|
||||||
|
mask-image: url('../icons/image-wrap-right.svg');
|
||||||
}
|
}
|
||||||
.davek {
|
.davek {
|
||||||
content: url('../icons/Davek.svg');
|
mask-image: url('../icons/Davek.svg');
|
||||||
}
|
}
|
||||||
.rellanic {
|
.rellanic {
|
||||||
content: url('../icons/Rellanic.svg');
|
mask-image: url('../icons/Rellanic.svg');
|
||||||
}
|
}
|
||||||
.iokharic {
|
.iokharic {
|
||||||
content: url('../icons/Iokharic.svg');
|
mask-image: url('../icons/Iokharic.svg');
|
||||||
|
}
|
||||||
|
.zoom-to-fit {
|
||||||
|
mask-image: url('../icons/zoom-to-fit.svg');
|
||||||
|
}
|
||||||
|
.fit-width {
|
||||||
|
mask-image: url('../icons/fit-width.svg');
|
||||||
}
|
}
|
||||||
|
|||||||
15
client/icons/fit-width.svg
Normal file
15
client/icons/fit-width.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(1.07509,0,0,1.07509,-3.75511,-3.75468)">
|
||||||
|
<g transform="matrix(0.843549,0,0,0.950644,8.38004,4.39672)">
|
||||||
|
<path d="M28.455,52.413L28.455,58.581C28.455,59.719 27.684,60.745 26.501,61.181C25.318,61.616 23.956,61.375 23.051,60.571L11.114,49.96C9.878,48.862 9.878,47.08 11.114,45.981L23.051,35.371C23.956,34.566 25.318,34.326 26.501,34.761C27.684,35.197 28.455,36.223 28.455,37.361L28.455,43.528L70.223,43.528L70.223,37.361C70.223,36.223 70.995,35.197 72.177,34.761C73.36,34.326 74.722,34.566 75.627,35.371L87.564,45.981C88.8,47.08 88.8,48.862 87.564,49.96L75.627,60.571C74.722,61.375 73.36,61.616 72.177,61.181C70.995,60.745 70.223,59.719 70.223,58.581L70.223,52.413L28.455,52.413Z"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.46702,0,0,0.986488,-23.0335,3.50686)">
|
||||||
|
<path d="M23.967,5.877L23.967,88.383C23.967,90.556 22.781,92.321 21.319,92.321L21.157,92.321C19.695,92.321 18.509,90.556 18.509,88.383L18.509,5.877C18.509,3.703 19.695,1.939 21.157,1.939L21.319,1.939C22.781,1.939 23.967,3.703 23.967,5.877Z"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.46702,0,0,0.986488,60.7211,3.50686)">
|
||||||
|
<path d="M23.967,5.877L23.967,88.383C23.967,90.556 22.781,92.321 21.319,92.321L21.157,92.321C19.695,92.321 18.509,90.556 18.509,88.383L18.509,5.877C18.509,3.703 19.695,1.939 21.157,1.939L21.319,1.939C22.781,1.939 23.967,3.703 23.967,5.877Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
58
client/icons/image-wrap-left.svg
Normal file
58
client/icons/image-wrap-left.svg
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 512.00006 512"
|
||||||
|
xml:space="preserve"
|
||||||
|
id="svg10"
|
||||||
|
sodipodi:docname="noun-wrap-image-left-212078.svg"
|
||||||
|
width="512.00006"
|
||||||
|
height="512"
|
||||||
|
inkscape:export-filename="image-wrap-right.svg"
|
||||||
|
inkscape:export-xdpi="300"
|
||||||
|
inkscape:export-ydpi="300"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs10" /><sodipodi:namedview
|
||||||
|
id="namedview10"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#111111"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:deskcolor="#d1d1d1" /><path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 185.80018,144 H 32"
|
||||||
|
id="path11"
|
||||||
|
sodipodi:nodetypes="cc"
|
||||||
|
clip-path="none"
|
||||||
|
inkscape:export-filename="image-wrap-right.svg"
|
||||||
|
inkscape:export-xdpi="300"
|
||||||
|
inkscape:export-ydpi="300" /><path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 185.80018,368 H 32"
|
||||||
|
id="path11-8"
|
||||||
|
sodipodi:nodetypes="cc"
|
||||||
|
clip-path="none" /><path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 480.00007,32 H 32"
|
||||||
|
id="path11-8-2-67"
|
||||||
|
clip-path="none"
|
||||||
|
sodipodi:nodetypes="cc" /><path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 480.00008,480 H 32"
|
||||||
|
id="path11-8-2-67-2"
|
||||||
|
clip-path="none"
|
||||||
|
sodipodi:nodetypes="cc" /><path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 160.0001,255.98832 32,256.01162"
|
||||||
|
id="path11-0"
|
||||||
|
sodipodi:nodetypes="cc"
|
||||||
|
clip-path="none" /><path
|
||||||
|
id="path23"
|
||||||
|
style="opacity:0.922046;fill:#000000;fill-opacity:1;stroke-width:64;stroke-linecap:round;stroke-dasharray:none;paint-order:fill markers stroke"
|
||||||
|
d="m 416.00008,96 a 160,160 0 0 1 96,32.50977 v 254.98046 a 160,160 0 0 1 -96,32.50977 160,160 0 0 1 -160,-160 160,160 0 0 1 160,-160 z" /></svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
58
client/icons/image-wrap-right.svg
Normal file
58
client/icons/image-wrap-right.svg
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 512.00006 512"
|
||||||
|
xml:space="preserve"
|
||||||
|
id="svg10"
|
||||||
|
sodipodi:docname="noun-wrap-image-left-212078.svg"
|
||||||
|
width="512.00006"
|
||||||
|
height="512"
|
||||||
|
inkscape:export-filename="image-wrap-right.svg"
|
||||||
|
inkscape:export-xdpi="300"
|
||||||
|
inkscape:export-ydpi="300"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs10" /><sodipodi:namedview
|
||||||
|
id="namedview10"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#111111"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:deskcolor="#d1d1d1" /><path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 326.1999,144 H 480.00008"
|
||||||
|
id="path11"
|
||||||
|
sodipodi:nodetypes="cc"
|
||||||
|
clip-path="none"
|
||||||
|
inkscape:export-filename="image-wrap-right.svg"
|
||||||
|
inkscape:export-xdpi="300"
|
||||||
|
inkscape:export-ydpi="300" /><path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 326.1999,368 H 480.00008"
|
||||||
|
id="path11-8"
|
||||||
|
sodipodi:nodetypes="cc"
|
||||||
|
clip-path="none" /><path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 32.00001,32 H 480.00008"
|
||||||
|
id="path11-8-2-67"
|
||||||
|
clip-path="none"
|
||||||
|
sodipodi:nodetypes="cc" /><path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 32,480 H 480.00008"
|
||||||
|
id="path11-8-2-67-2"
|
||||||
|
clip-path="none"
|
||||||
|
sodipodi:nodetypes="cc" /><path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 351.99998,255.98832 128.0001,0.0233"
|
||||||
|
id="path11-0"
|
||||||
|
sodipodi:nodetypes="cc"
|
||||||
|
clip-path="none" /><path
|
||||||
|
id="path23"
|
||||||
|
style="opacity:0.922046;fill:#000000;fill-opacity:1;stroke-width:64;stroke-linecap:round;stroke-dasharray:none;paint-order:fill markers stroke"
|
||||||
|
d="M 96,96 A 160,160 0 0 0 0,128.50977 V 383.49023 A 160,160 0 0 0 96,416 160,160 0 0 0 256,256 160,160 0 0 0 96,96 Z" /></svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
12
client/icons/zoom-to-fit.svg
Normal file
12
client/icons/zoom-to-fit.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(1.0781,0,0,1.0781,-3.90545,-3.90502)">
|
||||||
|
<g transform="matrix(0.841196,0,0,0.947993,8.49652,4.52391)">
|
||||||
|
<path d="M44.333,52.413L28.455,52.413L28.455,58.581C28.455,59.719 27.684,60.745 26.501,61.181C25.318,61.616 23.956,61.375 23.051,60.571L11.114,49.96C9.878,48.862 9.878,47.08 11.114,45.981L23.051,35.371C23.956,34.566 25.318,34.326 26.501,34.761C27.684,35.197 28.455,36.223 28.455,37.361L28.455,43.528L44.333,43.528L44.333,29.439L37.382,29.439C36.099,29.439 34.943,28.755 34.452,27.705C33.961,26.656 34.233,25.448 35.14,24.644L47.097,14.052C48.335,12.956 50.343,12.956 51.581,14.052L63.539,24.644C64.446,25.448 64.717,26.656 64.226,27.705C63.735,28.755 62.579,29.439 61.296,29.439L54.346,29.439L54.346,43.528L70.223,43.528L70.223,37.361C70.223,36.223 70.995,35.197 72.177,34.761C73.36,34.326 74.722,34.566 75.627,35.371L87.564,45.981C88.8,47.08 88.8,48.862 87.564,49.96L75.627,60.571C74.722,61.375 73.36,61.616 72.177,61.181C70.995,60.745 70.223,59.719 70.223,58.581L70.223,52.413L54.346,52.413L54.346,66.502L61.296,66.502C62.579,66.502 63.735,67.187 64.226,68.236C64.717,69.286 64.446,70.494 63.539,71.297L51.581,81.889C50.343,82.986 48.335,82.986 47.097,81.889L35.14,71.297C34.233,70.494 33.961,69.286 34.452,68.236C34.943,67.187 36.099,66.502 37.382,66.502L44.333,66.503L44.333,52.413Z"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.0247,0,0,1.0247,-5.47698,-3.53855)">
|
||||||
|
<path d="M99.4,14.269L99.4,90.227C99.4,94.245 96.137,97.508 92.119,97.508L16.161,97.508C12.142,97.508 8.88,94.245 8.88,90.227L8.88,14.269C8.88,10.25 12.142,6.988 16.161,6.988L92.119,6.988C96.137,6.988 99.4,10.25 99.4,14.269ZM93.633,14.269C93.633,13.433 92.955,12.755 92.119,12.755L16.161,12.755C15.325,12.755 14.647,13.433 14.647,14.269L14.647,90.227C14.647,91.062 15.325,91.741 16.161,91.741L92.119,91.741C92.955,91.741 93.633,91.062 93.633,90.227L93.633,14.269Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -4,6 +4,7 @@
|
|||||||
"secret" : "secret",
|
"secret" : "secret",
|
||||||
"web_port" : 8000,
|
"web_port" : 8000,
|
||||||
"enable_v3" : true,
|
"enable_v3" : true,
|
||||||
|
"enable_themes" : true,
|
||||||
"local_environments" : ["docker", "local"],
|
"local_environments" : ["docker", "local"],
|
||||||
"publicUrl" : "https://homebrewery.naturalcrit.com"
|
"publicUrl" : "https://homebrewery.naturalcrit.com"
|
||||||
}
|
}
|
||||||
|
|||||||
71
eslint.config.mjs
Normal file
71
eslint.config.mjs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import react from "eslint-plugin-react";
|
||||||
|
import jest from "eslint-plugin-jest";
|
||||||
|
import globals from "globals";
|
||||||
|
|
||||||
|
export default [{
|
||||||
|
ignores: ["build/"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files : ['**/*.js', '**/*.jsx'],
|
||||||
|
plugins : { react, jest },
|
||||||
|
languageOptions : {
|
||||||
|
ecmaVersion : "latest",
|
||||||
|
sourceType : "module",
|
||||||
|
parserOptions : { ecmaFeatures: { jsx: true } },
|
||||||
|
globals : { ...globals.browser, ...globals.node }
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
/** Errors **/
|
||||||
|
"camelcase" : ["error", { properties: "never" }],
|
||||||
|
"no-array-constructor" : "error",
|
||||||
|
"no-iterator" : "error",
|
||||||
|
"no-nested-ternary" : "error",
|
||||||
|
"no-new-object" : "error",
|
||||||
|
"no-proto" : "error",
|
||||||
|
"react/jsx-no-bind" : ["error", { allowArrowFunctions: true }],
|
||||||
|
"react/jsx-uses-react" : "error",
|
||||||
|
"react/prefer-es6-class" : ["error", "never"],
|
||||||
|
"jest/valid-expect" : ["error", { maxArgs: 3 }],
|
||||||
|
|
||||||
|
/** Warnings **/
|
||||||
|
"max-lines" : ["warn", { max: 200, skipComments: true, skipBlankLines: true }],
|
||||||
|
"max-depth" : ["warn", { max: 4 }],
|
||||||
|
"max-params" : ["warn", { max: 5 }],
|
||||||
|
"no-restricted-syntax" : ["warn", "ClassDeclaration", "SwitchStatement"],
|
||||||
|
"no-unused-vars" : ["warn", { vars: "all", args: "none", varsIgnorePattern: "config|_|cx|createClass" }],
|
||||||
|
"react/jsx-uses-vars" : "warn",
|
||||||
|
|
||||||
|
/** Fixable **/
|
||||||
|
"arrow-parens" : ["warn", "always"],
|
||||||
|
"brace-style" : ["warn", "1tbs", { allowSingleLine: true }],
|
||||||
|
"jsx-quotes" : ["warn", "prefer-single"],
|
||||||
|
"no-var" : "warn",
|
||||||
|
"prefer-const" : "warn",
|
||||||
|
"prefer-template" : "warn",
|
||||||
|
"quotes" : ["warn", "single", { allowTemplateLiterals: true }],
|
||||||
|
"semi" : ["warn", "always"],
|
||||||
|
|
||||||
|
/** Whitespace **/
|
||||||
|
"array-bracket-spacing" : ["warn", "never"],
|
||||||
|
"arrow-spacing" : ["warn", { before: false, after: false }],
|
||||||
|
"comma-spacing" : ["warn", { before: false, after: true }],
|
||||||
|
"indent" : ["warn", "tab", { MemberExpression: "off" }],
|
||||||
|
"linebreak-style" : "off",
|
||||||
|
"no-trailing-spaces" : "warn",
|
||||||
|
"no-whitespace-before-property" : "warn",
|
||||||
|
"object-curly-spacing" : ["warn", "always"],
|
||||||
|
"react/jsx-indent-props" : ["warn", "tab"],
|
||||||
|
"space-in-parens" : ["warn", "never"],
|
||||||
|
"template-curly-spacing" : ["warn", "never"],
|
||||||
|
"keyword-spacing" : ["warn", {
|
||||||
|
before : true,
|
||||||
|
after : true,
|
||||||
|
overrides : { if: { before: false, after: false } }
|
||||||
|
}],
|
||||||
|
"key-spacing" : ["warn", {
|
||||||
|
multiLine : { beforeColon: true, afterColon: true, align: "colon" },
|
||||||
|
singleLine : { beforeColon: false, afterColon: true }
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
30452
package-lock.json
generated
30452
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
66
package.json
66
package.json
@@ -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.12.0",
|
"version": "3.15.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": "^10.2.x",
|
"npm": "^10.2.x",
|
||||||
"node": "^20.8.x"
|
"node": "^20.8.x"
|
||||||
@@ -15,14 +15,16 @@
|
|||||||
"quick": "node scripts/quick.js",
|
"quick": "node scripts/quick.js",
|
||||||
"build": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
|
"build": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
|
||||||
"builddev": "node scripts/buildHomebrew.js --dev",
|
"builddev": "node scripts/buildHomebrew.js --dev",
|
||||||
"lint": "eslint --fix **/*.{js,jsx}",
|
"lint": "eslint --fix",
|
||||||
"lint:dry": "eslint **/*.{js,jsx}",
|
"lint:dry": "eslint",
|
||||||
"stylelint": "stylelint --fix **/*.{less}",
|
"stylelint": "stylelint --fix **/*.{less}",
|
||||||
"stylelint:dry": "stylelint **/*.less",
|
"stylelint:dry": "stylelint **/*.less",
|
||||||
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
||||||
"verify": "npm run lint && npm test",
|
"verify": "npm run lint && npm test",
|
||||||
"test": "jest --runInBand",
|
"test": "jest --runInBand",
|
||||||
"test:api-unit": "jest server/*.spec.js --verbose",
|
"test:api-unit": "jest \"server/.*.spec.js\" --verbose",
|
||||||
|
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --verbose",
|
||||||
|
"test:api-unit:css": "jest \"server/.*.spec.js\" -t \"Get CSS\" --verbose",
|
||||||
"test:coverage": "jest --coverage --silent --runInBand",
|
"test:coverage": "jest --coverage --silent --runInBand",
|
||||||
"test:dev": "jest --verbose --watch",
|
"test:dev": "jest --verbose --watch",
|
||||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
"test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace",
|
"test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace",
|
||||||
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
|
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
|
||||||
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
||||||
|
"test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
|
||||||
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
||||||
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||||
"phb": "node scripts/phb.js",
|
"phb": "node scripts/phb.js",
|
||||||
@@ -56,15 +59,15 @@
|
|||||||
],
|
],
|
||||||
"coverageThreshold": {
|
"coverageThreshold": {
|
||||||
"global": {
|
"global": {
|
||||||
"statements": 25,
|
"statements": 50,
|
||||||
"branches": 10,
|
"branches": 40,
|
||||||
"functions": 22,
|
"functions": 40,
|
||||||
"lines": 25
|
"lines": 50
|
||||||
},
|
},
|
||||||
"server/homebrew.api.js": {
|
"server/homebrew.api.js": {
|
||||||
"statements": 65,
|
"statements": 70,
|
||||||
"branches": 50,
|
"branches": 50,
|
||||||
"functions": 60,
|
"functions": 65,
|
||||||
"lines": 70
|
"lines": 70
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -82,57 +85,58 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.24.5",
|
"@babel/core": "^7.25.2",
|
||||||
"@babel/plugin-transform-runtime": "^7.24.3",
|
"@babel/plugin-transform-runtime": "^7.25.4",
|
||||||
"@babel/preset-env": "^7.24.5",
|
"@babel/preset-env": "^7.25.4",
|
||||||
"@babel/preset-react": "^7.24.1",
|
"@babel/preset-react": "^7.24.7",
|
||||||
"@googleapis/drive": "^8.8.0",
|
"@googleapis/drive": "^8.14.0",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"create-react-class": "^15.7.0",
|
"create-react-class": "^15.7.0",
|
||||||
"dedent-tabs": "^0.10.3",
|
"dedent-tabs": "^0.10.3",
|
||||||
"dompurify": "^3.1.4",
|
"dompurify": "^3.1.6",
|
||||||
"expr-eval": "^2.0.2",
|
"expr-eval": "^2.0.2",
|
||||||
"express": "^4.19.2",
|
"express": "^4.21.0",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-rate-limit": "^7.2.0",
|
"express-rate-limit": "^7.2.0",
|
||||||
"express-static-gzip": "2.1.7",
|
"express-static-gzip": "2.1.8",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.2.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"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.0",
|
"marked-emoji": "^1.4.2",
|
||||||
"marked-extended-tables": "^1.0.8",
|
"marked-extended-tables": "^1.0.10",
|
||||||
"marked-gfm-heading-id": "^3.1.3",
|
"marked-gfm-heading-id": "^3.2.0",
|
||||||
"marked-smartypants-lite": "^1.0.2",
|
"marked-smartypants-lite": "^1.0.2",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mongoose": "^8.4.0",
|
"mongoose": "^8.6.2",
|
||||||
"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.23.1",
|
"react-router-dom": "6.26.2",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^9.0.2",
|
"superagent": "^10.1.0",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.57.0",
|
"@stylistic/stylelint-plugin": "^3.0.1",
|
||||||
"eslint-plugin-jest": "^28.5.0",
|
"eslint": "^9.10.0",
|
||||||
"eslint-plugin-react": "^7.34.1",
|
"eslint-plugin-jest": "^28.8.3",
|
||||||
|
"eslint-plugin-react": "^7.36.1",
|
||||||
|
"globals": "^15.9.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^15.11.0",
|
"stylelint": "^16.9.0",
|
||||||
"stylelint-config-recess-order": "^4.6.0",
|
"stylelint-config-recess-order": "^5.1.0",
|
||||||
"stylelint-config-recommended": "^13.0.0",
|
"stylelint-config-recommended": "^14.0.1",
|
||||||
"stylelint-stylistic": "^0.4.3",
|
|
||||||
"supertest": "^7.0.0"
|
"supertest": "^7.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1065
server/app.js
1065
server/app.js
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,16 @@ const Markdown = require('../shared/naturalcrit/markdown.js');
|
|||||||
const yaml = require('js-yaml');
|
const yaml = require('js-yaml');
|
||||||
const asyncHandler = require('express-async-handler');
|
const asyncHandler = require('express-async-handler');
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
|
const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
|
||||||
|
|
||||||
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
|
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
|
||||||
|
|
||||||
|
const Themes = require('../themes/themes.json');
|
||||||
|
|
||||||
|
const isStaticTheme = (renderer, themeName)=>{
|
||||||
|
return Themes[renderer]?.[themeName] !== undefined;
|
||||||
|
};
|
||||||
|
|
||||||
// const getTopBrews = (cb) => {
|
// const getTopBrews = (cb) => {
|
||||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||||
// cb(brews);
|
// cb(brews);
|
||||||
@@ -37,6 +44,43 @@ const api = {
|
|||||||
}
|
}
|
||||||
return { id, googleId };
|
return { id, googleId };
|
||||||
},
|
},
|
||||||
|
//Get array of any of this user's brews tagged with `meta:theme`
|
||||||
|
getUsersBrewThemes : async (username)=>{
|
||||||
|
if(!username)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
'title',
|
||||||
|
'tags',
|
||||||
|
'shareId',
|
||||||
|
'thumbnail',
|
||||||
|
'textBin',
|
||||||
|
'text',
|
||||||
|
'authors',
|
||||||
|
'renderer'
|
||||||
|
];
|
||||||
|
|
||||||
|
const userThemes = {};
|
||||||
|
|
||||||
|
const brews = await HomebrewModel.getByUser(username, true, fields, { tags: { $in: ['meta:theme', 'meta:Theme'] } });
|
||||||
|
|
||||||
|
if(brews) {
|
||||||
|
for (const brew of brews) {
|
||||||
|
userThemes[brew.renderer] ??= {};
|
||||||
|
userThemes[brew.renderer][brew.shareId] = {
|
||||||
|
name : brew.title,
|
||||||
|
renderer : brew.renderer,
|
||||||
|
baseTheme : brew.theme,
|
||||||
|
baseSnippets : false,
|
||||||
|
author : brew.authors[0],
|
||||||
|
path : brew.shareId,
|
||||||
|
thumbnail : brew.thumbnail || '/assets/naturalCritLogoWhite.svg'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userThemes;
|
||||||
|
},
|
||||||
getBrew : (accessType, stubOnly = false)=>{
|
getBrew : (accessType, stubOnly = false)=>{
|
||||||
// Create middleware with the accessType passed in as part of the scope
|
// Create middleware with the accessType passed in as part of the scope
|
||||||
return async (req, res, next)=>{
|
return async (req, res, next)=>{
|
||||||
@@ -55,7 +99,7 @@ const api = {
|
|||||||
stub = stub?.toObject();
|
stub = stub?.toObject();
|
||||||
|
|
||||||
if(stub?.lock?.locked && accessType != 'edit') {
|
if(stub?.lock?.locked && accessType != 'edit') {
|
||||||
throw { HBErrorCode: '100', code: stub.lock.code, message: stub.lock.message, brewId: stub.shareId, brewTitle: stub.title };
|
throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title };
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is a google id, try to find the google brew
|
// If there is a google id, try to find the google brew
|
||||||
@@ -104,6 +148,20 @@ const api = {
|
|||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCSS : async (req, res)=>{
|
||||||
|
const { brew } = req;
|
||||||
|
if(!brew) return res.status(404).send('');
|
||||||
|
splitTextStyleAndMetadata(brew);
|
||||||
|
if(!brew.style) return res.status(404).send('');
|
||||||
|
|
||||||
|
res.set({
|
||||||
|
'Cache-Control' : 'no-cache',
|
||||||
|
'Content-Type' : 'text/css'
|
||||||
|
});
|
||||||
|
return res.status(200).send(brew.style);
|
||||||
|
},
|
||||||
|
|
||||||
mergeBrewText : (brew)=>{
|
mergeBrewText : (brew)=>{
|
||||||
let text = brew.text;
|
let text = brew.text;
|
||||||
if(brew.style !== undefined) {
|
if(brew.style !== undefined) {
|
||||||
@@ -142,7 +200,7 @@ const api = {
|
|||||||
return modified;
|
return modified;
|
||||||
},
|
},
|
||||||
excludeStubProps : (brew)=>{
|
excludeStubProps : (brew)=>{
|
||||||
const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount'];
|
const propsToExclude = ['text', 'textBin'];
|
||||||
for (const prop of propsToExclude) {
|
for (const prop of propsToExclude) {
|
||||||
brew[prop] = undefined;
|
brew[prop] = undefined;
|
||||||
}
|
}
|
||||||
@@ -209,6 +267,58 @@ const api = {
|
|||||||
|
|
||||||
res.status(200).send(saved);
|
res.status(200).send(saved);
|
||||||
},
|
},
|
||||||
|
getThemeBundle : async(req, res)=>{
|
||||||
|
/* getThemeBundle: Collects the theme and all parent themes
|
||||||
|
returns an object containing an array of css, and an array of snippets, in render order
|
||||||
|
|
||||||
|
req.params.id : The shareId ( User theme ) or name ( static theme )
|
||||||
|
req.params.renderer : The Markdown renderer used for this theme */
|
||||||
|
|
||||||
|
req.params.renderer = _.upperFirst(req.params.renderer);
|
||||||
|
let currentTheme;
|
||||||
|
const completeStyles = [];
|
||||||
|
const completeSnippets = [];
|
||||||
|
|
||||||
|
while (req.params.id) {
|
||||||
|
//=== User Themes ===//
|
||||||
|
if(!isStaticTheme(req.params.renderer, req.params.id)) {
|
||||||
|
await api.getBrew('share')(req, res, ()=>{})
|
||||||
|
.catch((err)=>{
|
||||||
|
if(err.HBErrorCode == '05')
|
||||||
|
err = { ...err, name: 'ThemeLoad Error', message: 'Theme Not Found', HBErrorCode: '09' };
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
currentTheme = req.brew;
|
||||||
|
splitTextStyleAndMetadata(currentTheme);
|
||||||
|
|
||||||
|
// If there is anything in the snippets or style members, append them to the appropriate array
|
||||||
|
if(currentTheme?.snippets) completeSnippets.push(JSON.parse(currentTheme.snippets));
|
||||||
|
if(currentTheme?.style) completeStyles.push(`/* From Brew: ${req.protocol}://${req.get('host')}/share/${req.params.id} */\n\n${currentTheme.style}`);
|
||||||
|
|
||||||
|
req.params.id = currentTheme.theme;
|
||||||
|
req.params.renderer = currentTheme.renderer;
|
||||||
|
}
|
||||||
|
//=== Static Themes ===//
|
||||||
|
else {
|
||||||
|
const localSnippets = `${req.params.renderer}_${req.params.id}`; // Just log the name for loading on client
|
||||||
|
const localStyle = `@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");`;
|
||||||
|
completeSnippets.push(localSnippets);
|
||||||
|
completeStyles.push(`/* From Theme ${req.params.id} */\n\n${localStyle}`);
|
||||||
|
|
||||||
|
req.params.id = Themes[req.params.renderer][req.params.id].baseTheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const returnObj = {
|
||||||
|
// Reverse the order of the arrays so they are listed oldest parent to youngest child.
|
||||||
|
styles : completeStyles.reverse(),
|
||||||
|
snippets : completeSnippets.reverse()
|
||||||
|
};
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
return res.status(200).send(returnObj);
|
||||||
|
},
|
||||||
updateBrew : async (req, res)=>{
|
updateBrew : async (req, res)=>{
|
||||||
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||||
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||||
@@ -369,5 +479,6 @@ router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api
|
|||||||
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
||||||
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
||||||
|
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
||||||
|
|
||||||
module.exports = api;
|
module.exports = api;
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ describe('Tests for api', ()=>{
|
|||||||
let saved;
|
let saved;
|
||||||
|
|
||||||
beforeEach(()=>{
|
beforeEach(()=>{
|
||||||
|
jest.resetModules();
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
|
||||||
saved = undefined;
|
saved = undefined;
|
||||||
saveFunc = jest.fn(async function() {
|
saveFunc = jest.fn(async function() {
|
||||||
saved = { ...this, _id: '1' };
|
saved = { ...this, _id: '1' };
|
||||||
@@ -45,8 +48,10 @@ describe('Tests for api', ()=>{
|
|||||||
model.mockImplementation((brew)=>modelBrew(brew));
|
model.mockImplementation((brew)=>modelBrew(brew));
|
||||||
|
|
||||||
res = {
|
res = {
|
||||||
status : jest.fn(()=>res),
|
status : jest.fn(()=>res),
|
||||||
send : jest.fn(()=>{})
|
send : jest.fn(()=>{}),
|
||||||
|
set : jest.fn(()=>{}),
|
||||||
|
setHeader : jest.fn(()=>{})
|
||||||
};
|
};
|
||||||
|
|
||||||
api = require('./homebrew.api');
|
api = require('./homebrew.api');
|
||||||
@@ -81,10 +86,6 @@ describe('Tests for api', ()=>{
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(()=>{
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getId', ()=>{
|
describe('getId', ()=>{
|
||||||
it('should return only id if google id is not present', ()=>{
|
it('should return only id if google id is not present', ()=>{
|
||||||
const { id, googleId } = api.getId({
|
const { id, googleId } = api.getId({
|
||||||
@@ -300,7 +301,7 @@ describe('Tests for api', ()=>{
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('access is denied to a locked brew', async()=>{
|
it('access is denied to a locked brew', async()=>{
|
||||||
const lockBrew = { title: 'test brew', shareId: '1', lock: { locked: true, code: 404, message: 'brew locked' } };
|
const lockBrew = { title: 'test brew', shareId: '1', lock: { locked: true, code: 404, shareMessage: 'brew locked' } };
|
||||||
model.get = jest.fn(()=>toBrewPromise(lockBrew));
|
model.get = jest.fn(()=>toBrewPromise(lockBrew));
|
||||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
|
||||||
@@ -308,7 +309,7 @@ describe('Tests for api', ()=>{
|
|||||||
const req = { brew: {} };
|
const req = { brew: {} };
|
||||||
const next = jest.fn();
|
const next = jest.fn();
|
||||||
|
|
||||||
await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '100', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' });
|
await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '51', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -408,8 +409,8 @@ brew`);
|
|||||||
expect(sent).not.toEqual(googleBrew);
|
expect(sent).not.toEqual(googleBrew);
|
||||||
expect(result.text).toBeUndefined();
|
expect(result.text).toBeUndefined();
|
||||||
expect(result.textBin).toBeUndefined();
|
expect(result.textBin).toBeUndefined();
|
||||||
expect(result.renderer).toBeUndefined();
|
expect(result.renderer).toBe('v3');
|
||||||
expect(result.pageCount).toBeUndefined();
|
expect(result.pageCount).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -540,9 +541,9 @@ brew`);
|
|||||||
description : '',
|
description : '',
|
||||||
editId : expect.any(String),
|
editId : expect.any(String),
|
||||||
gDrive : false,
|
gDrive : false,
|
||||||
pageCount : undefined,
|
pageCount : 1,
|
||||||
published : false,
|
published : false,
|
||||||
renderer : undefined,
|
renderer : 'V3',
|
||||||
lang : 'en',
|
lang : 'en',
|
||||||
shareId : expect.any(String),
|
shareId : expect.any(String),
|
||||||
googleId : expect.any(String),
|
googleId : expect.any(String),
|
||||||
@@ -581,6 +582,121 @@ brew`);
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Theme bundle', ()=>{
|
||||||
|
it('should return Theme Bundle for a User Theme', async ()=>{
|
||||||
|
const brews = {
|
||||||
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||||
|
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||||
|
|
||||||
|
await api.getThemeBundle(req, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
styles : ['/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'],
|
||||||
|
snippets : []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Theme Bundle for nested User Themes', async ()=>{
|
||||||
|
const brews = {
|
||||||
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
||||||
|
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
|
||||||
|
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: null, shareId: 'userThemeCID', style: 'User Theme C Style' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||||
|
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||||
|
|
||||||
|
await api.getThemeBundle(req, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
styles : [
|
||||||
|
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
|
||||||
|
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
|
||||||
|
'/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'
|
||||||
|
],
|
||||||
|
snippets : []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Theme Bundle for a Static Theme', async ()=>{
|
||||||
|
const req = { params: { renderer: 'V3', id: '5ePHB' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||||
|
|
||||||
|
await api.getThemeBundle(req, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
styles : [
|
||||||
|
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
||||||
|
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`
|
||||||
|
],
|
||||||
|
snippets : [
|
||||||
|
'V3_Blank',
|
||||||
|
'V3_5ePHB'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Theme Bundle for nested User and Static Themes together', async ()=>{
|
||||||
|
const brews = {
|
||||||
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
||||||
|
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
|
||||||
|
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: '5eDMG', shareId: 'userThemeCID', style: 'User Theme C Style' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||||
|
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||||
|
|
||||||
|
await api.getThemeBundle(req, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
styles : [
|
||||||
|
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
||||||
|
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`,
|
||||||
|
`/* From Theme 5eDMG */\n\n@import url("/themes/V3/5eDMG/style.css");`,
|
||||||
|
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
|
||||||
|
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
|
||||||
|
'/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'
|
||||||
|
],
|
||||||
|
snippets : [
|
||||||
|
'V3_Blank',
|
||||||
|
'V3_5ePHB',
|
||||||
|
'V3_5eDMG'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail for an invalid Theme in the chain', async()=>{
|
||||||
|
const brews = {
|
||||||
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'missingTheme', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||||
|
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||||
|
|
||||||
|
let err;
|
||||||
|
await api.getThemeBundle(req, res)
|
||||||
|
.catch((e)=>err = e);
|
||||||
|
|
||||||
|
expect(err).toEqual({
|
||||||
|
HBErrorCode : '09',
|
||||||
|
accessType : 'share',
|
||||||
|
brewId : 'missingTheme',
|
||||||
|
message : 'Theme Not Found',
|
||||||
|
name : 'ThemeLoad Error',
|
||||||
|
status : 404 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('deleteBrew', ()=>{
|
describe('deleteBrew', ()=>{
|
||||||
it('should handle case where fetching the brew returns an error', async ()=>{
|
it('should handle case where fetching the brew returns an error', async ()=>{
|
||||||
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
|
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
|
||||||
@@ -801,4 +917,66 @@ brew`);
|
|||||||
expect(saved.googleId).toEqual(brew.googleId);
|
expect(saved.googleId).toEqual(brew.googleId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('Get CSS', ()=>{
|
||||||
|
it('should return brew style content as CSS text', async ()=>{
|
||||||
|
const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n````\n\n' };
|
||||||
|
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', true);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
await api.getCSS(req, res);
|
||||||
|
|
||||||
|
expect(req.brew).toEqual(testBrew);
|
||||||
|
expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith('\nI Have a style!\n');
|
||||||
|
expect(res.set).toHaveBeenCalledWith({
|
||||||
|
'Cache-Control' : 'no-cache',
|
||||||
|
'Content-Type' : 'text/css'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 404 when brew has no style content', async ()=>{
|
||||||
|
const testBrew = { title: 'test brew', text: 'I don\'t have a style!' };
|
||||||
|
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', true);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
await api.getCSS(req, res);
|
||||||
|
|
||||||
|
expect(req.brew).toEqual(testBrew);
|
||||||
|
expect(req.brew).toHaveProperty('style');
|
||||||
|
expect(res.status).toHaveBeenCalledWith(404);
|
||||||
|
expect(res.send).toHaveBeenCalledWith('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 404 when brew does not exist', async ()=>{
|
||||||
|
const testBrew = { };
|
||||||
|
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', true);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
await api.getCSS(req, res);
|
||||||
|
|
||||||
|
expect(req.brew).toEqual(testBrew);
|
||||||
|
expect(req.brew).toHaveProperty('style');
|
||||||
|
expect(res.status).toHaveBeenCalledWith(404);
|
||||||
|
expect(res.send).toHaveBeenCalledWith('');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ HomebrewSchema.statics.get = async function(query, fields=null){
|
|||||||
return brew;
|
return brew;
|
||||||
};
|
};
|
||||||
|
|
||||||
HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null){
|
HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null, filter=null){
|
||||||
const query = { authors: username, published: true };
|
const query = { authors: username, published: true, ...filter };
|
||||||
if(allowAccess){
|
if(allowAccess){
|
||||||
delete query.published;
|
delete query.published;
|
||||||
}
|
}
|
||||||
|
|||||||
102
server/vault.api.js
Normal file
102
server/vault.api.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const asyncHandler = require('express-async-handler');
|
||||||
|
const HomebrewModel = require('./homebrew.model.js').model;
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const titleConditions = (title)=>{
|
||||||
|
if(!title) return {};
|
||||||
|
return {
|
||||||
|
$text : {
|
||||||
|
$search : title,
|
||||||
|
$caseSensitive : false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const authorConditions = (author)=>{
|
||||||
|
if(!author) return {};
|
||||||
|
return { authors: author };
|
||||||
|
};
|
||||||
|
|
||||||
|
const rendererConditions = (legacy, v3)=>{
|
||||||
|
if(legacy === 'true' && v3 !== 'true')
|
||||||
|
return { renderer: 'legacy' };
|
||||||
|
|
||||||
|
if(v3 === 'true' && legacy !== 'true')
|
||||||
|
return { renderer: 'V3' };
|
||||||
|
|
||||||
|
return {}; // If all renderers selected, renderer field not needed in query for speed
|
||||||
|
};
|
||||||
|
|
||||||
|
const findBrews = async (req, res)=>{
|
||||||
|
const title = req.query.title || '';
|
||||||
|
const author = req.query.author || '';
|
||||||
|
const page = Math.max(parseInt(req.query.page) || 1, 1);
|
||||||
|
const count = Math.max(parseInt(req.query.count) || 20, 10);
|
||||||
|
const skip = (page - 1) * count;
|
||||||
|
|
||||||
|
const combinedQuery = {
|
||||||
|
$and : [
|
||||||
|
{ published: true },
|
||||||
|
rendererConditions(req.query.legacy, req.query.v3),
|
||||||
|
titleConditions(title),
|
||||||
|
authorConditions(author)
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const projection = {
|
||||||
|
editId : 0,
|
||||||
|
googleId : 0,
|
||||||
|
text : 0,
|
||||||
|
textBin : 0,
|
||||||
|
version : 0
|
||||||
|
};
|
||||||
|
|
||||||
|
await HomebrewModel.find(combinedQuery, projection)
|
||||||
|
.skip(skip)
|
||||||
|
.limit(count)
|
||||||
|
.maxTimeMS(5000)
|
||||||
|
.exec()
|
||||||
|
.then((brews)=>{
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
|
||||||
|
const processedBrews = brews.map((brew)=>{
|
||||||
|
brew.authors = brew.authors.map((author)=>emailRegex.test(author) ? 'hidden' : author
|
||||||
|
);
|
||||||
|
return brew;
|
||||||
|
});
|
||||||
|
res.json({ brews: processedBrews, page });
|
||||||
|
})
|
||||||
|
.catch((error)=>{
|
||||||
|
throw { ...error, message: 'Error finding brews in Vault search', HBErrorCode: 90 };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const findTotal = async (req, res)=>{
|
||||||
|
const title = req.query.title || '';
|
||||||
|
const author = req.query.author || '';
|
||||||
|
|
||||||
|
const combinedQuery = {
|
||||||
|
$and : [
|
||||||
|
{ published: true },
|
||||||
|
rendererConditions(req.query.legacy, req.query.v3),
|
||||||
|
titleConditions(title),
|
||||||
|
authorConditions(author)
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await HomebrewModel.countDocuments(combinedQuery)
|
||||||
|
.then((totalBrews)=>{
|
||||||
|
console.log(`when returning, the total of brews is ${totalBrews} for the query ${JSON.stringify(combinedQuery)}`);
|
||||||
|
res.json({ totalBrews });
|
||||||
|
})
|
||||||
|
.catch((error)=>{
|
||||||
|
throw { ...error, message: 'Error finding brews in Vault search findTotal function', HBErrorCode: 91 };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
router.get('/api/vault/total', asyncHandler(findTotal));
|
||||||
|
router.get('/api/vault', asyncHandler(findBrews));
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const yaml = require('js-yaml');
|
const yaml = require('js-yaml');
|
||||||
|
const request = require('../client/homebrew/utils/request-middleware.js');
|
||||||
|
|
||||||
const splitTextStyleAndMetadata = (brew)=>{
|
const splitTextStyleAndMetadata = (brew)=>{
|
||||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||||
@@ -15,8 +16,43 @@ 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 = ()=>{
|
||||||
|
if(window.typeof !== 'undefined') {
|
||||||
|
window.frames['BrewRenderer'].contentWindow.print();
|
||||||
|
//Force DOM reflow; Print dialog causes a repaint, and @media print CSS somehow makes out-of-view pages disappear
|
||||||
|
const node = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0);
|
||||||
|
node.style.display='none';
|
||||||
|
node.offsetHeight; // accessing this is enough to trigger a reflow
|
||||||
|
node.style.display='';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
fetchThemeBundle,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const React = require('react');
|
|||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const DISMISS_KEY = 'dismiss_render_warning';
|
import Dialog from '../../../client/components/dialog.jsx';
|
||||||
|
|
||||||
const RenderWarnings = createClass({
|
const RenderWarnings = createClass({
|
||||||
displayName : 'RenderWarnings',
|
displayName : 'RenderWarnings',
|
||||||
@@ -34,9 +34,6 @@ const RenderWarnings = createClass({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
checkWarnings : function(){
|
checkWarnings : function(){
|
||||||
const hideDismiss = localStorage.getItem(DISMISS_KEY);
|
|
||||||
if(hideDismiss) return this.setState({ warnings: {} });
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
warnings : _.reduce(this.warnings, (r, fn, type)=>{
|
warnings : _.reduce(this.warnings, (r, fn, type)=>{
|
||||||
const element = fn();
|
const element = fn();
|
||||||
@@ -45,20 +42,18 @@ const RenderWarnings = createClass({
|
|||||||
}, {})
|
}, {})
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
dismiss : function(){
|
|
||||||
localStorage.setItem(DISMISS_KEY, true);
|
|
||||||
this.checkWarnings();
|
|
||||||
},
|
|
||||||
render : function(){
|
render : function(){
|
||||||
if(_.isEmpty(this.state.warnings)) return null;
|
if(_.isEmpty(this.state.warnings)) return null;
|
||||||
|
|
||||||
return <div className='renderWarnings'>
|
const DISMISS_KEY = 'dismiss_render_warning';
|
||||||
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
|
const DISMISS_TEXT = <i className='fas fa-times dismiss' />;
|
||||||
|
|
||||||
|
return <Dialog className='renderWarnings' dismissKey={DISMISS_KEY} closeText={DISMISS_TEXT}>
|
||||||
<i className='fas fa-exclamation-triangle ohno' />
|
<i className='fas fa-exclamation-triangle ohno' />
|
||||||
<h3>Render Warnings</h3>
|
<h3>Render Warnings</h3>
|
||||||
<small>If this homebrew is rendering badly if might be because of the following:</small>
|
<small>If this homebrew is rendering badly if might be because of the following:</small>
|
||||||
<ul>{_.values(this.state.warnings)}</ul>
|
<ul>{_.values(this.state.warnings)}</ul>
|
||||||
</div>;
|
</Dialog>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +1,48 @@
|
|||||||
.renderWarnings{
|
.renderWarnings {
|
||||||
position : relative;
|
position : relative;
|
||||||
float : right;
|
float : right;
|
||||||
display : inline-block;
|
|
||||||
width : 350px;
|
width : 350px;
|
||||||
padding : 20px;
|
padding : 20px;
|
||||||
padding-bottom : 10px;
|
padding-bottom : 10px;
|
||||||
padding-left : 85px;
|
padding-left : 85px;
|
||||||
margin-bottom : 10px;
|
margin-bottom : 10px;
|
||||||
background-color : @yellow;
|
|
||||||
color : white;
|
color : white;
|
||||||
a{
|
background-color : @yellow;
|
||||||
font-weight : 800;
|
border : none;
|
||||||
}
|
a { font-weight : 800; }
|
||||||
i.ohno{
|
i.ohno {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 24px;
|
top : 24px;
|
||||||
left : 24px;
|
left : 24px;
|
||||||
opacity : 0.8;
|
|
||||||
font-size : 2.5em;
|
font-size : 2.5em;
|
||||||
|
opacity : 0.8;
|
||||||
}
|
}
|
||||||
i.dismiss{
|
button.dismiss {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 10px;
|
top : 10px;
|
||||||
right : 10px;
|
right : 10px;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
opacity : 0.6;
|
background-color : transparent;
|
||||||
&:hover{
|
opacity : 0.6;
|
||||||
opacity : 1;
|
&:hover { opacity : 1; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
small{
|
small {
|
||||||
opacity : 0.7;
|
|
||||||
font-size : 0.6em;
|
font-size : 0.6em;
|
||||||
|
opacity : 0.7;
|
||||||
}
|
}
|
||||||
h3{
|
h3 {
|
||||||
font-size : 1.1em;
|
font-size : 1.1em;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
}
|
}
|
||||||
ul{
|
ul {
|
||||||
margin-top : 15px;
|
margin-top : 15px;
|
||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
list-style-position : outside;
|
list-style-position : outside;
|
||||||
list-style-type : disc;
|
list-style-type : disc;
|
||||||
li{
|
li {
|
||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
line-height : 1.6em;
|
line-height : 1.6em;
|
||||||
em{
|
em { font-weight : 800; }
|
||||||
font-weight : 800;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,8 +39,10 @@ if(typeof window !== 'undefined'){
|
|||||||
//Autocompletion
|
//Autocompletion
|
||||||
require('codemirror/addon/hint/show-hint.js');
|
require('codemirror/addon/hint/show-hint.js');
|
||||||
|
|
||||||
const foldCode = require('./fold-code');
|
const foldPagesCode = require('./fold-pages');
|
||||||
foldCode.registerHomebreweryHelper(CodeMirror);
|
foldPagesCode.registerHomebreweryHelper(CodeMirror);
|
||||||
|
const foldCSSCode = require('./fold-css');
|
||||||
|
foldCSSCode.registerHomebreweryHelper(CodeMirror);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CodeEditor = createClass({
|
const CodeEditor = createClass({
|
||||||
@@ -395,6 +397,11 @@ const CodeEditor = createClass({
|
|||||||
getCursorPosition : function(){
|
getCursorPosition : function(){
|
||||||
return this.codeMirror.getCursor();
|
return this.codeMirror.getCursor();
|
||||||
},
|
},
|
||||||
|
getTopVisibleLine : function(){
|
||||||
|
const rect = this.codeMirror.getWrapperElement().getBoundingClientRect();
|
||||||
|
const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, 'window');
|
||||||
|
return topVisibleLine;
|
||||||
|
},
|
||||||
updateSize : function(){
|
updateSize : function(){
|
||||||
this.codeMirror.refresh();
|
this.codeMirror.refresh();
|
||||||
},
|
},
|
||||||
@@ -411,11 +418,11 @@ const CodeEditor = createClass({
|
|||||||
foldOptions : function(cm){
|
foldOptions : function(cm){
|
||||||
return {
|
return {
|
||||||
scanUp : true,
|
scanUp : true,
|
||||||
rangeFinder : CodeMirror.fold.homebrewery,
|
rangeFinder : this.props.language === 'css' ? CodeMirror.fold.homebrewerycss : CodeMirror.fold.homebrewery,
|
||||||
widget : (from, to)=>{
|
widget : (from, to)=>{
|
||||||
let text = '';
|
let text = '';
|
||||||
let currentLine = from.line;
|
let currentLine = from.line;
|
||||||
const maxLength = 50;
|
let maxLength = 50;
|
||||||
|
|
||||||
let foldPreviewText = '';
|
let foldPreviewText = '';
|
||||||
while (currentLine <= to.line && text.length <= maxLength) {
|
while (currentLine <= to.line && text.length <= maxLength) {
|
||||||
@@ -430,10 +437,15 @@ const CodeEditor = createClass({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
text = foldPreviewText || `Lines ${from.line+1}-${to.line+1}`;
|
text = foldPreviewText || `Lines ${from.line+1}-${to.line+1}`;
|
||||||
|
text = text.replace('{', '').trim();
|
||||||
|
|
||||||
|
// Truncate data URLs at `data:`
|
||||||
|
const startOfData = text.indexOf('data:');
|
||||||
|
if(startOfData > 0)
|
||||||
|
maxLength = Math.min(startOfData + 5, maxLength);
|
||||||
|
|
||||||
text = text.trim();
|
|
||||||
if(text.length > maxLength)
|
if(text.length > maxLength)
|
||||||
text = `${text.substr(0, maxLength)}...`;
|
text = `${text.slice(0, maxLength)}...`;
|
||||||
|
|
||||||
return `\u21A4 ${text} \u21A6`;
|
return `\u21A4 ${text} \u21A6`;
|
||||||
}
|
}
|
||||||
@@ -450,3 +462,4 @@ const CodeEditor = createClass({
|
|||||||
});
|
});
|
||||||
|
|
||||||
module.exports = CodeEditor;
|
module.exports = CodeEditor;
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
@import (less) './themes/fonts/iconFonts/diceFont.less';
|
@import (less) './themes/fonts/iconFonts/diceFont.less';
|
||||||
@import (less) './themes/fonts/iconFonts/elderberryInn.less';
|
@import (less) './themes/fonts/iconFonts/elderberryInn.less';
|
||||||
@import (less) './themes/fonts/iconFonts/gameIcons.less';
|
@import (less) './themes/fonts/iconFonts/gameIcons.less';
|
||||||
|
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
||||||
|
|
||||||
@keyframes sourceMoveAnimation {
|
@keyframes sourceMoveAnimation {
|
||||||
50% {background-color: red; color: white;}
|
50% {background-color: red; color: white;}
|
||||||
|
|||||||
44
shared/naturalcrit/codeEditor/fold-css.js
Normal file
44
shared/naturalcrit/codeEditor/fold-css.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
registerHomebreweryHelper : function(CodeMirror) {
|
||||||
|
CodeMirror.registerHelper('fold', 'homebrewerycss', function(cm, start) {
|
||||||
|
|
||||||
|
// BRACE FOLDING
|
||||||
|
const startMatcher = /\{[ \t]*$/;
|
||||||
|
const endMatcher = /\}[ \t]*$/;
|
||||||
|
const activeLine = cm.getLine(start.line);
|
||||||
|
|
||||||
|
|
||||||
|
if(activeLine.match(startMatcher)) {
|
||||||
|
const lastLineNo = cm.lastLine();
|
||||||
|
let end = start.line + 1;
|
||||||
|
let braceCount = 1;
|
||||||
|
|
||||||
|
while (end < lastLineNo) {
|
||||||
|
const curLine = cm.getLine(end);
|
||||||
|
if(curLine.match(startMatcher)) braceCount++;
|
||||||
|
if(curLine.match(endMatcher)) braceCount--;
|
||||||
|
if(braceCount == 0) break;
|
||||||
|
++end;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
from : CodeMirror.Pos(start.line, 0),
|
||||||
|
to : CodeMirror.Pos(end, cm.getLine(end).length)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// @import and data-url folding
|
||||||
|
const importMatcher = /^@import.*?;/;
|
||||||
|
const dataURLMatcher = /url\(.*?data\:.*\)/;
|
||||||
|
|
||||||
|
if(activeLine.match(importMatcher) || activeLine.match(dataURLMatcher)) {
|
||||||
|
return {
|
||||||
|
from : CodeMirror.Pos(start.line, 0),
|
||||||
|
to : CodeMirror.Pos(start.line, activeLine.length)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ const _ = require('lodash');
|
|||||||
const Marked = require('marked');
|
const Marked = require('marked');
|
||||||
const MarkedExtendedTables = require('marked-extended-tables');
|
const MarkedExtendedTables = require('marked-extended-tables');
|
||||||
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
|
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
|
||||||
const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id');
|
const { gfmHeadingId: MarkedGFMHeadingId, resetHeadings: MarkedGFMResetHeadingIDs } = require('marked-gfm-heading-id');
|
||||||
const { markedEmoji: MarkedEmojis } = require('marked-emoji');
|
const { markedEmoji: MarkedEmojis } = require('marked-emoji');
|
||||||
|
|
||||||
//Icon fonts included so they can appear in emoji autosuggest dropdown
|
//Icon fonts included so they can appear in emoji autosuggest dropdown
|
||||||
@@ -28,17 +28,18 @@ const mathParser = new MathParser({
|
|||||||
round : true,
|
round : true,
|
||||||
floor : true,
|
floor : true,
|
||||||
ceil : true,
|
ceil : true,
|
||||||
|
abs : true,
|
||||||
|
|
||||||
sin : false, cos : false, tan : false, asin : false, acos : false,
|
sin : false, cos : false, tan : false, asin : false, acos : false,
|
||||||
atan : false, sinh : false, cosh : false, tanh : false, asinh : false,
|
atan : false, sinh : false, cosh : false, tanh : false, asinh : false,
|
||||||
acosh : false, atanh : false, sqrt : false, cbrt : false, log : false,
|
acosh : false, atanh : false, sqrt : false, cbrt : false, log : false,
|
||||||
log2 : false, ln : false, lg : false, log10 : false, expm1 : false,
|
log2 : false, ln : false, lg : false, log10 : false, expm1 : false,
|
||||||
log1p : false, abs : false, trunc : false, join : false, sum : false,
|
log1p : false, trunc : false, join : false, sum : false, indexOf : false,
|
||||||
'-' : false, '+' : false, exp : false, not : false, length : false,
|
'-' : false, '+' : false, exp : false, not : false, length : false,
|
||||||
'!' : false, sign : false, random : false, fac : false, min : false,
|
'!' : false, sign : false, random : false, fac : false, min : false,
|
||||||
max : false, hypot : false, pyt : false, pow : false, atan2 : false,
|
max : false, hypot : false, pyt : false, pow : false, atan2 : false,
|
||||||
'if' : false, gamma : false, roundTo : false, map : false, fold : false,
|
'if' : false, gamma : false, roundTo : false, map : false, fold : false,
|
||||||
filter : false, indexOf : false,
|
filter : false,
|
||||||
|
|
||||||
remainder : false, factorial : false,
|
remainder : false, factorial : false,
|
||||||
comparison : false, concatenate : false,
|
comparison : false, concatenate : false,
|
||||||
@@ -46,6 +47,16 @@ const mathParser = new MathParser({
|
|||||||
array : false, fndef : false
|
array : false, fndef : false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Add sign function
|
||||||
|
mathParser.functions.sign = function (a) {
|
||||||
|
if(a >= 0) return '+';
|
||||||
|
return '-';
|
||||||
|
};
|
||||||
|
// Add signed function
|
||||||
|
mathParser.functions.signed = function (a) {
|
||||||
|
if(a >= 0) return `+${a}`;
|
||||||
|
return `${a}`;
|
||||||
|
};
|
||||||
|
|
||||||
//Processes the markdown within an HTML block if it's just a class-wrapper
|
//Processes the markdown within an HTML block if it's just a class-wrapper
|
||||||
renderer.html = function (html) {
|
renderer.html = function (html) {
|
||||||
@@ -75,7 +86,7 @@ renderer.link = function (href, title, text) {
|
|||||||
if(href[0] == '#') {
|
if(href[0] == '#') {
|
||||||
self = true;
|
self = true;
|
||||||
}
|
}
|
||||||
href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
|
href = cleanUrl(href);
|
||||||
|
|
||||||
if(href === null) {
|
if(href === null) {
|
||||||
return text;
|
return text;
|
||||||
@@ -91,6 +102,20 @@ renderer.link = function (href, title, text) {
|
|||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Expose `src` attribute as `--HB_src` to make the URL accessible via CSS
|
||||||
|
renderer.image = function (href, title, text) {
|
||||||
|
href = cleanUrl(href);
|
||||||
|
if(href === null)
|
||||||
|
return text;
|
||||||
|
|
||||||
|
let out = `<img src="${href}" alt="${text}" style="--HB_src:url(${href});"`;
|
||||||
|
if(title)
|
||||||
|
out += ` title="${title}"`;
|
||||||
|
|
||||||
|
out += '>';
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
// Disable default reflink behavior, as it steps on our variables extension
|
// Disable default reflink behavior, as it steps on our variables extension
|
||||||
tokenizer.def = function () {
|
tokenizer.def = function () {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -102,7 +127,7 @@ const mustacheSpans = {
|
|||||||
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
|
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
|
||||||
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
|
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
|
||||||
const match = completeSpan.exec(src);
|
const match = completeSpan.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
//Find closing delimiter
|
//Find closing delimiter
|
||||||
@@ -159,7 +184,7 @@ const mustacheDivs = {
|
|||||||
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
|
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
|
||||||
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
|
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
|
||||||
const match = completeBlock.exec(src);
|
const match = completeBlock.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
//Find closing delimiter
|
//Find closing delimiter
|
||||||
@@ -214,7 +239,7 @@ const mustacheInjectInline = {
|
|||||||
level : 'inline',
|
level : 'inline',
|
||||||
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g;
|
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g;
|
||||||
const match = inlineRegex.exec(src);
|
const match = inlineRegex.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
const lastToken = tokens[tokens.length - 1];
|
const lastToken = tokens[tokens.length - 1];
|
||||||
@@ -265,7 +290,7 @@ const mustacheInjectBlock = {
|
|||||||
level : 'block',
|
level : 'block',
|
||||||
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
|
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
|
||||||
const match = inlineRegex.exec(src);
|
const match = inlineRegex.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
const lastToken = tokens[tokens.length - 1];
|
const lastToken = tokens[tokens.length - 1];
|
||||||
@@ -345,6 +370,27 @@ const superSubScripts = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const forcedParagraphBreaks = {
|
||||||
|
name : 'hardBreaks',
|
||||||
|
level : 'block',
|
||||||
|
start(src) { return src.match(/\n:+$/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
const regex = /^(:+)(?:\n|$)/ym;
|
||||||
|
const match = regex.exec(src);
|
||||||
|
if(match?.length) {
|
||||||
|
return {
|
||||||
|
type : 'hardBreaks', // Should match "name" above
|
||||||
|
raw : match[0], // Text to consume from the source
|
||||||
|
length : match[1].length,
|
||||||
|
text : ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
return `<div class='blank'></div>`.repeat(token.length).concat('\n');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const definitionListsSingleLine = {
|
const definitionListsSingleLine = {
|
||||||
name : 'definitionListsSingleLine',
|
name : 'definitionListsSingleLine',
|
||||||
level : 'block',
|
level : 'block',
|
||||||
@@ -389,9 +435,9 @@ const definitionListsSingleLine = {
|
|||||||
const definitionListsMultiLine = {
|
const definitionListsMultiLine = {
|
||||||
name : 'definitionListsMultiLine',
|
name : 'definitionListsMultiLine',
|
||||||
level : 'block',
|
level : 'block',
|
||||||
start(src) { return src.match(/\n[^\n]*\n::/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/\n[^\n]*\n::[^:\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::))|\n::(.(?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y;
|
const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::[^:\n]))|\n::([^:\n](?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y;
|
||||||
let match;
|
let match;
|
||||||
let endIndex = 0;
|
let endIndex = 0;
|
||||||
const definitions = [];
|
const definitions = [];
|
||||||
@@ -441,7 +487,7 @@ const replaceVar = function(input, hoist=false, allowUnresolved=false) {
|
|||||||
const label = match[2];
|
const label = match[2];
|
||||||
|
|
||||||
//v=====--------------------< HANDLE MATH >-------------------=====v//
|
//v=====--------------------< HANDLE MATH >-------------------=====v//
|
||||||
const mathRegex = /[a-z]+\(|[+\-*/^()]/g;
|
const mathRegex = /[a-z]+\(|[+\-*/^(),]/g;
|
||||||
const matches = label.split(mathRegex);
|
const matches = label.split(mathRegex);
|
||||||
const mathVars = matches.filter((match)=>isNaN(match))?.map((s)=>s.trim()); // Capture any variable names
|
const mathVars = matches.filter((match)=>isNaN(match))?.map((s)=>s.trim()); // Capture any variable names
|
||||||
|
|
||||||
@@ -451,7 +497,7 @@ const replaceVar = function(input, hoist=false, allowUnresolved=false) {
|
|||||||
mathVars?.forEach((variable)=>{
|
mathVars?.forEach((variable)=>{
|
||||||
const foundVar = lookupVar(variable, globalPageNumber, hoist);
|
const foundVar = lookupVar(variable, globalPageNumber, hoist);
|
||||||
if(foundVar && foundVar.resolved && foundVar.content && !isNaN(foundVar.content)) // Only subsitute math values if fully resolved, not empty strings, and numbers
|
if(foundVar && foundVar.resolved && foundVar.content && !isNaN(foundVar.content)) // Only subsitute math values if fully resolved, not empty strings, and numbers
|
||||||
replacedLabel = replacedLabel.replaceAll(variable, foundVar.content);
|
replacedLabel = replacedLabel.replaceAll(new RegExp(`(?<!\\w)(${variable})(?!\\w)`, 'g'), foundVar.content);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -695,34 +741,27 @@ const MarkedEmojiOptions = {
|
|||||||
renderer : (token)=>`<i class="${token.emoji}"></i>`
|
renderer : (token)=>`<i class="${token.emoji}"></i>`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tableTerminators = [
|
||||||
|
`:+\\n`, // hardBreak
|
||||||
|
` *{[^\n]+}`, // blockInjector
|
||||||
|
` *{{[^{\n]*\n.*?\n}}` // mustacheDiv
|
||||||
|
];
|
||||||
|
|
||||||
Marked.use(MarkedVariables());
|
Marked.use(MarkedVariables());
|
||||||
Marked.use({ extensions: [definitionListsMultiLine, definitionListsSingleLine, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts,
|
||||||
|
mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
||||||
Marked.use(mustacheInjectBlock);
|
Marked.use(mustacheInjectBlock);
|
||||||
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
|
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
|
||||||
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
|
Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
|
||||||
|
|
||||||
const nonWordAndColonTest = /[^\w:]/g;
|
function cleanUrl(href) {
|
||||||
const cleanUrl = function (sanitize, base, href) {
|
|
||||||
if(sanitize) {
|
|
||||||
let prot;
|
|
||||||
try {
|
|
||||||
prot = decodeURIComponent(unescape(href))
|
|
||||||
.replace(nonWordAndColonTest, '')
|
|
||||||
.toLowerCase();
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
href = encodeURI(href).replace(/%25/g, '%');
|
href = encodeURI(href).replace(/%25/g, '%');
|
||||||
} catch (e) {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return href;
|
return href;
|
||||||
};
|
}
|
||||||
|
|
||||||
const escapeTest = /[&<>"']/;
|
const escapeTest = /[&<>"']/;
|
||||||
const escapeReplace = /[&<>"']/g;
|
const escapeReplace = /[&<>"']/g;
|
||||||
@@ -771,7 +810,8 @@ const processStyleTags = (string)=>{
|
|||||||
const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"'))
|
const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"'))
|
||||||
?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
|
?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
|
||||||
.reduce((obj, attr)=>{
|
.reduce((obj, attr)=>{
|
||||||
let [key, value] = attr.split('=');
|
const index = attr.indexOf('=');
|
||||||
|
let [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
|
||||||
value = value.replace(/"/g, '');
|
value = value.replace(/"/g, '');
|
||||||
obj[key] = value;
|
obj[key] = value;
|
||||||
return obj;
|
return obj;
|
||||||
@@ -786,14 +826,17 @@ const processStyleTags = (string)=>{
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//Given a string representing an HTML element, extract all of its properties (id, class, style, and other attributes)
|
||||||
const extractHTMLStyleTags = (htmlString)=>{
|
const extractHTMLStyleTags = (htmlString)=>{
|
||||||
const id = htmlString.match(/id="([^"]*)"/)?.[1] || null;
|
const firstElementOnly = htmlString.split('>')[0];
|
||||||
const classes = htmlString.match(/class="([^"]*)"/)?.[1] || null;
|
const id = firstElementOnly.match(/id="([^"]*)"/)?.[1] || null;
|
||||||
const styles = htmlString.match(/style="([^"]*)"/)?.[1] || null;
|
const classes = firstElementOnly.match(/class="([^"]*)"/)?.[1] || null;
|
||||||
const attributes = htmlString.match(/[a-zA-Z]+="[^"]*"/g)
|
const styles = firstElementOnly.match(/style="([^"]*)"/)?.[1] || null;
|
||||||
|
const attributes = firstElementOnly.match(/[a-zA-Z]+="[^"]*"/g)
|
||||||
?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
|
?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
|
||||||
.reduce((obj, attr)=>{
|
.reduce((obj, attr)=>{
|
||||||
let [key, value] = attr.split('=');
|
const index = attr.indexOf('=');
|
||||||
|
let [key, value] = [attr.substring(0, index), attr.substring(index + 1)];
|
||||||
value = value.replace(/"/g, '');
|
value = value.replace(/"/g, '');
|
||||||
obj[key] = value;
|
obj[key] = value;
|
||||||
return obj;
|
return obj;
|
||||||
@@ -813,13 +856,15 @@ let globalPageNumber = 0;
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
marked : Marked,
|
marked : Marked,
|
||||||
render : (rawBrewText, pageNumber=1)=>{
|
render : (rawBrewText, pageNumber=0)=>{
|
||||||
globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order
|
globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order
|
||||||
varsQueue = []; //Could move into MarkedVariables()
|
varsQueue = []; //Could move into MarkedVariables()
|
||||||
globalPageNumber = pageNumber;
|
globalPageNumber = pageNumber;
|
||||||
|
if(pageNumber==0) {
|
||||||
|
MarkedGFMResetHeadingIDs();
|
||||||
|
}
|
||||||
|
|
||||||
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
|
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`);
|
||||||
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
|
|
||||||
const opts = Marked.defaults;
|
const opts = Marked.defaults;
|
||||||
|
|
||||||
rawBrewText = opts.hooks.preprocess(rawBrewText);
|
rawBrewText = opts.hooks.preprocess(rawBrewText);
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ const Nav = {
|
|||||||
color : null
|
color : null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleClick : function(){
|
handleClick : function(e){
|
||||||
this.props.onClick();
|
this.props.onClick(e);
|
||||||
},
|
},
|
||||||
render : function(){
|
render : function(){
|
||||||
const classes = cx('navItem', this.props.color, this.props.className);
|
const classes = cx('navItem', this.props.color, this.props.className);
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ const SplitPane = createClass({
|
|||||||
displayName : 'SplitPane',
|
displayName : 'SplitPane',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
storageKey : 'naturalcrit-pane-split',
|
storageKey : 'naturalcrit-pane-split',
|
||||||
onDragFinish : function(){} //fires when dragging
|
onDragFinish : function(){}, //fires when dragging
|
||||||
|
showDividerButtons : true
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -41,6 +42,10 @@ const SplitPane = createClass({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', this.handleWindowResize);
|
window.addEventListener('resize', this.handleWindowResize);
|
||||||
|
|
||||||
|
// This lives here instead of in the initial render because you cannot touch localStorage until the componant mounts.
|
||||||
|
const loadLiveScroll = window.localStorage.getItem('liveScroll') === 'true';
|
||||||
|
this.setState({ liveScroll: loadLiveScroll });
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
@@ -88,6 +93,11 @@ const SplitPane = createClass({
|
|||||||
userSetDividerPos : newSize
|
userSetDividerPos : newSize
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
liveScrollToggle : function() {
|
||||||
|
window.localStorage.setItem('liveScroll', String(!this.state.liveScroll));
|
||||||
|
this.setState({ liveScroll: !this.state.liveScroll });
|
||||||
|
},
|
||||||
/*
|
/*
|
||||||
unFocus : function() {
|
unFocus : function() {
|
||||||
if(document.selection){
|
if(document.selection){
|
||||||
@@ -119,13 +129,18 @@ const SplitPane = createClass({
|
|||||||
onClick={()=>this.setState({ moveBrew: !this.state.moveBrew })} >
|
onClick={()=>this.setState({ moveBrew: !this.state.moveBrew })} >
|
||||||
<i className='fas fa-arrow-right' />
|
<i className='fas fa-arrow-right' />
|
||||||
</div>
|
</div>
|
||||||
|
<div id='scrollToggleDiv' className={this.state.liveScroll ? 'arrow lock' : 'arrow unlock'}
|
||||||
|
style={{ left: this.state.currentDividerPos-4 }}
|
||||||
|
onClick={this.liveScrollToggle} >
|
||||||
|
<i id='scrollToggle' className={this.state.liveScroll ? 'fas fa-lock' : 'fas fa-unlock'} />
|
||||||
|
</div>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
renderDivider : function(){
|
renderDivider : function(){
|
||||||
return <>
|
return <>
|
||||||
{this.renderMoveArrows()}
|
{this.props.showDividerButtons && this.renderMoveArrows()}
|
||||||
<div className='divider' onPointerDown={this.handleDown} >
|
<div className='divider' onPointerDown={this.handleDown} >
|
||||||
<div className='dots'>
|
<div className='dots'>
|
||||||
<i className='fas fa-circle' />
|
<i className='fas fa-circle' />
|
||||||
@@ -142,9 +157,12 @@ const SplitPane = createClass({
|
|||||||
width={this.state.currentDividerPos}
|
width={this.state.currentDividerPos}
|
||||||
>
|
>
|
||||||
{React.cloneElement(this.props.children[0], {
|
{React.cloneElement(this.props.children[0], {
|
||||||
moveBrew : this.state.moveBrew,
|
...(this.props.showDividerButtons && {
|
||||||
moveSource : this.state.moveSource,
|
moveBrew : this.state.moveBrew,
|
||||||
setMoveArrows : this.setMoveArrows
|
moveSource : this.state.moveSource,
|
||||||
|
liveScroll : this.state.liveScroll,
|
||||||
|
setMoveArrows : this.setMoveArrows,
|
||||||
|
}),
|
||||||
})}
|
})}
|
||||||
</Pane>
|
</Pane>
|
||||||
{this.renderDivider()}
|
{this.renderDivider()}
|
||||||
|
|||||||
@@ -53,6 +53,15 @@
|
|||||||
.tooltipRight('Jump to location in Preview');
|
.tooltipRight('Jump to location in Preview');
|
||||||
top : 60px;
|
top : 60px;
|
||||||
}
|
}
|
||||||
|
&.lock{
|
||||||
|
.tooltipRight('De-sync Editor and Preview locations.');
|
||||||
|
top : 90px;
|
||||||
|
background: #666;
|
||||||
|
}
|
||||||
|
&.unlock{
|
||||||
|
.tooltipRight('Sync Editor and Preview locations');
|
||||||
|
top : 90px;
|
||||||
|
}
|
||||||
&:hover{
|
&:hover{
|
||||||
background-color: #666;
|
background-color: #666;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const stylelint = require('stylelint');
|
const stylelint = require('stylelint');
|
||||||
const { isNumber } = require('stylelint/lib/utils/validateTypes');
|
const { isNumber } = require('stylelint/lib/utils/validateTypes.cjs');
|
||||||
|
|
||||||
const { report, ruleMessages, validateOptions } = stylelint.utils;
|
const { report, ruleMessages, validateOptions } = stylelint.utils;
|
||||||
const ruleName = 'naturalcrit/declaration-block-multi-line-min-declarations';
|
const ruleName = 'naturalcrit/declaration-block-multi-line-min-declarations';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const stylelint = require('stylelint');
|
const stylelint = require('stylelint');
|
||||||
const { isNumber } = require('stylelint/lib/utils/validateTypes');
|
const { isNumber } = require('stylelint/lib/utils/validateTypes.cjs');
|
||||||
|
|
||||||
const { report, ruleMessages, validateOptions } = stylelint.utils;
|
const { report, ruleMessages, validateOptions } = stylelint.utils;
|
||||||
const ruleName = 'naturalcrit/declaration-colon-min-space-before';
|
const ruleName = 'naturalcrit/declaration-colon-min-space-before';
|
||||||
|
|||||||
@@ -88,4 +88,16 @@ describe('Multiline Definition Lists', ()=>{
|
|||||||
const rendered = Markdown.render(source).trim();
|
const rendered = Markdown.render(source).trim();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Inline definition 1</dd>\n<dt></dt><dd>Inline definition 2 (no DT)</dd>\n</dl>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Inline definition 1</dd>\n<dt></dt><dd>Inline definition 2 (no DT)</dd>\n</dl>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Multiline Definition Term must have at least one non-empty Definition', function() {
|
||||||
|
const source = 'Term 1\n::';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<div class='blank'></div><div class='blank'></div>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiline Definition List must have at least one non-newline character after ::', function() {
|
||||||
|
const source = 'Term 1\n::\nDefinition 1\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Term 1</p>\n<div class='blank'></div><div class='blank'></div>\n<p>Definition 1</p>`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
47
tests/markdown/hard-breaks.test.js
Normal file
47
tests/markdown/hard-breaks.test.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
|
describe('Hard Breaks', ()=>{
|
||||||
|
test('Single Break', function() {
|
||||||
|
const source = ':\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Double Break', function() {
|
||||||
|
const source = '::\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Triple Break', function() {
|
||||||
|
const source = ':::\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div><div class='blank'></div>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Many Break', function() {
|
||||||
|
const source = '::::::::::\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div><div class='blank'></div>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple sets of Breaks', function() {
|
||||||
|
const source = ':::\n:::\n:::';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class='blank'></div><div class='blank'></div><div class='blank'></div>\n<div class='blank'></div><div class='blank'></div><div class='blank'></div>\n<div class='blank'></div><div class='blank'></div><div class='blank'></div>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Break directly between two paragraphs', function() {
|
||||||
|
const source = 'Line 1\n::\nLine 2';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>Line 1</p>\n<div class='blank'></div><div class='blank'></div>\n<p>Line 2</p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ignored inside a code block', function() {
|
||||||
|
const source = '```\n\n:\n\n```\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<pre><code>\n:\n</code></pre>`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -322,9 +322,9 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Renders an image element with injected style', function() {
|
it('Renders an image element with injected style', function() {
|
||||||
const source = '{position:absolute}';
|
const source = '{position:absolute}';
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img style="position:absolute;" src="http://i.imgur.com/hMna6G0.png" alt="alt text"></p>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute;" src="https://i.imgur.com/hMna6G0.png" alt="alt text"></p>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders an element modified by only the first of two consecutive injections', function() {
|
it('Renders an element modified by only the first of two consecutive injections', function() {
|
||||||
@@ -333,10 +333,29 @@ describe('Injection: When an injection tag follows an element', ()=>{
|
|||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><span class="inline-block" style="color:red;">text</span>{background:blue}</p>');
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><span class="inline-block" style="color:red;">text</span>{background:blue}</p>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Renders an parent and child element, each modified by an injector', function() {
|
||||||
|
const source = dedent`**bolded text**{color:red}
|
||||||
|
{color:blue}`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p style="color:blue;"><strong style="color:red;">bolded text</strong></p>');
|
||||||
|
});
|
||||||
|
|
||||||
it('Renders an image with added attributes', function() {
|
it('Renders an image with added attributes', function() {
|
||||||
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
|
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e"></p>`);
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e"></p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders an image with "=" in the url, and added attributes', function() {
|
||||||
|
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png?auth=12345&height=1024); position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png?auth=12345&height=1024" alt="homebrew mug" a="b and c" d="e"></p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders an image and added attributes with "=" in the value, ', function() {
|
||||||
|
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e,otherUrl="url?auth=12345"}`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img style="--HB_src:url(https://i.imgur.com/hMna6G0.png); position:absolute; bottom:20px; left:130px; width:220px;" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug" a="b and c" d="e" otherUrl="url?auth=12345"></p>`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -315,21 +315,21 @@ describe('Normal Links and Images', ()=>{
|
|||||||
const source = ``;
|
const source = ``;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
<p><img src="url" alt="alt text"></p>`.trimReturns());
|
<p><img src="url" alt="alt text" style="--HB_src:url(url);"></p>`.trimReturns());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders normal images with a title', function() {
|
it('Renders normal images with a title', function() {
|
||||||
const source = 'An image !';
|
const source = 'An image !';
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
<p>An image <img src="url" alt="alt text" title="and title">!</p>`.trimReturns());
|
<p>An image <img src="url" alt="alt text" style="--HB_src:url(url);" title="and title">!</p>`.trimReturns());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Applies curly injectors to images', function() {
|
it('Applies curly injectors to images', function() {
|
||||||
const source = `{width:100px}`;
|
const source = `{width:100px}`;
|
||||||
const rendered = Markdown.render(source).trimReturns();
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
|
||||||
<p><img style="width:100px;" src="url" alt="alt text"></p>`.trimReturns());
|
<p><img style="--HB_src:url(url); width:100px;" src="url" alt="alt text"></p>`.trimReturns());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders normal links', function() {
|
it('Renders normal links', function() {
|
||||||
@@ -370,4 +370,36 @@ describe('Cross-page variables', ()=>{
|
|||||||
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
|
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
|
||||||
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>two</p><p>one</p>\\page<p>two</p>');
|
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>two</p><p>one</p>\\page<p>two</p>');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Math function parameter handling', ()=>{
|
||||||
|
it('allows variables in single-parameter functions', function() {
|
||||||
|
const source = '[var]:4.1\n\n$[floor(var)]';
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>4</p>`);
|
||||||
|
});
|
||||||
|
it('allows one variable and a number in two-parameter functions', function() {
|
||||||
|
const source = '[var]:4\n\n$[min(1,var)]';
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>1</p>`);
|
||||||
|
});
|
||||||
|
it('allows two variables in two-parameter functions', function() {
|
||||||
|
const source = '[var1]:4\n\n[var2]:8\n\n$[min(var1,var2)]';
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>4</p>`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Variable names that are subsets of other names', ()=>{
|
||||||
|
it('do not conflict with function names', function() {
|
||||||
|
const source = `[a]: -1\n\n$[abs(a)]`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered).toBe('<p>1</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('do not conflict with other variable names', function() {
|
||||||
|
const source = `[ab]: 2\n\n[aba]: 8\n\n[ba]: 4\n\n$[ab + aba + ba]`;
|
||||||
|
const rendered = Markdown.render(source).trimReturns();
|
||||||
|
expect(rendered).toBe('<p>14</p>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name" : "5e PHB",
|
"name" : "5e PHB",
|
||||||
"renderer" : "V3",
|
"renderer" : "V3",
|
||||||
"baseTheme" : false,
|
"baseTheme" : "Blank",
|
||||||
"baseSnippets" : false
|
"baseSnippets" : false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,162 @@ module.exports = [
|
|||||||
view : 'text',
|
view : 'text',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Table of Contents',
|
name : 'Table of Contents',
|
||||||
icon : 'fas fa-book',
|
icon : 'fas fa-book',
|
||||||
gen : TableOfContentsGen
|
gen : TableOfContentsGen,
|
||||||
|
experimental : true,
|
||||||
|
subsnippets : [
|
||||||
|
{
|
||||||
|
name : 'Generate Table of Contents',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : TableOfContentsGen,
|
||||||
|
experimental : true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Table of Contents Individual Inclusion',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : dedent `\n{{tocInclude# CHANGE # to your header level
|
||||||
|
}}\n`,
|
||||||
|
subsnippets : [
|
||||||
|
{
|
||||||
|
name : 'Individual Inclusion H1',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : dedent `\n{{tocIncludeH1 \n
|
||||||
|
}}\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Individual Inclusion H2',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : dedent `\n{{tocIncludeH2 \n
|
||||||
|
}}\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Individual Inclusion H3',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : dedent `\n{{tocIncludeH3 \n
|
||||||
|
}}\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Individual Inclusion H4',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : dedent `\n{{tocIncludeH4 \n
|
||||||
|
}}\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Individual Inclusion H5',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : dedent `\n{{tocIncludeH5 \n
|
||||||
|
}}\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Individual Inclusion H6',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : dedent `\n{{tocIncludeH6 \n
|
||||||
|
}}\n`,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Table of Contents Range Inclusion',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : dedent `\n{{tocDepthH3
|
||||||
|
}}\n`,
|
||||||
|
subsnippets : [
|
||||||
|
{
|
||||||
|
name : 'Include in ToC up to H3',
|
||||||
|
icon : 'fas fa-dice-three',
|
||||||
|
gen : dedent `\n{{tocDepthH3
|
||||||
|
}}\n`,
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Include in ToC up to H4',
|
||||||
|
icon : 'fas fa-dice-four',
|
||||||
|
gen : dedent `\n{{tocDepthH4
|
||||||
|
}}\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Include in ToC up to H5',
|
||||||
|
icon : 'fas fa-dice-five',
|
||||||
|
gen : dedent `\n{{tocDepthH5
|
||||||
|
}}\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Include in ToC up to H6',
|
||||||
|
icon : 'fas fa-dice-six',
|
||||||
|
gen : dedent `\n{{tocDepthH6
|
||||||
|
}}\n`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Table of Contents Individual Exclusion',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : dedent `\n{{tocExcludeH1 \n
|
||||||
|
}}\n`,
|
||||||
|
subsnippets : [
|
||||||
|
{
|
||||||
|
name : 'Individual Exclusion H1',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : dedent `\n{{tocExcludeH1 \n
|
||||||
|
}}\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Individual Exclusion H2',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : dedent `\n{{tocExcludeH2 \n
|
||||||
|
}}\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Individual Exclusion H3',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : dedent `\n{{tocExcludeH3 \n
|
||||||
|
}}\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Individual Exclusion H4',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : dedent `\n{{tocExcludeH4 \n
|
||||||
|
}}\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Individual Exclusion H5',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : dedent `\n{{tocExcludeH5 \n
|
||||||
|
}}\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Individual Exclusion H6',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : dedent `\n{{tocExcludeH6 \n
|
||||||
|
}}\n`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name : 'Table of Contents Toggles',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : `{{tocGlobalH4}}\n\n`,
|
||||||
|
subsnippets : [
|
||||||
|
{
|
||||||
|
name : 'Enable H1-H4 all pages',
|
||||||
|
icon : 'fas fa-dice-four',
|
||||||
|
gen : `{{tocGlobalH4}}\n\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Enable H1-H5 all pages',
|
||||||
|
icon : 'fas fa-dice-five',
|
||||||
|
gen : `{{tocGlobalH5}}\n\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Enable H1-H6 all pages',
|
||||||
|
icon : 'fas fa-dice-six',
|
||||||
|
gen : `{{tocGlobalH6}}\n\n`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Index',
|
name : 'Index',
|
||||||
@@ -60,7 +213,7 @@ module.exports = [
|
|||||||
background-image: linear-gradient(-45deg, #322814, #998250, #322814);
|
background-image: linear-gradient(-45deg, #322814, #998250, #322814);
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
}\n\n`
|
}\n\n`
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -315,7 +468,7 @@ module.exports = [
|
|||||||
/* Ink Friendly */
|
/* Ink Friendly */
|
||||||
*:is(.page,.monster,.note,.descriptive) {
|
*:is(.page,.monster,.note,.descriptive) {
|
||||||
background : white !important;
|
background : white !important;
|
||||||
filter : drop-shadow(0px 0px 3px #888) !important;
|
box-shadow : 1px 4px 14px #888 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page img {
|
.page img {
|
||||||
|
|||||||
@@ -1,86 +1,78 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const getTOC = (pages)=>{
|
// Map each actual page to its footer label, accounting for skips or numbering resets
|
||||||
const add1 = (title, page)=>{
|
const mapPages = (pages)=>{
|
||||||
res.push({
|
let actualPage = 0;
|
||||||
title : title,
|
let mappedPage = 0; // Number displayed in footer
|
||||||
page : page + 1,
|
const pageMap = [];
|
||||||
children : []
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const add2 = (title, page)=>{
|
|
||||||
if(!_.last(res)) add1(null, page);
|
|
||||||
_.last(res).children.push({
|
|
||||||
title : title,
|
|
||||||
page : page + 1,
|
|
||||||
children : []
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const add3 = (title, page)=>{
|
|
||||||
if(!_.last(res)) add1(null, page);
|
|
||||||
if(!_.last(_.last(res).children)) add2(null, page);
|
|
||||||
_.last(_.last(res).children).children.push({
|
|
||||||
title : title,
|
|
||||||
page : page + 1,
|
|
||||||
children : []
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = [];
|
pages.forEach((page)=>{
|
||||||
_.each(pages, (page, pageNum)=>{
|
actualPage++;
|
||||||
if(!page.includes('{{frontCover}}') && !page.includes('{{insideCover}}') && !page.includes('{{partCover}}') && !page.includes('{{backCover}}')) {
|
const doSkip = page.querySelector('.skipCounting');
|
||||||
const lines = page.split('\n');
|
const doReset = page.querySelector('.resetCounting');
|
||||||
_.each(lines, (line)=>{
|
|
||||||
if(_.startsWith(line, '# ')){
|
if(doReset)
|
||||||
const title = line.replace('# ', '');
|
mappedPage = 1;
|
||||||
add1(title, pageNum);
|
if(!doSkip && !doReset)
|
||||||
}
|
mappedPage++;
|
||||||
if(_.startsWith(line, '## ')){
|
|
||||||
const title = line.replace('## ', '');
|
pageMap[actualPage] = {
|
||||||
add2(title, pageNum);
|
mappedPage : mappedPage,
|
||||||
}
|
showPage : !doSkip
|
||||||
if(_.startsWith(line, '### ')){
|
};
|
||||||
const title = line.replace('### ', '');
|
|
||||||
add3(title, pageNum);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return res;
|
return pageMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMarkdown = (headings, pageMap)=>{
|
||||||
|
const levelPad = ['- ###', ' - ####', ' -', ' -', ' -', ' -'];
|
||||||
|
|
||||||
|
const allMarkdown = [];
|
||||||
|
const depthChain = [0];
|
||||||
|
|
||||||
|
headings.forEach((heading)=>{
|
||||||
|
const page = parseInt(heading.closest('.page').id?.replace(/^p/, ''));
|
||||||
|
const mappedPage = pageMap[page].mappedPage;
|
||||||
|
const showPage = pageMap[page].showPage;
|
||||||
|
const title = heading.textContent.trim();
|
||||||
|
const ToCExclude = getComputedStyle(heading).getPropertyValue('--TOC');
|
||||||
|
const depth = parseInt(heading.tagName.substring(1));
|
||||||
|
|
||||||
|
if(!title || !showPage || ToCExclude == 'exclude')
|
||||||
|
return;
|
||||||
|
|
||||||
|
//If different header depth than last, remove indents until nearest higher-level header, then indent once
|
||||||
|
if(depth !== depthChain[depthChain.length -1]) {
|
||||||
|
while (depth <= depthChain[depthChain.length - 1]) {
|
||||||
|
depthChain.pop();
|
||||||
|
}
|
||||||
|
depthChain.push(depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
const markdown = `${levelPad[depthChain.length - 2]} [{{ ${title}}}{{ ${mappedPage}}}](#p${page})`;
|
||||||
|
allMarkdown.push(markdown);
|
||||||
|
});
|
||||||
|
return allMarkdown.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTOC = ()=>{
|
||||||
|
const iframe = document.getElementById('BrewRenderer');
|
||||||
|
const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
const headings = iframeDocument.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||||
|
const pages = iframeDocument.querySelectorAll('.page');
|
||||||
|
|
||||||
|
const pageMap = mapPages(pages);
|
||||||
|
return getMarkdown(headings, pageMap);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = function(props){
|
module.exports = function(props){
|
||||||
const pages = props.brew.text.split('\\page');
|
const TOC = getTOC();
|
||||||
const TOC = getTOC(pages);
|
|
||||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
|
||||||
if(g1.title !== null) {
|
|
||||||
r.push(`- ### [{{ ${g1.title}}}{{ ${g1.page}}}](#p${g1.page})`);
|
|
||||||
}
|
|
||||||
if(g1.children.length){
|
|
||||||
_.each(g1.children, (g2, idx2)=>{
|
|
||||||
if(g2.title !== null) {
|
|
||||||
r.push(` - #### [{{ ${g2.title}}}{{ ${g2.page}}}](#p${g2.page})`);
|
|
||||||
}
|
|
||||||
if(g2.children.length){
|
|
||||||
_.each(g2.children, (g3, idx3)=>{
|
|
||||||
if(g2.title !== null) {
|
|
||||||
r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
|
|
||||||
} else { // Don't over-indent if no level-2 parent entry
|
|
||||||
r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}, []).join('\n');
|
|
||||||
|
|
||||||
return dedent`
|
return dedent`
|
||||||
{{toc,wide
|
{{toc,wide
|
||||||
# Contents
|
# Contents
|
||||||
|
|
||||||
${markdown}
|
${TOC}
|
||||||
}}
|
}}
|
||||||
\n`;
|
\n`;
|
||||||
};
|
};
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
--HB_Color_CaptionText : #766649; // Brown
|
--HB_Color_CaptionText : #766649; // Brown
|
||||||
--HB_Color_WatercolorStain : #BBAD82; // Light brown
|
--HB_Color_WatercolorStain : #BBAD82; // Light brown
|
||||||
--HB_Color_Footnotes : #C9AD6A; // Gold
|
--HB_Color_Footnotes : #C9AD6A; // Gold
|
||||||
|
--TOC : 'include';
|
||||||
}
|
}
|
||||||
|
|
||||||
.useSansSerif() {
|
.useSansSerif() {
|
||||||
@@ -382,6 +383,14 @@
|
|||||||
.useColumns(0.96, @fillMode: balance);
|
.useColumns(0.96, @fillMode: balance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//only for IOS devices
|
||||||
|
@supports (-webkit-touch-callout: none) {
|
||||||
|
.page .monster.frame {
|
||||||
|
background-repeat : no-repeat;
|
||||||
|
background-size : cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// *****************************
|
// *****************************
|
||||||
// * FOOTER
|
// * FOOTER
|
||||||
// *****************************/
|
// *****************************/
|
||||||
@@ -459,6 +468,7 @@
|
|||||||
margin-left : 1.5em;
|
margin-left : 1.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// *****************************
|
// *****************************
|
||||||
// * SPELL LIST
|
// * SPELL LIST
|
||||||
// *****************************/
|
// *****************************/
|
||||||
@@ -786,6 +796,65 @@
|
|||||||
// *****************************
|
// *****************************
|
||||||
// * TABLE OF CONTENTS
|
// * TABLE OF CONTENTS
|
||||||
// *****************************/
|
// *****************************/
|
||||||
|
|
||||||
|
// Default Exclusions
|
||||||
|
// Anything not excluded is included, default Headers are H1, H2, and H3.
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
.page:has(.frontCover),
|
||||||
|
.page:has(.backCover),
|
||||||
|
.page:has(.insideCover),
|
||||||
|
.monster,
|
||||||
|
.noToC,
|
||||||
|
.toc { --TOC: exclude; }
|
||||||
|
|
||||||
|
|
||||||
|
// Brew level default inclusion changes.
|
||||||
|
// These add Headers 'back' to inclusion.
|
||||||
|
.pages:has(.tocGlobalH4) {
|
||||||
|
h4 {--TOC: include; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.pages:has(.tocGlobalH5) {
|
||||||
|
h4, h5 {--TOC: include; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.pages:has(.tocGlobalH6) {
|
||||||
|
h4, h5, h6 {--TOC: include; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block level inclusion changes
|
||||||
|
// These include either a single (include) or a range (depth)
|
||||||
|
.tocIncludeH1 h1 {--TOC: include; }
|
||||||
|
.tocIncludeH2 h2 {--TOC: include; }
|
||||||
|
.tocIncludeH3 h3 {--TOC: include; }
|
||||||
|
.tocIncludeH4 h4 {--TOC: include; }
|
||||||
|
.tocIncludeH5 h5 {--TOC: include; }
|
||||||
|
.tocIncludeH6 h6 {--TOC: include; }
|
||||||
|
|
||||||
|
.tocDepthH2 :is(h1, h2) {--TOC: include; }
|
||||||
|
.tocDepthH3 :is(h1, h2, h3) {--TOC: include; }
|
||||||
|
.tocDepthH4 :is(h1, h2, h3, h4) {--TOC: include; }
|
||||||
|
.tocDepthH5 :is(h1, h2, h3, h4, h5) {--TOC: include; }
|
||||||
|
.tocDepthH6 :is(h1, h2, h3, h4, h5, h6) {--TOC: include; }
|
||||||
|
|
||||||
|
// Block level exclusion changes
|
||||||
|
// These exclude a single block level
|
||||||
|
.tocExcludeH1 h1 {--TOC: exclude; }
|
||||||
|
.tocExcludeH2 h2 {--TOC: exclude; }
|
||||||
|
.tocExcludeH3 h3 {--TOC: exclude; }
|
||||||
|
.tocExcludeH4 h4 {--TOC: exclude; }
|
||||||
|
.tocExcludeH5 h5 {--TOC: exclude; }
|
||||||
|
.tocExcludeH6 h6 {--TOC: exclude; }
|
||||||
|
|
||||||
|
.page:has(.partCover) {
|
||||||
|
--TOC: exclude;
|
||||||
|
& h1 {
|
||||||
|
--TOC: include;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
&:has(.toc)::after { display : none; }
|
&:has(.toc)::after { display : none; }
|
||||||
.toc {
|
.toc {
|
||||||
@@ -850,6 +919,9 @@
|
|||||||
.useColumns(0.96, @fillMode: balance);
|
.useColumns(0.96, @fillMode: balance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.toc.wide li {
|
||||||
|
break-inside: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// *****************************
|
// *****************************
|
||||||
@@ -874,6 +946,10 @@
|
|||||||
|
|
||||||
.page h1 + * { margin-top : 0; }
|
.page h1 + * { margin-top : 0; }
|
||||||
|
|
||||||
|
.page .descriptive.wide + * {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * RUNE TABLE
|
// * RUNE TABLE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
|
|||||||
@@ -23,14 +23,30 @@ module.exports = [
|
|||||||
gen : '\n\\page\n'
|
gen : '\n\\page\n'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Page Number',
|
name : 'Page Numbering',
|
||||||
icon : 'fas fa-bookmark',
|
icon : 'fas fa-bookmark',
|
||||||
gen : '{{pageNumber 1}}\n'
|
subsnippets : [
|
||||||
},
|
{
|
||||||
{
|
name : 'Page Number',
|
||||||
name : 'Auto-incrementing Page Number',
|
icon : 'fas fa-bookmark',
|
||||||
icon : 'fas fa-sort-numeric-down',
|
gen : '{{pageNumber 1}}\n'
|
||||||
gen : '{{pageNumber,auto}}\n'
|
},
|
||||||
|
{
|
||||||
|
name : 'Auto-incrementing Page Number',
|
||||||
|
icon : 'fas fa-sort-numeric-down',
|
||||||
|
gen : '{{pageNumber,auto}}\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Skip Page Number Increment this Page',
|
||||||
|
icon : 'fas fa-xmark',
|
||||||
|
gen : '{{skipCounting}}\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Restart Numbering',
|
||||||
|
icon : 'fas fa-arrow-rotate-left',
|
||||||
|
gen : '{{resetCounting}}\n'
|
||||||
|
},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Footer',
|
name : 'Footer',
|
||||||
@@ -153,6 +169,18 @@ module.exports = [
|
|||||||
gen : dedent`
|
gen : dedent`
|
||||||
 {width:325px,mix-blend-mode:multiply}`
|
 {width:325px,mix-blend-mode:multiply}`
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Image Wrap Left',
|
||||||
|
icon : 'fac image-wrap-left',
|
||||||
|
gen : dedent`
|
||||||
|
 {width:280px,margin-right:-3cm,wrapLeft}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Image Wrap Right',
|
||||||
|
icon : 'fac image-wrap-right',
|
||||||
|
gen : dedent`
|
||||||
|
 {width:280px,margin-left:-3cm,wrapRight}`
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name : 'Background Image',
|
name : 'Background Image',
|
||||||
icon : 'fas fa-tree',
|
icon : 'fas fa-tree',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@import (less) './themes/fonts/iconFonts/elderberryInn.less';
|
@import (less) './themes/fonts/iconFonts/elderberryInn.less';
|
||||||
@import (less) './themes/fonts/iconFonts/diceFont.less';
|
@import (less) './themes/fonts/iconFonts/diceFont.less';
|
||||||
@import (less) './themes/fonts/iconFonts/gameIcons.less';
|
@import (less) './themes/fonts/iconFonts/gameIcons.less';
|
||||||
|
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
//Colors
|
//Colors
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@page { margin : 0; }
|
@page { margin : 0; }
|
||||||
body { counter-reset : page-numbers; }
|
body { counter-reset : page-numbers 0; }
|
||||||
* { -webkit-print-color-adjust : exact; }
|
* { -webkit-print-color-adjust : exact; }
|
||||||
|
|
||||||
//*****************************
|
//*****************************
|
||||||
@@ -50,7 +51,6 @@ body { counter-reset : page-numbers; }
|
|||||||
height : 279.4mm;
|
height : 279.4mm;
|
||||||
padding : 1.4cm 1.9cm 1.7cm;
|
padding : 1.4cm 1.9cm 1.7cm;
|
||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
counter-increment : page-numbers;
|
|
||||||
background-color : var(--HB_Color_Background);
|
background-color : var(--HB_Color_Background);
|
||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
contain : size;
|
contain : size;
|
||||||
@@ -155,6 +155,19 @@ body { counter-reset : page-numbers; }
|
|||||||
break-inside : avoid;
|
break-inside : avoid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Wrap Text */
|
||||||
|
.wrapLeft {
|
||||||
|
shape-outside : var(--HB_src);
|
||||||
|
float : right;
|
||||||
|
shape-margin : 0.2cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapRight {
|
||||||
|
shape-outside : var(--HB_src);
|
||||||
|
float : left;
|
||||||
|
shape-margin : 0.2cm;
|
||||||
|
}
|
||||||
|
|
||||||
/* Watermark */
|
/* Watermark */
|
||||||
.watermark {
|
.watermark {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
@@ -235,7 +248,7 @@ body { counter-reset : page-numbers; }
|
|||||||
left : 50%;
|
left : 50%;
|
||||||
width : 50%;
|
width : 50%;
|
||||||
height : 50%;
|
height : 50%;
|
||||||
transform : translateX(-50%) translateY(50%) rotate(calc(-1deg * var(--rotation))) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)));
|
transform : translateX(-50%) translateY(50%) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY))) rotate(calc(-1deg * var(--rotation)));
|
||||||
}
|
}
|
||||||
& img {
|
& img {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
@@ -480,4 +493,13 @@ body { counter-reset : page-numbers; }
|
|||||||
&:nth-child(even) {
|
&:nth-child(even) {
|
||||||
.pageNumber { left : 30px; }
|
.pageNumber { left : 30px; }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.resetCounting {
|
||||||
|
counter-set : page-numbers 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:has(.skipCounting)) {
|
||||||
|
counter-increment : page-numbers;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name" : "Journal",
|
"name" : "Journal",
|
||||||
"renderer" : "V3",
|
"renderer" : "V3",
|
||||||
"baseTheme" : false,
|
"baseTheme" : "Blank",
|
||||||
"baseSnippets" : "5ePHB"
|
"baseSnippets" : "5ePHB"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
@noteBorderImage : url('/assets/noteBorder.png');
|
@noteBorderImage : url('/assets/noteBorder.png');
|
||||||
@descriptiveBoxImage : url('/assets/descriptiveBorder.png');
|
@descriptiveBoxImage : url('/assets/descriptiveBorder.png');
|
||||||
@monsterBlockBackground : url('/assets/parchmentBackgroundGrayscale.jpg');
|
@monsterBlockBackground : url('/assets/parchmentBackgroundGrayscale.jpg');
|
||||||
|
@monsterBlockOverlay : url('/assets/parchmentBackgroundOverlayed.jpg');
|
||||||
@monsterBorderImage : url('/assets/monsterBorderFancy.png');
|
@monsterBorderImage : url('/assets/monsterBorderFancy.png');
|
||||||
@codeBorderImage : url('/assets/codeBorder.png');
|
@codeBorderImage : url('/assets/codeBorder.png');
|
||||||
@classTableDecoration : url('/assets/classTableDecoration.png');
|
@classTableDecoration : url('/assets/classTableDecoration.png');
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
background-color: rgba(35,153,153,0.5);
|
background-color: rgba(35,153,153,0.5);
|
||||||
}
|
}
|
||||||
.pageLine {
|
.pageLine {
|
||||||
background-color: rgba(255,255,255,0.75);
|
background-color: rgba(255,255,255,0.5);
|
||||||
& ~ pre.CodeMirror-line {
|
& ~ pre.CodeMirror-line {
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
@@ -85,4 +85,4 @@
|
|||||||
// Future styling for themes with light backgrounds
|
// Future styling for themes with light backgrounds
|
||||||
--dummyVar: 'currently unused';
|
--dummyVar: 'currently unused';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,8 +74,9 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: SolberaImitationRemake; //Tweaked 5e version
|
font-family: SolberaImitationRemake; //Tweaked 5e version
|
||||||
src: url('../../../fonts/5e/Solbera Imitation Tweak.woff2');
|
src: url('../../../fonts/5e/Solbera Imitation Tweak.woff2');
|
||||||
font-weight: normal;
|
font-weight: 100 1000;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cover Page */
|
/* Cover Page */
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.df {
|
.df {
|
||||||
display : inline-block;
|
display : inline;
|
||||||
font-family : 'DiceFont';
|
font-family : 'DiceFont';
|
||||||
font-style : normal;
|
font-style : normal;
|
||||||
font-weight : normal;
|
font-weight : normal;
|
||||||
@@ -16,8 +16,11 @@
|
|||||||
text-decoration : inherit;
|
text-decoration : inherit;
|
||||||
text-transform : none;
|
text-transform : none;
|
||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
-moz-osx-font-smoothing : grayscale;
|
|
||||||
|
/* Better Font Rendering =========== */
|
||||||
-webkit-font-smoothing : antialiased;
|
-webkit-font-smoothing : antialiased;
|
||||||
|
-moz-osx-font-smoothing : grayscale;
|
||||||
|
|
||||||
&.F::before { content : '\f190'; }
|
&.F::before { content : '\f190'; }
|
||||||
&.F-minus::before { content : '\f191'; }
|
&.F-minus::before { content : '\f191'; }
|
||||||
&.F-plus::before { content : '\f192'; }
|
&.F-plus::before { content : '\f192'; }
|
||||||
|
|||||||
@@ -7,15 +7,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ei {
|
.ei {
|
||||||
display : inline-block;
|
display : inline;
|
||||||
margin-right : 3px;
|
|
||||||
font-family : 'Elderberry-Inn';
|
font-family : 'Elderberry-Inn';
|
||||||
line-height : 1;
|
line-height : 1;
|
||||||
vertical-align : baseline;
|
vertical-align : baseline;
|
||||||
-moz-osx-font-smoothing : grayscale;
|
|
||||||
-webkit-font-smoothing : antialiased;
|
|
||||||
text-rendering : auto;
|
text-rendering : auto;
|
||||||
|
|
||||||
|
/* Better Font Rendering =========== */
|
||||||
|
-webkit-font-smoothing : antialiased;
|
||||||
|
-moz-osx-font-smoothing : grayscale;
|
||||||
|
|
||||||
&.book::before { content : '\E900'; }
|
&.book::before { content : '\E900'; }
|
||||||
&.screen::before { content : '\E901'; }
|
&.screen::before { content : '\E901'; }
|
||||||
|
|
||||||
|
|||||||
2
themes/fonts/iconFonts/fontAwesome.less
Normal file
2
themes/fonts/iconFonts/fontAwesome.less
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/* Icon Font: Font Awesome */
|
||||||
|
.far,.fas,.fab { display : inline; }
|
||||||
@@ -8,19 +8,15 @@
|
|||||||
|
|
||||||
.gi {
|
.gi {
|
||||||
/* use !important to prevent issues with browser extensions that change fonts */
|
/* use !important to prevent issues with browser extensions that change fonts */
|
||||||
display : inline-block;
|
display : inline;
|
||||||
margin-right : 3px;
|
|
||||||
font-family : 'Game-Icons' !important;
|
font-family : 'Game-Icons' !important;
|
||||||
line-height : 1;
|
line-height : 1;
|
||||||
vertical-align : baseline;
|
vertical-align : baseline;
|
||||||
-moz-osx-font-smoothing : grayscale;
|
|
||||||
-webkit-font-smoothing : antialiased;
|
|
||||||
text-rendering : auto;
|
text-rendering : auto;
|
||||||
|
|
||||||
/* Better Font Rendering =========== */
|
/* Better Font Rendering =========== */
|
||||||
-webkit-font-smoothing : antialiased;
|
-webkit-font-smoothing : antialiased;
|
||||||
-moz-osx-font-smoothing : grayscale;
|
-moz-osx-font-smoothing : grayscale;
|
||||||
|
|
||||||
|
|
||||||
&.zigzag-leaf::before { content : '\e900'; }
|
&.zigzag-leaf::before { content : '\e900'; }
|
||||||
&.zebra-shield::before { content : '\e901'; }
|
&.zebra-shield::before { content : '\e901'; }
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"5ePHB": {
|
"5ePHB": {
|
||||||
"name": "5e PHB",
|
"name": "5e PHB",
|
||||||
"renderer": "V3",
|
"renderer": "V3",
|
||||||
"baseTheme": false,
|
"baseTheme": "Blank",
|
||||||
"baseSnippets": false,
|
"baseSnippets": false,
|
||||||
"path": "5ePHB"
|
"path": "5ePHB"
|
||||||
},
|
},
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"Journal": {
|
"Journal": {
|
||||||
"name": "Journal",
|
"name": "Journal",
|
||||||
"renderer": "V3",
|
"renderer": "V3",
|
||||||
"baseTheme": false,
|
"baseTheme": "Blank",
|
||||||
"baseSnippets": "5ePHB",
|
"baseSnippets": "5ePHB",
|
||||||
"path": "Journal"
|
"path": "Journal"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user