mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-27 09:33:08 +00:00
Compare commits
212 Commits
pr/3820
...
toWellForm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8eedcf6d6 | ||
|
|
92d1238a46 | ||
|
|
dceb5e516b | ||
|
|
adb1db1d3c | ||
|
|
e8d1e632b4 | ||
|
|
50fcffb253 | ||
|
|
aae5367ad2 | ||
|
|
40b0c1ce3a | ||
|
|
ba83dfacd9 | ||
|
|
2717e6a9a4 | ||
|
|
d576bddd32 | ||
|
|
fde21868cd | ||
|
|
ed8c4d0eef | ||
|
|
6e9d293bbe | ||
|
|
7e1312805f | ||
|
|
d629fa1731 | ||
|
|
6301a66fd3 | ||
|
|
980a7bd57e | ||
|
|
6b0022ad00 | ||
|
|
0f33973e58 | ||
|
|
7a41a140fd | ||
|
|
57467701d0 | ||
|
|
9dbfb26e6c | ||
|
|
7a169cbd9e | ||
|
|
2dc8a8fbe9 | ||
|
|
5f14f656ef | ||
|
|
6e8a0d7314 | ||
|
|
e61144beb8 | ||
|
|
64b792c645 | ||
|
|
aee5b7a8cc | ||
|
|
912f9f0cf6 | ||
|
|
c63b6ffaf0 | ||
|
|
0c90d1a14d | ||
|
|
0148eafce0 | ||
|
|
a3ec5b8d3b | ||
|
|
4ded48df1e | ||
|
|
bc14246fe7 | ||
|
|
fcf985a115 | ||
|
|
a060fd123c | ||
|
|
7c7e143365 | ||
|
|
efa8f3fedf | ||
|
|
972a93d292 | ||
|
|
35be1e9b94 | ||
|
|
1a91c390f8 | ||
|
|
206e4fbda8 | ||
|
|
af98cb3867 | ||
|
|
f8fc6f7aa4 | ||
|
|
eb0fa28a03 | ||
|
|
4ab1a22eb3 | ||
|
|
962a46a670 | ||
|
|
cb16b32016 | ||
|
|
56f348f7ed | ||
|
|
b7c99b2d52 | ||
|
|
889f80f537 | ||
|
|
c270a69bb9 | ||
|
|
db0df82202 | ||
|
|
1346361f80 | ||
|
|
fdaf9d4808 | ||
|
|
3cdfae4270 | ||
|
|
a9275698fa | ||
|
|
99f2972079 | ||
|
|
afc92c4545 | ||
|
|
b26526a2f1 | ||
|
|
4f57f006ce | ||
|
|
666a94cd65 | ||
|
|
f0c094e9d8 | ||
|
|
a1c228b1d1 | ||
|
|
5e5c637c79 | ||
|
|
d573129f31 | ||
|
|
abd52f93d8 | ||
|
|
57cb334c15 | ||
|
|
c29e1905bf | ||
|
|
52d00b17a4 | ||
|
|
35364c400a | ||
|
|
77f0c1bf56 | ||
|
|
2d281072fa | ||
|
|
83b8f9c3b7 | ||
|
|
3a20452214 | ||
|
|
3e4ba89ed9 | ||
|
|
2c5c3d40df | ||
|
|
213240327d | ||
|
|
eca0f59b40 | ||
|
|
51936a1b99 | ||
|
|
6136b78395 | ||
|
|
81f56ec91d | ||
|
|
c7d94b0779 | ||
|
|
9758797e2b | ||
|
|
74a7983757 | ||
|
|
4eb8abf1e7 | ||
|
|
23910cc94c | ||
|
|
ef0ee78758 | ||
|
|
1b20c00842 | ||
|
|
db9212bd12 | ||
|
|
7348ecbb3d | ||
|
|
31a22703c1 | ||
|
|
33f8f6bf38 | ||
|
|
a62d2bd457 | ||
|
|
ffa9666bb9 | ||
|
|
406f5d4e14 | ||
|
|
ed404d3906 | ||
|
|
3178c8722e | ||
|
|
596c4ad68d | ||
|
|
14a0f79ac8 | ||
|
|
e7f4611a00 | ||
|
|
136a6d4024 | ||
|
|
737e27f062 | ||
|
|
ee9143fa35 | ||
|
|
c62bb53660 | ||
|
|
4e5a971f0a | ||
|
|
16184f1b8d | ||
|
|
23bd0309b9 | ||
|
|
20dba6b7b3 | ||
|
|
5177c9a64e | ||
|
|
d179c18c35 | ||
|
|
6e4e35c7ad | ||
|
|
6c2721d49f | ||
|
|
029d61b6ad | ||
|
|
f58040e9a4 | ||
|
|
9f9948f531 | ||
|
|
2743ab869a | ||
|
|
4b21538e3e | ||
|
|
e17db0788c | ||
|
|
bea74c3b46 | ||
|
|
e252a39bd2 | ||
|
|
7ef259ddbe | ||
|
|
d18005fad4 | ||
|
|
86402cdbc8 | ||
|
|
e28b4e8c20 | ||
|
|
7c09680939 | ||
|
|
3f0a6a577f | ||
|
|
6f4cc0d91b | ||
|
|
e711a1c207 | ||
|
|
add088c2a9 | ||
|
|
6d8415bfeb | ||
|
|
decb334808 | ||
|
|
7a76c67038 | ||
|
|
b45686eb3b | ||
|
|
66f71972eb | ||
|
|
ebe8e1067c | ||
|
|
9807cf762b | ||
|
|
b58563cb42 | ||
|
|
7c3f3b87af | ||
|
|
e7daad592c | ||
|
|
992359e239 | ||
|
|
b2546f3458 | ||
|
|
6f016bf5b6 | ||
|
|
7cd3c69fbd | ||
|
|
9b1507d4f5 | ||
|
|
2e49bf4fa8 | ||
|
|
108d368d45 | ||
|
|
bd413cfc55 | ||
|
|
1af13b4e94 | ||
|
|
e5624434d6 | ||
|
|
1850173f87 | ||
|
|
fb9148ada5 | ||
|
|
b857a91ab8 | ||
|
|
b7c49218ae | ||
|
|
f4d4334a75 | ||
|
|
38b4c285a3 | ||
|
|
cf46a975aa | ||
|
|
9f693547f7 | ||
|
|
a69dd998f5 | ||
|
|
f141515446 | ||
|
|
f749706cb3 | ||
|
|
b22f3d041c | ||
|
|
dd8692d82b | ||
|
|
0d2dfe66bc | ||
|
|
0437635861 | ||
|
|
a5f12ca0b4 | ||
|
|
07e0a7c1b5 | ||
|
|
2e9c7b1d9b | ||
|
|
0ddc3ae5b9 | ||
|
|
107aa34ee4 | ||
|
|
e006826e3e | ||
|
|
4e4463fe4d | ||
|
|
1a56c393ab | ||
|
|
9bc4b1fb56 | ||
|
|
234d484a74 | ||
|
|
b7b1981bde | ||
|
|
2e8368d08c | ||
|
|
2abc2b13f0 | ||
|
|
ca90e1804a | ||
|
|
db75e0dd66 | ||
|
|
35856ad01e | ||
|
|
766fd40b72 | ||
|
|
3e6884b506 | ||
|
|
2118142faa | ||
|
|
2b270ccdb7 | ||
|
|
08eabf8102 | ||
|
|
c1d85bc216 | ||
|
|
3a2c213cf8 | ||
|
|
99dc0deb08 | ||
|
|
a96ff6ecb3 | ||
|
|
5af45f16b0 | ||
|
|
a9b6d5ff38 | ||
|
|
433f016c25 | ||
|
|
10a9bc2906 | ||
|
|
b585e85f0f | ||
|
|
8a67e1eccd | ||
|
|
7ea1696065 | ||
|
|
5b4a7c168f | ||
|
|
a54adc1e4b | ||
|
|
c1288ce4bb | ||
|
|
c65210b3ed | ||
|
|
70a3cb9ef9 | ||
|
|
d1686c4c8f | ||
|
|
c5033db336 | ||
|
|
36aa4ea508 | ||
|
|
d5c5b4315b | ||
|
|
d505e4e24c | ||
|
|
ea7f18e3b0 | ||
|
|
e8e16f4d66 |
@@ -70,9 +70,15 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Test - Hard Breaks
|
name: Test - Hard Breaks
|
||||||
command: npm run test:hard-breaks
|
command: npm run test:hard-breaks
|
||||||
|
- run:
|
||||||
|
name: Test - Non-Breaking Spaces
|
||||||
|
command: npm run test:non-breaking-spaces
|
||||||
- run:
|
- run:
|
||||||
name: Test - Variables
|
name: Test - Variables
|
||||||
command: npm run test:variables
|
command: npm run test:variables
|
||||||
|
- run:
|
||||||
|
name: Test - Emojis
|
||||||
|
command: npm run test:emojis
|
||||||
- run:
|
- run:
|
||||||
name: Test - Routes
|
name: Test - Routes
|
||||||
command: npm run test:route
|
command: npm run test:route
|
||||||
@@ -82,6 +88,9 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Test - Coverage
|
name: Test - Coverage
|
||||||
command: npm run test:coverage
|
command: npm run test:coverage
|
||||||
|
- run:
|
||||||
|
name: Test - Content Negotiation
|
||||||
|
command: npm run test:content-negotiation
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
build_and_test:
|
build_and_test:
|
||||||
|
|||||||
25
.github/pull_request_template.md
vendored
25
.github/pull_request_template.md
vendored
@@ -1,26 +1,29 @@
|
|||||||
<!--
|
> [!TIP]
|
||||||
Before submitting a Pull Request, please consider the following to speed up reviews:
|
> 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.
|
> - 👷♀️ 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.
|
> - 🚩 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?
|
> - 🔧 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.
|
> - 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
|
||||||
-->
|
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
|
_Describe what your PR accomplishes. Consider walking through the main changes to aid reviewers in following your code, especially if it covers multiple files._
|
||||||
|
|
||||||
## Related Issues or Discussions
|
## Related Issues or Discussions
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> If no issue exists yet, create it, and get agreement on the approach (or paste in a previous agreement from chat, etc.) before moving forward. (Experimental PRs are OK without prior discussion, but do not expect to get merged.)
|
||||||
|
|
||||||
- Closes #
|
- Closes #
|
||||||
|
|
||||||
## QA Instructions, Screenshots, Recordings
|
## QA Instructions, Screenshots, Recordings
|
||||||
|
|
||||||
_Please replace this line with instructions on how to test or view your changes, as well as any before/after
|
_Replace this line with instructions on how to test or view your changes, as well as any before/after
|
||||||
images for UI changes._
|
screenshots or recordings for UI changes._
|
||||||
|
|
||||||
### Reviewer Checklist
|
### Reviewer Checklist
|
||||||
|
|
||||||
_Please replace the list below with specific features you want reviewers to look at._
|
_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 *
|
*Reviewers, refer to this list when testing features, or suggest new items *
|
||||||
- [ ] Verify new features are functional
|
- [ ] Verify new features are functional
|
||||||
@@ -32,5 +35,3 @@ _Please replace the list below with specific features you want reviewers to look
|
|||||||
- [ ] Feature A handles negative numbers
|
- [ ] Feature A handles negative numbers
|
||||||
- [ ] Identify opportunities for simplification and refactoring
|
- [ ] Identify opportunities for simplification and refactoring
|
||||||
- [ ] Check for code legibility and appropriate comments
|
- [ ] Check for code legibility and appropriate comments
|
||||||
|
|
||||||
<details><summary>Copy this list</summary>
|
|
||||||
|
|||||||
10
babel.config.json
Normal file
10
babel.config.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"@babel/preset-env",
|
||||||
|
"@babel/preset-react"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@babel/plugin-transform-runtime",
|
||||||
|
"babel-plugin-transform-import-meta"
|
||||||
|
]
|
||||||
|
}
|
||||||
46
changelog.md
46
changelog.md
@@ -85,6 +85,52 @@ 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 11/27/2024 - v3.16.1
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Allow linking to specific HTML IDs via `#ID` at the end of the URL, e.g.: `homebrewery.naturalcrit.com/share/share/a6RCXwaDS58i#p4` to link to Page 4 directly
|
||||||
|
|
||||||
|
Fixes issues [#2820](https://github.com/naturalcrit/homebrewery/issues/2820), [#3505](https://github.com/naturalcrit/homebrewery/issues/3505)
|
||||||
|
|
||||||
|
* [x] Fix generation of link to certain Google Drive brews
|
||||||
|
|
||||||
|
Fixes issue [#3776](https://github.com/naturalcrit/homebrewery/issues/3776)
|
||||||
|
|
||||||
|
##### abquintic
|
||||||
|
|
||||||
|
* [x] Fix blank pages appearing when pasting text
|
||||||
|
|
||||||
|
Fixes issue [#3718](https://github.com/naturalcrit/homebrewery/issues/3718)
|
||||||
|
|
||||||
|
##### Gazook89
|
||||||
|
|
||||||
|
* [x] Add new brew viewing options to the view toolbar
|
||||||
|
- {{fac,single-spread}} {{openSans **SINGLE PAGE**}}
|
||||||
|
- {{fac,facing-spread}} {{openSans **TWO PAGE**}}
|
||||||
|
- {{fac,flow-spread}} {{openSans **GRID**}}
|
||||||
|
|
||||||
|
Fixes issue [#1379](https://github.com/naturalcrit/homebrewery/issues/1379)
|
||||||
|
|
||||||
|
* [x] Updates to tag input boxes
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Admin tools to fix certain corrupted documents
|
||||||
|
|
||||||
|
Fixes issue [#3801](https://github.com/naturalcrit/homebrewery/issues/3801)
|
||||||
|
|
||||||
|
* [x] Fix print window being affected by document zoom
|
||||||
|
|
||||||
|
Fixes issue [#3744](https://github.com/naturalcrit/homebrewery/issues/3744)
|
||||||
|
|
||||||
|
|
||||||
|
##### calculuschild, 5e-Cleric, G-Ambatte, Gazook89, abquintic
|
||||||
|
|
||||||
|
* [x] Multiple code refactors, cleanups, and security fixes
|
||||||
|
}}
|
||||||
|
|
||||||
### Saturday 10/12/2024 - v3.16.0
|
### Saturday 10/12/2024 - v3.16.0
|
||||||
|
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ const BrewLookup = createClass({
|
|||||||
<dt>Num of Views</dt>
|
<dt>Num of Views</dt>
|
||||||
<dd>{brew.views}</dd>
|
<dd>{brew.views}</dd>
|
||||||
|
|
||||||
<dt>Number of SCRIPT tags detected</dt>
|
<dt>SCRIPT tags detected</dt>
|
||||||
<dd>{this.state.scriptCount}</dd>
|
<dd>{this.state.scriptCount}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
{this.state.scriptCount > 0 &&
|
{this.state.scriptCount > 0 &&
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const { useState, useRef, useCallback, useMemo } = 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');
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
const ErrorBar = require('./errorBar/errorBar.jsx');
|
const ErrorBar = require('./errorBar/errorBar.jsx');
|
||||||
const ToolBar = require('./toolBar/toolBar.jsx');
|
const ToolBar = require('./toolBar/toolBar.jsx');
|
||||||
|
|
||||||
@@ -161,6 +161,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
|
||||||
|
if(rawPages.length > props.currentEditorCursorPageNum -1)
|
||||||
renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1);
|
renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1);
|
||||||
|
|
||||||
_.forEach(rawPages, (page, index)=>{
|
_.forEach(rawPages, (page, index)=>{
|
||||||
@@ -216,7 +217,7 @@ const BrewRenderer = (props)=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
|
const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
|
||||||
renderedPages = useMemo(()=>renderPages(), [props.text]);
|
renderedPages = useMemo(()=>renderPages(), [displayOptions.pageShadows, props.text]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
require('./notificationPopup.less');
|
require('./notificationPopup.less');
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
const request = require('../../utils/request-middleware.js');
|
import request from '../../utils/request-middleware.js';
|
||||||
|
|
||||||
import Dialog from '../../../components/dialog.jsx';
|
import Dialog from '../../../components/dialog.jsx';
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
height : auto;
|
height : auto;
|
||||||
padding : 2px 0;
|
padding : 2px 0;
|
||||||
font-family : 'Open Sans', sans-serif;
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
font-size : 13px;
|
||||||
color : #CCCCCC;
|
color : #CCCCCC;
|
||||||
background-color : #555555;
|
background-color : #555555;
|
||||||
& > *:not(.toggleButton) {
|
& > *:not(.toggleButton) {
|
||||||
@@ -114,10 +115,10 @@
|
|||||||
color : #D3D3D3;
|
color : #D3D3D3;
|
||||||
accent-color : #D3D3D3;
|
accent-color : #D3D3D3;
|
||||||
|
|
||||||
&::-webkit-slider-thumb, &::-moz-slider-thumb {
|
&::-webkit-slider-thumb, &::-moz-range-thumb {
|
||||||
width : 5px;
|
width : 5px;
|
||||||
height : 5px;
|
height : 5px;
|
||||||
cursor : pointer;
|
cursor : ew-resize;
|
||||||
outline : none;
|
outline : none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,13 +155,6 @@
|
|||||||
width : auto;
|
width : auto;
|
||||||
min-width : 46px;
|
min-width : 46px;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
padding : 0 0px;
|
|
||||||
font-weight : unset;
|
|
||||||
color : inherit;
|
|
||||||
background-color : unset;
|
|
||||||
|
|
||||||
&:not(button:has(i, svg)) { padding : 0 8px; }
|
|
||||||
|
|
||||||
&:hover { background-color : #444444; }
|
&:hover { background-color : #444444; }
|
||||||
&:focus { border : 1px solid #D3D3D3;outline : none;}
|
&:focus { border : 1px solid #D3D3D3;outline : none;}
|
||||||
&:disabled {
|
&:disabled {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const React = require('react');
|
|||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
const Markdown = require('../../../shared/naturalcrit/markdown.js');
|
import Markdown from '../../../shared/naturalcrit/markdown.js';
|
||||||
|
|
||||||
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
||||||
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ require('./metadataEditor.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 request = require('../../utils/request-middleware.js');
|
import request from '../../utils/request-middleware.js';
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
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 TagInput = require('../tagInput/tagInput.jsx');
|
||||||
|
|
||||||
|
|
||||||
const Themes = require('themes/themes.json');
|
const Themes = require('themes/themes.json');
|
||||||
@@ -341,10 +341,11 @@ const MetadataEditor = createClass({
|
|||||||
{this.renderThumbnail()}
|
{this.renderThumbnail()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StringArrayEditor label='tags' valuePatterns={[/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.\-]{0,40}$/]}
|
<TagInput label='tags' valuePatterns={[/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.\-]{0,40}$/]}
|
||||||
placeholder='add tag' unique={true}
|
placeholder='add tag' unique={true}
|
||||||
values={this.props.metadata.tags}
|
values={this.props.metadata.tags}
|
||||||
onChange={(e)=>this.handleFieldChange('tags', e)}/>
|
onChange={(e)=>this.handleFieldChange('tags', e)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className='field systems'>
|
<div className='field systems'>
|
||||||
<label>systems</label>
|
<label>systems</label>
|
||||||
@@ -363,12 +364,13 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
{this.renderAuthors()}
|
{this.renderAuthors()}
|
||||||
|
|
||||||
<StringArrayEditor label='invited authors' valuePatterns={[/.+/]}
|
<TagInput label='invited authors' valuePatterns={[/.+/]}
|
||||||
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
|
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
|
||||||
placeholder='invite author' unique={true}
|
placeholder='invite author' unique={true}
|
||||||
values={this.props.metadata.invitedAuthors}
|
values={this.props.metadata.invitedAuthors}
|
||||||
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
|
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
|
||||||
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/>
|
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
|
||||||
|
/>
|
||||||
|
|
||||||
<h2>Privacy</h2>
|
<h2>Privacy</h2>
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@
|
|||||||
text-overflow : ellipsis;
|
text-overflow : ellipsis;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
|
.colorButton();
|
||||||
padding : 0px 5px;
|
padding : 0px 5px;
|
||||||
color : white;
|
color : white;
|
||||||
background-color : black;
|
background-color : black;
|
||||||
@@ -138,16 +139,16 @@
|
|||||||
margin-bottom : 15px;
|
margin-bottom : 15px;
|
||||||
button { width : 100%; }
|
button { width : 100%; }
|
||||||
button.publish {
|
button.publish {
|
||||||
.button(@blueLight);
|
.colorButton(@blueLight);
|
||||||
}
|
}
|
||||||
button.unpublish {
|
button.unpublish {
|
||||||
.button(@silver);
|
.colorButton(@silver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete.field .value {
|
.delete.field .value {
|
||||||
button {
|
button {
|
||||||
.button(@red);
|
.colorButton(@red);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.authors.field .value {
|
.authors.field .value {
|
||||||
@@ -271,7 +272,7 @@
|
|||||||
&:last-child { border-radius : 0 0.5em 0.5em 0; }
|
&:last-child { border-radius : 0 0.5em 0.5em 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.tag {
|
||||||
padding : 0.3em;
|
padding : 0.3em;
|
||||||
margin : 2px;
|
margin : 2px;
|
||||||
font-size : 0.9em;
|
font-size : 0.9em;
|
||||||
|
|||||||
@@ -207,19 +207,11 @@ const Snippetbar = createClass({
|
|||||||
renderEditorButtons : function(){
|
renderEditorButtons : function(){
|
||||||
if(!this.props.showEditButtons) return;
|
if(!this.props.showEditButtons) return;
|
||||||
|
|
||||||
const foldButtons = <>
|
|
||||||
<div className={`editorTool foldAll ${this.props.view !== 'meta' && this.props.foldCode ? 'active' : ''}`}
|
|
||||||
onClick={this.props.foldCode} >
|
|
||||||
<i className='fas fa-compress-alt' />
|
|
||||||
</div>
|
|
||||||
<div className={`editorTool unfoldAll ${this.props.view !== 'meta' && this.props.unfoldCode ? 'active' : ''}`}
|
|
||||||
onClick={this.props.unfoldCode} >
|
|
||||||
<i className='fas fa-expand-alt' />
|
|
||||||
</div>
|
|
||||||
</>;
|
|
||||||
|
|
||||||
return <div className='editors'>
|
|
||||||
<div className='historyTools'>
|
return (
|
||||||
|
<div className='editors'>
|
||||||
|
{this.props.view !== 'meta' && <><div className='historyTools'>
|
||||||
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
||||||
onClick={this.toggleHistoryMenu} >
|
onClick={this.toggleHistoryMenu} >
|
||||||
<i className='fas fa-clock-rotate-left' />
|
<i className='fas fa-clock-rotate-left' />
|
||||||
@@ -235,13 +227,20 @@ const Snippetbar = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='codeTools'>
|
<div className='codeTools'>
|
||||||
{foldButtons}
|
<div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
|
||||||
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
onClick={this.props.foldCode} >
|
||||||
|
<i className='fas fa-compress-alt' />
|
||||||
|
</div>
|
||||||
|
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
|
||||||
|
onClick={this.props.unfoldCode} >
|
||||||
|
<i className='fas fa-expand-alt' />
|
||||||
|
</div>
|
||||||
|
<div className={`editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||||
onClick={this.toggleThemeSelector} >
|
onClick={this.toggleThemeSelector} >
|
||||||
<i className='fas fa-palette' />
|
<i className='fas fa-palette' />
|
||||||
{this.state.themeSelector && this.renderThemeSelector()}
|
{this.state.themeSelector && this.renderThemeSelector()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div></>}
|
||||||
|
|
||||||
|
|
||||||
<div className='tabs'>
|
<div className='tabs'>
|
||||||
@@ -259,7 +258,8 @@ const Snippetbar = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>;
|
</div>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
justify-content : flex-end;
|
justify-content : flex-end;
|
||||||
min-width : 225px;
|
min-width : 225px;
|
||||||
|
|
||||||
&:only-child { margin-left : auto; }
|
&:only-child { margin-left : auto;min-width:unset;}
|
||||||
|
|
||||||
>div {
|
>div {
|
||||||
display : flex;
|
display : flex;
|
||||||
@@ -38,6 +38,11 @@
|
|||||||
line-height : @menuHeight;
|
line-height : @menuHeight;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
|
|
||||||
|
&.editorTool:not(.active) {
|
||||||
|
cursor:not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover,&.selected { background-color : #999999; }
|
&:hover,&.selected { background-color : #999999; }
|
||||||
&.text {
|
&.text {
|
||||||
.tooltipLeft('Brew Editor');
|
.tooltipLeft('Brew Editor');
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const StringArrayEditor = createClass({
|
|
||||||
displayName : 'StringArrayEditor',
|
|
||||||
getDefaultProps : function() {
|
|
||||||
return {
|
|
||||||
label : '',
|
|
||||||
values : [],
|
|
||||||
valuePatterns : null,
|
|
||||||
validators : [],
|
|
||||||
placeholder : '',
|
|
||||||
notes : [],
|
|
||||||
unique : false,
|
|
||||||
cannotEdit : [],
|
|
||||||
onChange : ()=>{}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function() {
|
|
||||||
return {
|
|
||||||
valueContext : !!this.props.values ? this.props.values.map((value)=>({
|
|
||||||
value,
|
|
||||||
editing : false
|
|
||||||
})) : [],
|
|
||||||
temporaryValue : '',
|
|
||||||
updateValue : ''
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidUpdate : function(prevProps) {
|
|
||||||
if(!_.eq(this.props.values, prevProps.values)) {
|
|
||||||
this.setState({
|
|
||||||
valueContext : this.props.values ? this.props.values.map((newValue)=>({
|
|
||||||
value : newValue,
|
|
||||||
editing : this.state.valueContext.find(({ value })=>value === newValue)?.editing || false
|
|
||||||
})) : []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleChange : function(value) {
|
|
||||||
this.props.onChange({
|
|
||||||
target : {
|
|
||||||
value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
addValue : function(value){
|
|
||||||
this.handleChange(_.uniq([...this.props.values, value]));
|
|
||||||
this.setState({
|
|
||||||
temporaryValue : ''
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
removeValue : function(index){
|
|
||||||
this.handleChange(this.props.values.filter((_, i)=>i !== index));
|
|
||||||
},
|
|
||||||
|
|
||||||
updateValue : function(value, index){
|
|
||||||
const valueContext = this.state.valueContext;
|
|
||||||
valueContext[index].value = value;
|
|
||||||
valueContext[index].editing = false;
|
|
||||||
this.handleChange(valueContext.map((context)=>context.value));
|
|
||||||
this.setState({ valueContext, updateValue: '' });
|
|
||||||
},
|
|
||||||
|
|
||||||
editValue : function(index){
|
|
||||||
if(!!this.props.cannotEdit && this.props.cannotEdit.includes(this.props.values[index])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const valueContext = this.state.valueContext.map((context, i)=>{
|
|
||||||
context.editing = index === i;
|
|
||||||
return context;
|
|
||||||
});
|
|
||||||
this.setState({ valueContext, updateValue: this.props.values[index] });
|
|
||||||
},
|
|
||||||
|
|
||||||
valueIsValid : function(value, index) {
|
|
||||||
const values = _.clone(this.props.values);
|
|
||||||
if(index !== undefined) {
|
|
||||||
values.splice(index, 1);
|
|
||||||
}
|
|
||||||
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
|
|
||||||
const uniqueIfSet = !this.props.unique || !values.includes(value);
|
|
||||||
const passesValidators = !this.props.validators || this.props.validators.every((validator)=>validator(value));
|
|
||||||
return matchesPatterns && uniqueIfSet && passesValidators;
|
|
||||||
},
|
|
||||||
|
|
||||||
handleValueInputKeyDown : function(event, index) {
|
|
||||||
if(event.key === 'Enter') {
|
|
||||||
if(this.valueIsValid(event.target.value, index)) {
|
|
||||||
if(index !== undefined) {
|
|
||||||
this.updateValue(event.target.value, index);
|
|
||||||
} else {
|
|
||||||
this.addValue(event.target.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if(event.key === 'Escape') {
|
|
||||||
this.closeEditInput(index);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
closeEditInput : function(index) {
|
|
||||||
const valueContext = this.state.valueContext;
|
|
||||||
valueContext[index].editing = false;
|
|
||||||
this.setState({ valueContext, updateValue: '' });
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function() {
|
|
||||||
const valueElements = Object.values(this.state.valueContext).map((context, i)=>context.editing
|
|
||||||
? <React.Fragment key={i}>
|
|
||||||
<div className='input-group'>
|
|
||||||
<input type='text' className={`value ${this.valueIsValid(this.state.updateValue, i) ? '' : 'invalid'}`} autoFocus placeholder={this.props.placeholder}
|
|
||||||
value={this.state.updateValue}
|
|
||||||
onKeyDown={(e)=>this.handleValueInputKeyDown(e, i)}
|
|
||||||
onChange={(e)=>this.setState({ updateValue: e.target.value })}/>
|
|
||||||
{<div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.closeEditInput(i); }}><i className='fa fa-undo fa-fw'/></div>}
|
|
||||||
{this.valueIsValid(this.state.updateValue, i) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.updateValue(this.state.updateValue, i); }}><i className='fa fa-check fa-fw'/></div> : null}
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
: <div className='badge' key={i} onClick={()=>this.editValue(i)}>{context.value}
|
|
||||||
{!!this.props.cannotEdit && this.props.cannotEdit.includes(context.value) ? null : <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.removeValue(i); }}><i className='fa fa-times fa-fw'/></div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return <div className='field'>
|
|
||||||
<label>{this.props.label}</label>
|
|
||||||
<div style={{ flex: '1 0' }} className='value'>
|
|
||||||
<div className='list'>
|
|
||||||
{valueElements}
|
|
||||||
<div className='input-group'>
|
|
||||||
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
|
|
||||||
value={this.state.temporaryValue}
|
|
||||||
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
|
|
||||||
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
|
|
||||||
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.props.notes ? this.props.notes.map((n, index)=><p key={index}><small>{n}</small></p>) : null}
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = StringArrayEditor;
|
|
||||||
105
client/homebrew/editor/tagInput/tagInput.jsx
Normal file
105
client/homebrew/editor/tagInput/tagInput.jsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
require('./tagInput.less');
|
||||||
|
const React = require('react');
|
||||||
|
const { useState, useEffect } = React;
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const TagInput = ({ unique = true, values = [], ...props }) => {
|
||||||
|
const [tempInputText, setTempInputText] = useState('');
|
||||||
|
const [tagList, setTagList] = useState(values.map((value) => ({ value, editing: false })));
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
handleChange(tagList.map((context)=>context.value))
|
||||||
|
}, [tagList])
|
||||||
|
|
||||||
|
const handleChange = (value)=>{
|
||||||
|
props.onChange({
|
||||||
|
target : { value }
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputKeyDown = ({ evt, value, index, options = {} }) => {
|
||||||
|
if (_.includes(['Enter', ','], evt.key)) {
|
||||||
|
evt.preventDefault();
|
||||||
|
submitTag(evt.target.value, value, index);
|
||||||
|
if (options.clear) {
|
||||||
|
setTempInputText('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitTag = (newValue, originalValue, index) => {
|
||||||
|
setTagList((prevContext) => {
|
||||||
|
// remove existing tag
|
||||||
|
if(newValue === null){
|
||||||
|
return [...prevContext].filter((context, i)=>i !== index);
|
||||||
|
}
|
||||||
|
// add new tag
|
||||||
|
if(originalValue === null){
|
||||||
|
return [...prevContext, { value: newValue, editing: false }]
|
||||||
|
}
|
||||||
|
// update existing tag
|
||||||
|
return prevContext.map((context, i) => {
|
||||||
|
if (i === index) {
|
||||||
|
return { ...context, value: newValue, editing: false };
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const editTag = (index) => {
|
||||||
|
setTagList((prevContext) => {
|
||||||
|
return prevContext.map((context, i) => {
|
||||||
|
if (i === index) {
|
||||||
|
return { ...context, editing: true };
|
||||||
|
}
|
||||||
|
return { ...context, editing: false };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderReadTag = (context, index) => {
|
||||||
|
return (
|
||||||
|
<li key={index}
|
||||||
|
data-value={context.value}
|
||||||
|
className='tag'
|
||||||
|
onClick={() => editTag(index)}>
|
||||||
|
{context.value}
|
||||||
|
<button onClick={(evt)=>{evt.stopPropagation(); submitTag(null, context.value, index)}}><i className='fa fa-times fa-fw'/></button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderWriteTag = (context, index) => {
|
||||||
|
return (
|
||||||
|
<input type='text'
|
||||||
|
key={index}
|
||||||
|
defaultValue={context.value}
|
||||||
|
onKeyDown={(evt) => handleInputKeyDown({evt, value: context.value, index: index})}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='field'>
|
||||||
|
<label>{props.label}</label>
|
||||||
|
<div className='value'>
|
||||||
|
<ul className='list'>
|
||||||
|
{tagList.map((context, index) => { return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='value'
|
||||||
|
placeholder={props.placeholder}
|
||||||
|
value={tempInputText}
|
||||||
|
onChange={(e) => setTempInputText(e.target.value)}
|
||||||
|
onKeyDown={(evt) => handleInputKeyDown({ evt, value: null, options: { clear: true } })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = TagInput;
|
||||||
0
client/homebrew/editor/tagInput/tagInput.less
Normal file
0
client/homebrew/editor/tagInput/tagInput.less
Normal file
@@ -1,8 +1,12 @@
|
|||||||
|
//╔===--------------- Polyfills --------------===╗//
|
||||||
|
import 'core-js/es/string/to-well-formed.js';
|
||||||
|
//╚===--------------- ---------------===╝//
|
||||||
|
|
||||||
require('./homebrew.less');
|
require('./homebrew.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const { StaticRouter:Router } = require('react-router-dom/server');
|
const { StaticRouter:Router } = require('react-router');
|
||||||
const { Route, Routes, useParams, useSearchParams } = require('react-router-dom');
|
const { Route, Routes, useParams, useSearchParams } = require('react-router');
|
||||||
|
|
||||||
const HomePage = require('./pages/homePage/homePage.jsx');
|
const HomePage = require('./pages/homePage/homePage.jsx');
|
||||||
const EditPage = require('./pages/editPage/editPage.jsx');
|
const EditPage = require('./pages/editPage/editPage.jsx');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ require('./brewItem.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const request = require('../../../../utils/request-middleware.js');
|
import request from '../../../../utils/request-middleware.js';
|
||||||
|
|
||||||
const googleDriveIcon = require('../../../../googleDrive.svg');
|
const googleDriveIcon = require('../../../../googleDrive.svg');
|
||||||
const homebreweryIcon = require('../../../../thumbnail.png');
|
const homebreweryIcon = require('../../../../thumbnail.png');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const React = require('react');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
|
|
||||||
const request = require('../../utils/request-middleware.js');
|
import request from '../../utils/request-middleware.js';
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
@@ -16,6 +16,7 @@ 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;
|
||||||
|
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
||||||
|
|
||||||
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');
|
||||||
@@ -23,7 +24,7 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
|||||||
|
|
||||||
const LockNotification = require('./lockNotification/lockNotification.jsx');
|
const LockNotification = require('./lockNotification/lockNotification.jsx');
|
||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
|
||||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||||
@@ -380,7 +381,7 @@ const EditPage = createClass({
|
|||||||
|
|
||||||
**[Homebrewery Link](${global.config.publicUrl}/share/${shareLink})**`;
|
**[Homebrewery Link](${global.config.publicUrl}/share/${shareLink})**`;
|
||||||
|
|
||||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title)}&text=${encodeURIComponent(text)}`;
|
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
@@ -417,6 +418,7 @@ const EditPage = createClass({
|
|||||||
</Nav.item>
|
</Nav.item>
|
||||||
</Nav.dropdown>
|
</Nav.dropdown>
|
||||||
<PrintNavItem />
|
<PrintNavItem />
|
||||||
|
<VaultNavItem />
|
||||||
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
||||||
<Account />
|
<Account />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
require('./errorPage.less');
|
require('./errorPage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||||
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
|
import Markdown from '../../../../shared/naturalcrit/markdown.js';
|
||||||
const ErrorIndex = require('./errors/errorIndex.js');
|
const ErrorIndex = require('./errors/errorIndex.js');
|
||||||
|
|
||||||
const ErrorPage = ({ brew })=>{
|
const ErrorPage = ({ brew })=>{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ require('./homePage.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const request = require('../../utils/request-middleware.js');
|
import request from '../../utils/request-middleware.js';
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
require('./newPage.less');
|
require('./newPage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const request = require('../../utils/request-middleware.js');
|
import request from '../../utils/request-middleware.js';
|
||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
import Markdown from '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 PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const Navbar = require('../../navbar/navbar.jsx');
|
|||||||
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
||||||
const PrintNavItem = 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 VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
||||||
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');
|
||||||
|
|
||||||
@@ -110,6 +111,7 @@ const SharePage = createClass({
|
|||||||
</Nav.item>
|
</Nav.item>
|
||||||
</Nav.dropdown>
|
</Nav.dropdown>
|
||||||
</>}
|
</>}
|
||||||
|
<VaultNavItem/>
|
||||||
<RecentNavItem brew={this.props.brew} storageKey='view' />
|
<RecentNavItem brew={this.props.brew} storageKey='view' />
|
||||||
<Account />
|
<Account />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
|
|||||||
const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx');
|
const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx');
|
||||||
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
|
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
|
||||||
|
|
||||||
const request = require('../../utils/request-middleware.js');
|
import request from '../../utils/request-middleware.js';
|
||||||
|
|
||||||
const VaultPage = (props)=>{
|
const VaultPage = (props)=>{
|
||||||
const [pageState, setPageState] = useState(parseInt(props.query.page) || 1);
|
const [pageState, setPageState] = useState(parseInt(props.query.page) || 1);
|
||||||
|
|||||||
@@ -92,49 +92,11 @@
|
|||||||
|
|
||||||
&:invalid { background : rgb(255, 188, 181); }
|
&: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 {
|
#searchButton {
|
||||||
|
.colorButton(@green);
|
||||||
position : absolute;
|
position : absolute;
|
||||||
right : 20px;
|
right : 20px;
|
||||||
bottom : 0;
|
bottom : 0;
|
||||||
@@ -152,7 +114,6 @@
|
|||||||
flex-direction : column;
|
flex-direction : column;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
overflow-y : auto;
|
overflow-y : auto;
|
||||||
font-family : 'BookInsanityRemake';
|
|
||||||
font-size : 0.34cm;
|
font-size : 0.34cm;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
@@ -356,6 +317,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
.colorButton(@green);
|
||||||
width : max-content;
|
width : max-content;
|
||||||
|
|
||||||
&.previousPage { grid-area : previousPage; }
|
&.previousPage { grid-area : previousPage; }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const request = require('superagent');
|
import request from 'superagent';
|
||||||
|
|
||||||
const addHeader = (request)=>request.set('Homebrewery-Version', global.version);
|
const addHeader = (request)=>request.set('Homebrewery-Version', global.version);
|
||||||
|
|
||||||
@@ -9,4 +9,4 @@ const requestMiddleware = {
|
|||||||
delete : (path)=>addHeader(request.delete(path)),
|
delete : (path)=>addHeader(request.delete(path)),
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = requestMiddleware;
|
export default requestMiddleware;
|
||||||
@@ -8,6 +8,8 @@ const template = async function(name, title='', props = {}){
|
|||||||
});
|
});
|
||||||
const ogMetaTags = ogTags.join('\n');
|
const ogMetaTags = ogTags.join('\n');
|
||||||
|
|
||||||
|
const ssrModule = await import(`../build/${name}/ssr.cjs`);
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -21,7 +23,7 @@ const template = async function(name, title='', props = {}){
|
|||||||
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
<main id="reactRoot">${ssrModule.default(props)}</main>
|
||||||
<script src=${`/${name}/bundle.js`}></script>
|
<script src=${`/${name}/bundle.js`}></script>
|
||||||
<script>start_app(${JSON.stringify(props)})</script>
|
<script>start_app(${JSON.stringify(props)})</script>
|
||||||
</body>
|
</body>
|
||||||
@@ -29,4 +31,4 @@ const template = async function(name, title='', props = {}){
|
|||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = template;
|
export default template;
|
||||||
17
faq.md
17
faq.md
@@ -69,7 +69,6 @@ pre {
|
|||||||
|
|
||||||
You can check the site status here: [Everyone or Just Me](https://downforeveryoneorjustme.com/homebrewery.naturalcrit.com)
|
You can check the site status here: [Everyone or Just Me](https://downforeveryoneorjustme.com/homebrewery.naturalcrit.com)
|
||||||
|
|
||||||
|
|
||||||
### Why am I getting an error when trying to save, and my account is linked to Google?
|
### Why am I getting an error when trying to save, and my account is linked to Google?
|
||||||
|
|
||||||
A sign-in with Google only lasts a year until the authentication expires. You must go [here](https://www.naturalcrit.com/login), click the *Log-out* button, and then sign back in using your Google account.
|
A sign-in with Google only lasts a year until the authentication expires. You must go [here](https://www.naturalcrit.com/login), click the *Log-out* button, and then sign back in using your Google account.
|
||||||
@@ -82,12 +81,17 @@ If you have linked your account with a Google account, you would change your pas
|
|||||||
|
|
||||||
### Is there a way to restore a previous version of my brew?
|
### Is there a way to restore a previous version of my brew?
|
||||||
|
|
||||||
Currently, there is no way to do this through the site yourself. This would take too much of a toll on the amount of storage the homebrewery requires. However, we do have daily backups of our database that we keep for 8 days, and you can contact the moderators on [the subreddit](https://www.reddit.com/r/homebrewery) with your Homebrewery username, the name of the lost brew, and the last known time it was working properly. We can manually look through our backups and restore it if it exists.
|
In your brew, there is an icon, :fas_clock_rotate_left:, that button opens up a menu with versions of your brew, stored in order from newer to older, up to a week old. Because of the amount of duplicates this function creates, this information is stored in **your browser**, so if you were to uninstall it or clear your cookies and site data, or change computers, the info will not be there.
|
||||||
|
|
||||||
|
Also, we do have daily backups of our database that we keep for 8 days, and you can contact the moderators on [the subreddit](https://www.reddit.com/r/homebrewery) with your Homebrewery username, the name of the lost brew, and the last known time it was working properly. We can manually look through our backups and restore it if it exists.
|
||||||
|
|
||||||
|
|
||||||
### I worked on a brew for X hours, and suddenly all the text disappeared!
|
### I worked on a brew for X hours, and suddenly all the text disappeared!
|
||||||
|
|
||||||
This usually happens if you accidentally drag-select all of your text and then start typing which overwrites the selection. Do not panic, and do not refresh the page or reload your brew quite yet as it is probably auto-saved in this state already. Simply press CTRL+Z as many times as needed to undo your last few changes and you will be back to where you were, then make sure to save your brew in the "good" state.
|
This usually happens if you accidentally drag-select all of your text and then start typing which overwrites the selection. Do not panic, and do not refresh the page or reload your brew quite yet as it is probably auto-saved in this state already. Simply press CTRL+Z as many times as needed to undo your last few changes and you will be back to where you were, then make sure to save your brew in the "good" state.
|
||||||
|
|
||||||
|
You can also load a history version old enough to have all the text, using the :fas_clock_rotate_left: history versions button.
|
||||||
|
|
||||||
\column
|
\column
|
||||||
|
|
||||||
### Why is only Chrome supported?
|
### Why is only Chrome supported?
|
||||||
@@ -114,9 +118,6 @@ Once you have an image you would like to use, it is recommended to host it somew
|
|||||||
### A particular font does not work for my language, what do I do?
|
### A particular font does not work for my language, what do I do?
|
||||||
The fonts used were originally created for use with the English language, though revisions since then have added more support for other languages. They are still not complete sets and may be missing a glyph/character you need. Unfortunately, the volunteer group as it stands at the time of this writing does not have a font guru, so it would be difficult to add more glyphs (especially complicated glyphs). Let us know which glyph is missing on the subreddit, but you may need to search [Google Fonts](https://fonts.google.com) for an alternative font if you need something fast.
|
The fonts used were originally created for use with the English language, though revisions since then have added more support for other languages. They are still not complete sets and may be missing a glyph/character you need. Unfortunately, the volunteer group as it stands at the time of this writing does not have a font guru, so it would be difficult to add more glyphs (especially complicated glyphs). Let us know which glyph is missing on the subreddit, but you may need to search [Google Fonts](https://fonts.google.com) for an alternative font if you need something fast.
|
||||||
|
|
||||||
### Whenever I click on the "Get PDF" button, instead of getting a download, it opens Print Preview in another tab.
|
|
||||||
Yes, this is by design. In the print preview, select "Save as PDF" as the Destination, and then click "Save". There will be a normal download dialog where you can save your brew as a PDF.
|
|
||||||
|
|
||||||
### I have white borders on the bottom/sides of the print preview.
|
### I have white borders on the bottom/sides of the print preview.
|
||||||
|
|
||||||
The Homebrewery paper size and your print paper size do not match.
|
The Homebrewery paper size and your print paper size do not match.
|
||||||
@@ -126,4 +127,8 @@ The Homebrewery defaults to creating US Letter page sizes. If you are printing
|
|||||||
|
|
||||||
### Typing `#### Adhesion` in the text editor doesn't show the header at all in the completed page?
|
### Typing `#### Adhesion` in the text editor doesn't show the header at all in the completed page?
|
||||||
|
|
||||||
Your ad-blocking software is mistakenly assuming your text to be an ad. Whitelist homebrewery.naturalcrit.com in your ad-blocking software.
|
Your ad-blocking software is mistakenly assuming your text to be an ad. We recommend whitelisting homebrewery.naturalcrit.com in your ad-blocking software, as we have no ads.
|
||||||
|
|
||||||
|
### My username appears as _hidden_ when checking my brews in the Vault, why is that?
|
||||||
|
|
||||||
|
Your username is most likely your e-mail adress, and our code is picking that up and protecting your identity. This will remain as is, but you can ask for a name change by contacting the moderators on [the subreddit](https://www.reddit.com/r/homebrewery) with your Homebrewery username, and your desired new name. You will also be asked to provide details about some of your unpublished brews, to verify your identity. No information will be leaked or shared.
|
||||||
|
|||||||
1420
package-lock.json
generated
1420
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"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.16.0",
|
"version": "3.16.1",
|
||||||
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": "^10.2.x",
|
"npm": "^10.2.x",
|
||||||
"node": "^20.18.x"
|
"node": "^20.18.x"
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --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:api-unit:css": "jest \"server/.*.spec.js\" -t \"Get CSS\" --verbose",
|
||||||
"test:api-unit:notifications": "jest \"server/.*.spec.js\" -t \"Notifications\" --verbose",
|
"test:api-unit:notifications": "jest \"server/.*.spec.js\" -t \"Notifications\" --verbose",
|
||||||
|
"test:content-negotiation": "jest \"server/middleware/.*.spec.js\" --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",
|
||||||
@@ -36,6 +38,7 @@
|
|||||||
"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:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
|
||||||
|
"test:non-breaking-spaces": "jest tests/markdown/non-breaking-spaces.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",
|
||||||
"test:safehtml": "jest tests/html/safeHTML.test.js --verbose",
|
"test:safehtml": "jest tests/html/safeHTML.test.js --verbose",
|
||||||
@@ -56,6 +59,9 @@
|
|||||||
"shared",
|
"shared",
|
||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"node_modules/(?!nanoid/).*"
|
||||||
|
],
|
||||||
"coveragePathIgnorePatterns": [
|
"coveragePathIgnorePatterns": [
|
||||||
"build/*"
|
"build/*"
|
||||||
],
|
],
|
||||||
@@ -77,32 +83,25 @@
|
|||||||
"jest-expect-message"
|
"jest-expect-message"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"babel": {
|
|
||||||
"presets": [
|
|
||||||
"@babel/preset-env",
|
|
||||||
"@babel/preset-react"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"@babel/plugin-transform-runtime"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.26.0",
|
||||||
"@babel/plugin-transform-runtime": "^7.25.9",
|
"@babel/plugin-transform-runtime": "^7.25.9",
|
||||||
"@babel/preset-env": "^7.26.0",
|
"@babel/preset-env": "^7.26.0",
|
||||||
"@babel/preset-react": "^7.25.9",
|
"@babel/preset-react": "^7.26.3",
|
||||||
"@googleapis/drive": "^8.14.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.7",
|
"cookie-parser": "^1.4.7",
|
||||||
|
"core-js": "^3.39.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"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.7",
|
"dompurify": "^3.2.3",
|
||||||
"expr-eval": "^2.0.2",
|
"expr-eval": "^2.0.2",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.2",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.1.8",
|
"express-static-gzip": "2.2.0",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.2.0",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
@@ -110,34 +109,35 @@
|
|||||||
"less": "^3.13.1",
|
"less": "^3.13.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "11.2.0",
|
"marked": "11.2.0",
|
||||||
"marked-emoji": "^1.4.2",
|
"marked-emoji": "^1.4.3",
|
||||||
"marked-extended-tables": "^1.0.10",
|
"marked-extended-tables": "^1.0.10",
|
||||||
"marked-gfm-heading-id": "^3.2.0",
|
"marked-gfm-heading-id": "^3.2.0",
|
||||||
"marked-smartypants-lite": "^1.0.2",
|
"marked-smartypants-lite": "^1.0.2",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mongoose": "^8.7.3",
|
"mongoose": "^8.9.2",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "5.0.9",
|
||||||
"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.28.0",
|
"react-router": "^7.0.2",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^10.1.1",
|
"superagent": "^10.1.1",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/stylelint-plugin": "^3.1.1",
|
"@stylistic/stylelint-plugin": "^3.1.1",
|
||||||
"eslint": "^9.14.0",
|
"babel-plugin-transform-import-meta": "^2.2.1",
|
||||||
"eslint-plugin-jest": "^28.9.0",
|
"eslint": "^9.17.0",
|
||||||
|
"eslint-plugin-jest": "^28.10.0",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"globals": "^15.12.0",
|
"globals": "^15.14.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"jsdom-global": "^3.0.2",
|
"jsdom-global": "^3.0.2",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^16.10.0",
|
"stylelint": "^16.12.0",
|
||||||
"stylelint-config-recess-order": "^5.1.1",
|
"stylelint-config-recess-order": "^5.1.1",
|
||||||
"stylelint-config-recommended": "^14.0.1",
|
"stylelint-config-recommended": "^14.0.1",
|
||||||
"supertest": "^7.0.0"
|
"supertest": "^7.0.0"
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
const fs = require('fs-extra');
|
|
||||||
const Proj = require('./project.json');
|
|
||||||
|
|
||||||
const { pack } = require('vitreum');
|
import fs from 'fs-extra';
|
||||||
|
import Proj from './project.json' with { type: 'json' };
|
||||||
|
import vitreum from 'vitreum';
|
||||||
|
const { pack } = vitreum;
|
||||||
|
|
||||||
|
import lessTransform from 'vitreum/transforms/less.js';
|
||||||
|
import assetTransform from 'vitreum/transforms/asset.js';
|
||||||
|
|
||||||
const isDev = !!process.argv.find((arg)=>arg=='--dev');
|
const isDev = !!process.argv.find((arg)=>arg=='--dev');
|
||||||
|
|
||||||
const lessTransform = require('vitreum/transforms/less.js');
|
|
||||||
const assetTransform = require('vitreum/transforms/asset.js');
|
|
||||||
//const Meta = require('vitreum/headtags');
|
|
||||||
|
|
||||||
const transforms = {
|
const transforms = {
|
||||||
'.less' : lessTransform,
|
'.less' : lessTransform,
|
||||||
'*' : assetTransform('./build')
|
'*' : assetTransform('./build')
|
||||||
@@ -17,7 +18,7 @@ const build = async ({ bundle, render, ssr })=>{
|
|||||||
const css = await lessTransform.generate({ paths: './shared' });
|
const css = await lessTransform.generate({ paths: './shared' });
|
||||||
await fs.outputFile('./build/admin/bundle.css', css);
|
await fs.outputFile('./build/admin/bundle.css', css);
|
||||||
await fs.outputFile('./build/admin/bundle.js', bundle);
|
await fs.outputFile('./build/admin/bundle.js', bundle);
|
||||||
await fs.outputFile('./build/admin/ssr.js', ssr);
|
await fs.outputFile('./build/admin/ssr.cjs', ssr);
|
||||||
};
|
};
|
||||||
|
|
||||||
fs.emptyDirSync('./build/admin');
|
fs.emptyDirSync('./build/admin');
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
const fs = require('fs-extra');
|
import fs from 'fs-extra';
|
||||||
const zlib = require('zlib');
|
import zlib from 'zlib';
|
||||||
const Proj = require('./project.json');
|
import Proj from './project.json' with { type: 'json' };
|
||||||
|
import vitreum from 'vitreum';
|
||||||
|
const { pack, watchFile, livereload } = vitreum;
|
||||||
|
|
||||||
const { pack, watchFile, livereload } = require('vitreum');
|
import lessTransform from 'vitreum/transforms/less.js';
|
||||||
const isDev = !!process.argv.find((arg)=>arg=='--dev');
|
import assetTransform from 'vitreum/transforms/asset.js';
|
||||||
|
import babel from '@babel/core';
|
||||||
|
import babelConfig from '../babel.config.json' with { type : 'json' };
|
||||||
|
import less from 'less';
|
||||||
|
|
||||||
const lessTransform = require('vitreum/transforms/less.js');
|
const isDev = !!process.argv.find((arg) => arg === '--dev');
|
||||||
const assetTransform = require('vitreum/transforms/asset.js');
|
|
||||||
const babel = require('@babel/core');
|
|
||||||
const less = require('less');
|
|
||||||
|
|
||||||
const babelify = async (code)=>(await babel.transformAsync(code, { presets: [['@babel/preset-env', { 'exclude': ['proposal-dynamic-import'] }], '@babel/preset-react'], plugins: ['@babel/plugin-transform-runtime'] })).code;
|
const babelify = async (code)=>(await babel.transformAsync(code, babelConfig)).code;
|
||||||
|
|
||||||
const transforms = {
|
const transforms = {
|
||||||
'.js' : (code, filename, opts)=>babelify(code),
|
'.js' : (code, filename, opts)=>babelify(code),
|
||||||
@@ -24,7 +26,7 @@ const build = async ({ bundle, render, ssr })=>{
|
|||||||
//css = `@layer bundle {\n${css}\n}`;
|
//css = `@layer bundle {\n${css}\n}`;
|
||||||
await fs.outputFile('./build/homebrew/bundle.css', css);
|
await fs.outputFile('./build/homebrew/bundle.css', css);
|
||||||
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
||||||
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
await fs.outputFile('./build/homebrew/ssr.cjs', ssr);
|
||||||
|
|
||||||
await fs.copy('./client/homebrew/favicon.ico', './build/assets/favicon.ico');
|
await fs.copy('./client/homebrew/favicon.ico', './build/assets/favicon.ico');
|
||||||
|
|
||||||
@@ -51,7 +53,7 @@ fs.emptyDirSync('./build');
|
|||||||
const themes = { Legacy: {}, V3: {} };
|
const themes = { Legacy: {}, V3: {} };
|
||||||
|
|
||||||
let themeFiles = fs.readdirSync('./themes/Legacy');
|
let themeFiles = fs.readdirSync('./themes/Legacy');
|
||||||
for (dir of themeFiles) {
|
for (let dir of themeFiles) {
|
||||||
const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`).toString());
|
const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`).toString());
|
||||||
themeData.path = dir;
|
themeData.path = dir;
|
||||||
themes.Legacy[dir] = (themeData);
|
themes.Legacy[dir] = (themeData);
|
||||||
@@ -68,7 +70,7 @@ fs.emptyDirSync('./build');
|
|||||||
}
|
}
|
||||||
|
|
||||||
themeFiles = fs.readdirSync('./themes/V3');
|
themeFiles = fs.readdirSync('./themes/V3');
|
||||||
for (dir of themeFiles) {
|
for (let dir of themeFiles) {
|
||||||
const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`).toString());
|
const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`).toString());
|
||||||
themeData.path = dir;
|
themeData.path = dir;
|
||||||
themes.V3[dir] = (themeData);
|
themes.V3[dir] = (themeData);
|
||||||
@@ -104,14 +106,14 @@ fs.emptyDirSync('./build');
|
|||||||
const editorThemesBuildDir = './build/homebrew/cm-themes';
|
const editorThemesBuildDir = './build/homebrew/cm-themes';
|
||||||
await fs.copy('./node_modules/codemirror/theme', editorThemesBuildDir);
|
await fs.copy('./node_modules/codemirror/theme', editorThemesBuildDir);
|
||||||
await fs.copy('./themes/codeMirror/customThemes', editorThemesBuildDir);
|
await fs.copy('./themes/codeMirror/customThemes', editorThemesBuildDir);
|
||||||
editorThemeFiles = fs.readdirSync(editorThemesBuildDir);
|
const editorThemeFiles = fs.readdirSync(editorThemesBuildDir);
|
||||||
|
|
||||||
const editorThemeFile = './themes/codeMirror/editorThemes.json';
|
const editorThemeFile = './themes/codeMirror/editorThemes.json';
|
||||||
if(fs.existsSync(editorThemeFile)) fs.rmSync(editorThemeFile);
|
if(fs.existsSync(editorThemeFile)) fs.rmSync(editorThemeFile);
|
||||||
const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' });
|
const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' });
|
||||||
stream.write('[\n"default"');
|
stream.write('[\n"default"');
|
||||||
|
|
||||||
for (themeFile of editorThemeFiles) {
|
for (let themeFile of editorThemeFiles) {
|
||||||
stream.write(`,\n"${themeFile.slice(0, -4)}"`);
|
stream.write(`,\n"${themeFile.slice(0, -4)}"`);
|
||||||
}
|
}
|
||||||
stream.write('\n]\n');
|
stream.write('\n]\n');
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
const DB = require('./server/db.js');
|
import DB from './server/db.js';
|
||||||
const server = require('./server/app.js');
|
import server from './server/app.js';
|
||||||
const config = require('./server/config.js');
|
import config from './server/config.js';
|
||||||
|
|
||||||
DB.connect(config).then(()=>{
|
DB.connect(config).then(()=>{
|
||||||
// Ensure that we have successfully connected to the database
|
// Ensure that we have successfully connected to the database
|
||||||
// before launching server
|
// before launching server
|
||||||
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
||||||
server.app.listen(PORT, ()=>{
|
server.listen(PORT, ()=>{
|
||||||
const reset = '\x1b[0m'; // Reset to default style
|
const reset = '\x1b[0m'; // Reset to default style
|
||||||
const bright = '\x1b[1m'; // Bright (bold) style
|
const bright = '\x1b[1m'; // Bright (bold) style
|
||||||
const cyan = '\x1b[36m'; // Cyan color
|
const cyan = '\x1b[36m'; // Cyan color
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
const HomebrewModel = require('./homebrew.model.js').model;
|
import { model as HomebrewModel } from './homebrew.model.js';
|
||||||
const NotificationModel = require('./notifications.model.js').model;
|
import { model as NotificationModel } from './notifications.model.js';
|
||||||
const router = require('express').Router();
|
import express from 'express';
|
||||||
const Moment = require('moment');
|
import Moment from 'moment';
|
||||||
const templateFn = require('../client/template.js');
|
import zlib from 'zlib';
|
||||||
const zlib = require('zlib');
|
import templateFn from '../client/template.js';
|
||||||
|
|
||||||
const HomebrewAPI = require('./homebrew.api.js');
|
import HomebrewAPI from './homebrew.api.js';
|
||||||
const asyncHandler = require('express-async-handler');
|
import asyncHandler from 'express-async-handler';
|
||||||
const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
|
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
|
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
|
||||||
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
|
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
|
||||||
@@ -106,6 +108,9 @@ router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin',
|
|||||||
|
|
||||||
req.body = brew;
|
req.body = brew;
|
||||||
|
|
||||||
|
// Remove Account from request to prevent Admin user from being added to brew as an Author
|
||||||
|
req.account = undefined;
|
||||||
|
|
||||||
return await HomebrewAPI.updateBrew(req, res);
|
return await HomebrewAPI.updateBrew(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,4 +195,4 @@ router.get('/admin', mw.adminOnly, (req, res)=>{
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
export default router;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
const supertest = require('supertest');
|
import supertest from 'supertest';
|
||||||
|
import HBApp from './app.js';
|
||||||
|
import {model as NotificationModel } from './notifications.model.js';
|
||||||
|
|
||||||
const app = supertest.agent(require('app.js').app)
|
|
||||||
.set('X-Forwarded-Proto', 'https');
|
|
||||||
|
|
||||||
const NotificationModel = require('./notifications.model.js').model;
|
// Mimic https responses to avoid being redirected all the time
|
||||||
|
const app = supertest.agent(HBApp).set('X-Forwarded-Proto', 'https');
|
||||||
|
|
||||||
describe('Tests for admin api', ()=>{
|
describe('Tests for admin api', ()=>{
|
||||||
afterEach(()=>{
|
afterEach(()=>{
|
||||||
|
|||||||
149
server/app.js
149
server/app.js
@@ -1,25 +1,41 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
// Set working directory to project root
|
// Set working directory to project root
|
||||||
|
import { dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import packageJSON from './../package.json' with { type: 'json' };
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
process.chdir(`${__dirname}/..`);
|
process.chdir(`${__dirname}/..`);
|
||||||
|
const version = packageJSON.version;
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
import jwt from 'jwt-simple';
|
||||||
|
import express from 'express';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
import config from './config.js';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
|
||||||
const _ = require('lodash');
|
|
||||||
const jwt = require('jwt-simple');
|
|
||||||
const express = require('express');
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const config = require('./config.js');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
|
|
||||||
const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = require('./homebrew.api.js');
|
import api from './homebrew.api.js';
|
||||||
const GoogleActions = require('./googleActions.js');
|
const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = api;
|
||||||
const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
import adminApi from './admin.api.js';
|
||||||
const sanitizeFilename = require('sanitize-filename');
|
import vaultApi from './vault.api.js';
|
||||||
const asyncHandler = require('express-async-handler');
|
import GoogleActions from './googleActions.js';
|
||||||
const templateFn = require('./../client/template.js');
|
import serveCompressedStaticAssets from './static-assets.mv.js';
|
||||||
|
import sanitizeFilename from 'sanitize-filename';
|
||||||
|
import asyncHandler from 'express-async-handler';
|
||||||
|
import templateFn from '../client/template.js';
|
||||||
|
import { model as HomebrewModel } from './homebrew.model.js';
|
||||||
|
|
||||||
const { DEFAULT_BREW } = require('./brewDefaults.js');
|
import { DEFAULT_BREW } from './brewDefaults.js';
|
||||||
|
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||||
|
|
||||||
const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
|
//==== Middleware Imports ====//
|
||||||
|
import contentNegotiation from './middleware/content-negotiation.js';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
|
import forceSSL from './forcessl.mw.js';
|
||||||
|
|
||||||
|
|
||||||
const sanitizeBrew = (brew, accessType)=>{
|
const sanitizeBrew = (brew, accessType)=>{
|
||||||
@@ -31,13 +47,47 @@ const sanitizeBrew = (brew, accessType)=>{
|
|||||||
return brew;
|
return brew;
|
||||||
};
|
};
|
||||||
|
|
||||||
app.set('trust proxy', 1 /* number of proxies between user and server */)
|
app.set('trust proxy', 1 /* number of proxies between user and server */);
|
||||||
|
|
||||||
app.use('/', serveCompressedStaticAssets(`build`));
|
app.use('/', serveCompressedStaticAssets(`build`));
|
||||||
app.use(require('./middleware/content-negotiation.js'));
|
app.use(contentNegotiation);
|
||||||
app.use(require('body-parser').json({ limit: '25mb' }));
|
app.use(bodyParser.json({ limit: '25mb' }));
|
||||||
app.use(require('cookie-parser')());
|
app.use(cookieParser());
|
||||||
app.use(require('./forcessl.mw.js'));
|
app.use(forceSSL);
|
||||||
|
|
||||||
|
import cors from 'cors';
|
||||||
|
|
||||||
|
const nodeEnv = config.get('node_env');
|
||||||
|
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||||
|
|
||||||
|
const corsOptions = {
|
||||||
|
origin : (origin, callback)=>{
|
||||||
|
|
||||||
|
const allowedOrigins = [
|
||||||
|
'https://homebrewery.naturalcrit.com',
|
||||||
|
'https://www.naturalcrit.com',
|
||||||
|
'https://naturalcrit-stage.herokuapp.com',
|
||||||
|
'https://homebrewery-stage.herokuapp.com',
|
||||||
|
];
|
||||||
|
|
||||||
|
if(isLocalEnvironment) {
|
||||||
|
allowedOrigins.push('http://localhost:8000', 'http://localhost:8010');
|
||||||
|
}
|
||||||
|
|
||||||
|
const herokuRegex = /^https:\/\/(?:homebrewery-pr-\d+\.herokuapp\.com|naturalcrit-pr-\d+\.herokuapp\.com)$/; // Matches any Heroku app
|
||||||
|
|
||||||
|
if(!origin || allowedOrigins.includes(origin) || herokuRegex.test(origin)) {
|
||||||
|
callback(null, true);
|
||||||
|
} else {
|
||||||
|
console.log(origin, 'not allowed');
|
||||||
|
callback(new Error('Not allowed by CORS, if you think this is an error, please contact us'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods : ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
credentials : true,
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use(cors(corsOptions));
|
||||||
|
|
||||||
//Account Middleware
|
//Account Middleware
|
||||||
app.use((req, res, next)=>{
|
app.use((req, res, next)=>{
|
||||||
@@ -46,7 +96,9 @@ app.use((req, res, next)=>{
|
|||||||
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
||||||
//console.log("Just loaded up JWT from cookie:");
|
//console.log("Just loaded up JWT from cookie:");
|
||||||
//console.log(req.account);
|
//console.log(req.account);
|
||||||
} catch (e){}
|
} catch (e){
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.config = {
|
req.config = {
|
||||||
@@ -57,15 +109,14 @@ app.use((req, res, next)=>{
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.use(homebrewApi);
|
app.use(homebrewApi);
|
||||||
app.use(require('./admin.api.js'));
|
app.use(adminApi);
|
||||||
app.use(require('./vault.api.js'));
|
app.use(vaultApi);
|
||||||
|
|
||||||
const HomebrewModel = require('./homebrew.model.js').model;
|
const welcomeText = fs.readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
||||||
const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
const welcomeTextLegacy = fs.readFileSync('client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8');
|
||||||
const welcomeTextLegacy = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8');
|
const migrateText = fs.readFileSync('client/homebrew/pages/homePage/migrate.md', 'utf8');
|
||||||
const migrateText = require('fs').readFileSync('client/homebrew/pages/homePage/migrate.md', 'utf8');
|
const changelogText = fs.readFileSync('changelog.md', 'utf8');
|
||||||
const changelogText = require('fs').readFileSync('changelog.md', 'utf8');
|
const faqText = fs.readFileSync('faq.md', 'utf8');
|
||||||
const faqText = require('fs').readFileSync('faq.md', 'utf8');
|
|
||||||
|
|
||||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||||
|
|
||||||
@@ -258,7 +309,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
brews.forEach(brew => brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
||||||
|
|
||||||
if(ownAccount && req?.account?.googleId){
|
if(ownAccount && req?.account?.googleId){
|
||||||
const auth = await GoogleActions.authCheck(req.account, res);
|
const auth = await GoogleActions.authCheck(req.account, res);
|
||||||
@@ -297,6 +348,34 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Change author name on brews
|
||||||
|
app.put('/api/user/rename', async (req, res)=>{
|
||||||
|
const { username, newUsername } = req.body;
|
||||||
|
const ownAccount = req.account && (req.account.username == newUsername);
|
||||||
|
|
||||||
|
if(!username || !newUsername)
|
||||||
|
return res.status(400).json({ error: 'Username and newUsername are required.' });
|
||||||
|
if(!ownAccount)
|
||||||
|
return res.status(403).json({ error: 'Must be logged in to change your username' });
|
||||||
|
try {
|
||||||
|
const brews = await HomebrewModel.getByUser(username, true, ['authors']);
|
||||||
|
const renamePromises = brews.map(async (brew)=>{
|
||||||
|
const updatedAuthors = brew.authors.map((author)=>author === username ? newUsername : author
|
||||||
|
);
|
||||||
|
return HomebrewModel.updateOne(
|
||||||
|
{ _id: brew._id },
|
||||||
|
{ $set: { authors: updatedAuthors } }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await Promise.all(renamePromises);
|
||||||
|
|
||||||
|
return res.json({ success: true, message: `Brews for ${username} renamed to ${newUsername}.` });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error renaming brews:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to rename brews.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
//Edit Page
|
//Edit Page
|
||||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
||||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||||
@@ -398,7 +477,7 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
let googleCount = [];
|
let googleCount = [];
|
||||||
if(req.account) {
|
if(req.account) {
|
||||||
if(req.account.googleId) {
|
if(req.account.googleId) {
|
||||||
auth = await GoogleActions.authCheck(req.account, res, false)
|
auth = await GoogleActions.authCheck(req.account, res, false);
|
||||||
|
|
||||||
googleCount = await GoogleActions.listGoogleBrews(auth)
|
googleCount = await GoogleActions.listGoogleBrews(auth)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
@@ -433,8 +512,6 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
return next();
|
return next();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const nodeEnv = config.get('node_env');
|
|
||||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
|
||||||
// Local only
|
// Local only
|
||||||
if(isLocalEnvironment){
|
if(isLocalEnvironment){
|
||||||
// Login
|
// Login
|
||||||
@@ -462,7 +539,7 @@ app.get('/vault', asyncHandler(async(req, res, next)=>{
|
|||||||
|
|
||||||
//Send rendered page
|
//Send rendered page
|
||||||
app.use(asyncHandler(async (req, res, next)=>{
|
app.use(asyncHandler(async (req, res, next)=>{
|
||||||
if (!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
if(!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
||||||
|
|
||||||
const page = await renderPage(req, res);
|
const page = await renderPage(req, res);
|
||||||
if(!page) return;
|
if(!page) return;
|
||||||
@@ -479,7 +556,7 @@ const renderPage = async (req, res)=>{
|
|||||||
deployment : config.get('heroku_app_name') ?? ''
|
deployment : config.get('heroku_app_name') ?? ''
|
||||||
};
|
};
|
||||||
const props = {
|
const props = {
|
||||||
version : require('./../package.json').version,
|
version : version,
|
||||||
url : req.customUrl || req.originalUrl,
|
url : req.customUrl || req.originalUrl,
|
||||||
brew : req.brew,
|
brew : req.brew,
|
||||||
brews : req.brews,
|
brews : req.brews,
|
||||||
@@ -556,6 +633,4 @@ app.use((req, res)=>{
|
|||||||
});
|
});
|
||||||
//^=====--------------------------------------=====^//
|
//^=====--------------------------------------=====^//
|
||||||
|
|
||||||
module.exports = {
|
export default app;
|
||||||
app : app
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const _ = require('lodash');
|
import _ from 'lodash';
|
||||||
|
|
||||||
// Default properties for newly-created brews
|
// Default properties for newly-created brews
|
||||||
const DEFAULT_BREW = {
|
const DEFAULT_BREW = {
|
||||||
@@ -32,7 +32,7 @@ const DEFAULT_BREW_LOAD = _.defaults(
|
|||||||
},
|
},
|
||||||
DEFAULT_BREW);
|
DEFAULT_BREW);
|
||||||
|
|
||||||
module.exports = {
|
export {
|
||||||
DEFAULT_BREW,
|
DEFAULT_BREW,
|
||||||
DEFAULT_BREW_LOAD
|
DEFAULT_BREW_LOAD
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
module.exports = require('nconf')
|
import nconf from 'nconf';
|
||||||
|
|
||||||
|
export default nconf
|
||||||
.argv()
|
.argv()
|
||||||
.env({ lowerCase: true })
|
.env({ lowerCase: true })
|
||||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// reused by both the main application and all tests which require database
|
// reused by both the main application and all tests which require database
|
||||||
// connection.
|
// connection.
|
||||||
|
|
||||||
const Mongoose = require('mongoose');
|
import Mongoose from 'mongoose';
|
||||||
|
|
||||||
const getMongoDBURL = (config)=>{
|
const getMongoDBURL = (config)=>{
|
||||||
return config.get('mongodb_uri') ||
|
return config.get('mongodb_uri') ||
|
||||||
@@ -31,7 +31,7 @@ const connect = async (config)=>{
|
|||||||
.catch((error)=>handleConnectionError(error));
|
.catch((error)=>handleConnectionError(error));
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
export default {
|
||||||
connect : connect,
|
connect,
|
||||||
disconnect : disconnect
|
disconnect
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
module.exports = (req, res, next)=>{
|
export default (req, res, next)=>{
|
||||||
if(process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'docker') return next();
|
if(process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'docker') return next();
|
||||||
if(req.header('x-forwarded-proto') !== 'https') {
|
if(req.header('x-forwarded-proto') !== 'https') {
|
||||||
return res.redirect(302, `https://${req.get('Host')}${req.url}`);
|
return res.redirect(302, `https://${req.get('Host')}${req.url}`);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
const googleDrive = require('@googleapis/drive');
|
import googleDrive from '@googleapis/drive';
|
||||||
const { nanoid } = require('nanoid');
|
import { nanoid } from 'nanoid';
|
||||||
const token = require('./token.js');
|
import token from './token.js';
|
||||||
const config = require('./config.js');
|
import config from './config.js';
|
||||||
|
|
||||||
|
|
||||||
let serviceAuth;
|
let serviceAuth;
|
||||||
if(!config.get('service_account')){
|
if(!config.get('service_account')){
|
||||||
@@ -59,7 +60,7 @@ const GoogleActions = {
|
|||||||
account.googleRefreshToken = tokens.refresh_token;
|
account.googleRefreshToken = tokens.refresh_token;
|
||||||
}
|
}
|
||||||
account.googleAccessToken = tokens.access_token;
|
account.googleAccessToken = tokens.access_token;
|
||||||
const JWTToken = token.generateAccessToken(account);
|
const JWTToken = token(account);
|
||||||
|
|
||||||
//Save updated token to cookie
|
//Save updated token to cookie
|
||||||
//res.cookie('nc_session', JWTToken, { maxAge: 1000*60*60*24*365, path: '/', sameSite: 'lax' });
|
//res.cookie('nc_session', JWTToken, { maxAge: 1000*60*60*24*365, path: '/', sameSite: 'lax' });
|
||||||
@@ -72,7 +73,7 @@ const GoogleActions = {
|
|||||||
getGoogleFolder : async (auth)=>{
|
getGoogleFolder : async (auth)=>{
|
||||||
const drive = googleDrive.drive({ version: 'v3', auth });
|
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
fileMetadata = {
|
const fileMetadata = {
|
||||||
'name' : 'Homebrewery',
|
'name' : 'Homebrewery',
|
||||||
'mimeType' : 'application/vnd.google-apps.folder'
|
'mimeType' : 'application/vnd.google-apps.folder'
|
||||||
};
|
};
|
||||||
@@ -240,8 +241,8 @@ const GoogleActions = {
|
|||||||
return obj.data.id;
|
return obj.data.id;
|
||||||
},
|
},
|
||||||
|
|
||||||
getGoogleBrew : async (id, accessId, accessType)=>{
|
getGoogleBrew : async (auth = defaultAuth, id, accessId, accessType)=>{
|
||||||
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
const drive = googleDrive.drive({ version: 'v3', auth: auth });
|
||||||
|
|
||||||
const obj = await drive.files.get({
|
const obj = await drive.files.get({
|
||||||
fileId : id,
|
fileId : id,
|
||||||
@@ -344,4 +345,4 @@ const GoogleActions = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = GoogleActions;
|
export default GoogleActions;
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
const _ = require('lodash');
|
import _ from 'lodash';
|
||||||
const HomebrewModel = require('./homebrew.model.js').model;
|
import {model as HomebrewModel} from './homebrew.model.js';
|
||||||
const router = require('express').Router();
|
import express from 'express';
|
||||||
const zlib = require('zlib');
|
import zlib from 'zlib';
|
||||||
const GoogleActions = require('./googleActions.js');
|
import GoogleActions from './googleActions.js';
|
||||||
const Markdown = require('../shared/naturalcrit/markdown.js');
|
import Markdown from '../shared/naturalcrit/markdown.js';
|
||||||
const yaml = require('js-yaml');
|
import yaml from 'js-yaml';
|
||||||
const asyncHandler = require('express-async-handler');
|
import asyncHandler from 'express-async-handler';
|
||||||
const { nanoid } = require('nanoid');
|
import { nanoid } from 'nanoid';
|
||||||
const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
|
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||||
|
import checkClientVersion from './middleware/check-client-version.js';
|
||||||
|
|
||||||
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
|
const router = express.Router();
|
||||||
|
|
||||||
const Themes = require('../themes/themes.json');
|
import { DEFAULT_BREW, DEFAULT_BREW_LOAD } from './brewDefaults.js';
|
||||||
|
import Themes from '../themes/themes.json' with { type: 'json' };
|
||||||
|
|
||||||
const isStaticTheme = (renderer, themeName)=>{
|
const isStaticTheme = (renderer, themeName)=>{
|
||||||
return Themes[renderer]?.[themeName] !== undefined;
|
return Themes[renderer]?.[themeName] !== undefined;
|
||||||
@@ -85,76 +87,68 @@ const api = {
|
|||||||
// 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)=>{
|
||||||
// Get relevant IDs for the brew
|
// Get relevant IDs for the brew
|
||||||
const { id, googleId } = api.getId(req);
|
let { id, googleId } = api.getId(req);
|
||||||
|
|
||||||
const accessMap = {
|
const accessMap = {
|
||||||
edit : { editId: id },
|
edit : { editId: id },
|
||||||
share : { shareId: id },
|
share : { shareId: id },
|
||||||
admin : {
|
admin : { $or : [{ editId: id }, { shareId: id }] }
|
||||||
$or : [
|
|
||||||
{ editId: id },
|
|
||||||
{ shareId: id },
|
|
||||||
] }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
|
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
|
||||||
let stub = await HomebrewModel.get(accessMap[accessType])
|
let stub = await HomebrewModel.get(accessMap[accessType])
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
if(googleId) {
|
if(googleId)
|
||||||
console.warn(`Unable to find document stub for ${accessType}Id ${id}`);
|
console.warn(`Unable to find document stub for ${accessType}Id ${id}`);
|
||||||
} else {
|
else
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
stub = stub?.toObject();
|
stub = stub?.toObject();
|
||||||
|
googleId ??= stub?.googleId;
|
||||||
|
|
||||||
|
const isOwner = (accessType == 'edit' && (!stub || stub?.authors?.length === 0)) || stub?.authors?.[0] === req.account?.username;
|
||||||
|
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||||
|
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||||
|
|
||||||
|
if(accessType === 'edit' && !(isOwner || isAuthor || isInvited)) {
|
||||||
|
const accessError = { name: 'Access Error', status: 401, authors: stub?.authors, brewTitle: stub?.title, shareId: stub?.shareId };
|
||||||
|
if(req.account)
|
||||||
|
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03' };
|
||||||
|
else
|
||||||
|
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04' };
|
||||||
|
}
|
||||||
|
|
||||||
if(stub?.lock?.locked && accessType != 'edit') {
|
if(stub?.lock?.locked && accessType != 'edit') {
|
||||||
throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title };
|
throw { HBErrorCode: '51', code: stub?.lock.code, message: stub?.lock.shareMessage, brewId: stub?.shareId, brewTitle: stub?.title };
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is a google id, try to find the google brew
|
// If there's a google id, get it if requesting the full brew or if no stub found yet
|
||||||
if(!stubOnly && (googleId || stub?.googleId)) {
|
if(googleId && (!stubOnly || !stub)) {
|
||||||
let googleError;
|
const oAuth2Client = isOwner ? GoogleActions.authCheck(req.account, res) : undefined;
|
||||||
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
|
|
||||||
.catch((err)=>{
|
const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType)
|
||||||
googleError = err;
|
.catch((googleError)=>{
|
||||||
});
|
|
||||||
// Throw any error caught while attempting to retrieve Google brew.
|
|
||||||
if(googleError) {
|
|
||||||
const reason = googleError.errors?.[0].reason;
|
const reason = googleError.errors?.[0].reason;
|
||||||
if(reason == 'notFound') {
|
if(reason == 'notFound')
|
||||||
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
|
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
|
||||||
} else {
|
else
|
||||||
throw { ...googleError, HBErrorCode: '01' };
|
throw { ...googleError, HBErrorCode: '01' };
|
||||||
}
|
});
|
||||||
}
|
|
||||||
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
||||||
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
|
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
|
||||||
}
|
}
|
||||||
const authorsExist = stub?.authors?.length > 0;
|
|
||||||
const isAuthor = stub?.authors?.includes(req.account?.username);
|
|
||||||
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
|
||||||
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
|
|
||||||
const accessError = { name: 'Access Error', status: 401 };
|
|
||||||
if(req.account){
|
|
||||||
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
|
|
||||||
}
|
|
||||||
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
|
|
||||||
}
|
|
||||||
|
|
||||||
// If after all of that we still don't have a brew, throw an exception
|
// If after all of that we still don't have a brew, throw an exception
|
||||||
if(!stub && !stubOnly) {
|
if(!stub)
|
||||||
throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id };
|
throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id };
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up brew: fill in missing fields with defaults / fix old invalid values
|
// Clean up brew: fill in missing fields with defaults / fix old invalid values
|
||||||
if(stub) {
|
|
||||||
stub.tags = stub.tags || undefined; // Clear empty strings
|
stub.tags = stub.tags || undefined; // Clear empty strings
|
||||||
stub.renderer = stub.renderer || undefined; // Clear empty strings
|
stub.renderer = stub.renderer || undefined; // Clear empty strings
|
||||||
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
|
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
|
||||||
}
|
|
||||||
|
|
||||||
req.brew = stub ?? {};
|
req.brew = stub;
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -473,12 +467,11 @@ const api = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
router.use('/api', require('./middleware/check-client-version.js'));
|
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
||||||
router.post('/api', asyncHandler(api.newBrew));
|
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
router.put('/api/update/:id', checkClientVersion, 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', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||||
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||||
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
|
||||||
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
||||||
|
|
||||||
module.exports = api;
|
export default api;
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||||
|
|
||||||
describe('Tests for api', ()=>{
|
describe('Tests for api', ()=>{
|
||||||
let api;
|
let api;
|
||||||
let google;
|
let google;
|
||||||
@@ -36,8 +38,9 @@ describe('Tests for api', ()=>{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
google = require('./googleActions.js');
|
google = require('./googleActions.js').default;
|
||||||
model = require('./homebrew.model.js').model;
|
model = require('./homebrew.model.js').model;
|
||||||
|
api = require('./homebrew.api').default;
|
||||||
|
|
||||||
jest.mock('./googleActions.js');
|
jest.mock('./googleActions.js');
|
||||||
google.authCheck = jest.fn(()=>'client');
|
google.authCheck = jest.fn(()=>'client');
|
||||||
@@ -54,8 +57,6 @@ describe('Tests for api', ()=>{
|
|||||||
setHeader : jest.fn(()=>{})
|
setHeader : jest.fn(()=>{})
|
||||||
};
|
};
|
||||||
|
|
||||||
api = require('./homebrew.api');
|
|
||||||
|
|
||||||
hbBrew = {
|
hbBrew = {
|
||||||
text : `brew text`,
|
text : `brew text`,
|
||||||
style : 'hello yes i am css',
|
style : 'hello yes i am css',
|
||||||
@@ -297,7 +298,7 @@ describe('Tests for api', ()=>{
|
|||||||
expect(next).toHaveBeenCalled();
|
expect(next).toHaveBeenCalled();
|
||||||
expect(api.getId).toHaveBeenCalledWith(req);
|
expect(api.getId).toHaveBeenCalledWith(req);
|
||||||
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||||
expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share');
|
expect(google.getGoogleBrew).toHaveBeenCalledWith(undefined, '2', '1', 'share');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('access is denied to a locked brew', async()=>{
|
it('access is denied to a locked brew', async()=>{
|
||||||
@@ -969,4 +970,57 @@ brew`);
|
|||||||
expect(res.send).toHaveBeenCalledWith('');
|
expect(res.send).toHaveBeenCalledWith('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('Split Text, Style, and Metadata', ()=>{
|
||||||
|
|
||||||
|
it('basic splitting', async ()=>{
|
||||||
|
const testBrew = {
|
||||||
|
text : '```metadata\n' +
|
||||||
|
'title: title\n' +
|
||||||
|
'description: description\n' +
|
||||||
|
'tags: [ \'tag a\' , \'tag b\' ]\n' +
|
||||||
|
'systems: [ test system ]\n' +
|
||||||
|
'renderer: legacy\n' +
|
||||||
|
'theme: 5ePHB\n' +
|
||||||
|
'lang: en\n' +
|
||||||
|
'\n' +
|
||||||
|
'```\n' +
|
||||||
|
'\n' +
|
||||||
|
'```css\n' +
|
||||||
|
'style\n' +
|
||||||
|
'style\n' +
|
||||||
|
'style\n' +
|
||||||
|
'```\n' +
|
||||||
|
'\n' +
|
||||||
|
'text\n'
|
||||||
|
};
|
||||||
|
|
||||||
|
splitTextStyleAndMetadata(testBrew);
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
expect(testBrew.title).toEqual('title');
|
||||||
|
expect(testBrew.description).toEqual('description');
|
||||||
|
expect(testBrew.tags).toEqual(['tag a', 'tag b']);
|
||||||
|
expect(testBrew.systems).toEqual(['test system']);
|
||||||
|
expect(testBrew.renderer).toEqual('legacy');
|
||||||
|
expect(testBrew.theme).toEqual('5ePHB');
|
||||||
|
expect(testBrew.lang).toEqual('en');
|
||||||
|
// Style
|
||||||
|
expect(testBrew.style).toEqual('style\nstyle\nstyle');
|
||||||
|
// Text
|
||||||
|
expect(testBrew.text).toEqual('text\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('convert tags string to array', async ()=>{
|
||||||
|
const testBrew = {
|
||||||
|
text : '```metadata\n' +
|
||||||
|
'tags: tag a\n' +
|
||||||
|
'```\n\n'
|
||||||
|
};
|
||||||
|
|
||||||
|
splitTextStyleAndMetadata(testBrew);
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
expect(testBrew.tags).toEqual(['tag a']);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
const mongoose = require('mongoose');
|
import mongoose from 'mongoose';
|
||||||
const { nanoid } = require('nanoid');
|
import { nanoid } from 'nanoid';
|
||||||
const _ = require('lodash');
|
import _ from 'lodash';
|
||||||
const zlib = require('zlib');
|
import zlib from 'zlib';
|
||||||
|
|
||||||
|
|
||||||
const HomebrewSchema = mongoose.Schema({
|
const HomebrewSchema = mongoose.Schema({
|
||||||
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
|
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
|
||||||
@@ -44,7 +45,7 @@ HomebrewSchema.statics.get = async function(query, fields=null){
|
|||||||
const brew = await Homebrew.findOne(query, fields).orFail()
|
const brew = await Homebrew.findOne(query, fields).orFail()
|
||||||
.catch((error)=>{throw 'Can not find brew';});
|
.catch((error)=>{throw 'Can not find brew';});
|
||||||
if(!_.isNil(brew.textBin)) { // Uncompress zipped text field
|
if(!_.isNil(brew.textBin)) { // Uncompress zipped text field
|
||||||
unzipped = zlib.inflateRawSync(brew.textBin);
|
const unzipped = zlib.inflateRawSync(brew.textBin);
|
||||||
brew.text = unzipped.toString();
|
brew.text = unzipped.toString();
|
||||||
}
|
}
|
||||||
return brew;
|
return brew;
|
||||||
@@ -62,7 +63,7 @@ HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, f
|
|||||||
|
|
||||||
const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
|
const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
|
||||||
|
|
||||||
module.exports = {
|
export {
|
||||||
schema : HomebrewSchema,
|
HomebrewSchema as schema,
|
||||||
model : Homebrew,
|
Homebrew as model
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
module.exports = (req, res, next)=>{
|
import packageJSON from '../../package.json' with { type: 'json' };
|
||||||
const userVersion = req.get('Homebrewery-Version');
|
|
||||||
const version = require('../../package.json').version;
|
|
||||||
|
|
||||||
if(userVersion != version) {
|
export default (req, res, next)=>{
|
||||||
|
const userVersion = req.get('Homebrewery-Version');
|
||||||
|
const version = packageJSON.version;
|
||||||
|
|
||||||
|
if(userVersion !== version) {
|
||||||
return res.status(412).send({
|
return res.status(412).send({
|
||||||
message : `Client version ${userVersion} is out of date. Please save your changes elsewhere and refresh to pick up client version ${version}.`
|
message : `Client version ${userVersion} is out of date. Please save your changes elsewhere and refresh to pick up client version ${version}.`
|
||||||
});
|
});
|
||||||
@@ -10,3 +12,4 @@ module.exports = (req, res, next)=>{
|
|||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
const config = require('../config.js');
|
import config from '../config.js';
|
||||||
const nodeEnv = config.get('node_env');
|
const nodeEnv = config.get('node_env');
|
||||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||||
|
|
||||||
module.exports = (req, res, next)=>{
|
export default (req, res, next)=>{
|
||||||
const isImageRequest = req.get('Accept')?.split(',')
|
const isImageRequest = req.get('Accept')?.split(',')
|
||||||
?.filter((h)=>!h.includes('q='))
|
?.filter((h)=>!h.includes('q='))
|
||||||
?.every((h)=>/image\/.*/.test(h));
|
?.every((h)=>/image\/.*/.test(h));
|
||||||
if(isImageRequest && !isLocalEnvironment && !req.url?.startsWith('/staticImages')) {
|
if(isImageRequest && !(isLocalEnvironment && req.url?.startsWith('/staticImages'))) {
|
||||||
return res.status(406).send({
|
return res.status(406).send({
|
||||||
message : 'Request for image at this URL is not supported'
|
message : 'Request for image at this URL is not supported'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const contentNegotiationMiddleware = require('./content-negotiation.js');
|
import contentNegotiationMiddleware from './content-negotiation.js';
|
||||||
|
|
||||||
describe('content-negotiation-middleware', ()=>{
|
describe('content-negotiation-middleware', ()=>{
|
||||||
let request;
|
let request;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const mongoose = require('mongoose');
|
import mongoose from 'mongoose';
|
||||||
const _ = require('lodash');
|
import _ from 'lodash';
|
||||||
|
|
||||||
const NotificationSchema = new mongoose.Schema({
|
const NotificationSchema = new mongoose.Schema({
|
||||||
dismissKey : { type: String, unique: true, required: true },
|
dismissKey : { type: String, unique: true, required: true },
|
||||||
@@ -56,7 +56,7 @@ NotificationSchema.statics.getAll = async function() {
|
|||||||
|
|
||||||
const Notification = mongoose.model('Notification', NotificationSchema);
|
const Notification = mongoose.model('Notification', NotificationSchema);
|
||||||
|
|
||||||
module.exports = {
|
export {
|
||||||
schema : NotificationSchema,
|
NotificationSchema as schema,
|
||||||
model : Notification,
|
Notification as model
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const expressStaticGzip = require('express-static-gzip');
|
import expressStaticGzip from 'express-static-gzip';
|
||||||
|
|
||||||
// Serve brotli-compressed static files if available
|
// Serve brotli-compressed static files if available
|
||||||
const customCacheControlHandler=(response, path)=>{
|
const customCacheControlHandler=(response, path)=>{
|
||||||
@@ -28,4 +28,4 @@ const init=(pathToAssets)=>{
|
|||||||
} });
|
} });
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = init;
|
export default init;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
const jwt = require('jwt-simple');
|
import jwt from 'jwt-simple';
|
||||||
|
import config from './config.js';
|
||||||
// Load configuration values
|
|
||||||
const config = require('./config.js');
|
|
||||||
|
|
||||||
// Generate an Access Token for the given User ID
|
// Generate an Access Token for the given User ID
|
||||||
const generateAccessToken = (account)=>{
|
const generateAccessToken = (account)=>{
|
||||||
@@ -24,6 +22,4 @@ const generateAccessToken = (account)=>{
|
|||||||
return token;
|
return token;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
export default generateAccessToken;
|
||||||
generateAccessToken : generateAccessToken
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
const express = require('express');
|
import express from 'express';
|
||||||
const asyncHandler = require('express-async-handler');
|
import asyncHandler from 'express-async-handler';
|
||||||
const HomebrewModel = require('./homebrew.model.js').model;
|
import {model as HomebrewModel } from './homebrew.model.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -106,4 +106,4 @@ const findTotal = async (req, res)=>{
|
|||||||
router.get('/api/vault/total', asyncHandler(findTotal));
|
router.get('/api/vault/total', asyncHandler(findTotal));
|
||||||
router.get('/api/vault', asyncHandler(findBrews));
|
router.get('/api/vault', asyncHandler(findBrews));
|
||||||
|
|
||||||
module.exports = router;
|
export default router;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const _ = require('lodash');
|
import _ from 'lodash';
|
||||||
const yaml = require('js-yaml');
|
import yaml from 'js-yaml';
|
||||||
const request = require('../client/homebrew/utils/request-middleware.js');
|
import request from '../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');
|
||||||
@@ -21,6 +21,9 @@ const splitTextStyleAndMetadata = (brew)=>{
|
|||||||
brew.snippets = brew.text.slice(11, index - 1);
|
brew.snippets = brew.text.slice(11, index - 1);
|
||||||
brew.text = brew.text.slice(index + 5);
|
brew.text = brew.text.slice(index + 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle old brews that still have empty strings in the tags metadata
|
||||||
|
if(typeof brew.tags === 'string') brew.tags = brew.tags ? [brew.tags] : [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const printCurrentBrew = ()=>{
|
const printCurrentBrew = ()=>{
|
||||||
@@ -51,7 +54,7 @@ const fetchThemeBundle = async (obj, renderer, theme)=>{
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
export {
|
||||||
splitTextStyleAndMetadata,
|
splitTextStyleAndMetadata,
|
||||||
printCurrentBrew,
|
printCurrentBrew,
|
||||||
fetchThemeBundle,
|
fetchThemeBundle,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const diceFont = require('../../../themes/fonts/iconFonts/diceFont.js');
|
import diceFont from '../../../themes/fonts/iconFonts/diceFont.js';
|
||||||
const elderberryInn = require('../../../themes/fonts/iconFonts/elderberryInn.js');
|
import elderberryInn from '../../../themes/fonts/iconFonts/elderberryInn.js';
|
||||||
const fontAwesome = require('../../../themes/fonts/iconFonts/fontAwesome.js');
|
import fontAwesome from '../../../themes/fonts/iconFonts/fontAwesome.js';
|
||||||
const gameIcons = require('../../../themes/fonts/iconFonts/gameIcons.js');
|
import gameIcons from '../../../themes/fonts/iconFonts/gameIcons.js';
|
||||||
|
|
||||||
const emojis = {
|
const emojis = {
|
||||||
...diceFont,
|
...diceFont,
|
||||||
|
|||||||
@@ -11,33 +11,38 @@
|
|||||||
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
||||||
|
|
||||||
@keyframes sourceMoveAnimation {
|
@keyframes sourceMoveAnimation {
|
||||||
50% {background-color: red; color: white;}
|
50% { color : white;background-color : red;}
|
||||||
100% {background-color: unset; color: unset;}
|
100% { color : unset;background-color : unset;}
|
||||||
}
|
}
|
||||||
|
|
||||||
.codeEditor{
|
.codeEditor {
|
||||||
@media screen and (pointer : coarse) {
|
@media screen and (pointer : coarse) {
|
||||||
font-size : 16px;
|
font-size : 16px;
|
||||||
}
|
}
|
||||||
.CodeMirror-foldmarker {
|
.CodeMirror-foldmarker {
|
||||||
font-family: inherit;
|
font-family : inherit;
|
||||||
text-shadow: none;
|
font-weight : 600;
|
||||||
font-weight: 600;
|
color : grey;
|
||||||
color: grey;
|
text-shadow : none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sourceMoveFlash .CodeMirror-line{
|
.CodeMirror-foldgutter {
|
||||||
animation-name: sourceMoveAnimation;
|
cursor : pointer;
|
||||||
animation-duration: 0.4s;
|
border-left : 1px solid #EEEEEE;
|
||||||
|
transition : background 0.1s;
|
||||||
|
&:hover { background : #DDDDDD; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sourceMoveFlash .CodeMirror-line {
|
||||||
|
animation-name : sourceMoveAnimation;
|
||||||
|
animation-duration : 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-vscrollbar {
|
.CodeMirror-vscrollbar {
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar { width : 20px; }
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
width: 20px;
|
width : 20px;
|
||||||
background: linear-gradient(90deg, #858585 15px, #808080 15px);
|
background : linear-gradient(90deg, #858585 15px, #808080 15px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +59,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.emojiPreview {
|
.emojiPreview {
|
||||||
font-size: 1.5em;
|
font-size : 1.5em;
|
||||||
line-height: 1.2em;
|
line-height : 1.2em;
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
const _ = require('lodash');
|
import _ from 'lodash';
|
||||||
const Marked = require('marked');
|
import { Parser as MathParser } from 'expr-eval';
|
||||||
const MarkedExtendedTables = require('marked-extended-tables');
|
import { marked as Marked } from 'marked';
|
||||||
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
|
import MarkedExtendedTables from 'marked-extended-tables';
|
||||||
const { gfmHeadingId: MarkedGFMHeadingId, resetHeadings: MarkedGFMResetHeadingIDs } = require('marked-gfm-heading-id');
|
import { markedSmartypantsLite as MarkedSmartypantsLite } from 'marked-smartypants-lite';
|
||||||
const { markedEmoji: MarkedEmojis } = require('marked-emoji');
|
import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id';
|
||||||
|
import { markedEmoji as MarkedEmojis } from 'marked-emoji';
|
||||||
|
|
||||||
//Icon fonts included so they can appear in emoji autosuggest dropdown
|
//Icon fonts included so they can appear in emoji autosuggest dropdown
|
||||||
const diceFont = require('../../themes/fonts/iconFonts/diceFont.js');
|
import diceFont from '../../themes/fonts/iconFonts/diceFont.js';
|
||||||
const elderberryInn = require('../../themes/fonts/iconFonts/elderberryInn.js');
|
import elderberryInn from '../../themes/fonts/iconFonts/elderberryInn.js';
|
||||||
const fontAwesome = require('../../themes/fonts/iconFonts/fontAwesome.js');
|
import gameIcons from '../../themes/fonts/iconFonts/gameIcons.js';
|
||||||
const gameIcons = require('../../themes/fonts/iconFonts/gameIcons.js');
|
import fontAwesome from '../../themes/fonts/iconFonts/fontAwesome.js';
|
||||||
|
|
||||||
const MathParser = require('expr-eval').Parser;
|
|
||||||
const renderer = new Marked.Renderer();
|
const renderer = new Marked.Renderer();
|
||||||
const tokenizer = new Marked.Tokenizer();
|
const tokenizer = new Marked.Tokenizer();
|
||||||
|
|
||||||
@@ -391,6 +391,27 @@ const forcedParagraphBreaks = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const nonbreakingSpaces = {
|
||||||
|
name : 'nonbreakingSpaces',
|
||||||
|
level : 'inline',
|
||||||
|
start(src) { return src.match(/:>+/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
const regex = /:(>+)/ym;
|
||||||
|
const match = regex.exec(src);
|
||||||
|
if(match?.length) {
|
||||||
|
return {
|
||||||
|
type : 'nonbreakingSpaces', // Should match "name" above
|
||||||
|
raw : match[0], // Text to consume from the source
|
||||||
|
length : match[1].length,
|
||||||
|
text : ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
return ` `.repeat(token.length).concat('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const definitionListsSingleLine = {
|
const definitionListsSingleLine = {
|
||||||
name : 'definitionListsSingleLine',
|
name : 'definitionListsSingleLine',
|
||||||
level : 'block',
|
level : 'block',
|
||||||
@@ -748,11 +769,12 @@ const tableTerminators = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
Marked.use(MarkedVariables());
|
Marked.use(MarkedVariables());
|
||||||
Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts,
|
Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks,
|
||||||
mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
nonbreakingSpaces, 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(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
|
Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }),
|
||||||
|
MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
|
||||||
|
|
||||||
function cleanUrl(href) {
|
function cleanUrl(href) {
|
||||||
try {
|
try {
|
||||||
@@ -854,7 +876,7 @@ const globalVarsList = {};
|
|||||||
let varsQueue = [];
|
let varsQueue = [];
|
||||||
let globalPageNumber = 0;
|
let globalPageNumber = 0;
|
||||||
|
|
||||||
module.exports = {
|
const Markdown = {
|
||||||
marked : Marked,
|
marked : Marked,
|
||||||
render : (rawBrewText, pageNumber=0)=>{
|
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
|
||||||
@@ -865,6 +887,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`);
|
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`);
|
||||||
|
|
||||||
const opts = Marked.defaults;
|
const opts = Marked.defaults;
|
||||||
|
|
||||||
rawBrewText = opts.hooks.preprocess(rawBrewText);
|
rawBrewText = opts.hooks.preprocess(rawBrewText);
|
||||||
@@ -935,3 +958,6 @@ module.exports = {
|
|||||||
return errors;
|
return errors;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default Markdown;
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ html,body, #reactRoot{
|
|||||||
*{
|
*{
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
}
|
}
|
||||||
button{
|
.colorButton(@backgroundColor : @green){
|
||||||
.button();
|
|
||||||
}
|
|
||||||
.button(@backgroundColor : @green){
|
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
padding : 0.6em 1.2em;
|
padding : 0.6em 1.2em;
|
||||||
@@ -46,5 +43,6 @@ button{
|
|||||||
}
|
}
|
||||||
&:disabled{
|
&:disabled{
|
||||||
background-color : @silver !important;
|
background-color : @silver !important;
|
||||||
|
cursor:not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
:where(html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video){
|
:where(html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,button,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video){
|
||||||
border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0
|
border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,3 +25,9 @@
|
|||||||
:where(table){
|
:where(table){
|
||||||
border-collapse:collapse;border-spacing:0
|
border-collapse:collapse;border-spacing:0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:where(button) {
|
||||||
|
background-color: unset;
|
||||||
|
text-transform: unset;
|
||||||
|
color: unset;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
|
||||||
test('Processes the markdown within an HTML block if its just a class wrapper', function() {
|
test('Processes the markdown within an HTML block if its just a class wrapper', function() {
|
||||||
const source = '<div>*Bold text*</div>';
|
const source = '<div>*Bold text*</div>';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
|
||||||
describe('Inline Definition Lists', ()=>{
|
describe('Inline Definition Lists', ()=>{
|
||||||
test('No Term 1 Definition', function() {
|
test('No Term 1 Definition', function() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const Markdown = require('naturalcrit/markdown.js');
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
// Marked.js adds line returns after closing tags on some default tokens.
|
// Marked.js adds line returns after closing tags on some default tokens.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
|
||||||
describe('Hard Breaks', ()=>{
|
describe('Hard Breaks', ()=>{
|
||||||
test('Single Break', function() {
|
test('Single Break', function() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
|
||||||
// Marked.js adds line returns after closing tags on some default tokens.
|
// Marked.js adds line returns after closing tags on some default tokens.
|
||||||
// This removes those line returns for comparison sake.
|
// This removes those line returns for comparison sake.
|
||||||
|
|||||||
72
tests/markdown/non-breaking-spaces.test.js
Normal file
72
tests/markdown/non-breaking-spaces.test.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
|
||||||
|
describe('Non-Breaking Spaces', ()=>{
|
||||||
|
test('Single Space', function() {
|
||||||
|
const source = ':>\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Double Space', function() {
|
||||||
|
const source = ':>>\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Triple Space', function() {
|
||||||
|
const source = ':>>>\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Many Space', function() {
|
||||||
|
const source = ':>>>>>>>>>>\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple sets of Spaces', function() {
|
||||||
|
const source = ':>>>\n:>>>\n:>>>';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> \n \n </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Pair of inline Spaces', function() {
|
||||||
|
const source = ':>>:>>';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Space 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\n \nLine 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>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('I am actually a single-line definition list!', function() {
|
||||||
|
const source = 'Term ::> Definition 1\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>Term</dt><dd>> Definition 1</dd>\n</dl>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('I am actually a definition list!', function() {
|
||||||
|
const source = 'Term\n::> Definition 1\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>Term</dt>\n<dd>> Definition 1</dd></dl>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('I am actually a two-term definition list!', function() {
|
||||||
|
const source = 'Term\n::> Definition 1\n::>> Definition 2';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>Term</dt>\n<dd>> Definition 1</dd>\n<dd>>> Definition 2</dd></dl>`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
|
||||||
// Marked.js adds line returns after closing tags on some default tokens.
|
// Marked.js adds line returns after closing tags on some default tokens.
|
||||||
// This removes those line returns for comparison sake.
|
// This removes those line returns for comparison sake.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
const supertest = require('supertest');
|
import supertest from 'supertest';
|
||||||
|
import HBApp from 'app.js';
|
||||||
|
|
||||||
// Mimic https responses to avoid being redirected all the time
|
// Mimic https responses to avoid being redirected all the time
|
||||||
const app = supertest.agent(require('app.js').app)
|
const app = supertest.agent(HBApp).set('X-Forwarded-Proto', 'https');
|
||||||
.set('X-Forwarded-Proto', 'https');
|
|
||||||
|
|
||||||
describe('Tests for static pages', ()=>{
|
describe('Tests for static pages', ()=>{
|
||||||
it('Home page works', ()=>{
|
it('Home page works', ()=>{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
|
import Markdown from '../../../../shared/naturalcrit/markdown.js';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createFooterFunc : function(headerSize=1){
|
createFooterFunc : function(headerSize=1){
|
||||||
|
|||||||
@@ -93,4 +93,4 @@ const diceFont = {
|
|||||||
'df_solid_small_dot_d6_6' : 'df solid-small-dot-d6-6'
|
'df_solid_small_dot_d6_6' : 'df solid-small-dot-d6-6'
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = diceFont;
|
export default diceFont;
|
||||||
@@ -206,4 +206,4 @@ const elderberryInn = {
|
|||||||
'ei_wish' : 'ei wish'
|
'ei_wish' : 'ei wish'
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = elderberryInn;
|
export default elderberryInn;
|
||||||
@@ -2051,4 +2051,4 @@ const fontAwesome = {
|
|||||||
'fab_zhihu' : 'fab fa-zhihu'
|
'fab_zhihu' : 'fab fa-zhihu'
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = fontAwesome;
|
export default fontAwesome;
|
||||||
@@ -506,4 +506,4 @@ const gameIcons = {
|
|||||||
'gi_acid' : 'gi acid'
|
'gi_acid' : 'gi acid'
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = gameIcons;
|
export default gameIcons;
|
||||||
Reference in New Issue
Block a user