0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-23 20:53:05 +00:00

Compare commits

..

174 Commits

Author SHA1 Message Date
Trevor Buckner
698d60e0f2 Adds routes to crawl google users for unsubbed brews 2025-07-28 13:28:48 -04:00
Trevor Buckner
990bf80b59 Comment out patch contents from logs
patch contents on failed patches clogging logs with pages and pages of text
2025-07-22 14:45:57 -04:00
Trevor Buckner
f16598f238 Fix Google ID Validation Regex
Google IDs with underscores were failing.

Regex found in Google drive documentation: https://developers.google.com/workspace/docs/api/concepts/document
2025-07-22 14:39:09 -04:00
Trevor Buckner
579e9e0ec5 Merge pull request #4347 from G-Ambatte/experimentalDiffSaveFix
Diff patching fix using encodeURI
2025-07-19 15:30:10 -04:00
Trevor Buckner
f6629f2f9e Merge pull request #4287 from dbolack-ab/opengraph_locale
Add brew locale to opengraph localization
2025-07-19 15:27:44 -04:00
G.Ambatte
b87c78474d Fix for diff patching using encodeURI 2025-07-19 14:49:02 +12:00
Trevor Buckner
958d282a58 Merge branch 'master' into opengraph_locale 2025-07-17 14:30:16 -04:00
David Bolack
7e56ae2019 locale typo. 2025-07-16 10:34:16 -05:00
David Bolack
ebca50ed4b Merge branch 'master' into opengraph_locale 2025-07-16 10:33:27 -05:00
Trevor Buckner
bfd14757c2 Merge pull request #4210 from dbolack-ab/legacy_gmb
Add column, columnbreak, and pagebreak compatibulity to Legacy
2025-07-15 15:58:17 -04:00
Trevor Buckner
3626ed5a31 Rename regex, move column replacement
Renaming COLUMNBREAK_REGEX_LEGACY for consistency in naming scheme with the other regexes.

Moving the legacy `\column` replacement down to `renderPages()` where we do similar text modification steps for V3.
2025-07-15 14:47:04 -04:00
Trevor Buckner
d385bacdd6 Merge branch 'master' into legacy_gmb 2025-07-15 14:22:31 -04:00
Trevor Buckner
cbbb2c0a7d Merge pull request #4225 from naturalcrit/fix-calc-in-curly-elements
Fix calc operator regex
2025-07-15 14:21:35 -04:00
Trevor Buckner
fbe637ff82 Add to non-quoted case as well
`{{greenBox,height:calc(10px*2) }}` should also be valid without using quotes.
2025-07-15 14:16:17 -04:00
Trevor Buckner
82bd16c623 Merge branch 'master' into fix-calc-in-curly-elements 2025-07-15 14:09:02 -04:00
Trevor Buckner
d1f13af67b Merge pull request #4340 from naturalcrit/MoreHomebrew.jsxCleanup
More homebrew.jsx cleanup
2025-07-15 13:56:39 -04:00
Trevor Buckner
b6c03e88b8 Looks like react is needed by some other components later on 2025-07-15 17:53:01 +00:00
Trevor Buckner
b587d17397 Remove unused React import 2025-07-15 17:41:56 +00:00
Trevor Buckner
0a02f910f8 Clean up WithRoute 2025-07-15 17:32:10 +00:00
Trevor Buckner
ddfa06e76b Change requires to imports 2025-07-15 17:17:09 +00:00
Trevor Buckner
0c2b1fec04 Merge pull request #4226 from naturalcrit/refactor-homebrew.jsx-to-functional
refactor homebrew.jsx to be functional
2025-07-15 12:59:19 -04:00
Trevor Buckner
6de7a64acd Add comment for to-well-formed 2025-07-15 12:58:06 -04:00
Trevor Buckner
b9fe4c3901 Merge branch 'master' into refactor-homebrew.jsx-to-functional 2025-07-15 11:32:28 -04:00
Trevor Buckner
5ae01862e5 Merge pull request #4339 from naturalcrit/dependabot/npm_and_yarn/prod-dependencies-b017ff4ab1
Bump the prod-dependencies group across 1 directory with 2 updates
2025-07-15 11:17:59 -04:00
Trevor Buckner
398df7a061 Merge branch 'master' into dependabot/npm_and_yarn/prod-dependencies-b017ff4ab1 2025-07-15 11:15:54 -04:00
Trevor Buckner
443b0f6a37 Merge pull request #4320 from G-Ambatte/experimentalIDValidations
Brew ID validations
2025-07-15 11:14:45 -04:00
Trevor Buckner
544175b994 Merge branch 'master' into experimentalIDValidations 2025-07-15 11:14:11 -04:00
Trevor Buckner
955602e7ee Merge pull request #4333 from G-Ambatte/addForceSSLTests
Add forceSSL middleware tests
2025-07-15 11:11:33 -04:00
G.Ambatte
90e577dd3f Rework tests 2025-07-15 09:02:57 +12:00
G.Ambatte
828208aadb Add more ID validation test cases 2025-07-15 08:19:05 +12:00
G.Ambatte
973e071e93 Slightly loosen Google ID match criteria, add comments 2025-07-15 08:13:35 +12:00
dependabot[bot]
f9e7aa355d Bump the prod-dependencies group across 1 directory with 2 updates
Bumps the prod-dependencies group with 2 updates in the / directory: [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) and [mongoose](https://github.com/Automattic/mongoose).


Updates `core-js` from 3.43.0 to 3.44.0
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/commits/v3.44.0/packages/core-js)

Updates `mongoose` from 8.16.1 to 8.16.3
- [Release notes](https://github.com/Automattic/mongoose/releases)
- [Changelog](https://github.com/Automattic/mongoose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Automattic/mongoose/compare/8.16.1...8.16.3)

---
updated-dependencies:
- dependency-name: core-js
  dependency-version: 3.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-dependencies
- dependency-name: mongoose
  dependency-version: 8.16.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 20:09:17 +00:00
Trevor Buckner
24dfd41714 Merge branch 'master' into experimentalIDValidations 2025-07-14 13:37:19 -04:00
Trevor Buckner
638e54535d Merge branch 'master' into addForceSSLTests 2025-07-14 13:16:14 -04:00
Trevor Buckner
cbc6956221 Merge pull request #4334 from G-Ambatte/addTokenTests
Add token.js tests
2025-07-14 13:15:43 -04:00
Trevor Buckner
248d2038ec Cleanup token.js 2025-07-14 13:10:19 -04:00
Trevor Buckner
5b66175b8c Merge pull request #4337 from naturalcrit/dependabot/npm_and_yarn/dev-dependencies-3db4bbab60
Bump the dev-dependencies group across 1 directory with 2 updates
2025-07-14 12:59:01 -04:00
Trevor Buckner
552aa7d41a Merge branch 'master' into dependabot/npm_and_yarn/dev-dependencies-3db4bbab60 2025-07-14 12:53:33 -04:00
Trevor Buckner
b0a108b543 Merge pull request #4338 from G-Ambatte/addSafeHTMLTest
Increase safeHTML testing to 100% coverage
2025-07-14 12:53:12 -04:00
G.Ambatte
505d2840c0 Merge branch 'master' into addSafeHTMLTest 2025-07-14 21:26:22 +12:00
G.Ambatte
41ff50fefe Add missing test 2025-07-14 21:23:38 +12:00
G.Ambatte
2fbcc84a50 Merge branch 'master' into experimentalIDValidations 2025-07-14 14:50:05 +12:00
G.Ambatte
45e4d27c0a Merge branch 'master' into addForceSSLTests 2025-07-14 14:46:10 +12:00
G.Ambatte
77bf3ffc6f Merge branch 'master' into addTokenTests 2025-07-14 14:46:07 +12:00
dependabot[bot]
bc045ec6c9 Bump the dev-dependencies group across 1 directory with 2 updates
Bumps the dev-dependencies group with 2 updates in the / directory: [eslint](https://github.com/eslint/eslint) and [supertest](https://github.com/ladjs/supertest).


Updates `eslint` from 9.30.1 to 9.31.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.30.1...v9.31.0)

Updates `supertest` from 7.1.1 to 7.1.3
- [Release notes](https://github.com/ladjs/supertest/releases)
- [Commits](https://github.com/ladjs/supertest/compare/v7.1.1...v7.1.3)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.31.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: supertest
  dependency-version: 7.1.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 00:02:03 +00:00
Trevor Buckner
6390ea076a Merge pull request #4330 from naturalcrit/CompressSaveDataUpload
Gzip brew object when sending for save update
2025-07-13 20:00:21 -04:00
Trevor Buckner
6affcb587d Merge branch 'master' into CompressSaveDataUpload 2025-07-13 19:57:10 -04:00
Trevor Buckner
7787afabff Merge pull request #4336 from naturalcrit/fixVersionTest
Fix test option 2 - Add text property to version check test object
2025-07-13 19:55:16 -04:00
Trevor Buckner
fb4a8e5cf1 Merge branch 'CompressSaveDataUpload' of https://github.com/naturalcrit/homebrewery into CompressSaveDataUpload 2025-07-13 19:38:10 -04:00
Trevor Buckner
8432a6e367 cleanup 2025-07-13 19:38:08 -04:00
G.Ambatte
90ee08de42 Add text property to test object 2025-07-14 11:06:35 +12:00
G.Ambatte
40839b18e4 Add tests for token.js 2025-07-14 00:14:58 +12:00
G.Ambatte
677c02cfa5 Add forceSSL tests 2025-07-13 22:36:53 +12:00
G.Ambatte
a7a8803e9d Merge branch 'master' into experimentalIDValidations 2025-07-13 20:52:52 +12:00
Trevor Buckner
5fbc111db7 Merge branch 'master' into CompressSaveDataUpload 2025-07-13 00:55:59 -04:00
Trevor Buckner
5edea7d0f4 Turns out body-parser automatically inflates gzip. Can remove. 2025-07-13 00:55:16 -04:00
Trevor Buckner
d3a9d813c9 Log brew compression size just for testing purposes 2025-07-13 00:54:51 -04:00
Trevor Buckner
fc475b2a7e Allow babel to transpile fflate 2025-07-13 00:52:06 -04:00
Trevor Buckner
76b76b3bb6 Merge pull request #4286 from dbolack-ab/snippets-save-history-too
Add brew snippets to local save history
2025-07-11 13:32:13 -04:00
Trevor Buckner
22ef3cbebc Gzip brew object when sending for save update 2025-07-11 16:55:30 +00:00
Trevor Buckner
9da8a17053 Remove text mismatch logs 2025-07-10 17:17:25 -04:00
Trevor Buckner
7cadbfbd7b allowExceedingIndices for our patch applier
Test if it allows patches to go through, and log error if it doesn't match the expected output.
2025-07-10 17:11:31 -04:00
Trevor Buckner
98b9e86787 Merge pull request #4329 from naturalcrit/AdditionalPatchLogging
On patch failure, compare client and server text bytewise
2025-07-10 12:05:24 -04:00
Trevor Buckner
489b4b2694 Also log differences on MD5 mismatch 2025-07-10 12:04:09 -04:00
Trevor Buckner
8d279260c2 Merge branch 'master' into AdditionalPatchLogging 2025-07-10 11:19:07 -04:00
Trevor Buckner
7c08c430d0 Merge pull request #4324 from G-Ambatte/experimentalSplitHashAndVersionChecks
Split hash and version checks in updateBrew function
2025-07-10 11:18:53 -04:00
Trevor Buckner
45689d119e Comment out one failing test
Patch failure test is no longer being thrown while we monitor in the background
2025-07-10 11:18:39 -04:00
Trevor Buckner
c5805af935 On patch failure, compare client and server text bytewise 2025-07-10 11:12:42 -04:00
Trevor Buckner
b2c4bb7082 Merge branch 'master' into experimentalSplitHashAndVersionChecks 2025-07-10 11:06:29 -04:00
Trevor Buckner
68460447dc Merge pull request #4327 from dbolack-ab/fallbackTest
Run patch processing in parallel to prior system
2025-07-10 09:50:43 -04:00
Trevor Buckner
440c7beff6 Up to 3.19.3 so users can get the update 2025-07-10 09:47:21 -04:00
David Bolack
c7610cf0f8 Run patch processing in parallel to prior system to attempt to narrow down not-quite-so-edge cases that did not come up prior to user testing. 2025-07-10 07:10:13 -05:00
G.Ambatte
7f3a818558 Add Homebrew API coverage tests 2025-07-10 23:25:57 +12:00
G.Ambatte
bc82afa5b2 Split version from hash checks 2025-07-10 21:42:51 +12:00
G.Ambatte
abef250631 Update ID validation check 2025-07-10 20:58:46 +12:00
G.Ambatte
1794e96d50 Update tests 2025-07-10 20:46:01 +12:00
G.Ambatte
25f25da499 Adjust validation regex for IDs 2025-07-10 20:39:12 +12:00
G.Ambatte
aa15bdaacb Initial pass at ID validations 2025-07-10 19:59:09 +12:00
Trevor Buckner
7ba7991631 Additional diff server error logging 2025-07-10 00:37:03 -04:00
Trevor Buckner
0e1ac26999 v3.19.2 2025-07-09 22:42:21 -04:00
Trevor Buckner
f49fed8c35 Merge pull request #4311 from dbolack-ab/md5Fix
Normalize strings before running MD5s
2025-07-09 22:40:15 -04:00
Trevor Buckner
a8236fbab4 Ok. I'm lowering the coverage threshold 2025-07-09 22:14:24 -04:00
Trevor Buckner
daf4eceedd Small rearrangement 2025-07-09 22:09:16 -04:00
David Bolack
a02361ee65 Move normalization to before diffing 2025-07-09 19:37:57 -05:00
David Bolack
81e20f032e NOrmalize strings before rnuning MD5s 2025-07-09 18:52:45 -05:00
Trevor Buckner
1d92b98568 Merge pull request #4310 from dbolack-ab/2025-06-09_Debug
Add Patch wrapper/unwrapper for saves
2025-07-09 18:42:23 -04:00
David Bolack
0f4157d084 Add Patch wrapper/unwrapper for saves
Object encapsulation for the win?
2025-07-09 17:16:47 -05:00
Trevor Buckner
4dcc3749d8 Merge branch 'master' into snippets-save-history-too 2025-07-09 13:04:08 -04:00
Trevor Buckner
8f058d56f2 Merge pull request #4304 from naturalcrit/3.19.1
up To V3.19.1
2025-07-09 11:11:59 -04:00
Trevor Buckner
d192a064d6 up To V3.19.1 2025-07-09 15:10:53 +00:00
Trevor Buckner
cccb531e17 Merge pull request #4280 from naturalcrit/SaveDiffs
Diff Saving
2025-07-09 09:55:33 -04:00
Trevor Buckner
6414e73e7d Cleanup and better handling of pre-save snapshot 2025-07-08 15:50:27 -04:00
Trevor Buckner
41daf8d172 comment out hash check 2025-07-07 21:05:50 +00:00
Trevor Buckner
4c897fdeb5 Add MD5 hash check 2025-07-07 21:00:03 +00:00
Trevor Buckner
89ce4de354 Add hash-wasm package for md5 2025-07-07 20:47:34 +00:00
Trevor Buckner
43095507ee Fix 2025-07-07 19:26:03 +00:00
Trevor Buckner
eb7fbbe018 Merge branch 'master' into SaveDiffs 2025-07-07 15:07:44 -04:00
Trevor Buckner
869958ec38 Don't save unless previous save is complete 2025-07-07 19:00:01 +00:00
David Bolack
99b90e0998 Include snippets in the restoration. 2025-07-07 13:54:29 -05:00
Trevor Buckner
57a48100d3 Merge pull request #4300 from naturalcrit/rename_isPending_to_unsavedChanges
Rename isPending state to more explicit unsavedChanges
2025-07-07 14:54:25 -04:00
Trevor Buckner
8538e4fadb Rename isPending state to more explicit unsavedChanges 2025-07-07 18:48:36 +00:00
Trevor Buckner
9a002511a3 Merge pull request #4254 from naturalcrit/dependabot/npm_and_yarn/stylelint-config-recess-order-7.1.0
Bump stylelint-config-recess-order from 6.0.0 to 7.1.0
2025-07-03 16:23:48 -04:00
Trevor Buckner
3fa3a52e05 Merge branch 'master' into dependabot/npm_and_yarn/stylelint-config-recess-order-7.1.0 2025-07-03 16:14:17 -04:00
Trevor Buckner
4fe920dac3 Merge pull request #4244 from naturalcrit/dependabot/npm_and_yarn/googleapis/drive-13.0.1
Bump @googleapis/drive from 12.1.0 to 13.0.1
2025-07-03 16:13:44 -04:00
dependabot[bot]
71dff5fbf9 Bump @googleapis/drive from 12.1.0 to 13.0.1
Bumps [@googleapis/drive](https://github.com/googleapis/google-api-nodejs-client) from 12.1.0 to 13.0.1.
- [Release notes](https://github.com/googleapis/google-api-nodejs-client/releases)
- [Changelog](https://github.com/googleapis/google-api-nodejs-client/blob/main/.release-please-manifest.json)
- [Commits](https://github.com/googleapis/google-api-nodejs-client/compare/iam-v12.1.0...iam-v13.0.1)

---
updated-dependencies:
- dependency-name: "@googleapis/drive"
  dependency-version: 13.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 20:03:12 +00:00
dependabot[bot]
26419d2ccb Bump stylelint-config-recess-order from 6.0.0 to 7.1.0
Bumps [stylelint-config-recess-order](https://github.com/stormwarning/stylelint-config-recess-order) from 6.0.0 to 7.1.0.
- [Release notes](https://github.com/stormwarning/stylelint-config-recess-order/releases)
- [Changelog](https://github.com/stormwarning/stylelint-config-recess-order/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stormwarning/stylelint-config-recess-order/compare/v6.0.0...v7.1.0)

---
updated-dependencies:
- dependency-name: stylelint-config-recess-order
  dependency-version: 7.1.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 20:03:10 +00:00
Trevor Buckner
f02fe2d8f3 Merge pull request #4261 from naturalcrit/dependabot/npm_and_yarn/eslint-plugin-jest-29.0.1
Bump eslint-plugin-jest from 28.11.0 to 29.0.1
2025-07-03 16:01:53 -04:00
dependabot[bot]
318fb53eb2 Bump eslint-plugin-jest from 28.11.0 to 29.0.1
Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 28.11.0 to 29.0.1.
- [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases)
- [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v28.11.0...v29.0.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-jest
  dependency-version: 29.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 19:32:43 +00:00
Trevor Buckner
6a32b7427b Merge pull request #4270 from naturalcrit/dependabot/npm_and_yarn/pbkdf2-3.1.3
Bump pbkdf2 from 3.1.2 to 3.1.3
2025-07-03 15:31:22 -04:00
Trevor Buckner
5886bd65e5 Merge branch 'master' into dependabot/npm_and_yarn/pbkdf2-3.1.3 2025-07-03 15:27:43 -04:00
Trevor Buckner
9c5f80cbdb Merge pull request #4295 from naturalcrit/dependabot/npm_and_yarn/dev-dependencies-6ab8ca7825
Bump the dev-dependencies group with 3 updates
2025-07-03 15:26:55 -04:00
dependabot[bot]
79d8956c4f Bump pbkdf2 from 3.1.2 to 3.1.3
Bumps [pbkdf2](https://github.com/crypto-browserify/pbkdf2) from 3.1.2 to 3.1.3.
- [Changelog](https://github.com/browserify/pbkdf2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/pbkdf2/compare/v3.1.2...v3.1.3)

---
updated-dependencies:
- dependency-name: pbkdf2
  dependency-version: 3.1.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 19:24:44 +00:00
Trevor Buckner
2e491b3556 Merge branch 'master' into dependabot/npm_and_yarn/dev-dependencies-6ab8ca7825 2025-07-03 15:23:48 -04:00
Trevor Buckner
d9a8afa272 Merge pull request #4296 from naturalcrit/dependabot/npm_and_yarn/prod-dependencies-2b50ddedc8
Bump the prod-dependencies group with 6 updates
2025-07-03 15:23:18 -04:00
dependabot[bot]
209195202c Bump the prod-dependencies group with 6 updates
Bumps the prod-dependencies group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) | `7.27.1` | `7.27.6` |
| [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) | `3.42.0` | `3.43.0` |
| [marked-emoji](https://github.com/UziTech/marked-emoji) | `2.0.0` | `2.0.1` |
| [mongoose](https://github.com/Automattic/mongoose) | `8.15.0` | `8.16.1` |
| [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `7.6.0` | `7.6.3` |
| [romans](https://github.com/qbunt/romans) | `3.0.0` | `3.1.0` |


Updates `@babel/runtime` from 7.27.1 to 7.27.6
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.27.6/packages/babel-runtime)

Updates `core-js` from 3.42.0 to 3.43.0
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/commits/v3.43.0/packages/core-js)

Updates `marked-emoji` from 2.0.0 to 2.0.1
- [Release notes](https://github.com/UziTech/marked-emoji/releases)
- [Changelog](https://github.com/UziTech/marked-emoji/blob/main/release.config.cjs)
- [Commits](https://github.com/UziTech/marked-emoji/compare/v2.0.0...v2.0.1)

Updates `mongoose` from 8.15.0 to 8.16.1
- [Release notes](https://github.com/Automattic/mongoose/releases)
- [Changelog](https://github.com/Automattic/mongoose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Automattic/mongoose/compare/8.15.0...8.16.1)

Updates `react-router` from 7.6.0 to 7.6.3
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.6.3/packages/react-router)

Updates `romans` from 3.0.0 to 3.1.0
- [Release notes](https://github.com/qbunt/romans/releases)
- [Commits](https://github.com/qbunt/romans/compare/v3.0.0...v3.1.0)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-version: 7.27.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-dependencies
- dependency-name: core-js
  dependency-version: 3.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-dependencies
- dependency-name: marked-emoji
  dependency-version: 2.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-dependencies
- dependency-name: mongoose
  dependency-version: 8.16.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-dependencies
- dependency-name: react-router
  dependency-version: 7.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-dependencies
- dependency-name: romans
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 18:54:23 +00:00
dependabot[bot]
64235c844a Bump the dev-dependencies group with 3 updates
Bumps the dev-dependencies group with 3 updates: [@stylistic/stylelint-plugin](https://github.com/stylelint-stylistic/stylelint-stylistic), [babel-plugin-transform-import-meta](https://github.com/javiertury/babel-plugin-transform-import-meta) and [stylelint](https://github.com/stylelint/stylelint).


Updates `@stylistic/stylelint-plugin` from 3.1.2 to 3.1.3
- [Release notes](https://github.com/stylelint-stylistic/stylelint-stylistic/releases)
- [Changelog](https://github.com/stylelint-stylistic/stylelint-stylistic/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint-stylistic/stylelint-stylistic/compare/v3.1.2...v3.1.3)

Updates `babel-plugin-transform-import-meta` from 2.3.2 to 2.3.3
- [Changelog](https://github.com/javiertury/babel-plugin-transform-import-meta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/javiertury/babel-plugin-transform-import-meta/compare/v2.3.2...v2.3.3)

Updates `stylelint` from 16.19.1 to 16.21.1
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/16.19.1...16.21.1)

---
updated-dependencies:
- dependency-name: "@stylistic/stylelint-plugin"
  dependency-version: 3.1.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: babel-plugin-transform-import-meta
  dependency-version: 2.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: stylelint
  dependency-version: 16.21.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 18:52:53 +00:00
Trevor Buckner
5d000a4599 Update dependabot.yml
Fixed bad spacing typo
2025-07-03 14:50:48 -04:00
Trevor Buckner
380e593b42 Group minor & patch dependency PRs
Instead of individual dependabot PRs that need to be merged, then we trigger a rebase, wait 5 minutes, then merge the next...

We can group dependency updates together in a single PR. This change makes all dev dependencies bundle minor and patch versions into one PR, and similarly with production dependencies. Major versions will still have separate PRs as they tend to break things.
2025-07-03 14:45:29 -04:00
Trevor Buckner
169f089d08 Merge pull request #4284 from naturalcrit/dependabot/npm_and_yarn/marked-gfm-heading-id-4.1.2
Bump marked-gfm-heading-id from 4.1.1 to 4.1.2
2025-07-03 14:03:01 -04:00
dependabot[bot]
b3977ed141 Bump marked-gfm-heading-id from 4.1.1 to 4.1.2
Bumps [marked-gfm-heading-id](https://github.com/markedjs/marked-gfm-heading-id) from 4.1.1 to 4.1.2.
- [Release notes](https://github.com/markedjs/marked-gfm-heading-id/releases)
- [Changelog](https://github.com/markedjs/marked-gfm-heading-id/blob/main/release.config.cjs)
- [Commits](https://github.com/markedjs/marked-gfm-heading-id/compare/v4.1.1...v4.1.2)

---
updated-dependencies:
- dependency-name: marked-gfm-heading-id
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 17:52:30 +00:00
Trevor Buckner
9800561de7 Merge pull request #4292 from naturalcrit/dependabot/npm_and_yarn/babel/plugin-transform-runtime-7.28.0
Bump @babel/plugin-transform-runtime from 7.27.1 to 7.28.0
2025-07-03 13:50:55 -04:00
dependabot[bot]
166af08e6a Bump @babel/plugin-transform-runtime from 7.27.1 to 7.28.0
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.27.1 to 7.28.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.0/packages/babel-plugin-transform-runtime)

---
updated-dependencies:
- dependency-name: "@babel/plugin-transform-runtime"
  dependency-version: 7.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 17:47:13 +00:00
Trevor Buckner
48f17f7c5e Merge pull request #4289 from naturalcrit/dependabot/npm_and_yarn/eslint-9.30.1
Bump eslint from 9.27.0 to 9.30.1
2025-07-03 13:45:34 -04:00
dependabot[bot]
87c9f52222 Bump eslint from 9.27.0 to 9.30.1
Bumps [eslint](https://github.com/eslint/eslint) from 9.27.0 to 9.30.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.27.0...v9.30.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.30.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 17:41:39 +00:00
Trevor Buckner
c80b7ffd66 Merge pull request #4290 from naturalcrit/dependabot/npm_and_yarn/globals-16.3.0
Bump globals from 16.1.0 to 16.3.0
2025-07-03 13:40:08 -04:00
dependabot[bot]
5f16ce3dbd Bump globals from 16.1.0 to 16.3.0
Bumps [globals](https://github.com/sindresorhus/globals) from 16.1.0 to 16.3.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.1.0...v16.3.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 16.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 17:31:36 +00:00
Trevor Buckner
b5ff26f857 Merge pull request #4293 from naturalcrit/dependabot/npm_and_yarn/babel/preset-env-7.28.0
Bump @babel/preset-env from 7.27.2 to 7.28.0
2025-07-03 13:30:02 -04:00
Trevor Buckner
578b01bbb1 Merge branch 'master' into dependabot/npm_and_yarn/babel/preset-env-7.28.0 2025-07-03 13:27:27 -04:00
Trevor Buckner
67467e0099 Merge pull request #4294 from naturalcrit/dependabot/npm_and_yarn/jest-30.0.4
Bump jest from 29.7.0 to 30.0.4
2025-07-03 13:27:15 -04:00
dependabot[bot]
da21bf20f9 Bump jest from 29.7.0 to 30.0.4
Bumps [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) from 29.7.0 to 30.0.4.
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.0.4/packages/jest)

---
updated-dependencies:
- dependency-name: jest
  dependency-version: 30.0.4
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 03:54:56 +00:00
dependabot[bot]
df7fcf1e5f Bump @babel/preset-env from 7.27.2 to 7.28.0
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.27.2 to 7.28.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.0/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-version: 7.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 03:54:32 +00:00
David Bolack
702ece6671 Add brew locale to opengraph localization 2025-06-30 12:39:30 -05:00
David Bolack
1008321957 Add brew snippets to local save history
solves #3113
2025-06-30 12:17:46 -05:00
David Bolack
b547486c48 Merge branch 'master' into legacy_gmb 2025-06-30 10:54:35 -05:00
Trevor Buckner
e1e661976d Initial test 2025-06-27 08:07:02 -04:00
Víctor Losada Hernández
7bdeeee9ef Merge pull request #4237 from naturalcrit/fix-toolbar-issues
Add defaults to the toolbar for rowGap and columnGap
2025-06-25 17:01:10 +02:00
Víctor Losada Hernández
becf35d336 Merge branch 'fix-toolbar-issues' of https://github.com/naturalcrit/homebrewery into fix-toolbar-issues 2025-06-25 16:58:26 +02:00
Víctor Losada Hernández
d7585767c9 remove too strong shadow 2025-06-25 16:58:24 +02:00
Víctor Losada Hernández
f9bb6209b7 Merge branch 'master' into fix-toolbar-issues 2025-06-25 16:55:20 +02:00
Víctor Losada Hernández
13702a2f62 last thing 2025-06-25 16:54:56 +02:00
Trevor Buckner
a6a684c89e Merge pull request #4272 from naturalcrit/RemoveTextBinFromSaveResponse
Don't send full text back to client on save
2025-06-24 17:45:40 -04:00
Trevor Buckner
862fa7de89 Don't send full text back to client on save
We return the stub after saving. When saving to HB MongoDB, the stub also includes the full text. This does not need to be sent back to the client.
2025-06-24 17:44:22 -04:00
Trevor Buckner
b671cf7b02 Merge pull request #4265 from naturalcrit/HandleRequestAbortError
Handle Request Abort Error
2025-06-20 17:58:17 -04:00
Trevor Buckner
d5dbe0b4ba Update error-navitem.jsx 2025-06-20 17:53:08 -04:00
Víctor Losada Hernández
c2cf695c17 add defaults 2025-06-02 13:50:51 +02:00
Víctor Losada Hernández
6d0d6f08b5 initial commit 2025-05-28 09:09:14 +02:00
Víctor Losada Hernández
77dcc9b433 initial commit 2025-05-28 08:34:52 +02:00
Víctor Losada Hernández
5f2f3a6f3d Merge pull request #4214 from dbolack-ab/issue_4211
Clone snippets
2025-05-25 15:29:59 +02:00
David Bolack
bbb812cb06 Clone snippets 2025-05-25 08:07:50 -05:00
David Bolack
5648e55774 Add column, columnbreak, and pagebreak compatibuility to Legacy 2025-05-23 14:45:37 -05:00
Trevor Buckner
c051580545 Merge pull request #4207 from naturalcrit/fix-codemirror-overflow
Fix Codemirror Overflow
2025-05-23 10:03:26 -04:00
Trevor Buckner
6e72fe2600 Merge branch 'master' into fix-codemirror-overflow 2025-05-23 10:00:33 -04:00
Trevor Buckner
03602ae1e0 Merge pull request #4208 from 5e-Cleric/add-icons-back-to-admin
bring back icons to admin
2025-05-22 23:47:39 -04:00
Víctor Losada Hernández
8de738a146 initial commit 2025-05-23 00:20:32 +02:00
Víctor Losada Hernández
6960beb739 updating changelog to reflect reality 2025-05-22 23:35:26 +02:00
Víctor Losada Hernández
6748639ec5 remove dumb console log from another pr 2025-05-22 23:35:00 +02:00
Víctor Losada Hernández
e5651807fd Merge branch 'master' into fix-codemirror-overflow 2025-05-22 23:30:02 +02:00
Víctor Losada Hernández
9adf6dee61 use js for it 2025-05-22 23:29:08 +02:00
Víctor Losada Hernández
03527a1f95 fix it damn it 2025-05-22 22:35:26 +02:00
Trevor Buckner
651863b0f7 Merge pull request #4185 from naturalcrit/dependabot/npm_and_yarn/express-static-gzip-3.0.0
Bump express-static-gzip from 2.2.0 to 3.0.0
2025-05-22 15:41:56 -04:00
Trevor Buckner
450ecd24b7 Merge branch 'master' into dependabot/npm_and_yarn/express-static-gzip-3.0.0 2025-05-22 15:08:56 -04:00
Trevor Buckner
995cfa2aa4 Merge pull request #4170 from dbolack-ab/issue_3318
Tweak icon height
2025-05-22 15:04:46 -04:00
Trevor Buckner
5eecb5ea20 Remove unnecessary properties 2025-05-22 15:01:38 -04:00
Trevor Buckner
0885473b66 Merge branch 'master' into pr/4170 2025-05-22 14:59:31 -04:00
dependabot[bot]
eabff4f6b2 Bump express-static-gzip from 2.2.0 to 3.0.0
Bumps [express-static-gzip](https://github.com/tkoenig89/express-static-gzip) from 2.2.0 to 3.0.0.
- [Release notes](https://github.com/tkoenig89/express-static-gzip/releases)
- [Commits](https://github.com/tkoenig89/express-static-gzip/compare/v2.2.0...v3.0.0)

---
updated-dependencies:
- dependency-name: express-static-gzip
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-22 18:40:45 +00:00
Trevor Buckner
a773df25d0 Merge pull request #4200 from naturalcrit/dependabot/npm_and_yarn/mongoose-8.15.0
Bump mongoose from 8.14.3 to 8.15.0
2025-05-22 14:39:30 -04:00
dependabot[bot]
b07f75ac36 Bump mongoose from 8.14.3 to 8.15.0
Bumps [mongoose](https://github.com/Automattic/mongoose) from 8.14.3 to 8.15.0.
- [Release notes](https://github.com/Automattic/mongoose/releases)
- [Changelog](https://github.com/Automattic/mongoose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Automattic/mongoose/compare/8.14.3...8.15.0)

---
updated-dependencies:
- dependency-name: mongoose
  dependency-version: 8.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-22 18:33:03 +00:00
Trevor Buckner
ed5fbadd73 Merge pull request #4206 from naturalcrit/v3.19.0
v3.19.0
2025-05-22 14:31:47 -04:00
David Bolack
7abf45e8ba Merge branch 'master' into issue_3318 2025-05-10 18:53:31 -05:00
David Bolack
bbae62e0b7 Merge branch 'issue_3318' of github.com:dbolack-ab/homebrewery into issue_3318 2025-05-10 18:53:19 -05:00
David Bolack
a9d71078d3 A better look, I think? 2025-05-10 18:52:32 -05:00
Víctor Losada Hernández
0ca53f8db6 Merge branch 'master' into issue_3318 2025-05-09 19:08:04 +02:00
David Bolack
6f52b8473f Swap in an svg 2025-04-30 10:38:15 -05:00
David Bolack
9ecd53267f Tweak icon height 2025-04-29 19:43:11 -05:00
32 changed files with 3086 additions and 2672 deletions

View File

@@ -5,6 +5,15 @@ updates:
schedule:
interval: daily
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:
- dependency-name: eslint
versions:

View File

@@ -88,6 +88,30 @@ pre {
## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Wednesday 7/09/2025 - v3.19.3
{{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
@@ -130,7 +154,7 @@ Fixes issue [#4146](https://github.com/naturalcrit/homebrewery/issues/4146)
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
* [x] Fix search bar covering up snippet bar
* [x] Fix search bar covering up snippet bar (3 times)
Fixes issue [#4098](https://github.com/naturalcrit/homebrewery/issues/4098)

View File

@@ -3,6 +3,7 @@
@import 'naturalcrit/styles/animations.less';
@import 'naturalcrit/styles/colors.less';
@import 'naturalcrit/styles/tooltip.less';
@import './themes/fonts/iconFonts/fontAwesome.less';
@import 'font-awesome/css/font-awesome.css';

View File

@@ -20,6 +20,8 @@ import HeaderNav from './headerNav/headerNav.jsx';
import { safeHTML } from './safeHTML.js';
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 INITIAL_CONTENT = dedent`
@@ -113,13 +115,14 @@ const BrewRenderer = (props)=>{
zoomLevel : 100,
spread : 'single',
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'));
console.log('toolbar state:', toolbarState);
toolbarState && setDisplayOptions(toolbarState);
}, []);
@@ -129,7 +132,7 @@ const BrewRenderer = (props)=>{
const pagesRef = useRef(null);
if(props.renderer == 'legacy') {
rawPages = props.text.split('\\page');
rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY);
} else {
rawPages = props.text.split(PAGEBREAK_REGEX_V3);
}
@@ -186,6 +189,7 @@ const BrewRenderer = (props)=>{
let attributes = {};
if(props.renderer == 'legacy') {
pageText.replace(COLUMNBREAK_REGEX_LEGACY, '```\n````\n'); // Allow Legacy brews to use `\column(break)`
const html = MarkdownLegacy.render(pageText);
return <BrewPage className='page phb' index={index} key={index} contents={html} style={styles} onVisibilityChange={handlePageVisibilityChange} />;

View File

@@ -21,8 +21,9 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
}, [visiblePages]);
useEffect(()=>{
const visibility = localStorage.getItem('hb_toolbarVisibility') === 'true';
setToolsVisible(visibility);
const Visibility = localStorage.getItem('hb_toolbarVisibility');
if (Visibility) setToolsVisible(Visibility === 'true');
}, []);
const handleZoomButton = (zoom)=>{
@@ -68,7 +69,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
} else if(mode == 'fit'){
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
let minDimRatio;
if(displayOptions.spread === 'active')
if(displayOptions.spread === 'single')
minDimRatio = [...pages].reduce(
(minRatio, page)=>Math.min(minRatio,
iframeWidth / page.offsetWidth,
@@ -165,7 +166,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
id='single-spread'
className='tool'
title='Single Page'
onClick={()=>{handleOptionChange('spread', 'active');}}
onClick={()=>{handleOptionChange('spread', 'single');}}
aria-checked={displayOptions.spread === 'single'}
><i className='fac single-spread' /></button>
<button role='radio'

View File

@@ -175,6 +175,10 @@
opacity : 0;
transition : all 0.2s ease;
}
.toggleButton button i {
filter: drop-shadow(0 0 2px black) drop-shadow(0 0 1px black);
}
}
}
@@ -184,9 +188,4 @@
z-index : 5;
display : flex;
height : 100%;
button i {
filter: drop-shadow(0 0 2px black) drop-shadow(0 0 1px black);
}
}

View File

@@ -14,7 +14,6 @@ const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
const SNIPPETBAR_HEIGHT = 25;
const DEFAULT_STYLE_TEXT = dedent`
/*=======--- Example CSS styling ---=======*/
/* Any CSS here will apply to your document! */
@@ -60,8 +59,9 @@ const Editor = createClass({
},
getInitialState : function() {
return {
editorTheme : this.props.editorTheme,
view : 'text' //'text', 'style', 'meta', 'snippet'
editorTheme : this.props.editorTheme,
view : 'text', //'text', 'style', 'meta', 'snippet'
snippetbarHeight : 25
};
},
@@ -88,6 +88,7 @@ const Editor = createClass({
editorTheme : editorTheme
});
}
this.setState({ snippetbarHeight: document.querySelector('.editor > .snippetBar').offsetHeight });
},
componentDidUpdate : function(prevProps, prevState, snapshot) {
@@ -412,6 +413,9 @@ const Editor = createClass({
//Called when there are changes to the editor's dimensions
update : function(){
this.codeEditor.current?.updateSize();
const snipHeight = document.querySelector('.editor > .snippetBar').offsetHeight;
if(snipHeight !== this.state.snippetbarHeight)
this.setState({ snippetbarHeight: snipHeight });
},
updateEditorTheme : function(newTheme){
@@ -436,7 +440,8 @@ const Editor = createClass({
value={this.props.brew.text}
onChange={this.props.onTextChange}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} />
rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
</>;
}
if(this.isStyle()){
@@ -449,7 +454,8 @@ const Editor = createClass({
onChange={this.props.onStyleChange}
enableFolding={true}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} />
rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
</>;
}
if(this.isMeta()){
@@ -478,7 +484,8 @@ const Editor = createClass({
onChange={this.props.onSnipChange}
enableFolding={true}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} />
rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
</>;
}
},

View File

@@ -5,7 +5,7 @@
height : 100%;
container : editor / inline-size;
.codeEditor {
height : 100%;
height : calc(100% - 25px);
.CodeMirror { height : 100%; }
.pageLine, .snippetLine {
background : #33333328;
@@ -108,9 +108,4 @@
span { padding : 2px 5px; }
}
}
@container editor (width < 683px) {
.editor .codeEditor .CodeMirror { height : calc(100% - 51px);}
.homePage .editor .codeEditor .CodeMirror { height : calc(100% - 25px);}
}

View File

@@ -1,95 +1,75 @@
//╔===--------------- Polyfills --------------===╗//
import 'core-js/es/string/to-well-formed.js';
//╚===--------------- ---------------===╝//
/* eslint-disable camelcase */
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');
const React = require('react');
const createClass = require('create-react-class');
const { StaticRouter:Router } = require('react-router');
const { Route, Routes, useParams, useSearchParams } = require('react-router');
import HomePage from './pages/homePage/homePage.jsx';
import EditPage from './pages/editPage/editPage.jsx';
import UserPage from './pages/userPage/userPage.jsx';
import SharePage from './pages/sharePage/sharePage.jsx';
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 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 WithRoute = ({ el: Element, ...rest })=>{
const params = useParams();
const [searchParams] = useSearchParams();
const queryParams = {};
for (const [key, value] of searchParams?.entries() || []) {
queryParams[key] = value;
}
const Element = props.el;
const allProps = {
...props,
...params,
query : queryParams,
el : undefined
};
return <Element {...allProps} />;
const queryParams = Object.fromEntries(searchParams?.entries() || []);
return <Element {...rest} {...params} query={queryParams} />;
};
const Homebrew = createClass({
displayName : 'Homebrewery',
getDefaultProps : function() {
return {
url : '',
welcomeText : '',
changelog : '',
version : '0.0.0',
account : null,
enable_v3 : false,
brew : {
title : '',
text : '',
shareId : null,
editId : null,
createdAt : null,
updatedAt : null,
lang : ''
}
};
},
const Homebrew = (props)=>{
const {
url = '',
version = '0.0.0',
account = null,
enable_v3 = false,
enable_themes,
config,
brew = {
title : '',
text : '',
shareId : null,
editId : null,
createdAt : null,
updatedAt : null,
lang : ''
},
userThemes,
brews
} = props;
getInitialState : function() {
global.account = this.props.account;
global.version = this.props.version;
global.enable_v3 = this.props.enable_v3;
global.enable_themes = this.props.enable_themes;
global.config = this.props.config;
global.account = account;
global.version = version;
global.enable_v3 = enable_v3;
global.enable_themes = enable_themes;
global.config = config;
return {};
},
render : function (){
return (
<Router location={this.props.url}>
<div className='homebrew'>
<Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
<Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } />
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
<Route path='/migrate' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} accountDetails={this.props.brew.accountDetails} />} />
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
</Routes>
</div>
</Router>
);
}
});
return (
<Router location={url}>
<div className='homebrew'>
<Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={brew} userThemes={userThemes}/>} />
<Route path='/new' element={<WithRoute el={NewPage} userThemes={userThemes}/> } />
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={brews} />} />
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
<Route path='/changelog' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
<Route path='/faq' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
<Route path='/migrate' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
<Route path='/account' element={<WithRoute el={AccountPage} brew={brew} accountDetails={brew.accountDetails} />} />
<Route path='/legacy' element={<WithRoute el={HomePage} brew={brew} />} />
<Route path='/error' element={<WithRoute el={ErrorPage} brew={brew} />} />
<Route path='/' element={<WithRoute el={HomePage} brew={brew} />} />
<Route path='/*' element={<WithRoute el={HomePage} brew={brew} />} />
</Routes>
</div>
</Router>
);
};
module.exports = Homebrew;

View File

@@ -23,14 +23,15 @@ const ErrorNavItem = createClass({
const error = this.props.error;
const response = error.response;
const status = response.status;
const HBErrorCode = response.body?.HBErrorCode;
const message = response.body?.message;
const status = response?.status;
const errorCode = error.code
const HBErrorCode = response?.body?.HBErrorCode;
const message = response?.body?.message;
let errMsg = '';
try {
errMsg += `${error.toString()}\n\n`;
errMsg += `\`\`\`\n${error.stack}\n`;
errMsg += `${JSON.stringify(response.error, null, ' ')}\n\`\`\``;
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``;
console.log(errMsg);
} catch (e){}
@@ -73,7 +74,7 @@ const ErrorNavItem = createClass({
</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'>
Oops!
<div className='errorContainer' onClick={clearError}>
@@ -82,7 +83,7 @@ const ErrorNavItem = createClass({
</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'>
Oops!
<div className='errorContainer' onClick={clearError}>
@@ -129,6 +130,18 @@ const ErrorNavItem = createClass({
</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'>
Oops!
<div className='errorContainer'>

View File

@@ -5,7 +5,7 @@ const moment = require('moment');
import request from '../../../../utils/request-middleware.js';
const googleDriveIcon = require('../../../../googleDrive.svg');
const homebreweryIcon = require('../../../../thumbnail.png');
const homebreweryIcon = require('../../../../thumbnail.svg');
const dedent = require('dedent-tabs').default;
const BrewItem = ({

View File

@@ -115,15 +115,15 @@
}
}
.googleDriveIcon {
height : 18px;
padding : 0px;
margin : -5px;
height : 18px;
}
.homebreweryIcon {
position : relative;
top : 5px;
left : -5px;
height : 24px;
mix-blend-mode : darken;
position : relative;
padding : 0px;
top : 5px;
left : -7.5px;
height : 18px;
}
}

View File

@@ -3,6 +3,9 @@ require('./editPage.less');
const React = require('react');
const _ = require('lodash');
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';
const { Meta } = require('vitreum/headtags');
@@ -47,7 +50,7 @@ const EditPage = createClass({
return {
brew : this.props.brew,
isSaving : false,
isPending : false,
unsavedChanges : false,
alertTrashedGoogleBrew : this.props.brew.trashed,
alertLoginToTransfer : false,
saveGoogle : this.props.brew.googleId ? true : false,
@@ -85,7 +88,7 @@ const EditPage = createClass({
});
window.onbeforeunload = ()=>{
if(this.state.isSaving || this.state.isPending){
if(this.state.isSaving || this.state.unsavedChanges){
return 'You have unsaved changes!';
}
};
@@ -104,9 +107,9 @@ const EditPage = createClass({
},
componentDidUpdate : function(){
const hasChange = this.hasChanges();
if(this.state.isPending != hasChange){
if(this.state.unsavedChanges != hasChange){
this.setState({
isPending : hasChange
unsavedChanges : hasChange
});
}
},
@@ -156,9 +159,9 @@ const EditPage = createClass({
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
this.setState((prevState)=>({
brew : { ...prevState.brew, snippets: snippet },
isPending : true,
htmlErrors : htmlErrors,
brew : { ...prevState.brew, snippets: snippet },
unsavedChanges : true,
htmlErrors : htmlErrors,
}), ()=>{if(this.state.autoSave) this.trySave();});
},
@@ -188,20 +191,28 @@ const EditPage = createClass({
this.setState((prevState)=>({
brew : {
...prevState.brew,
style : newData.style,
text : newData.text
style : newData.style,
text : newData.text,
snippets : newData.snippets
}
}));
},
trySave : function(immediate=false){
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
if(this.hasChanges()){
if(this.state.isSaving)
return;
if(immediate) {
this.debounceSave();
} else {
this.debounceSave.cancel();
this.debounceSave.flush();
return;
}
if(immediate) this.debounceSave.flush();
if(this.hasChanges())
this.debounceSave();
else
this.debounceSave.cancel();
},
handleGoogleClick : function(){
@@ -215,8 +226,7 @@ const EditPage = createClass({
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
}));
this.setState({
error : null,
isSaving : false
error : null
});
},
@@ -232,14 +242,16 @@ const EditPage = createClass({
toggleGoogleStorage : function(){
this.setState((prevState)=>({
saveGoogle : !prevState.saveGoogle,
isSaving : false,
error : null
}), ()=>this.save());
}), ()=>this.trySave(true));
},
save : async function(){
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
const brewState = this.state.brew; // freeze the current state
const preSaveSnapshot = { ...brewState };
this.setState((prevState)=>({
isSaving : true,
error : null,
@@ -249,15 +261,25 @@ const EditPage = createClass({
await updateHistory(this.state.brew).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 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 res = await request
.put(`/api/update/${brew.editId}${params}`)
.send(brew)
.set('Content-Encoding', 'gzip')
.set('Content-Type', 'application/json')
.send(compressedBrew)
.catch((err)=>{
console.log('Error Updating Local Brew');
this.setState({ error: err });
@@ -265,20 +287,28 @@ const EditPage = createClass({
if(!res) return;
this.savedBrew = {
...this.state.brew,
...preSaveSnapshot,
googleId : res.body.googleId ? res.body.googleId : null,
editId : res.body.editId,
shareId : res.body.shareId,
version : res.body.version
};
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
this.setState(()=>({
brew : this.savedBrew,
isPending : false,
this.setState((prevState) => ({
brew: {
...prevState.brew,
googleId : res.body.googleId ? res.body.googleId : null,
editId : res.body.editId,
shareId : res.body.shareId,
version : res.body.version
},
isSaving : false,
unsavedTime : new Date()
}));
}), ()=>{
this.setState({ unsavedChanges : this.hasChanges() });
});
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
},
renderGoogleDriveIcon : function(){
@@ -336,7 +366,7 @@ const EditPage = createClass({
}
// #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();
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.`;
@@ -351,7 +381,7 @@ const EditPage = createClass({
// #3 - Unsaved changes exist, click to save, show SAVE NOW
// 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>;
}
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED

View File

@@ -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.`,
// 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
'50' : dedent`
## You are not signed in

View File

@@ -148,7 +148,6 @@ const NewPage = createClass({
this.setState((prevState)=>({
brew : { ...prevState.brew, snippets: snippet },
isPending : true,
htmlErrors : htmlErrors,
}), ()=>{if(this.state.autoSave) this.trySave();});
},

View 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

View File

@@ -42,6 +42,7 @@ function parseBrewForStorage(brew, slot = 0) {
title : brew.title,
text : brew.text,
style : brew.style,
snippets : brew.snippets,
version : brew.version,
shareId : brew.shareId,
savedAt : brew?.savedAt || new Date(),

4706
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.19.0",
"version": "3.19.3",
"type": "module",
"engines": {
"npm": "^10.8.x",
@@ -72,7 +72,7 @@
"lines": 50
},
"server/homebrew.api.js": {
"statements": 70,
"statements": 60,
"branches": 50,
"functions": 65,
"lines": 70
@@ -84,24 +84,28 @@
},
"dependencies": {
"@babel/core": "^7.27.1",
"@babel/plugin-transform-runtime": "^7.27.1",
"@babel/preset-env": "^7.27.2",
"@babel/plugin-transform-runtime": "^7.28.0",
"@babel/preset-env": "^7.28.0",
"@babel/preset-react": "^7.27.1",
"@babel/runtime": "^7.27.1",
"@googleapis/drive": "^12.1.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",
"classnames": "^2.5.1",
"codemirror": "^5.65.6",
"cookie-parser": "^1.4.7",
"core-js": "^3.42.0",
"core-js": "^3.44.0",
"cors": "^2.8.5",
"create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3",
"expr-eval": "^2.0.2",
"express": "^5.1.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",
"hash-wasm": "^4.12.0",
"idb-keyval": "^6.2.2",
"js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6",
@@ -110,41 +114,41 @@
"marked": "15.0.12",
"marked-alignment-paragraphs": "^1.0.0",
"marked-definition-lists": "^1.0.1",
"marked-emoji": "^2.0.0",
"marked-emoji": "^2.0.1",
"marked-extended-tables": "^2.0.1",
"marked-gfm-heading-id": "^4.0.1",
"marked-gfm-heading-id": "^4.1.2",
"marked-nonbreaking-spaces": "^1.0.1",
"marked-smartypants-lite": "^1.0.3",
"marked-subsuper-text": "^1.0.3",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1",
"mongoose": "^8.14.3",
"mongoose": "^8.16.3",
"nanoid": "5.1.5",
"nconf": "^0.13.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-frame-component": "^4.1.3",
"react-router": "^7.6.0",
"romans": "^3.0.0",
"react-router": "^7.6.3",
"romans": "^3.1.0",
"sanitize-filename": "1.6.3",
"superagent": "^10.2.1",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git",
"written-number": "^0.11.1"
},
"devDependencies": {
"@stylistic/stylelint-plugin": "^3.1.2",
"babel-plugin-transform-import-meta": "^2.3.2",
"eslint": "^9.27.0",
"eslint-plugin-jest": "^28.11.0",
"@stylistic/stylelint-plugin": "^3.1.3",
"babel-plugin-transform-import-meta": "^2.3.3",
"eslint": "^9.31.0",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.1.0",
"jest": "^29.7.0",
"globals": "^16.3.0",
"jest": "^30.0.4",
"jest-expect-message": "^1.1.3",
"jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0",
"stylelint": "^16.19.1",
"stylelint-config-recess-order": "^6.0.0",
"stylelint": "^16.21.1",
"stylelint-config-recess-order": "^7.1.0",
"stylelint-config-recommended": "^16.0.0",
"supertest": "^7.1.1"
"supertest": "^7.1.3"
}
}

View File

@@ -161,7 +161,7 @@ fs.emptyDirSync('./build');
livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
ext : 'js json' // Extensions to watch (only .js/.json by default)
ext : 'js' // Extensions to watch (only .js/.json by default)
//watch : ['./server', './themes'], // Watch additional folders if needed
});
}

View File

@@ -27,6 +27,8 @@
"codemirror/addon/selection/active-line.js",
"codemirror/addon/hint/show-hint.js",
"moment",
"superagent"
"superagent",
"@sanity/diff-match-patch",
"fflate"
]
}

View File

@@ -147,6 +147,109 @@ app.get('/', (req, res, next)=>{
return next();
});
app.get('/analyze', async (req, res, next) => {
const accounts = JSON.parse(fs.readFileSync('accounts.json', 'utf8'));
let totalBrewsStubbed = accounts.reduce((sum, account) => {
if (account.brewsStubbed) {
return sum + account.brewsStubbed;
}
return sum;
}, 0);
let totalAccountsProcessed = accounts.filter(account => account.fullyProcessed).length;
let totalAccountsWithInvalidCredentials = accounts.filter(account => account.invalidCredentials).length;
console.log(`Total Brews Stubbed: ${totalBrewsStubbed}`);
console.log(`Total Accounts Processed: ${totalAccountsProcessed}`);
console.log(`Total Accounts with Invalid Credentials: ${totalAccountsWithInvalidCredentials}`);
});
app.get('/destroy', async (req, res, next) => {
const accounts = JSON.parse(fs.readFileSync('accounts.json', 'utf8'));
let updated = false;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
for (let i = 40000; i < accounts.length; i++) {
if (accounts[i].fullyProcessed || accounts[i].invalidCredentials) continue;
const originalAccount = { ...accounts[i] };
const account = accounts[i];
console.log(`Processing account: ${account.username}`);
let googleBrews;
let auth;
try {
auth = await GoogleActions.authCheck(account, res);
googleBrews = await GoogleActions.listGoogleBrews(auth);
} catch (err) {
console.error(`Auth error for ${account.username}`);
accounts[i] = { ...originalAccount, invalidCredentials: true };
updated = true;
continue;
}
if (!auth) {
accounts[i] = { ...originalAccount, missingAuth: true };
updated = true;
continue;
}
console.log('Google Brews:', googleBrews.length);
if (googleBrews.length === 0) {
accounts[i] = { ...originalAccount, fullyProcessed: true, brewsStubbed: 0 };
updated = true;
continue;
}
const fields = ['googleId', 'title', 'editId', 'shareId'];
let brews = await HomebrewModel.getByUser(account.username, true, fields).catch(err => console.error(err));
const stubbedEditIds = new Set(brews.map(b => b.editId));
googleBrews = googleBrews.filter(b => !stubbedEditIds.has(b.editId));
console.log('Unstubbed Google Brews:', googleBrews.length);
const results = await Promise.all(
googleBrews.map(async (brew) => {
let brewFromServer = await GoogleActions.getGoogleBrew(auth, brew.googleId, brew.editId, 'edit');
splitTextStyleAndMetadata(brewFromServer);
brewFromServer.authors = [account.username];
api.excludeStubProps(brewFromServer);
console.log(`Trying to Stub: ${brewFromServer.title} (${brewFromServer.shareId})`);
let saved = await new HomebrewModel(brewFromServer).save().catch(err => console.error(err));
if (saved) {
console.log(`Saved Stub: ${saved.title} (${saved.shareId})`);
return true;
}
return false;
})
);
const stubCount = results.filter(Boolean).length;
console.log('Brews stubbed:', stubCount);
accounts[i] = {
...originalAccount,
brewsStubbed: stubCount,
fullyProcessed: stubCount === googleBrews.length
};
updated = true;
sleep(1000);
}
if (updated) {
fs.writeFileSync('accounts.json', JSON.stringify(accounts, null, 2), 'utf8');
console.log('accounts.json updated');
}
res.send('One account processed');
});
//Home page Legacy
app.get('/legacy', (req, res, next)=>{
req.brew = {
@@ -383,6 +486,7 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res,
title : req.brew.title || 'Untitled Brew',
description : req.brew.description || 'No description.',
image : req.brew.thumbnail || defaultMetaTags.image,
locale : req.brew.lang,
type : 'article'
};
@@ -404,6 +508,7 @@ app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res,
renderer : req.brew.renderer,
theme : req.brew.theme,
tags : req.brew.tags,
snippets : req.brew.snippets
};
req.brew = _.defaults(brew, DEFAULT_BREW);

View File

@@ -8,7 +8,8 @@
import Mongoose from 'mongoose';
const getMongoDBURL = (config)=>{
return config.get('mongodb_uri') ||
console.log('mongodb uri', config.get('MONGODB_URI'));
return config.get('MONGODB_URI') ||
config.get('mongolab_uri') ||
'mongodb://127.0.0.1/homebrewery'; // changed from mongodb://localhost/homebrewery to accommodate versions 16+ of node.
};

View 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();
});
});

View File

@@ -8,8 +8,10 @@ import Markdown from '../shared/naturalcrit/markdown.js';
import yaml from 'js-yaml';
import asyncHandler from 'express-async-handler';
import { nanoid } from 'nanoid';
import {makePatches, applyPatches, stringifyPatches, parsePatch} from '@sanity/diff-match-patch';
import { md5 } from 'hash-wasm';
import { splitTextStyleAndMetadata,
brewSnippetsToJSON } from '../shared/helpers.js';
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
import checkClientVersion from './middleware/check-client-version.js';
@@ -46,6 +48,20 @@ const api = {
}
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 };
},
//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
const brewFromClient = api.excludePropsFromUpdate(req.body);
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}`);
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 { saveToGoogle, removeFromGoogle } = req.query;
let afterSave = async ()=>true;
brew.title = brew.title.trim();
brew.description = brew.description.trim() || '';
brew.text = api.mergeBrewText(brew);
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
afterSave = async ()=>{
@@ -412,6 +459,8 @@ const api = {
const after = await afterSave();
if(!after) return;
saved.textBin = undefined; // Remove textBin from the saved object to save bandwidth
res.status(200).send(saved);
},
deleteGoogleBrew : async (account, id, editId, res)=>{
@@ -482,10 +531,10 @@ const api = {
};
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), 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.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
export default api;
export default api;

View File

@@ -99,18 +99,87 @@ describe('Tests for api', ()=>{
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', ()=>{
const { id, googleId } = api.getId({
params : {
id : 'abcdefgh'
id : 'abcdefghijkl'
},
body : {
googleId : '12345'
googleId : '123456789012345678901234567890123'
}
});
expect(id).toEqual('abcdefgh');
expect(googleId).toEqual('12345');
expect(id).toEqual('abcdefghijkl');
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', ()=>{
@@ -1052,4 +1121,83 @@ brew`);
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
})
);
});
});
});

View File

@@ -5,21 +5,16 @@ import config from './config.js';
const generateAccessToken = (account)=>{
const payload = account;
// When the token was issued
payload.issued = (new Date());
// Which service issued the Token
payload.issuer = config.get('authentication_token_issuer');
// Which service is the token intended for
payload.audience = config.get('authentication_token_audience');
// The signing key for signing the token
payload.issued = (new Date()); // When the token was issued
payload.issuer = config.get('authentication_token_issuer'); // Which service issued the Token
payload.audience = config.get('authentication_token_audience'); // Which service is the token intended for
const secret = config.get('authentication_token_secret'); // The signing key for signing the token
delete payload.password;
delete payload._id;
const secret = config.get('authentication_token_secret');
const token = jwt.encode(payload, secret);
return token;
};
export default generateAccessToken;
export default generateAccessToken;

27
server/token.spec.js Normal file
View 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');
});
});

View File

@@ -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 {
splitTextStyleAndMetadata,
printCurrentBrew,
fetchThemeBundle,
brewSnippetsToJSON
brewSnippetsToJSON,
debugTextMismatch
};

View File

@@ -185,7 +185,7 @@ const mustacheSpans = {
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) {
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);
if(match) {
//Find closing delimiter
@@ -242,7 +242,7 @@ const mustacheDivs = {
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) {
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);
if(match) {
//Find closing delimiter
@@ -297,7 +297,7 @@ const mustacheInjectInline = {
level : 'inline',
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) {
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g;
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/g;
const match = inlineRegex.exec(src);
if(match) {
const lastToken = tokens[tokens.length - 1];
@@ -343,7 +343,7 @@ const mustacheInjectBlock = {
level : 'block',
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) {
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
const match = inlineRegex.exec(src);
if(match) {
const lastToken = tokens[tokens.length - 1];

View File

@@ -4,6 +4,17 @@ require('jsdom-global')();
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() {
const source = `<a href="javascript:alert('This is a JavaScript injection via href attribute')">Click me</a>`;
const rendered = safeHTML(source);

View 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