mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-25 09:43:03 +00:00
Compare commits
317 Commits
rebuildPac
...
editBasePa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90ceb52ffc | ||
|
|
6f9caf0590 | ||
|
|
253dbb358b | ||
|
|
719edd82c5 | ||
|
|
63d957fdc6 | ||
|
|
7751c0e37b | ||
|
|
990bf80b59 | ||
|
|
f16598f238 | ||
|
|
579e9e0ec5 | ||
|
|
f6629f2f9e | ||
|
|
b87c78474d | ||
|
|
958d282a58 | ||
|
|
7e56ae2019 | ||
|
|
ebca50ed4b | ||
|
|
bfd14757c2 | ||
|
|
3626ed5a31 | ||
|
|
d385bacdd6 | ||
|
|
cbbb2c0a7d | ||
|
|
fbe637ff82 | ||
|
|
82bd16c623 | ||
|
|
d1f13af67b | ||
|
|
b6c03e88b8 | ||
|
|
b587d17397 | ||
|
|
0a02f910f8 | ||
|
|
ddfa06e76b | ||
|
|
0c2b1fec04 | ||
|
|
6de7a64acd | ||
|
|
b9fe4c3901 | ||
|
|
5ae01862e5 | ||
|
|
398df7a061 | ||
|
|
443b0f6a37 | ||
|
|
544175b994 | ||
|
|
955602e7ee | ||
|
|
90e577dd3f | ||
|
|
828208aadb | ||
|
|
973e071e93 | ||
|
|
f9e7aa355d | ||
|
|
24dfd41714 | ||
|
|
638e54535d | ||
|
|
cbc6956221 | ||
|
|
248d2038ec | ||
|
|
5b66175b8c | ||
|
|
552aa7d41a | ||
|
|
b0a108b543 | ||
|
|
505d2840c0 | ||
|
|
41ff50fefe | ||
|
|
2fbcc84a50 | ||
|
|
45e4d27c0a | ||
|
|
77bf3ffc6f | ||
|
|
bc045ec6c9 | ||
|
|
6390ea076a | ||
|
|
6affcb587d | ||
|
|
7787afabff | ||
|
|
fb4a8e5cf1 | ||
|
|
8432a6e367 | ||
|
|
90ee08de42 | ||
|
|
40839b18e4 | ||
|
|
677c02cfa5 | ||
|
|
a7a8803e9d | ||
|
|
5fbc111db7 | ||
|
|
5edea7d0f4 | ||
|
|
d3a9d813c9 | ||
|
|
fc475b2a7e | ||
|
|
76b76b3bb6 | ||
|
|
22ef3cbebc | ||
|
|
9da8a17053 | ||
|
|
7cadbfbd7b | ||
|
|
98b9e86787 | ||
|
|
489b4b2694 | ||
|
|
8d279260c2 | ||
|
|
7c08c430d0 | ||
|
|
45689d119e | ||
|
|
c5805af935 | ||
|
|
b2c4bb7082 | ||
|
|
68460447dc | ||
|
|
440c7beff6 | ||
|
|
c7610cf0f8 | ||
|
|
7f3a818558 | ||
|
|
bc82afa5b2 | ||
|
|
abef250631 | ||
|
|
1794e96d50 | ||
|
|
25f25da499 | ||
|
|
aa15bdaacb | ||
|
|
7ba7991631 | ||
|
|
0e1ac26999 | ||
|
|
f49fed8c35 | ||
|
|
a8236fbab4 | ||
|
|
daf4eceedd | ||
|
|
a02361ee65 | ||
|
|
81e20f032e | ||
|
|
1d92b98568 | ||
|
|
0f4157d084 | ||
|
|
4dcc3749d8 | ||
|
|
8f058d56f2 | ||
|
|
d192a064d6 | ||
|
|
cccb531e17 | ||
|
|
6414e73e7d | ||
|
|
41daf8d172 | ||
|
|
4c897fdeb5 | ||
|
|
89ce4de354 | ||
|
|
43095507ee | ||
|
|
eb7fbbe018 | ||
|
|
869958ec38 | ||
|
|
99b90e0998 | ||
|
|
57a48100d3 | ||
|
|
8538e4fadb | ||
|
|
9a002511a3 | ||
|
|
3fa3a52e05 | ||
|
|
4fe920dac3 | ||
|
|
71dff5fbf9 | ||
|
|
26419d2ccb | ||
|
|
f02fe2d8f3 | ||
|
|
318fb53eb2 | ||
|
|
6a32b7427b | ||
|
|
5886bd65e5 | ||
|
|
9c5f80cbdb | ||
|
|
79d8956c4f | ||
|
|
2e491b3556 | ||
|
|
d9a8afa272 | ||
|
|
209195202c | ||
|
|
64235c844a | ||
|
|
5d000a4599 | ||
|
|
380e593b42 | ||
|
|
169f089d08 | ||
|
|
b3977ed141 | ||
|
|
9800561de7 | ||
|
|
166af08e6a | ||
|
|
48f17f7c5e | ||
|
|
87c9f52222 | ||
|
|
c80b7ffd66 | ||
|
|
5f16ce3dbd | ||
|
|
b5ff26f857 | ||
|
|
578b01bbb1 | ||
|
|
67467e0099 | ||
|
|
da21bf20f9 | ||
|
|
df7fcf1e5f | ||
|
|
702ece6671 | ||
|
|
1008321957 | ||
|
|
b547486c48 | ||
|
|
e1e661976d | ||
|
|
7bdeeee9ef | ||
|
|
becf35d336 | ||
|
|
d7585767c9 | ||
|
|
f9bb6209b7 | ||
|
|
13702a2f62 | ||
|
|
a6a684c89e | ||
|
|
862fa7de89 | ||
|
|
b671cf7b02 | ||
|
|
d5dbe0b4ba | ||
|
|
c2cf695c17 | ||
|
|
6d0d6f08b5 | ||
|
|
77dcc9b433 | ||
|
|
5f2f3a6f3d | ||
|
|
bbb812cb06 | ||
|
|
5648e55774 | ||
|
|
c051580545 | ||
|
|
6e72fe2600 | ||
|
|
03602ae1e0 | ||
|
|
8de738a146 | ||
|
|
6960beb739 | ||
|
|
6748639ec5 | ||
|
|
e5651807fd | ||
|
|
9adf6dee61 | ||
|
|
03527a1f95 | ||
|
|
651863b0f7 | ||
|
|
450ecd24b7 | ||
|
|
995cfa2aa4 | ||
|
|
5eecb5ea20 | ||
|
|
0885473b66 | ||
|
|
eabff4f6b2 | ||
|
|
a773df25d0 | ||
|
|
b07f75ac36 | ||
|
|
ed5fbadd73 | ||
|
|
c74c2c8efe | ||
|
|
1efe570dae | ||
|
|
2571460f42 | ||
|
|
dbb67113b9 | ||
|
|
33e3e018f3 | ||
|
|
07adf0342d | ||
|
|
b2b1cb4985 | ||
|
|
c4d6cc4579 | ||
|
|
01fbb4439e | ||
|
|
eb48d981d6 | ||
|
|
3624fcef0f | ||
|
|
ab62f0fcf9 | ||
|
|
9e78671e4f | ||
|
|
f64a7b38ae | ||
|
|
3fdedd8861 | ||
|
|
1d4ebbb689 | ||
|
|
c4f148a3a1 | ||
|
|
7abf45e8ba | ||
|
|
bbae62e0b7 | ||
|
|
a9d71078d3 | ||
|
|
5bde870586 | ||
|
|
7ea78870bf | ||
|
|
393caa86eb | ||
|
|
9b7a3c5c70 | ||
|
|
fe69bd50b5 | ||
|
|
a2c4f604b3 | ||
|
|
083e8c9b52 | ||
|
|
d2a025ca41 | ||
|
|
181d6b7e0a | ||
|
|
dd20fc8475 | ||
|
|
33ea397915 | ||
|
|
320fb02543 | ||
|
|
e127a6a557 | ||
|
|
e774dfd97d | ||
|
|
1dcea0fe6a | ||
|
|
0ca53f8db6 | ||
|
|
5395a759ed | ||
|
|
8f470fb000 | ||
|
|
90c375a5c8 | ||
|
|
e8cc4a0c58 | ||
|
|
cf68cc46ad | ||
|
|
653e20b4e4 | ||
|
|
e97d45e5b5 | ||
|
|
691cd048e2 | ||
|
|
5071105f8c | ||
|
|
9cd009e89b | ||
|
|
acaf293c7c | ||
|
|
79503dd17f | ||
|
|
485b6a0041 | ||
|
|
983781303b | ||
|
|
9c8e03f961 | ||
|
|
a298288888 | ||
|
|
c48703aed5 | ||
|
|
09000bd20f | ||
|
|
237caa84f7 | ||
|
|
d292d60ee9 | ||
|
|
395e406d65 | ||
|
|
806c3f63bb | ||
|
|
4a296809a0 | ||
|
|
f8361fa141 | ||
|
|
8542056d6e | ||
|
|
f23be91b6d | ||
|
|
f810bea4c8 | ||
|
|
42136b89fd | ||
|
|
eb604d9201 | ||
|
|
e341069196 | ||
|
|
3a54ac9d7d | ||
|
|
42d8c1b33f | ||
|
|
f700620373 | ||
|
|
0f059bce66 | ||
|
|
0eb68aaf72 | ||
|
|
b9f825c168 | ||
|
|
58c2504394 | ||
|
|
a9aadbfef9 | ||
|
|
dae5922fd0 | ||
|
|
5fb20991bb | ||
|
|
75fe7b2c67 | ||
|
|
ab400b82d6 | ||
|
|
6867cb5a4a | ||
|
|
742de8582c | ||
|
|
600ff5f367 | ||
|
|
e751facf32 | ||
|
|
959d5fb6c9 | ||
|
|
3456d503b2 | ||
|
|
9ef291a8ae | ||
|
|
ff174870e2 | ||
|
|
a015714d5e | ||
|
|
9bcab7b82b | ||
|
|
bc0cb0d0be | ||
|
|
ce4299a1f0 | ||
|
|
398e985e65 | ||
|
|
a5f597f598 | ||
|
|
beb7ecd0a9 | ||
|
|
ea625a0fbc | ||
|
|
932120883b | ||
|
|
b29406da8b | ||
|
|
4cc2d429c5 | ||
|
|
77563d12a6 | ||
|
|
b914bf3bf5 | ||
|
|
6f52b8473f | ||
|
|
44713eda4e | ||
|
|
e552282299 | ||
|
|
9ecd53267f | ||
|
|
5ee1cf6aa5 | ||
|
|
1295f635dc | ||
|
|
60142d9467 | ||
|
|
6dc4355972 | ||
|
|
555a26f0d6 | ||
|
|
abce7d8531 | ||
|
|
678d981121 | ||
|
|
32f8c18adc | ||
|
|
0aead96dcf | ||
|
|
c238094e4c | ||
|
|
657eeea4d5 | ||
|
|
1e34e85aab | ||
|
|
b747968e74 | ||
|
|
25629173c9 | ||
|
|
96642c07d3 | ||
|
|
2bd0f909f3 | ||
|
|
9b4047f3f9 | ||
|
|
91e2916199 | ||
|
|
3fcc677f96 | ||
|
|
3f77e32550 | ||
|
|
c4903c4993 | ||
|
|
630f9002aa | ||
|
|
aea7809fbd | ||
|
|
30e644d5e0 | ||
|
|
fe2f5a405c | ||
|
|
07a1890ed9 | ||
|
|
fc400c226c | ||
|
|
8e3ccec855 | ||
|
|
25c09bc241 | ||
|
|
0eaba3de01 | ||
|
|
ece1a7e9a7 | ||
|
|
2ef7a1521b | ||
|
|
8f4c74d0ce | ||
|
|
2589e6d919 | ||
|
|
b7a7446f75 | ||
|
|
551763fecb | ||
|
|
4b9b1ec9ac | ||
|
|
01f075d3f5 | ||
|
|
de18a53efe | ||
|
|
caca578709 | ||
|
|
09ac8b8a32 |
@@ -64,9 +64,6 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Test - Mustache Spans
|
name: Test - Mustache Spans
|
||||||
command: npm run test:mustache-syntax
|
command: npm run test:mustache-syntax
|
||||||
- run:
|
|
||||||
name: Test - Definition Lists
|
|
||||||
command: npm run test:definition-lists
|
|
||||||
- run:
|
- run:
|
||||||
name: Test - Hard Breaks
|
name: Test - Hard Breaks
|
||||||
command: npm run test:hard-breaks
|
command: npm run test:hard-breaks
|
||||||
|
|||||||
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -5,6 +5,15 @@ updates:
|
|||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
open-pull-requests-limit: 99
|
open-pull-requests-limit: 99
|
||||||
|
groups:
|
||||||
|
dev-dependencies:
|
||||||
|
dependency-type: "development"
|
||||||
|
patterns: ["*"]
|
||||||
|
update-types: ["patch", "minor"]
|
||||||
|
prod-dependencies:
|
||||||
|
dependency-type: "production"
|
||||||
|
patterns: ["*"]
|
||||||
|
update-types: ["patch", "minor"]
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: eslint
|
- dependency-name: eslint
|
||||||
versions:
|
versions:
|
||||||
|
|||||||
86
changelog.md
86
changelog.md
@@ -88,24 +88,94 @@ pre {
|
|||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
### Tuesday 03/18/2025 - v3.18.1
|
### Wednesday 7/09/2025 - v3.19.3
|
||||||
|
|
||||||
{{taskList
|
{{taskList
|
||||||
|
##### calculuschild
|
||||||
|
* [x] Restoring original saving behavior; will continue investigating why save was failing for some users in background
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
### Wednesday 7/09/2025 - v3.19.2
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### calculuschild
|
||||||
|
* [x] Hotfix for saving issues - Please refresh your browser and report if problems continue
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Wednesday 7/09/2025 - v3.19.1
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### calculuschild
|
||||||
|
* [x] Send diffs instead of full file on save - should help with timeout/disconnect errors
|
||||||
|
}}
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
### Thursday 05/22/2025 - v3.19.0
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### abquintic
|
||||||
|
* [x] Fix crash due to colons after `\page`
|
||||||
|
|
||||||
|
Fixes issue [#4105](https://github.com/naturalcrit/homebrewery/issues/4105)
|
||||||
|
|
||||||
|
* [x] Fix images with spaces in alt text not rendering
|
||||||
|
|
||||||
|
Fixes issue [#3659](https://github.com/naturalcrit/homebrewery/issues/3659)
|
||||||
|
|
||||||
|
* [x] Custom snippets! Open the new {{openSans **:fas_table_list: SNIPPETS**}} tab (next to the {{openSans **:fas_paintbrush: STYLE**}} tab). Custom snippets will appear in a new snippet dropdown, and will be included when imported as a custom theme.
|
||||||
|
|
||||||
|
* [x] Move several generic styles/snippets from PHB to the Blank theme; generic snippets like image masks no longer require the PHB theme.
|
||||||
|
|
||||||
|
* [x] Extract several Markdown+ syntax extensions into their own NPM packages, for use by the wider community.
|
||||||
|
|
||||||
|
* [x] Allow `\pagebreak` and `\columnbreak` as alternatives to `\page` and `\column`
|
||||||
|
|
||||||
|
Partially fixes issue [#4035](https://github.com/naturalcrit/homebrewery/issues/4035)
|
||||||
|
|
||||||
|
* [x] Fix misbehaving column breaks on old Chrome
|
||||||
|
|
||||||
|
Fixes issue [#4192](https://github.com/naturalcrit/homebrewery/issues/4192)
|
||||||
|
|
||||||
|
* [x] Self-host font-awesome icons; fix missing icons on local installs
|
||||||
|
|
||||||
|
Fixes issue [#1965](https://github.com/naturalcrit/homebrewery/issues/1965)
|
||||||
|
Fixes issue [#1548](https://github.com/naturalcrit/homebrewery/issues/1548)
|
||||||
|
|
||||||
##### G-Ambatte
|
##### G-Ambatte
|
||||||
* [x] Revert colon rendering from br elements to blank divs
|
* [x] Fix CORS issue on local installs
|
||||||
|
|
||||||
|
* [x] Fix print size issues when using the Facing and Flow view options.
|
||||||
|
|
||||||
|
Fixes issue [#4146](https://github.com/naturalcrit/homebrewery/issues/4146)
|
||||||
|
|
||||||
|
* [x] New built-in `$[HB_pageNumber]` variable. Works with math operations or can be reassigned like any other variable for more customization over the old `{{pageNumber,auto}}` snippet.\
|
||||||
|
New snippet found at {{openSans **:fas_pencil: TEXT EDITOR :fas_arrow_right: :fas_bookmark: PAGE NUMBERING :fas_arrow_right: :fas_arrow_down_1_9: VARIABLE AUTO PAGE NUMBER**}}
|
||||||
|
|
||||||
##### 5e-Cleric
|
##### 5e-Cleric
|
||||||
* [x] Allow for local connections within a same network when running a local version
|
* [x] Fix search bar covering up snippet bar (3 times)
|
||||||
Fixes issue [#4094](https://github.com/naturalcrit/homebrewery/issues/4094)
|
|
||||||
|
Fixes issue [#4098](https://github.com/naturalcrit/homebrewery/issues/4098)
|
||||||
|
|
||||||
|
* [x] Save view toolbar settings across sessions
|
||||||
|
|
||||||
|
Fixes issue [#3835](https://github.com/naturalcrit/homebrewery/issues/3835)
|
||||||
|
|
||||||
|
* [x] Fix styling issues on the view toolbar
|
||||||
|
|
||||||
|
* [x] Update the Darkbrewery editor theme
|
||||||
|
|
||||||
|
Fixes issue [#3312](https://github.com/naturalcrit/homebrewery/issues/3312)
|
||||||
|
|
||||||
* [x] Add US Letter size page snippet
|
|
||||||
Fixes issue [#3893](https://github.com/naturalcrit/homebrewery/issues/3893)
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Monday 03/10/2025 - v3.18.0
|
### Monday 03/10/2025 - v3.18.0
|
||||||
|
|
||||||
{{taskList
|
{{taskList
|
||||||
##### dbolack
|
##### abquintic
|
||||||
* [x] Add ability to paste in any Share ID/URL into a brew's {{openSans :fas_circle_info: **Properties** :fas_arrow_right: **THEMES**}} selection, as long as that brew has been tagged as `meta:theme`. You can now share your custom brew themes without needing to make a personal copy.
|
* [x] Add ability to paste in any Share ID/URL into a brew's {{openSans :fas_circle_info: **Properties** :fas_arrow_right: **THEMES**}} selection, as long as that brew has been tagged as `meta:theme`. You can now share your custom brew themes without needing to make a personal copy.
|
||||||
* [x] Begin migration of custom Markdown extensions into their own NPM packages, for easier adoption by other users or projects
|
* [x] Begin migration of custom Markdown extensions into their own NPM packages, for easier adoption by other users or projects
|
||||||
* [x] Fix external HTML appearing in open codeblocks
|
* [x] Fix external HTML appearing in open codeblocks
|
||||||
@@ -167,7 +237,7 @@ Fixes issue [#4073](https://github.com/naturalcrit/homebrewery/issues/4073)
|
|||||||
|
|
||||||
* [x] Fix Reddit link crash when title has non-latin chars
|
* [x] Fix Reddit link crash when title has non-latin chars
|
||||||
|
|
||||||
##### dbolack
|
##### abquintic
|
||||||
|
|
||||||
* [x] Fix page shadows toolbar option
|
* [x] Fix page shadows toolbar option
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@import 'naturalcrit/styles/animations.less';
|
@import 'naturalcrit/styles/animations.less';
|
||||||
@import 'naturalcrit/styles/colors.less';
|
@import 'naturalcrit/styles/colors.less';
|
||||||
@import 'naturalcrit/styles/tooltip.less';
|
@import 'naturalcrit/styles/tooltip.less';
|
||||||
|
@import './themes/fonts/iconFonts/fontAwesome.less';
|
||||||
|
|
||||||
@import 'font-awesome/css/font-awesome.css';
|
@import 'font-awesome/css/font-awesome.css';
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,13 @@ const { printCurrentBrew } = require('../../../shared/helpers.js');
|
|||||||
import HeaderNav from './headerNav/headerNav.jsx';
|
import HeaderNav from './headerNav/headerNav.jsx';
|
||||||
import { safeHTML } from './safeHTML.js';
|
import { safeHTML } from './safeHTML.js';
|
||||||
|
|
||||||
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?: *{[^\n{}]*})?$)/m;
|
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||||
|
const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
|
||||||
|
const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m;
|
||||||
const PAGE_HEIGHT = 1056;
|
const PAGE_HEIGHT = 1056;
|
||||||
|
|
||||||
const INITIAL_CONTENT = dedent`
|
const INITIAL_CONTENT = dedent`
|
||||||
<!DOCTYPE html><html><head>
|
<!DOCTYPE html><html><head>
|
||||||
<link href="//use.fontawesome.com/releases/v6.5.1/css/all.css" rel="stylesheet" type="text/css" />
|
|
||||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
|
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
|
||||||
<base target=_blank>
|
<base target=_blank>
|
||||||
@@ -114,16 +115,24 @@ const BrewRenderer = (props)=>{
|
|||||||
zoomLevel : 100,
|
zoomLevel : 100,
|
||||||
spread : 'single',
|
spread : 'single',
|
||||||
startOnRight : true,
|
startOnRight : true,
|
||||||
pageShadows : true
|
pageShadows : true,
|
||||||
|
rowGap : 5,
|
||||||
|
columnGap : 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//useEffect to store or gather toolbar state from storage
|
||||||
|
useEffect(()=>{
|
||||||
|
const toolbarState = JSON.parse(window.localStorage.getItem('hb_toolbarState'));
|
||||||
|
toolbarState && setDisplayOptions(toolbarState);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [headerState, setHeaderState] = useState(false);
|
const [headerState, setHeaderState] = useState(false);
|
||||||
|
|
||||||
const mainRef = useRef(null);
|
const mainRef = useRef(null);
|
||||||
const pagesRef = useRef(null);
|
const pagesRef = useRef(null);
|
||||||
|
|
||||||
if(props.renderer == 'legacy') {
|
if(props.renderer == 'legacy') {
|
||||||
rawPages = props.text.split('\\page');
|
rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY);
|
||||||
} else {
|
} else {
|
||||||
rawPages = props.text.split(PAGEBREAK_REGEX_V3);
|
rawPages = props.text.split(PAGEBREAK_REGEX_V3);
|
||||||
}
|
}
|
||||||
@@ -180,6 +189,7 @@ const BrewRenderer = (props)=>{
|
|||||||
let attributes = {};
|
let attributes = {};
|
||||||
|
|
||||||
if(props.renderer == 'legacy') {
|
if(props.renderer == 'legacy') {
|
||||||
|
pageText.replace(COLUMNBREAK_REGEX_LEGACY, '```\n````\n'); // Allow Legacy brews to use `\column(break)`
|
||||||
const html = MarkdownLegacy.render(pageText);
|
const html = MarkdownLegacy.render(pageText);
|
||||||
|
|
||||||
return <BrewPage className='page phb' index={index} key={index} contents={html} style={styles} onVisibilityChange={handlePageVisibilityChange} />;
|
return <BrewPage className='page phb' index={index} key={index} contents={html} style={styles} onVisibilityChange={handlePageVisibilityChange} />;
|
||||||
@@ -196,6 +206,9 @@ const BrewRenderer = (props)=>{
|
|||||||
pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line
|
pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DO NOT REMOVE!!! REQUIRED FOR BACKWARDS COMPATIBILITY WITH NON-UPGRADABLE VERSIONS OF CHROME.
|
||||||
|
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||||
|
|
||||||
const html = Markdown.render(pageText, index);
|
const html = Markdown.render(pageText, index);
|
||||||
|
|
||||||
return <BrewPage className={classes} index={index} key={index} contents={html} style={styles} attributes={attributes} onVisibilityChange={handlePageVisibilityChange} />;
|
return <BrewPage className={classes} index={index} key={index} contents={html} style={styles} attributes={attributes} onVisibilityChange={handlePageVisibilityChange} />;
|
||||||
@@ -271,6 +284,7 @@ const BrewRenderer = (props)=>{
|
|||||||
|
|
||||||
const handleDisplayOptionsChange = (newDisplayOptions)=>{
|
const handleDisplayOptionsChange = (newDisplayOptions)=>{
|
||||||
setDisplayOptions(newDisplayOptions);
|
setDisplayOptions(newDisplayOptions);
|
||||||
|
localStorage.setItem('hb_toolbarState', JSON.stringify(newDisplayOptions));
|
||||||
};
|
};
|
||||||
|
|
||||||
const pagesStyle = {
|
const pagesStyle = {
|
||||||
|
|||||||
@@ -68,12 +68,16 @@
|
|||||||
@media print {
|
@media print {
|
||||||
.toolBar { display : none; }
|
.toolBar { display : none; }
|
||||||
.brewRenderer {
|
.brewRenderer {
|
||||||
height : 100%;
|
height : 100%;
|
||||||
padding-top : unset;
|
padding : unset;
|
||||||
overflow-y : unset;
|
overflow-y : unset;
|
||||||
|
&:has(.facing, .flow) {
|
||||||
|
padding : unset;
|
||||||
|
}
|
||||||
.pages {
|
.pages {
|
||||||
margin : 0px;
|
margin : 0px;
|
||||||
zoom : 100% !important;
|
zoom : 100% !important;
|
||||||
|
display : block;
|
||||||
& > .page { box-shadow : unset; }
|
& > .page { box-shadow : unset; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
|||||||
setPageNum(pageRange);
|
setPageNum(pageRange);
|
||||||
}, [visiblePages]);
|
}, [visiblePages]);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
const Visibility = localStorage.getItem('hb_toolbarVisibility');
|
||||||
|
if (Visibility) setToolsVisible(Visibility === 'true');
|
||||||
|
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleZoomButton = (zoom)=>{
|
const handleZoomButton = (zoom)=>{
|
||||||
handleOptionChange('zoomLevel', _.round(_.clamp(zoom, MIN_ZOOM, MAX_ZOOM)));
|
handleOptionChange('zoomLevel', _.round(_.clamp(zoom, MIN_ZOOM, MAX_ZOOM)));
|
||||||
};
|
};
|
||||||
@@ -55,15 +61,30 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
|||||||
// find widest page, in case pages are different widths, so that the zoom is adapted to not cut the widest page off screen.
|
// find widest page, in case pages are different widths, so that the zoom is adapted to not cut the widest page off screen.
|
||||||
const widestPage = _.maxBy([...pages], 'offsetWidth').offsetWidth;
|
const widestPage = _.maxBy([...pages], 'offsetWidth').offsetWidth;
|
||||||
|
|
||||||
desiredZoom = (iframeWidth / widestPage) * 100;
|
if(displayOptions.spread === 'facing')
|
||||||
|
desiredZoom = (iframeWidth / ((widestPage * 2) + parseInt(displayOptions.columnGap))) * 100;
|
||||||
|
else
|
||||||
|
desiredZoom = (iframeWidth / (widestPage + 20)) * 100;
|
||||||
|
|
||||||
} else if(mode == 'fit'){
|
} else if(mode == 'fit'){
|
||||||
let minDimRatio;
|
|
||||||
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
|
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
|
||||||
if(displayOptions.spread === 'facing')
|
let minDimRatio;
|
||||||
minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth / 2), Infinity); // if 'facing' spread, fit two pages in view
|
if(displayOptions.spread === 'single')
|
||||||
|
minDimRatio = [...pages].reduce(
|
||||||
|
(minRatio, page)=>Math.min(minRatio,
|
||||||
|
iframeWidth / page.offsetWidth,
|
||||||
|
iframeHeight / page.offsetHeight
|
||||||
|
),
|
||||||
|
Infinity
|
||||||
|
);
|
||||||
else
|
else
|
||||||
minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
|
minDimRatio = [...pages].reduce(
|
||||||
|
(minRatio, page)=>Math.min(minRatio,
|
||||||
|
iframeWidth / ((page.offsetWidth * 2) + parseInt(displayOptions.columnGap)),
|
||||||
|
iframeHeight / page.offsetHeight
|
||||||
|
),
|
||||||
|
Infinity
|
||||||
|
);
|
||||||
|
|
||||||
desiredZoom = minDimRatio * 100;
|
desiredZoom = minDimRatio * 100;
|
||||||
}
|
}
|
||||||
@@ -77,7 +98,10 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
|||||||
return (
|
return (
|
||||||
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
|
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
|
||||||
<div className='toggleButton'>
|
<div className='toggleButton'>
|
||||||
<button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
|
<button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{
|
||||||
|
setToolsVisible(!toolsVisible);
|
||||||
|
localStorage.setItem('hb_toolbarVisibility', !toolsVisible);
|
||||||
|
}}><i className='fas fa-glasses' /></button>
|
||||||
<button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button>
|
<button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button>
|
||||||
</div>
|
</div>
|
||||||
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
|
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
|
||||||
@@ -142,7 +166,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
|||||||
id='single-spread'
|
id='single-spread'
|
||||||
className='tool'
|
className='tool'
|
||||||
title='Single Page'
|
title='Single Page'
|
||||||
onClick={()=>{handleOptionChange('spread', 'active');}}
|
onClick={()=>{handleOptionChange('spread', 'single');}}
|
||||||
aria-checked={displayOptions.spread === 'single'}
|
aria-checked={displayOptions.spread === 'single'}
|
||||||
><i className='fac single-spread' /></button>
|
><i className='fac single-spread' /></button>
|
||||||
<button role='radio'
|
<button role='radio'
|
||||||
@@ -167,11 +191,11 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
|||||||
<h1>Options</h1>
|
<h1>Options</h1>
|
||||||
<label title='Modify the horizontal space between pages.'>
|
<label title='Modify the horizontal space between pages.'>
|
||||||
Column gap
|
Column gap
|
||||||
<input type='range' min={0} max={200} defaultValue={10} className='range-input' onChange={(evt)=>handleOptionChange('columnGap', evt.target.value)} />
|
<input type='range' min={0} max={200} defaultValue={displayOptions.columnGap || 10} className='range-input' onChange={(evt)=>handleOptionChange('columnGap', evt.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<label title='Modify the vertical space between rows of pages.'>
|
<label title='Modify the vertical space between rows of pages.'>
|
||||||
Row gap
|
Row gap
|
||||||
<input type='range' min={0} max={200} defaultValue={10} className='range-input' onChange={(evt)=>handleOptionChange('rowGap', evt.target.value)} />
|
<input type='range' min={0} max={200} defaultValue={displayOptions.rowGap || 10} className='range-input' onChange={(evt)=>handleOptionChange('rowGap', evt.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<label title='Start 1st page on the right side, such as if you have cover page.'>
|
<label title='Start 1st page on the right side, such as if you have cover page.'>
|
||||||
Start on right
|
Start on right
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
display : flex;
|
display : flex;
|
||||||
flex-wrap : wrap;
|
flex-wrap : wrap;
|
||||||
gap : 8px 30px;
|
gap : 8px 20px;
|
||||||
align-items : center;
|
align-items : center;
|
||||||
justify-content : center;
|
justify-content : center;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
height : auto;
|
height : auto;
|
||||||
padding : 2px 0;
|
padding : 2px 10px 2px 90px;
|
||||||
font-family : 'Open Sans', sans-serif;
|
font-family : 'Open Sans', sans-serif;
|
||||||
font-size : 13px;
|
font-size : 13px;
|
||||||
color : #CCCCCC;
|
color : #CCCCCC;
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
align-items : center;
|
align-items : center;
|
||||||
justify-content : center;
|
justify-content : center;
|
||||||
width : auto;
|
width : auto;
|
||||||
min-width : 46px;
|
min-width : 40px;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
&:hover { background-color : #444444; }
|
&:hover { background-color : #444444; }
|
||||||
&:focus {outline : none; border : 1px solid #D3D3D3;}
|
&:focus {outline : none; border : 1px solid #D3D3D3;}
|
||||||
@@ -169,12 +169,16 @@
|
|||||||
width : 92px;
|
width : 92px;
|
||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
background-color : unset;
|
background-color : unset;
|
||||||
opacity : 0.5;
|
opacity : 0.7;
|
||||||
transition : all 0.3s ease;
|
transition : all 0.3s ease;
|
||||||
& > *:not(.toggleButton) {
|
& > *:not(.toggleButton) {
|
||||||
opacity : 0;
|
opacity : 0;
|
||||||
transition : all 0.2s ease;
|
transition : all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggleButton button i {
|
||||||
|
filter: drop-shadow(0 0 2px black) drop-shadow(0 0 1px black);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +187,5 @@
|
|||||||
left : 0;
|
left : 0;
|
||||||
z-index : 5;
|
z-index : 5;
|
||||||
display : flex;
|
display : flex;
|
||||||
width : 32px;
|
|
||||||
min-width : unset;
|
|
||||||
height : 100%;
|
height : 100%;
|
||||||
}
|
}
|
||||||
@@ -12,9 +12,8 @@ const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
|||||||
|
|
||||||
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
||||||
|
|
||||||
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?: *{[^\n{}]*})?$)/m;
|
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||||
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
|
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
|
||||||
const SNIPPETBAR_HEIGHT = 25;
|
|
||||||
const DEFAULT_STYLE_TEXT = dedent`
|
const DEFAULT_STYLE_TEXT = dedent`
|
||||||
/*=======--- Example CSS styling ---=======*/
|
/*=======--- Example CSS styling ---=======*/
|
||||||
/* Any CSS here will apply to your document! */
|
/* Any CSS here will apply to your document! */
|
||||||
@@ -60,8 +59,9 @@ const Editor = createClass({
|
|||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
editorTheme : this.props.editorTheme,
|
editorTheme : this.props.editorTheme,
|
||||||
view : 'text' //'text', 'style', 'meta', 'snippet'
|
view : 'text', //'text', 'style', 'meta', 'snippet'
|
||||||
|
snippetbarHeight : 25
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -88,6 +88,7 @@ const Editor = createClass({
|
|||||||
editorTheme : editorTheme
|
editorTheme : editorTheme
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
this.setState({ snippetbarHeight: document.querySelector('.editor > .snippetBar').offsetHeight });
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
||||||
@@ -211,7 +212,7 @@ const Editor = createClass({
|
|||||||
|
|
||||||
// New Codemirror styling for V3 renderer
|
// New Codemirror styling for V3 renderer
|
||||||
if(this.props.renderer === 'V3') {
|
if(this.props.renderer === 'V3') {
|
||||||
if(line.match(/^\\column$/)){
|
if(line.match(/^\\column(?:break)?$/)){
|
||||||
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,6 +413,9 @@ const Editor = createClass({
|
|||||||
//Called when there are changes to the editor's dimensions
|
//Called when there are changes to the editor's dimensions
|
||||||
update : function(){
|
update : function(){
|
||||||
this.codeEditor.current?.updateSize();
|
this.codeEditor.current?.updateSize();
|
||||||
|
const snipHeight = document.querySelector('.editor > .snippetBar').offsetHeight;
|
||||||
|
if(snipHeight !== this.state.snippetbarHeight)
|
||||||
|
this.setState({ snippetbarHeight: snipHeight });
|
||||||
},
|
},
|
||||||
|
|
||||||
updateEditorTheme : function(newTheme){
|
updateEditorTheme : function(newTheme){
|
||||||
@@ -436,7 +440,8 @@ const Editor = createClass({
|
|||||||
value={this.props.brew.text}
|
value={this.props.brew.text}
|
||||||
onChange={this.props.onTextChange}
|
onChange={this.props.onTextChange}
|
||||||
editorTheme={this.state.editorTheme}
|
editorTheme={this.state.editorTheme}
|
||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent}
|
||||||
|
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
if(this.isStyle()){
|
if(this.isStyle()){
|
||||||
@@ -449,7 +454,8 @@ const Editor = createClass({
|
|||||||
onChange={this.props.onStyleChange}
|
onChange={this.props.onStyleChange}
|
||||||
enableFolding={true}
|
enableFolding={true}
|
||||||
editorTheme={this.state.editorTheme}
|
editorTheme={this.state.editorTheme}
|
||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent}
|
||||||
|
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
if(this.isMeta()){
|
if(this.isMeta()){
|
||||||
@@ -478,7 +484,8 @@ const Editor = createClass({
|
|||||||
onChange={this.props.onSnipChange}
|
onChange={this.props.onSnipChange}
|
||||||
enableFolding={true}
|
enableFolding={true}
|
||||||
editorTheme={this.state.editorTheme}
|
editorTheme={this.state.editorTheme}
|
||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent}
|
||||||
|
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
height : 100%;
|
height : 100%;
|
||||||
container : editor / inline-size;
|
container : editor / inline-size;
|
||||||
.codeEditor {
|
.codeEditor {
|
||||||
height : 100%;
|
height : calc(100% - 25px);
|
||||||
.CodeMirror { height : 100%; }
|
.CodeMirror { height : 100%; }
|
||||||
.pageLine, .snippetLine {
|
.pageLine, .snippetLine {
|
||||||
background : #33333328;
|
background : #33333328;
|
||||||
@@ -108,8 +108,4 @@
|
|||||||
span { padding : 2px 5px; }
|
span { padding : 2px 5px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@container editor (width < 553px) {
|
|
||||||
.editor .codeEditor .CodeMirror { height : calc(100% - 51px);}
|
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
&.meta {
|
&.meta {
|
||||||
.tooltipLeft('Properties');
|
.tooltipLeft('Properties');
|
||||||
}
|
}
|
||||||
&.snip {
|
&.snippet {
|
||||||
.tooltipLeft('Snippets');
|
.tooltipLeft('Snippets');
|
||||||
}
|
}
|
||||||
&.undo {
|
&.undo {
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
&.editorTheme {
|
&.editorTheme {
|
||||||
.tooltipLeft('Editor Themes');
|
.tooltipLeft('Editor Themes');
|
||||||
font-size : 0.75em;
|
font-size : 0.75em;
|
||||||
color : black;
|
color : inherit;
|
||||||
&.active {
|
&.active {
|
||||||
position : relative;
|
position : relative;
|
||||||
background-color : #999999;
|
background-color : #999999;
|
||||||
|
|||||||
@@ -1,95 +1,75 @@
|
|||||||
//╔===--------------- Polyfills --------------===╗//
|
/* eslint-disable camelcase */
|
||||||
import 'core-js/es/string/to-well-formed.js';
|
import 'core-js/es/string/to-well-formed.js'; //Polyfill for older browsers
|
||||||
//╚===--------------- ---------------===╝//
|
import './homebrew.less';
|
||||||
|
import React from 'react';
|
||||||
|
import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router';
|
||||||
|
|
||||||
require('./homebrew.less');
|
import HomePage from './pages/homePage/homePage.jsx';
|
||||||
const React = require('react');
|
import EditPage from './pages/editPage/editPage.jsx';
|
||||||
const createClass = require('create-react-class');
|
import UserPage from './pages/userPage/userPage.jsx';
|
||||||
const { StaticRouter:Router } = require('react-router');
|
import SharePage from './pages/sharePage/sharePage.jsx';
|
||||||
const { Route, Routes, useParams, useSearchParams } = require('react-router');
|
import NewPage from './pages/newPage/newPage.jsx';
|
||||||
|
import ErrorPage from './pages/errorPage/errorPage.jsx';
|
||||||
|
import VaultPage from './pages/vaultPage/vaultPage.jsx';
|
||||||
|
import AccountPage from './pages/accountPage/accountPage.jsx';
|
||||||
|
|
||||||
const HomePage = require('./pages/homePage/homePage.jsx');
|
const WithRoute = ({ el: Element, ...rest })=>{
|
||||||
const EditPage = require('./pages/editPage/editPage.jsx');
|
|
||||||
const UserPage = require('./pages/userPage/userPage.jsx');
|
|
||||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
|
||||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
|
||||||
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
|
||||||
const VaultPage = require('./pages/vaultPage/vaultPage.jsx');
|
|
||||||
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
|
||||||
|
|
||||||
const WithRoute = (props)=>{
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const queryParams = {};
|
const queryParams = Object.fromEntries(searchParams?.entries() || []);
|
||||||
for (const [key, value] of searchParams?.entries() || []) {
|
|
||||||
queryParams[key] = value;
|
return <Element {...rest} {...params} query={queryParams} />;
|
||||||
}
|
|
||||||
const Element = props.el;
|
|
||||||
const allProps = {
|
|
||||||
...props,
|
|
||||||
...params,
|
|
||||||
query : queryParams,
|
|
||||||
el : undefined
|
|
||||||
};
|
|
||||||
return <Element {...allProps} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Homebrew = createClass({
|
const Homebrew = (props)=>{
|
||||||
displayName : 'Homebrewery',
|
const {
|
||||||
getDefaultProps : function() {
|
url = '',
|
||||||
return {
|
version = '0.0.0',
|
||||||
url : '',
|
account = null,
|
||||||
welcomeText : '',
|
enable_v3 = false,
|
||||||
changelog : '',
|
enable_themes,
|
||||||
version : '0.0.0',
|
config,
|
||||||
account : null,
|
brew = {
|
||||||
enable_v3 : false,
|
title : '',
|
||||||
brew : {
|
text : '',
|
||||||
title : '',
|
shareId : null,
|
||||||
text : '',
|
editId : null,
|
||||||
shareId : null,
|
createdAt : null,
|
||||||
editId : null,
|
updatedAt : null,
|
||||||
createdAt : null,
|
lang : ''
|
||||||
updatedAt : null,
|
},
|
||||||
lang : ''
|
userThemes,
|
||||||
}
|
brews
|
||||||
};
|
} = props;
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function() {
|
global.account = account;
|
||||||
global.account = this.props.account;
|
global.version = version;
|
||||||
global.version = this.props.version;
|
global.enable_v3 = enable_v3;
|
||||||
global.enable_v3 = this.props.enable_v3;
|
global.enable_themes = enable_themes;
|
||||||
global.enable_themes = this.props.enable_themes;
|
global.config = config;
|
||||||
global.config = this.props.config;
|
|
||||||
|
|
||||||
return {};
|
return (
|
||||||
},
|
<Router location={url}>
|
||||||
|
<div className='homebrew'>
|
||||||
render : function (){
|
<Routes>
|
||||||
return (
|
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
|
||||||
<Router location={this.props.url}>
|
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />
|
||||||
<div className='homebrew'>
|
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={brew} userThemes={userThemes}/>} />
|
||||||
<Routes>
|
<Route path='/new' element={<WithRoute el={NewPage} userThemes={userThemes}/> } />
|
||||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={brews} />} />
|
||||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
|
||||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
<Route path='/changelog' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
|
||||||
<Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } />
|
<Route path='/faq' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
|
||||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
<Route path='/migrate' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
|
||||||
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
|
<Route path='/account' element={<WithRoute el={AccountPage} brew={brew} accountDetails={brew.accountDetails} />} />
|
||||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
<Route path='/legacy' element={<WithRoute el={HomePage} brew={brew} />} />
|
||||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
<Route path='/error' element={<WithRoute el={ErrorPage} brew={brew} />} />
|
||||||
<Route path='/migrate' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
<Route path='/' element={<WithRoute el={HomePage} brew={brew} />} />
|
||||||
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} accountDetails={this.props.brew.accountDetails} />} />
|
<Route path='/*' element={<WithRoute el={HomePage} brew={brew} />} />
|
||||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
</Routes>
|
||||||
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
|
</div>
|
||||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
</Router>
|
||||||
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
);
|
||||||
</Routes>
|
};
|
||||||
</div>
|
|
||||||
</Router>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Homebrew;
|
module.exports = Homebrew;
|
||||||
@@ -23,14 +23,15 @@ const ErrorNavItem = createClass({
|
|||||||
|
|
||||||
const error = this.props.error;
|
const error = this.props.error;
|
||||||
const response = error.response;
|
const response = error.response;
|
||||||
const status = response.status;
|
const status = response?.status;
|
||||||
const HBErrorCode = response.body?.HBErrorCode;
|
const errorCode = error.code
|
||||||
const message = response.body?.message;
|
const HBErrorCode = response?.body?.HBErrorCode;
|
||||||
|
const message = response?.body?.message;
|
||||||
let errMsg = '';
|
let errMsg = '';
|
||||||
try {
|
try {
|
||||||
errMsg += `${error.toString()}\n\n`;
|
errMsg += `${error.toString()}\n\n`;
|
||||||
errMsg += `\`\`\`\n${error.stack}\n`;
|
errMsg += `\`\`\`\n${error.stack}\n`;
|
||||||
errMsg += `${JSON.stringify(response.error, null, ' ')}\n\`\`\``;
|
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``;
|
||||||
console.log(errMsg);
|
console.log(errMsg);
|
||||||
} catch (e){}
|
} catch (e){}
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ const ErrorNavItem = createClass({
|
|||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(response.body?.errors?.[0].reason == 'storageQuotaExceeded') {
|
if(response?.body?.errors?.[0].reason == 'storageQuotaExceeded') {
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
<div className='errorContainer' onClick={clearError}>
|
<div className='errorContainer' onClick={clearError}>
|
||||||
@@ -82,7 +83,7 @@ const ErrorNavItem = createClass({
|
|||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(response.req.url.match(/^\/api.*Google.*$/m)){
|
if(response?.req.url.match(/^\/api.*Google.*$/m)){
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
<div className='errorContainer' onClick={clearError}>
|
<div className='errorContainer' onClick={clearError}>
|
||||||
@@ -129,6 +130,18 @@ const ErrorNavItem = createClass({
|
|||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(errorCode === 'ECONNABORTED') {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
The request to the server was interrupted or timed out.
|
||||||
|
This can happen due to a network issue, or if
|
||||||
|
trying to save a particularly large brew.
|
||||||
|
Please check your internet connection and try again.
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
<div className='errorContainer'>
|
<div className='errorContainer'>
|
||||||
|
|||||||
37
client/homebrew/pages/basePages/editPage/editPage.jsx
Normal file
37
client/homebrew/pages/basePages/editPage/editPage.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
require('./editPage.less');
|
||||||
|
const React = require('react');
|
||||||
|
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const Navbar = require('../../../navbar/navbar.jsx');
|
||||||
|
const NewBrewItem = require('../../../navbar/newbrew.navitem.jsx');
|
||||||
|
const HelpNavItem = require('../../../navbar/help.navitem.jsx');
|
||||||
|
const PrintNavItem = require('../../../navbar/print.navitem.jsx');
|
||||||
|
const ErrorNavItem = require('../../../navbar/error-navitem.jsx');
|
||||||
|
const AccountNavItem = require('../../../navbar/account.navitem.jsx');
|
||||||
|
const RecentNavItem = require('../../../navbar/recent.navitem.jsx').both;
|
||||||
|
const VaultNavItem = require('../../../navbar/vault.navitem.jsx');
|
||||||
|
|
||||||
|
const BaseEditPage = (props)=>{
|
||||||
|
return (
|
||||||
|
<div className={`sitePage ${props.className || ''}`}>
|
||||||
|
<Navbar>
|
||||||
|
<Nav.section>
|
||||||
|
<Nav.item className='brewTitle'>{props.brew.title}</Nav.item>
|
||||||
|
</Nav.section>
|
||||||
|
<Nav.section>
|
||||||
|
{props.navButtons}
|
||||||
|
<PrintNavItem />
|
||||||
|
<NewBrewItem />
|
||||||
|
<HelpNavItem />
|
||||||
|
<VaultNavItem />
|
||||||
|
<RecentNavItem brew={props.brew} storageKey={props.recentStorageKey} />
|
||||||
|
<AccountNavItem />
|
||||||
|
</Nav.section>
|
||||||
|
</Navbar>
|
||||||
|
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = BaseEditPage;
|
||||||
@@ -5,7 +5,7 @@ const moment = require('moment');
|
|||||||
import request from '../../../../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.svg');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const BrewItem = ({
|
const BrewItem = ({
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
border-radius : 4px;
|
border-radius : 4px;
|
||||||
&::before {
|
&::before {
|
||||||
margin-right : 3px;
|
margin-right : 3px;
|
||||||
font-family : 'Font Awesome 5 Free';
|
font-family : 'Font Awesome 6 Free';
|
||||||
font-size : 12px;
|
font-size : 12px;
|
||||||
}
|
}
|
||||||
&.type {
|
&.type {
|
||||||
@@ -115,15 +115,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.googleDriveIcon {
|
.googleDriveIcon {
|
||||||
height : 18px;
|
|
||||||
padding : 0px;
|
padding : 0px;
|
||||||
margin : -5px;
|
margin : -5px;
|
||||||
|
height : 18px;
|
||||||
}
|
}
|
||||||
.homebreweryIcon {
|
.homebreweryIcon {
|
||||||
position : relative;
|
position : relative;
|
||||||
top : 5px;
|
padding : 0px;
|
||||||
left : -5px;
|
top : 5px;
|
||||||
height : 24px;
|
left : -7.5px;
|
||||||
mix-blend-mode : darken;
|
height : 18px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
h1:hover { cursor : pointer; }
|
h1:hover { cursor : pointer; }
|
||||||
.active::before, .inactive::before {
|
.active::before, .inactive::before {
|
||||||
padding-right : 0.5em;
|
padding-right : 0.5em;
|
||||||
font-family : 'Font Awesome 5 Free';
|
font-family : 'Font Awesome 6 Free';
|
||||||
font-size : 0.6cm;
|
font-size : 0.6cm;
|
||||||
font-weight : 900;
|
font-weight : 900;
|
||||||
}
|
}
|
||||||
@@ -130,12 +130,12 @@
|
|||||||
border-radius : 3px;
|
border-radius : 3px;
|
||||||
&::before {
|
&::before {
|
||||||
margin-right : 3px;
|
margin-right : 3px;
|
||||||
font-family : 'Font Awesome 5 Free';
|
font-family : 'Font Awesome 6 Free';
|
||||||
font-size : 12px;
|
font-size : 12px;
|
||||||
}
|
}
|
||||||
&::after {
|
&::after {
|
||||||
margin-left : 3px;
|
margin-left : 3px;
|
||||||
font-family : 'Font Awesome 5 Free';
|
font-family : 'Font Awesome 6 Free';
|
||||||
font-size : 12px;
|
font-size : 12px;
|
||||||
content : '\f00d';
|
content : '\f00d';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
background-color : #00000077;
|
background-color : #00000077;
|
||||||
&::before {
|
&::before {
|
||||||
margin-right : 5px;
|
margin-right : 5px;
|
||||||
font-family : 'FONT AWESOME 5 FREE';
|
font-family : 'Font Awesome 6 Free';
|
||||||
content : '\f00c';
|
content : '\f00c';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,22 +3,19 @@ require('./editPage.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
|
import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch';
|
||||||
|
import { md5 } from 'hash-wasm';
|
||||||
|
import { gzipSync, strToU8 } from 'fflate';
|
||||||
|
|
||||||
import request from '../../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');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
|
||||||
|
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
|
||||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
|
||||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|
||||||
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
const BaseEditPage = require('../basePages/editPage/editPage.jsx');
|
||||||
|
const SplitPane = require('client/components/splitPane/splitPane.jsx');
|
||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
@@ -47,7 +44,7 @@ const EditPage = createClass({
|
|||||||
return {
|
return {
|
||||||
brew : this.props.brew,
|
brew : this.props.brew,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
isPending : false,
|
unsavedChanges : false,
|
||||||
alertTrashedGoogleBrew : this.props.brew.trashed,
|
alertTrashedGoogleBrew : this.props.brew.trashed,
|
||||||
alertLoginToTransfer : false,
|
alertLoginToTransfer : false,
|
||||||
saveGoogle : this.props.brew.googleId ? true : false,
|
saveGoogle : this.props.brew.googleId ? true : false,
|
||||||
@@ -85,7 +82,7 @@ const EditPage = createClass({
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.onbeforeunload = ()=>{
|
window.onbeforeunload = ()=>{
|
||||||
if(this.state.isSaving || this.state.isPending){
|
if(this.state.isSaving || this.state.unsavedChanges){
|
||||||
return 'You have unsaved changes!';
|
return 'You have unsaved changes!';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -104,9 +101,9 @@ const EditPage = createClass({
|
|||||||
},
|
},
|
||||||
componentDidUpdate : function(){
|
componentDidUpdate : function(){
|
||||||
const hasChange = this.hasChanges();
|
const hasChange = this.hasChanges();
|
||||||
if(this.state.isPending != hasChange){
|
if(this.state.unsavedChanges != hasChange){
|
||||||
this.setState({
|
this.setState({
|
||||||
isPending : hasChange
|
unsavedChanges : hasChange
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -156,9 +153,9 @@ const EditPage = createClass({
|
|||||||
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
|
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, snippets: snippet },
|
brew : { ...prevState.brew, snippets: snippet },
|
||||||
isPending : true,
|
unsavedChanges : true,
|
||||||
htmlErrors : htmlErrors,
|
htmlErrors : htmlErrors,
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -188,20 +185,28 @@ const EditPage = createClass({
|
|||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : {
|
brew : {
|
||||||
...prevState.brew,
|
...prevState.brew,
|
||||||
style : newData.style,
|
style : newData.style,
|
||||||
text : newData.text
|
text : newData.text,
|
||||||
|
snippets : newData.snippets
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
trySave : function(immediate=false){
|
trySave : function(immediate=false){
|
||||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||||
if(this.hasChanges()){
|
if(this.state.isSaving)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(immediate) {
|
||||||
this.debounceSave();
|
this.debounceSave();
|
||||||
} else {
|
this.debounceSave.flush();
|
||||||
this.debounceSave.cancel();
|
return;
|
||||||
}
|
}
|
||||||
if(immediate) this.debounceSave.flush();
|
|
||||||
|
if(this.hasChanges())
|
||||||
|
this.debounceSave();
|
||||||
|
else
|
||||||
|
this.debounceSave.cancel();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleGoogleClick : function(){
|
handleGoogleClick : function(){
|
||||||
@@ -215,8 +220,7 @@ const EditPage = createClass({
|
|||||||
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
||||||
}));
|
}));
|
||||||
this.setState({
|
this.setState({
|
||||||
error : null,
|
error : null
|
||||||
isSaving : false
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -232,14 +236,16 @@ const EditPage = createClass({
|
|||||||
toggleGoogleStorage : function(){
|
toggleGoogleStorage : function(){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
saveGoogle : !prevState.saveGoogle,
|
saveGoogle : !prevState.saveGoogle,
|
||||||
isSaving : false,
|
|
||||||
error : null
|
error : null
|
||||||
}), ()=>this.save());
|
}), ()=>this.trySave(true));
|
||||||
},
|
},
|
||||||
|
|
||||||
save : async function(){
|
save : async function(){
|
||||||
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
||||||
|
|
||||||
|
const brewState = this.state.brew; // freeze the current state
|
||||||
|
const preSaveSnapshot = { ...brewState };
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
isSaving : true,
|
isSaving : true,
|
||||||
error : null,
|
error : null,
|
||||||
@@ -249,15 +255,25 @@ const EditPage = createClass({
|
|||||||
await updateHistory(this.state.brew).catch(console.error);
|
await updateHistory(this.state.brew).catch(console.error);
|
||||||
await versionHistoryGarbageCollection().catch(console.error);
|
await versionHistoryGarbageCollection().catch(console.error);
|
||||||
|
|
||||||
|
//Prepare content to send to server
|
||||||
|
const brew = { ...brewState };
|
||||||
|
brew.text = brew.text.normalize('NFC');
|
||||||
|
this.savedBrew.text = this.savedBrew.text.normalize('NFC');
|
||||||
|
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||||
|
brew.patches = stringifyPatches(makePatches(encodeURI(this.savedBrew.text), encodeURI(brew.text)));
|
||||||
|
brew.hash = await md5(this.savedBrew.text);
|
||||||
|
//brew.text = undefined; - Temporary parallel path
|
||||||
|
brew.textBin = undefined;
|
||||||
|
|
||||||
|
const compressedBrew = gzipSync(strToU8(JSON.stringify(brew)));
|
||||||
|
|
||||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||||
|
|
||||||
const brew = this.state.brew;
|
|
||||||
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
|
||||||
|
|
||||||
const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`;
|
const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`;
|
||||||
const res = await request
|
const res = await request
|
||||||
.put(`/api/update/${brew.editId}${params}`)
|
.put(`/api/update/${brew.editId}${params}`)
|
||||||
.send(brew)
|
.set('Content-Encoding', 'gzip')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(compressedBrew)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error Updating Local Brew');
|
console.log('Error Updating Local Brew');
|
||||||
this.setState({ error: err });
|
this.setState({ error: err });
|
||||||
@@ -265,20 +281,28 @@ const EditPage = createClass({
|
|||||||
if(!res) return;
|
if(!res) return;
|
||||||
|
|
||||||
this.savedBrew = {
|
this.savedBrew = {
|
||||||
...this.state.brew,
|
...preSaveSnapshot,
|
||||||
googleId : res.body.googleId ? res.body.googleId : null,
|
googleId : res.body.googleId ? res.body.googleId : null,
|
||||||
editId : res.body.editId,
|
editId : res.body.editId,
|
||||||
shareId : res.body.shareId,
|
shareId : res.body.shareId,
|
||||||
version : res.body.version
|
version : res.body.version
|
||||||
};
|
};
|
||||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
|
||||||
|
|
||||||
this.setState(()=>({
|
this.setState((prevState) => ({
|
||||||
brew : this.savedBrew,
|
brew: {
|
||||||
isPending : false,
|
...prevState.brew,
|
||||||
|
googleId : res.body.googleId ? res.body.googleId : null,
|
||||||
|
editId : res.body.editId,
|
||||||
|
shareId : res.body.shareId,
|
||||||
|
version : res.body.version
|
||||||
|
},
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
unsavedTime : new Date()
|
unsavedTime : new Date()
|
||||||
}));
|
}), ()=>{
|
||||||
|
this.setState({ unsavedChanges : this.hasChanges() });
|
||||||
|
});
|
||||||
|
|
||||||
|
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderGoogleDriveIcon : function(){
|
renderGoogleDriveIcon : function(){
|
||||||
@@ -336,7 +360,7 @@ const EditPage = createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
|
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
|
||||||
if(this.state.isPending && this.state.autoSaveWarning){
|
if(this.state.unsavedChanges && this.state.autoSaveWarning){
|
||||||
this.setAutosaveWarning();
|
this.setAutosaveWarning();
|
||||||
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
||||||
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
||||||
@@ -351,7 +375,7 @@ const EditPage = createClass({
|
|||||||
|
|
||||||
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
||||||
// Use trySave(true) instead of save() to use debounced save function
|
// Use trySave(true) instead of save() to use debounced save function
|
||||||
if(this.state.isPending){
|
if(this.state.unsavedChanges){
|
||||||
return <Nav.item className='save' onClick={()=>this.trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
return <Nav.item className='save' onClick={()=>this.trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
||||||
}
|
}
|
||||||
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
|
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
|
||||||
@@ -411,11 +435,7 @@ const EditPage = createClass({
|
|||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
const shareLink = this.processShareId();
|
const shareLink = this.processShareId();
|
||||||
|
|
||||||
return <Navbar>
|
return <>
|
||||||
<Nav.section>
|
|
||||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
|
||||||
</Nav.section>
|
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.renderGoogleDriveIcon()}
|
{this.renderGoogleDriveIcon()}
|
||||||
{this.state.error ?
|
{this.state.error ?
|
||||||
@@ -425,8 +445,6 @@ const EditPage = createClass({
|
|||||||
{this.renderAutoSaveButton()}
|
{this.renderAutoSaveButton()}
|
||||||
</Nav.dropdown>
|
</Nav.dropdown>
|
||||||
}
|
}
|
||||||
<NewBrew />
|
|
||||||
<HelpNavItem/>
|
|
||||||
<Nav.dropdown>
|
<Nav.dropdown>
|
||||||
<Nav.item color='teal' icon='fas fa-share-alt'>
|
<Nav.item color='teal' icon='fas fa-share-alt'>
|
||||||
share
|
share
|
||||||
@@ -441,20 +459,19 @@ const EditPage = createClass({
|
|||||||
post to reddit
|
post to reddit
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
</Nav.dropdown>
|
</Nav.dropdown>
|
||||||
<PrintNavItem />
|
|
||||||
<VaultNavItem />
|
|
||||||
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
|
||||||
<Account />
|
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
</>;
|
||||||
</Navbar>;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='editPage sitePage'>
|
return <BaseEditPage
|
||||||
|
className="editPage"
|
||||||
|
errorState={this.state.error}
|
||||||
|
parent={this}
|
||||||
|
brew={this.state.brew}
|
||||||
|
navButtons={this.renderNavbar()}
|
||||||
|
recentStorageKey='edit'>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
{this.renderNavbar()}
|
|
||||||
|
|
||||||
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} reviewRequested={this.props.brew.lock.reviewRequested} />}
|
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} reviewRequested={this.props.brew.lock.reviewRequested} />}
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||||
@@ -492,7 +509,7 @@ const EditPage = createClass({
|
|||||||
/>
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</BaseEditPage>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,26 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
If the selected brew is your document, you may designate it as a theme by adding the \`theme:meta\` tag.`,
|
If the selected brew is your document, you may designate it as a theme by adding the \`theme:meta\` tag.`,
|
||||||
|
|
||||||
|
// ID validation error
|
||||||
|
'11' : dedent`
|
||||||
|
## No Homebrewery document could be found.
|
||||||
|
|
||||||
|
The server could not locate the Homebrewery document. The Brew ID failed the validation check.
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
// Google ID validation error
|
||||||
|
'12' : dedent`
|
||||||
|
## No Google document could be found.
|
||||||
|
|
||||||
|
The server could not locate the Google document. The Google ID failed the validation check.
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
//account page when account is not defined
|
//account page when account is not defined
|
||||||
'50' : dedent`
|
'50' : dedent`
|
||||||
## You are not signed in
|
## You are not signed in
|
||||||
|
|||||||
@@ -6,16 +6,11 @@ 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');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
|
||||||
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
|
||||||
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
|
||||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
const { fetchThemeBundle } = require('../../../../shared/helpers.js');
|
const { fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
const BaseEditPage = require('../basePages/editPage/editPage.jsx');
|
||||||
|
const SplitPane = require('client/components/splitPane/splitPane.jsx');
|
||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
@@ -25,13 +20,13 @@ const HomePage = createClass({
|
|||||||
displayName : 'HomePage',
|
displayName : 'HomePage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : DEFAULT_BREW,
|
brew : DEFAULT_BREW
|
||||||
ver : '0.0.0'
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
brew : this.props.brew,
|
brew : this.props.brew,
|
||||||
|
isSaving : false,
|
||||||
welcomeText : this.props.brew.text,
|
welcomeText : this.props.brew.text,
|
||||||
error : undefined,
|
error : undefined,
|
||||||
currentEditorViewPageNum : 1,
|
currentEditorViewPageNum : 1,
|
||||||
@@ -47,7 +42,11 @@ const HomePage = createClass({
|
|||||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSave : function(){
|
save : function(){
|
||||||
|
this.setState({
|
||||||
|
isSaving : true
|
||||||
|
});
|
||||||
|
|
||||||
request.post('/api')
|
request.post('/api')
|
||||||
.send(this.state.brew)
|
.send(this.state.brew)
|
||||||
.end((err, res)=>{
|
.end((err, res)=>{
|
||||||
@@ -57,6 +56,9 @@ const HomePage = createClass({
|
|||||||
}
|
}
|
||||||
const brew = res.body;
|
const brew = res.body;
|
||||||
window.location = `/edit/${brew.editId}`;
|
window.location = `/edit/${brew.editId}`;
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
this.setState({ isSaving: false, error: err });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleSplitMove : function(){
|
handleSplitMove : function(){
|
||||||
@@ -80,26 +82,38 @@ const HomePage = createClass({
|
|||||||
brew : { ...prevState.brew, text: text },
|
brew : { ...prevState.brew, text: text },
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderSaveButton : function(){
|
||||||
|
if(this.state.isSaving){
|
||||||
|
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
||||||
|
save...
|
||||||
|
</Nav.item>;
|
||||||
|
} else {
|
||||||
|
return <Nav.item icon='fas fa-save' className='save' onClick={this.save}>
|
||||||
|
save
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
return <Navbar ver={this.props.ver}>
|
return <>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.state.error ?
|
{this.state.error ?
|
||||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
<NewBrewItem />
|
|
||||||
<HelpNavItem />
|
|
||||||
<VaultNavItem />
|
|
||||||
<RecentNavItem />
|
|
||||||
<AccountNavItem />
|
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
</Navbar>;
|
</>;
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='homePage sitePage'>
|
return <BaseEditPage
|
||||||
|
className="homePage"
|
||||||
|
errorState={this.state.error}
|
||||||
|
parent={this}
|
||||||
|
brew={this.state.brew}
|
||||||
|
navButtons={this.renderNavbar()}>
|
||||||
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
||||||
{this.renderNavbar()}
|
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||||
<Editor
|
<Editor
|
||||||
@@ -127,14 +141,14 @@ const HomePage = createClass({
|
|||||||
/>
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
<div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.handleSave}>
|
<div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.save}>
|
||||||
Save current <i className='fas fa-save' />
|
Save current <i className='fas fa-save' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href='/new' className='floatingNewButton'>
|
<a href='/new' className='floatingNewButton'>
|
||||||
Create your own <i className='fas fa-magic' />
|
Create your own <i className='fas fa-magic' />
|
||||||
</a>
|
</a>
|
||||||
</div>;
|
</BaseEditPage>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,10 @@ import request from '../../utils/request-middleware.js';
|
|||||||
import Markdown from '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 Navbar = require('../../navbar/navbar.jsx');
|
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
|
||||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
const BaseEditPage = require('../basePages/editPage/editPage.jsx');
|
||||||
|
const SplitPane = require('client/components/splitPane/splitPane.jsx');
|
||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
@@ -148,7 +144,6 @@ const NewPage = createClass({
|
|||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, snippets: snippet },
|
brew : { ...prevState.brew, snippets: snippet },
|
||||||
isPending : true,
|
|
||||||
htmlErrors : htmlErrors,
|
htmlErrors : htmlErrors,
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
},
|
},
|
||||||
@@ -213,28 +208,23 @@ const NewPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
return <Navbar>
|
return <>
|
||||||
|
|
||||||
<Nav.section>
|
|
||||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
|
||||||
</Nav.section>
|
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.state.error ?
|
{this.state.error ?
|
||||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
this.renderSaveButton()
|
this.renderSaveButton()
|
||||||
}
|
}
|
||||||
<PrintNavItem />
|
|
||||||
<HelpNavItem />
|
|
||||||
<RecentNavItem />
|
|
||||||
<AccountNavItem />
|
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
</Navbar>;
|
</>;
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='newPage sitePage'>
|
return <BaseEditPage
|
||||||
{this.renderNavbar()}
|
className="newPage"
|
||||||
|
errorState={this.state.error}
|
||||||
|
parent={this}
|
||||||
|
brew={this.state.brew}
|
||||||
|
navButtons={this.renderNavbar()}>
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||||
<Editor
|
<Editor
|
||||||
@@ -269,7 +259,7 @@ const NewPage = createClass({
|
|||||||
/>
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</BaseEditPage>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const Account = require('../../navbar/account.navitem.jsx');
|
|||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
|
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
|
||||||
const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx');
|
const SplitPane = require('client/components/splitPane/splitPane.jsx');
|
||||||
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
|
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
|
||||||
|
|
||||||
import request from '../../utils/request-middleware.js';
|
import request from '../../utils/request-middleware.js';
|
||||||
|
|||||||
64
client/homebrew/thumbnail.svg
Normal file
64
client/homebrew/thumbnail.svg
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 94.65 94.6"
|
||||||
|
version="1.1"
|
||||||
|
id="svg11"
|
||||||
|
sodipodi:docname="thumbnail.svg"
|
||||||
|
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview13"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#111111"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="8.4989431"
|
||||||
|
inkscape:cx="38.887188"
|
||||||
|
inkscape:cy="47.417661"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1043"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg11" />
|
||||||
|
<defs
|
||||||
|
id="defs4">
|
||||||
|
<style
|
||||||
|
id="style2">.cls-1{fill:#ed1f24;}</style>
|
||||||
|
</defs>
|
||||||
|
<title
|
||||||
|
id="title6">NaturalCritLogo</title>
|
||||||
|
<g
|
||||||
|
id="Layer_2"
|
||||||
|
data-name="Layer 2"
|
||||||
|
style="fill:#000000;stroke:#000000">
|
||||||
|
<g
|
||||||
|
id="base"
|
||||||
|
style="fill:#000000;stroke:#000000">
|
||||||
|
<path
|
||||||
|
id="D20"
|
||||||
|
class="cls-1"
|
||||||
|
d="M63.45.09s-45.91,12.4-46,12.45a.71.71,0,0,0-.15.08l-.15.1-.12.11a1.07,1.07,0,0,0-.14.16l-.09.11-.12.23,0,.06L.2,54.9a1.59,1.59,0,0,0,.11,1.69L29.36,94h0l0,0,.08.08.08.08.09.09.08.06.13.07a0,0,0,0,0,0,0,1.59,1.59,0,0,0,.27.12l.13.05.06,0a1.55,1.55,0,0,0,.37,0,1.63,1.63,0,0,0,.31,0l45.67-8.3.16,0,.11,0,.12,0,.06,0s0,0,0,0l.06,0a1.65,1.65,0,0,0,.36-.28l0-.06a1.6,1.6,0,0,0,.26-.38s0,0,0,0v0h0a.14.14,0,0,1,0-.06L94.52,43.74a1.4,1.4,0,0,0,.11-.4.41.41,0,0,0,0-.11,1.13,1.13,0,0,0,0-.26.66.66,0,0,0,0-.14,2,2,0,0,0-.06-.26l0-.11a2.68,2.68,0,0,0-.18-.33v0L65.29.6C64.77-.31,63.45.09,63.45.09ZM74.9,81.7l-28.81-18L78.5,38.49ZM44.1,61l-11-40.17L77,35.39ZM82,37.78l8.92,5.95L79,73.48Zm4.46-1.1-4.6-3.06L75.69,21.36Zm-9.26-4.8-42.07-14,28.05-14ZM30.56,16.34l-6.49-2.16L47.85,7.7Zm-11.35-.21L27.88,19,7.64,45Zm10.73,5.76L40.78,61.64,4.64,54.42Zm10.82,43.2L30.26,89.6,5.75,58.09Zm3.16,1.24L71.74,83.72l-38.26,7Z"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:#000000" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<metadata
|
||||||
|
id="metadata1">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:title>NaturalCritLogo</dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -42,6 +42,7 @@ function parseBrewForStorage(brew, slot = 0) {
|
|||||||
title : brew.title,
|
title : brew.title,
|
||||||
text : brew.text,
|
text : brew.text,
|
||||||
style : brew.style,
|
style : brew.style,
|
||||||
|
snippets : brew.snippets,
|
||||||
version : brew.version,
|
version : brew.version,
|
||||||
shareId : brew.shareId,
|
shareId : brew.shareId,
|
||||||
savedAt : brew?.savedAt || new Date(),
|
savedAt : brew?.savedAt || new Date(),
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ const template = async function(name, title='', props = {}){
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
|
||||||
<link href="//use.fontawesome.com/releases/v6.5.1/css/all.css" rel="stylesheet" type="text/css" />
|
|
||||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
<link href=${`/${name}/bundle.css`} type="text/css" rel='stylesheet' />
|
<link href=${`/${name}/bundle.css`} type="text/css" rel='stylesheet' />
|
||||||
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
||||||
|
|||||||
3
font-awesome-source/README.md
Normal file
3
font-awesome-source/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# About
|
||||||
|
|
||||||
|
Run `deploy.bash` to download, extract, and deploy the font awesome files into place for building. Should only be needed when Font Awesome version changes and we want the new version.
|
||||||
42
font-awesome-source/deploy.bash
Normal file
42
font-awesome-source/deploy.bash
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Deploys the Font Awesome files for HB self-hosting to settle various issues.
|
||||||
|
|
||||||
|
THEURL=https://use.fontawesome.com/releases/v6.7.2/fontawesome-free-6.7.2-web.zip
|
||||||
|
THEFILE=fontawesome-free-6.7.2-web.zip
|
||||||
|
if [ ! "$(which wget)" ]; then
|
||||||
|
echo "Please manually download ${THEURL}"
|
||||||
|
exit -1
|
||||||
|
fi
|
||||||
|
|
||||||
|
wget ${THEURL}
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error downloading ${THEURL}"
|
||||||
|
exit -2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! "$(which unzip)" ]; then
|
||||||
|
echo "Please unzip the file with your tool of choice."
|
||||||
|
exit -3
|
||||||
|
fi
|
||||||
|
|
||||||
|
unzip fontawesome-free-6.7.2-web.zip
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error extracting ${THEFILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Copying fonts"
|
||||||
|
cp -rv fontawesome-free-*-web/webfonts/*.woff2 ../themes/fonts/iconFonts
|
||||||
|
echo "Copying and updating css"
|
||||||
|
|
||||||
|
echo "fontawesome-free.less"
|
||||||
|
sed 's/..\/webfonts/\/fonts\/iconFonts/g' fontawesome-free-*-web/css/fontawesome.css > ../themes/fonts/iconFonts/fontawesome-free.less
|
||||||
|
|
||||||
|
echo "fontawesome-solid.less"
|
||||||
|
sed 's/..\/webfonts/\/fonts\/iconFonts/g' fontawesome-free-*-web/css/solid.css > ../themes/fonts/iconFonts/fontawesome-solid.less
|
||||||
|
|
||||||
|
echo "fontawesome-brands.less"
|
||||||
|
sed 's/..\/webfonts/\/fonts\/iconFonts/g' fontawesome-free-*-web/css/brands.css > ../themes/fonts/iconFonts/fontawesome-brands.less
|
||||||
|
|
||||||
|
echo "fontawesome-regular.less"
|
||||||
|
sed 's/..\/webfonts/\/fonts\/iconFonts/g' fontawesome-free-*-web/css/regular.css > ../themes/fonts/iconFonts/fontawesome-regular.less
|
||||||
5474
package-lock.json
generated
5474
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
63
package.json
63
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "3.18.1",
|
"version": "3.19.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": "^10.8.x",
|
"npm": "^10.8.x",
|
||||||
@@ -36,7 +36,6 @@
|
|||||||
"test:mustache-syntax:inline": "jest \".*(mustache-syntax).*\" -t '^Inline:.*' --verbose --noStackTrace",
|
"test:mustache-syntax:inline": "jest \".*(mustache-syntax).*\" -t '^Inline:.*' --verbose --noStackTrace",
|
||||||
"test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace",
|
"test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace",
|
||||||
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
|
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
|
||||||
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
|
||||||
"test: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: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",
|
||||||
@@ -73,7 +72,7 @@
|
|||||||
"lines": 50
|
"lines": 50
|
||||||
},
|
},
|
||||||
"server/homebrew.api.js": {
|
"server/homebrew.api.js": {
|
||||||
"statements": 70,
|
"statements": 60,
|
||||||
"branches": 50,
|
"branches": 50,
|
||||||
"functions": 65,
|
"functions": 65,
|
||||||
"lines": 70
|
"lines": 70
|
||||||
@@ -84,66 +83,72 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.26.10",
|
"@babel/core": "^7.27.1",
|
||||||
"@babel/plugin-transform-runtime": "^7.26.10",
|
"@babel/plugin-transform-runtime": "^7.28.0",
|
||||||
"@babel/preset-env": "^7.26.9",
|
"@babel/preset-env": "^7.28.0",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.27.1",
|
||||||
"@googleapis/drive": "^11.0.0",
|
"@babel/runtime": "^7.27.6",
|
||||||
|
"@dmsnell/diff-match-patch": "^1.1.0",
|
||||||
|
"@googleapis/drive": "^13.0.1",
|
||||||
|
"@sanity/diff-match-patch": "^3.2.0",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"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.41.0",
|
"core-js": "^3.44.0",
|
||||||
"cors": "^2.8.5",
|
"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",
|
||||||
"expr-eval": "^2.0.2",
|
"expr-eval": "^2.0.2",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.2.0",
|
"express-static-gzip": "3.0.0",
|
||||||
|
"fflate": "^0.8.2",
|
||||||
"fs-extra": "11.3.0",
|
"fs-extra": "11.3.0",
|
||||||
"idb-keyval": "^6.2.1",
|
"hash-wasm": "^4.12.0",
|
||||||
|
"idb-keyval": "^6.2.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"less": "^3.13.1",
|
"less": "^3.13.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "15.0.8",
|
"marked": "15.0.12",
|
||||||
"marked-emoji": "^2.0.0",
|
|
||||||
"marked-extended-tables": "^2.0.1",
|
|
||||||
"marked-gfm-heading-id": "^4.0.1",
|
|
||||||
"marked-alignment-paragraphs": "^1.0.0",
|
"marked-alignment-paragraphs": "^1.0.0",
|
||||||
|
"marked-definition-lists": "^1.0.1",
|
||||||
|
"marked-emoji": "^2.0.1",
|
||||||
|
"marked-extended-tables": "^2.0.1",
|
||||||
|
"marked-gfm-heading-id": "^4.1.2",
|
||||||
"marked-nonbreaking-spaces": "^1.0.1",
|
"marked-nonbreaking-spaces": "^1.0.1",
|
||||||
"marked-smartypants-lite": "^1.0.3",
|
"marked-smartypants-lite": "^1.0.3",
|
||||||
"marked-subsuper-text": "^1.0.3",
|
"marked-subsuper-text": "^1.0.3",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mongoose": "^8.13.2",
|
"mongoose": "^8.16.3",
|
||||||
"nanoid": "5.1.5",
|
"nanoid": "5.1.5",
|
||||||
"nconf": "^0.12.1",
|
"nconf": "^0.13.0",
|
||||||
"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": "^7.5.0",
|
"react-router": "^7.6.3",
|
||||||
"romans": "^3.0.0",
|
"romans": "^3.1.0",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^10.2.0",
|
"superagent": "^10.2.1",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git",
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git",
|
||||||
"written-number": "^0.11.1"
|
"written-number": "^0.11.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/stylelint-plugin": "^3.1.2",
|
"@stylistic/stylelint-plugin": "^3.1.3",
|
||||||
"babel-plugin-transform-import-meta": "^2.3.2",
|
"babel-plugin-transform-import-meta": "^2.3.3",
|
||||||
"eslint": "^9.24.0",
|
"eslint": "^9.31.0",
|
||||||
"eslint-plugin-jest": "^28.11.0",
|
"eslint-plugin-jest": "^29.0.1",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.3.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^30.0.5",
|
||||||
"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.18.0",
|
"stylelint": "^16.22.0",
|
||||||
"stylelint-config-recess-order": "^6.0.0",
|
"stylelint-config-recess-order": "^7.1.0",
|
||||||
"stylelint-config-recommended": "^16.0.0",
|
"stylelint-config-recommended": "^16.0.0",
|
||||||
"supertest": "^7.1.0"
|
"supertest": "^7.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@
|
|||||||
"codemirror/addon/selection/active-line.js",
|
"codemirror/addon/selection/active-line.js",
|
||||||
"codemirror/addon/hint/show-hint.js",
|
"codemirror/addon/hint/show-hint.js",
|
||||||
"moment",
|
"moment",
|
||||||
"superagent"
|
"superagent",
|
||||||
|
"@sanity/diff-match-patch",
|
||||||
|
"fflate"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -383,6 +383,7 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res,
|
|||||||
title : req.brew.title || 'Untitled Brew',
|
title : req.brew.title || 'Untitled Brew',
|
||||||
description : req.brew.description || 'No description.',
|
description : req.brew.description || 'No description.',
|
||||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||||
|
locale : req.brew.lang,
|
||||||
type : 'article'
|
type : 'article'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -404,6 +405,7 @@ app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res,
|
|||||||
renderer : req.brew.renderer,
|
renderer : req.brew.renderer,
|
||||||
theme : req.brew.theme,
|
theme : req.brew.theme,
|
||||||
tags : req.brew.tags,
|
tags : req.brew.tags,
|
||||||
|
snippets : req.brew.snippets
|
||||||
};
|
};
|
||||||
req.brew = _.defaults(brew, DEFAULT_BREW);
|
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||||
|
|
||||||
@@ -433,7 +435,7 @@ app.get('/new', asyncHandler(async(req, res, next)=>{
|
|||||||
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||||
const { brew } = req;
|
const { brew } = req;
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
title : req.brew.title || 'Untitled Brew',
|
title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
|
||||||
description : req.brew.description || 'No description.',
|
description : req.brew.description || 'No description.',
|
||||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||||
type : 'article'
|
type : 'article'
|
||||||
|
|||||||
66
server/forcessl.mw.spec.js
Normal file
66
server/forcessl.mw.spec.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import forceSSL from './forcessl.mw';
|
||||||
|
|
||||||
|
describe('Tests for ForceSSL middleware', ()=>{
|
||||||
|
let originalEnv;
|
||||||
|
let nextFn;
|
||||||
|
|
||||||
|
let req = {};
|
||||||
|
let res = {};
|
||||||
|
|
||||||
|
beforeEach(()=>{
|
||||||
|
originalEnv = process.env.NODE_ENV;
|
||||||
|
nextFn = jest.fn();
|
||||||
|
|
||||||
|
req = {
|
||||||
|
header : ()=>{ return 'http'; },
|
||||||
|
get : ()=>{ return 'test'; },
|
||||||
|
url : 'URL'
|
||||||
|
};
|
||||||
|
|
||||||
|
res = {
|
||||||
|
redirect : jest.fn()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
afterEach(()=>{
|
||||||
|
process.env.NODE_ENV = originalEnv;
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not redirect when NODE_ENV is set to local', ()=>{
|
||||||
|
process.env.NODE_ENV = 'local';
|
||||||
|
|
||||||
|
forceSSL(null, null, nextFn);
|
||||||
|
|
||||||
|
expect(res.redirect).not.toHaveBeenCalled();
|
||||||
|
expect(nextFn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not redirect when NODE_ENV is set to docker', ()=>{
|
||||||
|
process.env.NODE_ENV = 'docker';
|
||||||
|
|
||||||
|
forceSSL(null, null, nextFn);
|
||||||
|
|
||||||
|
expect(res.redirect).not.toHaveBeenCalled();
|
||||||
|
expect(nextFn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect with 302 when header is not HTTPS and NODE_ENV is not local or docker', ()=>{
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
|
forceSSL(req, res, nextFn);
|
||||||
|
|
||||||
|
expect(res.redirect).toHaveBeenCalledWith(302, 'https://testURL');
|
||||||
|
expect(nextFn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not redirect when header is HTTPS and NODE_ENV is not local or docker', ()=>{
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
req.header = ()=>{ return 'https'; };
|
||||||
|
|
||||||
|
forceSSL(req, res, nextFn);
|
||||||
|
|
||||||
|
expect(res.redirect).not.toHaveBeenCalled();
|
||||||
|
expect(nextFn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -8,8 +8,10 @@ import Markdown from '../shared/naturalcrit/markdown.js';
|
|||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import asyncHandler from 'express-async-handler';
|
import asyncHandler from 'express-async-handler';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import {makePatches, applyPatches, stringifyPatches, parsePatch} from '@sanity/diff-match-patch';
|
||||||
|
import { md5 } from 'hash-wasm';
|
||||||
import { splitTextStyleAndMetadata,
|
import { splitTextStyleAndMetadata,
|
||||||
brewSnippetsToJSON } from '../shared/helpers.js';
|
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
|
||||||
import checkClientVersion from './middleware/check-client-version.js';
|
import checkClientVersion from './middleware/check-client-version.js';
|
||||||
|
|
||||||
|
|
||||||
@@ -46,6 +48,20 @@ const api = {
|
|||||||
}
|
}
|
||||||
id = id.slice(googleId.length);
|
id = id.slice(googleId.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID Validation Checks
|
||||||
|
// Homebrewery ID
|
||||||
|
// Typically 12 characters, but the DB shows a range of 7 to 14 characters
|
||||||
|
if(!id.match(/^[a-zA-Z0-9-_]{7,14}$/)){
|
||||||
|
throw { name: 'ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '11', brewId: id };
|
||||||
|
}
|
||||||
|
// Google ID
|
||||||
|
// Typically 33 characters, old format is 44 - always starts with a 1
|
||||||
|
// Managed by Google, may change outside of our control, so any length between 33 and 44 is acceptable
|
||||||
|
if(googleId && !googleId.match(/^1(?:[a-zA-Z0-9-_]{32,43})$/)){
|
||||||
|
throw { name: 'Google ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '12', brewId: id };
|
||||||
|
}
|
||||||
|
|
||||||
return { id, googleId };
|
return { id, googleId };
|
||||||
},
|
},
|
||||||
//Get array of any of this user's brews tagged with `meta:theme`
|
//Get array of any of this user's brews tagged with `meta:theme`
|
||||||
@@ -337,21 +353,52 @@ const api = {
|
|||||||
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||||
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||||
const brewFromServer = req.brew;
|
const brewFromServer = req.brew;
|
||||||
if(brewFromServer.version && brewFromClient.version && brewFromServer.version > brewFromClient.version) {
|
splitTextStyleAndMetadata(brewFromServer);
|
||||||
|
|
||||||
|
if(brewFromServer?.version !== brewFromClient?.version){
|
||||||
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
|
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
return res.status(409).send(JSON.stringify({ message: `The brew has been changed on a different device. Please save your changes elsewhere, refresh, and try again.` }));
|
return res.status(409).send(JSON.stringify({ message: `The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
|
||||||
}
|
}
|
||||||
|
|
||||||
let brew = _.assign(brewFromServer, brewFromClient);
|
brewFromServer.text = brewFromServer.text.normalize('NFC');
|
||||||
|
brewFromServer.hash = await md5(brewFromServer.text);
|
||||||
|
|
||||||
|
if(brewFromServer?.hash !== brewFromClient?.hash) {
|
||||||
|
console.log(`Hash mismatch on brew ${brewFromClient.editId}`);
|
||||||
|
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const patches = parsePatch(brewFromClient.patches);
|
||||||
|
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
|
||||||
|
const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]);
|
||||||
|
if(patchedResult != brewFromClient.text)
|
||||||
|
throw("Patches did not apply cleanly, text mismatch detected");
|
||||||
|
// brew.text = applyPatches(patches, brewFromServer.text)[0];
|
||||||
|
} catch (err) {
|
||||||
|
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||||
|
console.error('Failed to apply patches:', {
|
||||||
|
//patches : brewFromClient.patches,
|
||||||
|
brewId : brewFromClient.editId || 'unknown',
|
||||||
|
error : err
|
||||||
|
});
|
||||||
|
// While running in parallel, don't throw the error upstream.
|
||||||
|
// throw err; // rethrow to preserve the 500 behavior
|
||||||
|
}
|
||||||
|
|
||||||
|
let brew = _.assign(brewFromServer, brewFromClient);
|
||||||
|
brew.title = brew.title.trim();
|
||||||
|
brew.description = brew.description.trim() || '';
|
||||||
|
brew.text = api.mergeBrewText(brew);
|
||||||
|
|
||||||
const googleId = brew.googleId;
|
const googleId = brew.googleId;
|
||||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||||
let afterSave = async ()=>true;
|
let afterSave = async ()=>true;
|
||||||
|
|
||||||
brew.title = brew.title.trim();
|
|
||||||
brew.description = brew.description.trim() || '';
|
|
||||||
brew.text = api.mergeBrewText(brew);
|
|
||||||
|
|
||||||
if(brew.googleId && removeFromGoogle) {
|
if(brew.googleId && removeFromGoogle) {
|
||||||
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
||||||
afterSave = async ()=>{
|
afterSave = async ()=>{
|
||||||
@@ -412,6 +459,8 @@ const api = {
|
|||||||
const after = await afterSave();
|
const after = await afterSave();
|
||||||
if(!after) return;
|
if(!after) return;
|
||||||
|
|
||||||
|
saved.textBin = undefined; // Remove textBin from the saved object to save bandwidth
|
||||||
|
|
||||||
res.status(200).send(saved);
|
res.status(200).send(saved);
|
||||||
},
|
},
|
||||||
deleteGoogleBrew : async (account, id, editId, res)=>{
|
deleteGoogleBrew : async (account, id, editId, res)=>{
|
||||||
@@ -482,10 +531,10 @@ const api = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
||||||
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||||
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||||
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||||
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||||
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -99,18 +99,87 @@ describe('Tests for api', ()=>{
|
|||||||
expect(googleId).toBeUndefined();
|
expect(googleId).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw if id is too short', ()=>{
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
api.getId({
|
||||||
|
params : {
|
||||||
|
id : 'abcd'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(err).toEqual({ HBErrorCode: '11', brewId: 'abcd', message: 'Invalid ID', name: 'ID Error', status: 404 });
|
||||||
|
});
|
||||||
|
|
||||||
it('should return id and google id from request body', ()=>{
|
it('should return id and google id from request body', ()=>{
|
||||||
const { id, googleId } = api.getId({
|
const { id, googleId } = api.getId({
|
||||||
params : {
|
params : {
|
||||||
id : 'abcdefgh'
|
id : 'abcdefghijkl'
|
||||||
},
|
},
|
||||||
body : {
|
body : {
|
||||||
googleId : '12345'
|
googleId : '123456789012345678901234567890123'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(id).toEqual('abcdefgh');
|
expect(id).toEqual('abcdefghijkl');
|
||||||
expect(googleId).toEqual('12345');
|
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw invalid - google id right length but does not match pattern', ()=>{
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
api.getId({
|
||||||
|
params : {
|
||||||
|
id : 'abcdefghijkl'
|
||||||
|
},
|
||||||
|
body : {
|
||||||
|
googleId : '012345678901234567890123456789012'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw invalid - google id too short (32 char)', ()=>{
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
api.getId({
|
||||||
|
params : {
|
||||||
|
id : 'abcdefghijkl'
|
||||||
|
},
|
||||||
|
body : {
|
||||||
|
googleId : '12345678901234567890123456789012'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw invalid - google id too long (45 char)', ()=>{
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
api.getId({
|
||||||
|
params : {
|
||||||
|
id : 'abcdefghijkl'
|
||||||
|
},
|
||||||
|
body : {
|
||||||
|
googleId : '123456789012345678901234567890123456789012345'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 12-char id and google id from params', ()=>{
|
it('should return 12-char id and google id from params', ()=>{
|
||||||
@@ -1052,4 +1121,83 @@ brew`);
|
|||||||
expect(testBrew.tags).toEqual(['tag a']);
|
expect(testBrew.tags).toEqual(['tag a']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('updateBrew', ()=>{
|
||||||
|
it('should return error on version mismatch', async ()=>{
|
||||||
|
const brewFromClient = { version: 1 };
|
||||||
|
const brewFromServer = { version: 1000, text: '' };
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
brew : brewFromServer,
|
||||||
|
body : brewFromClient
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.updateBrew(req, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(409);
|
||||||
|
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error on hash mismatch', async ()=>{
|
||||||
|
const brewFromClient = { version: 1, hash: '1234' };
|
||||||
|
const brewFromServer = { version: 1, text: 'test' };
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
brew : brewFromServer,
|
||||||
|
body : brewFromClient
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.updateBrew(req, res);
|
||||||
|
|
||||||
|
expect(req.brew.hash).toBe('098f6bcd4621d373cade4e832627b4f6');
|
||||||
|
expect(res.status).toHaveBeenCalledWith(409);
|
||||||
|
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Commenting this one out for now, since we are no longer throwing this error while we monitor
|
||||||
|
// it('should return error on applying patches', async ()=>{
|
||||||
|
// const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' };
|
||||||
|
// const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
|
||||||
|
|
||||||
|
// const req = {
|
||||||
|
// brew : brewFromServer,
|
||||||
|
// body : brewFromClient,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// let err;
|
||||||
|
// try {
|
||||||
|
// await api.updateBrew(req, res);
|
||||||
|
// } catch (e) {
|
||||||
|
// err = e;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// expect(err).toEqual(Error('Invalid patch string: not a valid patch string'));
|
||||||
|
// });
|
||||||
|
|
||||||
|
it('should save brew, no ID', async ()=>{
|
||||||
|
const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: '' };
|
||||||
|
const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
|
||||||
|
|
||||||
|
model.save = jest.fn((brew)=>{return brew;});
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
brew : brewFromServer,
|
||||||
|
body : brewFromClient,
|
||||||
|
query : { saveToGoogle: false, removeFromGoogle: false }
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.updateBrew(req, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
_id : '1',
|
||||||
|
description : 'Test Description',
|
||||||
|
hash : '098f6bcd4621d373cade4e832627b4f6',
|
||||||
|
title : 'Test Title',
|
||||||
|
version : 2
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,21 +5,16 @@ import config from './config.js';
|
|||||||
const generateAccessToken = (account)=>{
|
const generateAccessToken = (account)=>{
|
||||||
const payload = account;
|
const payload = account;
|
||||||
|
|
||||||
// When the token was issued
|
payload.issued = (new Date()); // When the token was issued
|
||||||
payload.issued = (new Date());
|
payload.issuer = config.get('authentication_token_issuer'); // Which service issued the Token
|
||||||
// Which service issued the Token
|
payload.audience = config.get('authentication_token_audience'); // Which service is the token intended for
|
||||||
payload.issuer = config.get('authentication_token_issuer');
|
const secret = config.get('authentication_token_secret'); // The signing key for signing the token
|
||||||
// Which service is the token intended for
|
|
||||||
payload.audience = config.get('authentication_token_audience');
|
|
||||||
// The signing key for signing the token
|
|
||||||
delete payload.password;
|
delete payload.password;
|
||||||
delete payload._id;
|
delete payload._id;
|
||||||
|
|
||||||
const secret = config.get('authentication_token_secret');
|
|
||||||
|
|
||||||
const token = jwt.encode(payload, secret);
|
const token = jwt.encode(payload, secret);
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default generateAccessToken;
|
export default generateAccessToken;
|
||||||
|
|||||||
27
server/token.spec.js
Normal file
27
server/token.spec.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { expect, jest } from '@jest/globals';
|
||||||
|
import config from './config.js';
|
||||||
|
|
||||||
|
import generateAccessToken from './token';
|
||||||
|
|
||||||
|
describe('Tests for Token', ()=>{
|
||||||
|
it('Get token', ()=>{
|
||||||
|
|
||||||
|
// Mock the Config module, so we aren't grabbing actual secrets for testing
|
||||||
|
jest.mock('./config.js');
|
||||||
|
config.get = jest.fn((param)=>{
|
||||||
|
// The requested key name will be reflected to the output
|
||||||
|
return param;
|
||||||
|
});
|
||||||
|
|
||||||
|
const account = {};
|
||||||
|
|
||||||
|
const token = generateAccessToken(account);
|
||||||
|
|
||||||
|
// If these tests fail, the config mock has failed
|
||||||
|
expect(account).toHaveProperty('issuer', 'authentication_token_issuer');
|
||||||
|
expect(account).toHaveProperty('audience', 'authentication_token_audience');
|
||||||
|
|
||||||
|
// Because the inputs are fixed, this JWT key should be static
|
||||||
|
expect(typeof token).toBe('string');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -139,9 +139,45 @@ const fetchThemeBundle = async (obj, renderer, theme)=>{
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
|
||||||
|
const clientText = clientTextRaw?.normalize('NFC') || '';
|
||||||
|
const serverText = serverTextRaw?.normalize('NFC') || '';
|
||||||
|
|
||||||
|
const clientBuffer = Buffer.from(clientText, 'utf8');
|
||||||
|
const serverBuffer = Buffer.from(serverText, 'utf8');
|
||||||
|
|
||||||
|
if (clientBuffer.equals(serverBuffer)) {
|
||||||
|
console.log(`✅ ${label} text matches byte-for-byte.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(`❗${label} text mismatch detected.`);
|
||||||
|
console.log(`Client length: ${clientBuffer.length}`);
|
||||||
|
console.log(`Server length: ${serverBuffer.length}`);
|
||||||
|
|
||||||
|
// Byte-level diff
|
||||||
|
for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) {
|
||||||
|
if (clientBuffer[i] !== serverBuffer[i]) {
|
||||||
|
console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Char-level diff
|
||||||
|
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
|
||||||
|
if (clientText[i] !== serverText[i]) {
|
||||||
|
console.log(`Char mismatch at index ${i}:`);
|
||||||
|
console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
|
||||||
|
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
splitTextStyleAndMetadata,
|
splitTextStyleAndMetadata,
|
||||||
printCurrentBrew,
|
printCurrentBrew,
|
||||||
fetchThemeBundle,
|
fetchThemeBundle,
|
||||||
brewSnippetsToJSON
|
brewSnippetsToJSON,
|
||||||
|
debugTextMismatch
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import _ from 'lodash';
|
|||||||
import { Parser as MathParser } from 'expr-eval';
|
import { Parser as MathParser } from 'expr-eval';
|
||||||
import { marked as Marked } from 'marked';
|
import { marked as Marked } from 'marked';
|
||||||
import MarkedExtendedTables from 'marked-extended-tables';
|
import MarkedExtendedTables from 'marked-extended-tables';
|
||||||
|
import MarkedDefinitionLists from 'marked-definition-lists';
|
||||||
|
import MarkedAlignedParagraphs from 'marked-alignment-paragraphs';
|
||||||
|
import MarkedNonbreakingSpaces from 'marked-nonbreaking-spaces';
|
||||||
|
import MarkedSubSuperText from 'marked-subsuper-text';
|
||||||
import { markedSmartypantsLite as MarkedSmartypantsLite } from 'marked-smartypants-lite';
|
import { markedSmartypantsLite as MarkedSmartypantsLite } from 'marked-smartypants-lite';
|
||||||
import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id';
|
import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id';
|
||||||
import { markedEmoji as MarkedEmojis } from 'marked-emoji';
|
import { markedEmoji as MarkedEmojis } from 'marked-emoji';
|
||||||
import MarkedAlignedParagraphs from 'marked-alignment-paragraphs';
|
|
||||||
import MarkedNonbreakingSpaces from 'marked-nonbreaking-spaces';
|
|
||||||
import MarkedSubSuperText from 'marked-subsuper-text';
|
|
||||||
import { romanize } from 'romans';
|
import { romanize } from 'romans';
|
||||||
import writtenNumber from 'written-number';
|
import writtenNumber from 'written-number';
|
||||||
|
|
||||||
@@ -184,7 +185,7 @@ const mustacheSpans = {
|
|||||||
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
|
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
|
||||||
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
|
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
|
||||||
const match = completeSpan.exec(src);
|
const match = completeSpan.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
//Find closing delimiter
|
//Find closing delimiter
|
||||||
@@ -241,7 +242,7 @@ const mustacheDivs = {
|
|||||||
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
|
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
|
||||||
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
|
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
|
||||||
const match = completeBlock.exec(src);
|
const match = completeBlock.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
//Find closing delimiter
|
//Find closing delimiter
|
||||||
@@ -296,7 +297,7 @@ const mustacheInjectInline = {
|
|||||||
level : 'inline',
|
level : 'inline',
|
||||||
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g;
|
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/g;
|
||||||
const match = inlineRegex.exec(src);
|
const match = inlineRegex.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
const lastToken = tokens[tokens.length - 1];
|
const lastToken = tokens[tokens.length - 1];
|
||||||
@@ -342,7 +343,7 @@ const mustacheInjectBlock = {
|
|||||||
level : 'block',
|
level : 'block',
|
||||||
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
|
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
|
||||||
const match = inlineRegex.exec(src);
|
const match = inlineRegex.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
const lastToken = tokens[tokens.length - 1];
|
const lastToken = tokens[tokens.length - 1];
|
||||||
@@ -410,93 +411,6 @@ const forcedParagraphBreaks = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const definitionListsSingleLine = {
|
|
||||||
name : 'definitionListsSingleLine',
|
|
||||||
level : 'block',
|
|
||||||
start(src) { return src.match(/\n[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
|
||||||
tokenizer(src, tokens) {
|
|
||||||
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
|
||||||
let match;
|
|
||||||
let endIndex = 0;
|
|
||||||
const definitions = [];
|
|
||||||
while (match = regex.exec(src)) {
|
|
||||||
const originalLine = match[0]; // This line and below to handle conflict with emojis
|
|
||||||
let firstLine = originalLine; // Remove in V4 when definitionListsInline updated to
|
|
||||||
this.lexer.inlineTokens(firstLine.trim()) // require spaces around `::`
|
|
||||||
.filter((t)=>t.type == 'emoji')
|
|
||||||
.map((emoji)=>firstLine = firstLine.replace(emoji.raw, 'x'.repeat(emoji.raw.length)));
|
|
||||||
|
|
||||||
const newMatch = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym.exec(firstLine);
|
|
||||||
if(newMatch) {
|
|
||||||
definitions.push({
|
|
||||||
dt : this.lexer.inlineTokens(originalLine.slice(0, newMatch[1].length).trim()),
|
|
||||||
dd : this.lexer.inlineTokens(originalLine.slice(newMatch[1].length + 2).trim())
|
|
||||||
});
|
|
||||||
} // End of emoji hack.
|
|
||||||
endIndex = regex.lastIndex;
|
|
||||||
}
|
|
||||||
if(definitions.length) {
|
|
||||||
return {
|
|
||||||
type : 'definitionListsSingleLine',
|
|
||||||
raw : src.slice(0, endIndex),
|
|
||||||
definitions
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderer(token) {
|
|
||||||
return `<dl>${token.definitions.reduce((html, def)=>{
|
|
||||||
return `${html}<dt>${this.parser.parseInline(def.dt)}</dt>`
|
|
||||||
+ `<dd>${this.parser.parseInline(def.dd)}</dd>\n`;
|
|
||||||
}, '')}</dl>`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const definitionListsMultiLine = {
|
|
||||||
name : 'definitionListsMultiLine',
|
|
||||||
level : 'block',
|
|
||||||
start(src) { return src.match(/\n[^\n]*\n::[^:\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
|
||||||
tokenizer(src, tokens) {
|
|
||||||
const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::[^:\n]))|\n::([^:\n](?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y;
|
|
||||||
let match;
|
|
||||||
let endIndex = 0;
|
|
||||||
const definitions = [];
|
|
||||||
while (match = regex.exec(src)) {
|
|
||||||
if(match[1]) {
|
|
||||||
if(this.lexer.blockTokens(match[1].trim())[0]?.type !== 'paragraph') // DT must not be another block-level token besides <p>
|
|
||||||
break;
|
|
||||||
definitions.push({
|
|
||||||
dt : this.lexer.inlineTokens(match[1].trim()),
|
|
||||||
dds : []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if(match[2] && definitions.length) {
|
|
||||||
definitions[definitions.length - 1].dds.push(
|
|
||||||
this.lexer.inlineTokens(match[2].trim().replace(/\s/g, ' '))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
endIndex = regex.lastIndex;
|
|
||||||
}
|
|
||||||
if(definitions.length) {
|
|
||||||
return {
|
|
||||||
type : 'definitionListsMultiLine',
|
|
||||||
raw : src.slice(0, endIndex),
|
|
||||||
definitions
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderer(token) {
|
|
||||||
let returnVal = `<dl>`;
|
|
||||||
token.definitions.forEach((def)=>{
|
|
||||||
const dds = def.dds.map((s)=>{
|
|
||||||
return `\n<dd>${this.parser.parseInline(s).trim()}</dd>`;
|
|
||||||
}).join('');
|
|
||||||
returnVal += `<dt>${this.parser.parseInline(def.dt)}</dt>${dds}\n`;
|
|
||||||
});
|
|
||||||
returnVal = returnVal.trim();
|
|
||||||
return `${returnVal}</dl>`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//v=====--------------------< Variable Handling >-------------------=====v// 242 lines
|
//v=====--------------------< Variable Handling >-------------------=====v// 242 lines
|
||||||
const replaceVar = function(input, hoist=false, allowUnresolved=false) {
|
const replaceVar = function(input, hoist=false, allowUnresolved=false) {
|
||||||
const regex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g;
|
const regex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g;
|
||||||
@@ -765,8 +679,8 @@ const tableTerminators = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
Marked.use(MarkedVariables());
|
Marked.use(MarkedVariables());
|
||||||
Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks,
|
Marked.use(MarkedDefinitionLists());
|
||||||
mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
Marked.use({ extensions : [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
||||||
Marked.use(mustacheInjectBlock);
|
Marked.use(mustacheInjectBlock);
|
||||||
Marked.use(MarkedAlignedParagraphs());
|
Marked.use(MarkedAlignedParagraphs());
|
||||||
Marked.use(MarkedSubSuperText());
|
Marked.use(MarkedSubSuperText());
|
||||||
@@ -911,7 +825,7 @@ const Markdown = {
|
|||||||
MarkedGFMResetHeadingIDs();
|
MarkedGFMResetHeadingIDs();
|
||||||
}
|
}
|
||||||
|
|
||||||
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`);
|
rawBrewText = rawBrewText.replace(/^\\column(?:break)?$/gm, `\n<div class='columnSplit'></div>\n`);
|
||||||
|
|
||||||
const opts = Marked.defaults;
|
const opts = Marked.defaults;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,17 @@ require('jsdom-global')();
|
|||||||
|
|
||||||
import { safeHTML } from '../../client/homebrew/brewRenderer/safeHTML';
|
import { safeHTML } from '../../client/homebrew/brewRenderer/safeHTML';
|
||||||
|
|
||||||
|
test('Exit if no document', function() {
|
||||||
|
const doc = document;
|
||||||
|
document = undefined;
|
||||||
|
|
||||||
|
const result = safeHTML('');
|
||||||
|
|
||||||
|
document = doc;
|
||||||
|
|
||||||
|
expect(result).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
test('Javascript via href', function() {
|
test('Javascript via href', function() {
|
||||||
const source = `<a href="javascript:alert('This is a JavaScript injection via href attribute')">Click me</a>`;
|
const source = `<a href="javascript:alert('This is a JavaScript injection via href attribute')">Click me</a>`;
|
||||||
const rendered = safeHTML(source);
|
const rendered = safeHTML(source);
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ body { counter-reset : page-numbers 0; }
|
|||||||
width : 215.9mm;
|
width : 215.9mm;
|
||||||
height : 279.4mm;
|
height : 279.4mm;
|
||||||
padding : 1.4cm 1.9cm 1.7cm;
|
padding : 1.4cm 1.9cm 1.7cm;
|
||||||
overflow : hidden;
|
overflow : clip;
|
||||||
background-color : var(--HB_Color_Background);
|
background-color : var(--HB_Color_Background);
|
||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
contain : strict;
|
contain : strict;
|
||||||
|
|||||||
41
themes/assets/naturalCritLogoBlack.svg
Normal file
41
themes/assets/naturalCritLogoBlack.svg
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 94.65 94.6"
|
||||||
|
version="1.1"
|
||||||
|
id="svg11"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs4">
|
||||||
|
<style
|
||||||
|
id="style2">.cls-1{fill:#ed1f24;}</style>
|
||||||
|
</defs>
|
||||||
|
<title
|
||||||
|
id="title6">NaturalCritLogo</title>
|
||||||
|
<g
|
||||||
|
id="Layer_2"
|
||||||
|
data-name="Layer 2"
|
||||||
|
style="fill:#000000">
|
||||||
|
<g
|
||||||
|
id="base"
|
||||||
|
style="fill:#000000">
|
||||||
|
<path
|
||||||
|
id="D20"
|
||||||
|
class="cls-1"
|
||||||
|
d="M63.45.09s-45.91,12.4-46,12.45a.71.71,0,0,0-.15.08l-.15.1-.12.11a1.07,1.07,0,0,0-.14.16l-.09.11-.12.23,0,.06L.2,54.9a1.59,1.59,0,0,0,.11,1.69L29.36,94h0l0,0,.08.08.08.08.09.09.08.06.13.07a0,0,0,0,0,0,0,1.59,1.59,0,0,0,.27.12l.13.05.06,0a1.55,1.55,0,0,0,.37,0,1.63,1.63,0,0,0,.31,0l45.67-8.3.16,0,.11,0,.12,0,.06,0s0,0,0,0l.06,0a1.65,1.65,0,0,0,.36-.28l0-.06a1.6,1.6,0,0,0,.26-.38s0,0,0,0v0h0a.14.14,0,0,1,0-.06L94.52,43.74a1.4,1.4,0,0,0,.11-.4.41.41,0,0,0,0-.11,1.13,1.13,0,0,0,0-.26.66.66,0,0,0,0-.14,2,2,0,0,0-.06-.26l0-.11a2.68,2.68,0,0,0-.18-.33v0L65.29.6C64.77-.31,63.45.09,63.45.09ZM74.9,81.7l-28.81-18L78.5,38.49ZM44.1,61l-11-40.17L77,35.39ZM82,37.78l8.92,5.95L79,73.48Zm4.46-1.1-4.6-3.06L75.69,21.36Zm-9.26-4.8-42.07-14,28.05-14ZM30.56,16.34l-6.49-2.16L47.85,7.7Zm-11.35-.21L27.88,19,7.64,45Zm10.73,5.76L40.78,61.64,4.64,54.42Zm10.82,43.2L30.26,89.6,5.75,58.09Zm3.16,1.24L71.74,83.72l-38.26,7Z"
|
||||||
|
style="fill:#000000;fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<metadata
|
||||||
|
id="metadata1">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:title>NaturalCritLogo</dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -1,129 +0,0 @@
|
|||||||
/* Main BG color and normal text color */
|
|
||||||
.CodeMirror {
|
|
||||||
background: #293134;
|
|
||||||
color: #91A6AA;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Brew BG */
|
|
||||||
.brewRenderer {
|
|
||||||
background-color: #293134;
|
|
||||||
}
|
|
||||||
/* Blinking cursor */
|
|
||||||
.CodeMirror-cursor {
|
|
||||||
border-left: 1px solid #e0e2e4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* HB DARK NAV START*/
|
|
||||||
|
|
||||||
/* Bars at the top */
|
|
||||||
.snippetBar {
|
|
||||||
background-color: #2F393C;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
nav {
|
|
||||||
background-color: #293134;
|
|
||||||
}
|
|
||||||
nav .navItem {
|
|
||||||
background-color: #293134;
|
|
||||||
}
|
|
||||||
/* Fix for Homebrewery custom Snippet icons */
|
|
||||||
.snippetBar .fac {
|
|
||||||
filter: invert(1);
|
|
||||||
}
|
|
||||||
.snippetBar .snippetGroup .dropdown {
|
|
||||||
background-color: #2F393C;
|
|
||||||
}
|
|
||||||
/* HB DARK NAV END */
|
|
||||||
|
|
||||||
/* Line number stuff */
|
|
||||||
.CodeMirror-gutter-elt {
|
|
||||||
color: #81969A;
|
|
||||||
}
|
|
||||||
.CodeMirror-linenumber {
|
|
||||||
background-color: #293134;
|
|
||||||
}
|
|
||||||
.CodeMirror-gutter {
|
|
||||||
background-color: #293134;
|
|
||||||
}
|
|
||||||
/* column splits */
|
|
||||||
.editor .codeEditor .columnSplit {
|
|
||||||
font-style: italic;
|
|
||||||
color: inherit;
|
|
||||||
background-color:#1f5763;
|
|
||||||
border-bottom: #299 solid 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Colors for headings and such */
|
|
||||||
/* ###Headings */
|
|
||||||
.cm-s-default .cm-header {
|
|
||||||
color: #c51b1b;
|
|
||||||
-webkit-text-stroke-width: 0.1px;
|
|
||||||
-webkit-text-stroke-color: #000;
|
|
||||||
}
|
|
||||||
/* bold points */
|
|
||||||
.cm-header, .cm-strong {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #309dd2;
|
|
||||||
}
|
|
||||||
/* Link headings */
|
|
||||||
.cm-s-default .cm-link {
|
|
||||||
color: #dd6300;
|
|
||||||
}
|
|
||||||
/* links */
|
|
||||||
.cm-s-default .cm-string {
|
|
||||||
color: #aa8261;
|
|
||||||
}
|
|
||||||
/*@import*/
|
|
||||||
.cm-s-default .cm-def {
|
|
||||||
color:#2986cc;
|
|
||||||
}
|
|
||||||
/* Bullets and such */
|
|
||||||
.cm-s-default .cm-variable-2 {
|
|
||||||
color: #3cbf30;
|
|
||||||
}
|
|
||||||
/* blocks */
|
|
||||||
.editor .codeEditor .block:not(.cm-comment) {
|
|
||||||
color: #e3e3e3;
|
|
||||||
}
|
|
||||||
/* inline blocks */
|
|
||||||
.editor .codeEditor .inline-block {
|
|
||||||
color: #e3e3e3;
|
|
||||||
}
|
|
||||||
/* Tags (divs) */
|
|
||||||
.cm-s-default .cm-tag {
|
|
||||||
color: #e3ff00;
|
|
||||||
}
|
|
||||||
.cm-s-default .cm-attribute {
|
|
||||||
color: #e3ff00;
|
|
||||||
}
|
|
||||||
.cm-s-default .cm-atom {
|
|
||||||
color:#000;
|
|
||||||
}
|
|
||||||
.cm-s-default .cm-qualifier{
|
|
||||||
color:#ee1919;
|
|
||||||
}
|
|
||||||
.cm-s-default .cm-comment{
|
|
||||||
color:#bbc700;
|
|
||||||
}
|
|
||||||
.cm-s-default .cm-keyword {
|
|
||||||
color:#c302df;
|
|
||||||
background-color:#b1b1b1;
|
|
||||||
}
|
|
||||||
.cm-s-default .cm-property.cm-error {
|
|
||||||
color:#c50202;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-foldmarker {
|
|
||||||
color:#f0ff00;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* New page */
|
|
||||||
.editor .codeEditor .pageLine {
|
|
||||||
background: #000;
|
|
||||||
color:#000;
|
|
||||||
border-bottom: 1px solid #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-default .cm-builtin {
|
|
||||||
color:#fff;
|
|
||||||
}
|
|
||||||
134
themes/codeMirror/customThemes/darkbrewery.css
Normal file
134
themes/codeMirror/customThemes/darkbrewery.css
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
/*stylelint-disable*/
|
||||||
|
.editor .snippetBar {
|
||||||
|
color: white;
|
||||||
|
background-color: #2F393C;
|
||||||
|
.dropdown {
|
||||||
|
background-color: #2F393C;
|
||||||
|
}
|
||||||
|
.editors {
|
||||||
|
border-color: #ccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Main BG color and normal text color */
|
||||||
|
.CodeMirror {
|
||||||
|
--bg: #293134;
|
||||||
|
--highlight: #bcbcbc;
|
||||||
|
color: #91A6AA;
|
||||||
|
background: var(--bg);
|
||||||
|
.CodeMirror-scroll {
|
||||||
|
.CodeMirror-gutters {
|
||||||
|
border-right: 1px solid #555;
|
||||||
|
background: var(--bg);
|
||||||
|
.CodeMirror-gutter {
|
||||||
|
background-color: var(--bg);
|
||||||
|
&.CodeMirror-foldgutter {
|
||||||
|
cursor: pointer;
|
||||||
|
border-left: 1px solid #555;
|
||||||
|
transition: background 0.1s;
|
||||||
|
&:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.CodeMirror-lines {
|
||||||
|
/* Line numbers*/
|
||||||
|
.CodeMirror-linenumber.CodeMirror-gutter-elt {
|
||||||
|
background-color: var(--bg);
|
||||||
|
color: #81969A;
|
||||||
|
}
|
||||||
|
/* Blinking cursor */
|
||||||
|
.CodeMirror-cursor {
|
||||||
|
border-left: 1px solid #E0E2E4;
|
||||||
|
}
|
||||||
|
.pageLine {
|
||||||
|
color: #000000;
|
||||||
|
background: #000000;
|
||||||
|
border-bottom: 1px solid #FFFFFF;
|
||||||
|
}
|
||||||
|
.CodeMirror-code .CodeMirror-line {
|
||||||
|
&.columnSplit {
|
||||||
|
font-style: italic;
|
||||||
|
color: inherit;
|
||||||
|
background-color: #1F5763;
|
||||||
|
border-bottom: #229999 solid 1px;
|
||||||
|
}
|
||||||
|
/*syntax*/
|
||||||
|
.cm-header {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #C51B1B;
|
||||||
|
-webkit-text-stroke-width: 0.1px;
|
||||||
|
-webkit-text-stroke-color: #000000;
|
||||||
|
}
|
||||||
|
.cm-strong {
|
||||||
|
color: #309DD2;
|
||||||
|
}
|
||||||
|
.cm-em {
|
||||||
|
/*italics*/
|
||||||
|
}
|
||||||
|
.cm-link {
|
||||||
|
color: #DD6300;
|
||||||
|
}
|
||||||
|
.cm-string {
|
||||||
|
color: #AA8261;
|
||||||
|
}
|
||||||
|
/* @import */
|
||||||
|
.cm-def {
|
||||||
|
color: #2986CC;
|
||||||
|
}
|
||||||
|
/* Bullets and such */
|
||||||
|
.cm-variable-2 {
|
||||||
|
color: #3CBF30;
|
||||||
|
}
|
||||||
|
.block:not(.cm-comment) {
|
||||||
|
color: #E3E3E3;
|
||||||
|
}
|
||||||
|
.inline-block {
|
||||||
|
color: #E3E3E3;
|
||||||
|
}
|
||||||
|
.cm-tag {
|
||||||
|
color: #E3FF00;
|
||||||
|
}
|
||||||
|
.cm-attribute {
|
||||||
|
color: #E3FF00;
|
||||||
|
}
|
||||||
|
.cm-atom {
|
||||||
|
color: #c1939a;
|
||||||
|
}
|
||||||
|
.cm-number {
|
||||||
|
color: #2986CC;
|
||||||
|
}
|
||||||
|
.cm-property:not(.cm-error) ~ .cm-variable {
|
||||||
|
color:#9e1f9e;
|
||||||
|
}
|
||||||
|
.cm-qualifier {
|
||||||
|
color: #EE1919;
|
||||||
|
}
|
||||||
|
.cm-comment {
|
||||||
|
color: #BBC700;
|
||||||
|
}
|
||||||
|
.cm-keyword {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.cm-error {
|
||||||
|
color: #C50202;
|
||||||
|
}
|
||||||
|
.CodeMirror-foldmarker {
|
||||||
|
color: #F0FF00;
|
||||||
|
}
|
||||||
|
.cm-builtin {
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
.dt-highlight {
|
||||||
|
background: #ffffff14;
|
||||||
|
}
|
||||||
|
.dl-colon-highlight {
|
||||||
|
background: #ccc;
|
||||||
|
}
|
||||||
|
.dl-highlight.dd-highlight {
|
||||||
|
color: #b5858d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"cobalt",
|
"cobalt",
|
||||||
"colorforth",
|
"colorforth",
|
||||||
"darcula",
|
"darcula",
|
||||||
"darkbrewery-v301",
|
"darkbrewery",
|
||||||
"darkvision",
|
"darkvision",
|
||||||
"dracula",
|
"dracula",
|
||||||
"duotone-dark",
|
"duotone-dark",
|
||||||
|
|||||||
BIN
themes/fonts/iconFonts/fa-brands-400.woff2
Normal file
BIN
themes/fonts/iconFonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
themes/fonts/iconFonts/fa-regular-400.woff2
Normal file
BIN
themes/fonts/iconFonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
themes/fonts/iconFonts/fa-solid-900.woff2
Normal file
BIN
themes/fonts/iconFonts/fa-solid-900.woff2
Normal file
Binary file not shown.
BIN
themes/fonts/iconFonts/fa-v4compatibility.woff2
Normal file
BIN
themes/fonts/iconFonts/fa-v4compatibility.woff2
Normal file
Binary file not shown.
@@ -1,2 +1,10 @@
|
|||||||
|
@import (less) "./themes/fonts/iconFonts/fontawesome-free.less";
|
||||||
|
@import (less) "./themes/fonts/iconFonts/fontawesome-solid.less";
|
||||||
|
@import (less) "./themes/fonts/iconFonts/fontawesome-brands.less";
|
||||||
|
@import (less) "./themes/fonts/iconFonts/fontawesome-regular.less";
|
||||||
|
|
||||||
|
|
||||||
/* Icon Font: Font Awesome */
|
/* Icon Font: Font Awesome */
|
||||||
.far,.fas,.fab { display : inline; }
|
.far,.fas,.fab { display : inline; }
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1609
themes/fonts/iconFonts/fontawesome-brands.less
Normal file
1609
themes/fonts/iconFonts/fontawesome-brands.less
Normal file
File diff suppressed because it is too large
Load Diff
6243
themes/fonts/iconFonts/fontawesome-free.less
Normal file
6243
themes/fonts/iconFonts/fontawesome-free.less
Normal file
File diff suppressed because it is too large
Load Diff
19
themes/fonts/iconFonts/fontawesome-regular.less
Normal file
19
themes/fonts/iconFonts/fontawesome-regular.less
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2024 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-style-family-classic: 'Font Awesome 6 Free';
|
||||||
|
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Free';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: block;
|
||||||
|
src: url("/fonts/iconFonts/fa-regular-400.woff2") format("woff2"), url("/fonts/iconFonts/fa-regular-400.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
.far,
|
||||||
|
.fa-regular {
|
||||||
|
font-weight: 400; }
|
||||||
19
themes/fonts/iconFonts/fontawesome-solid.less
Normal file
19
themes/fonts/iconFonts/fontawesome-solid.less
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2024 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-style-family-classic: 'Font Awesome 6 Free';
|
||||||
|
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Free';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 900;
|
||||||
|
font-display: block;
|
||||||
|
src: url("/fonts/iconFonts/fa-solid-900.woff2") format("woff2"), url("/fonts/iconFonts/fa-solid-900.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
.fas,
|
||||||
|
.fa-solid {
|
||||||
|
font-weight: 900; }
|
||||||
Reference in New Issue
Block a user